Compare commits
68
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a03a7c6db | ||
|
|
a66a444433 | ||
|
|
a5794d83e0 | ||
|
|
008b93d07a | ||
|
|
50b26164f5 | ||
|
|
2c7fe99816 | ||
|
|
2e22072934 | ||
|
|
4142cdba43 | ||
|
|
bb54d2a681 | ||
|
|
4d3f42b6af | ||
|
|
fec2a427e7 | ||
|
|
bd346e5877 | ||
|
|
fcd5020577 | ||
|
|
1e592a1a52 | ||
|
|
55e71b5cb2 | ||
|
|
1a37c94cb4 | ||
|
|
6c93f6b5b5 | ||
|
|
7f651b235d | ||
|
|
e36dbd410a | ||
|
|
0235ba6a6d | ||
|
|
efaa8ce4bf | ||
|
|
d5ffb16259 | ||
|
|
c8cd5d37fe | ||
|
|
c672696ce6 | ||
|
|
8ac444dd55 | ||
|
|
d46d651e61 | ||
|
|
3df24e57fe | ||
|
|
6fbcab8e8a | ||
|
|
cb4f9beb40 | ||
|
|
afe2a053d0 | ||
|
|
d299ff655b | ||
|
|
fadaf96b5e | ||
|
|
cb582b3165 | ||
|
|
ccdc4f0196 | ||
|
|
86a077802e | ||
|
|
268191e139 | ||
|
|
b08f98eefb | ||
|
|
83340e359d | ||
|
|
a0fc05abd6 | ||
|
|
56f05b9c7a | ||
|
|
6f290b3256 | ||
|
|
085db75b8a | ||
|
|
ceaf2d6544 | ||
|
|
1ca498bf41 | ||
|
|
a685d34975 | ||
|
|
592d12d5e7 | ||
|
|
176b47ff1c | ||
|
|
3d2c1ab961 | ||
|
|
d92845caea | ||
|
|
c850933c13 | ||
|
|
c8c7dc6439 | ||
|
|
ed8b604d76 | ||
|
|
b5924f2824 | ||
|
|
a6c09aa601 | ||
|
|
c67dbcba7e | ||
|
|
1a5fd1825e | ||
|
|
160641737c | ||
|
|
18fd1d336b | ||
|
|
d8698c7e7a | ||
|
|
cc72454668 | ||
|
|
9336deb49b | ||
|
|
8dffdc8e0f | ||
|
|
6baea6d218 | ||
|
|
7243fece9f | ||
|
|
028d58cc88 | ||
|
|
3d6c80f4a6 | ||
|
|
21c42d29dd | ||
|
|
e3cf383daa |
@@ -0,0 +1,4 @@
|
||||
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
# Ignore build and test binaries.
|
||||
bin/
|
||||
testbin/
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
|
||||
# S3 конфиги с кредами — не коммитим
|
||||
.s3cfg*
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
bin
|
||||
testbin/*
|
||||
hack/local.env
|
||||
Dockerfile.cross
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Kubernetes Generated files - skip generated files, except for vendored files
|
||||
|
||||
!vendor/**/zz_generated.*
|
||||
|
||||
# editor and IDE paraphernalia
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
terraform/provider/build/
|
||||
|
||||
# Terraform state и кэш провайдеров (не коммитим)
|
||||
**/.terraform/
|
||||
**/.terraform.lock.hcl
|
||||
**/terraform.tfstate
|
||||
**/terraform.tfstate.backup
|
||||
**/terraform.tfstate.*.backup
|
||||
# Собранные zip-архивы функций (генерируются при terraform apply)
|
||||
examples/*/dist/
|
||||
**/handler.zip
|
||||
test.token
|
||||
/deployments/k8s/operator-demo.yaml
|
||||
/doc/decisions/log.md
|
||||
/doc/log.md
|
||||
/s3cfg_registry
|
||||
/sqs-operator/quick_test.sh
|
||||
/sqs-operator/sqs_load_test.sh
|
||||
/.s3cfg_registry
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Изменено: 2026-03-07
|
||||
# Multi-stage build для sless оператора.
|
||||
# Stage 1: сборка бинаря (golang:1.23-alpine)
|
||||
# Stage 2: минимальный образ (alpine:3.19, не distroless — нужен ca-certificates для S3/HTTPS)
|
||||
FROM golang:1.25-alpine AS builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /workspace
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
RUN go mod download
|
||||
|
||||
# Копируем весь исходный код (включая internal/ — там основная логика)
|
||||
COPY main.go main.go
|
||||
COPY api/ api/
|
||||
COPY controllers/ controllers/
|
||||
COPY internal/ internal/
|
||||
COPY migrations/ migrations/
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go
|
||||
|
||||
FROM alpine:3.19
|
||||
# ca-certificates нужны для TLS (S3 HTTPS, DockerHub)
|
||||
RUN apk add --no-cache ca-certificates
|
||||
WORKDIR /
|
||||
COPY --from=builder /workspace/manager .
|
||||
# migrations нужны при старте — оператор читает SQL файлы для инициализации БД
|
||||
COPY migrations/ migrations/
|
||||
# Запускаем от непривилегированного пользователя
|
||||
USER 65532:65532
|
||||
|
||||
ENTRYPOINT ["/manager"]
|
||||
@@ -0,0 +1,260 @@
|
||||
# VERSION defines the project version for the bundle.
|
||||
# Update this value when you upgrade the version of your project.
|
||||
# To re-generate a bundle for another specific version without changing the standard setup, you can:
|
||||
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
|
||||
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
|
||||
VERSION ?= 0.0.1
|
||||
|
||||
# CHANNELS define the bundle channels used in the bundle.
|
||||
# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
|
||||
# To re-generate a bundle for other specific channels without changing the standard setup, you can:
|
||||
# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable)
|
||||
# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable")
|
||||
ifneq ($(origin CHANNELS), undefined)
|
||||
BUNDLE_CHANNELS := --channels=$(CHANNELS)
|
||||
endif
|
||||
|
||||
# DEFAULT_CHANNEL defines the default channel used in the bundle.
|
||||
# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable")
|
||||
# To re-generate a bundle for any other default channel without changing the default setup, you can:
|
||||
# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable)
|
||||
# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable")
|
||||
ifneq ($(origin DEFAULT_CHANNEL), undefined)
|
||||
BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL)
|
||||
endif
|
||||
BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
|
||||
|
||||
# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images.
|
||||
# This variable is used to construct full image tags for bundle and catalog images.
|
||||
#
|
||||
# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both
|
||||
# kube5s.ru/sless-bundle:$VERSION and kube5s.ru/sless-catalog:$VERSION.
|
||||
IMAGE_TAG_BASE ?= kube5s.ru/sless
|
||||
|
||||
# BUNDLE_IMG defines the image:tag used for the bundle.
|
||||
# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>)
|
||||
BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION)
|
||||
|
||||
# BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command
|
||||
BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS)
|
||||
|
||||
# USE_IMAGE_DIGESTS defines if images are resolved via tags or digests
|
||||
# You can enable this value if you would like to use SHA Based Digests
|
||||
# To enable set flag to true
|
||||
USE_IMAGE_DIGESTS ?= false
|
||||
ifeq ($(USE_IMAGE_DIGESTS), true)
|
||||
BUNDLE_GEN_FLAGS += --use-image-digests
|
||||
endif
|
||||
|
||||
# Image URL to use all building/pushing image targets
|
||||
IMG ?= controller:latest
|
||||
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
|
||||
ENVTEST_K8S_VERSION = 1.26.0
|
||||
|
||||
# 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
|
||||
|
||||
# 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 commands is responsible for reading the
|
||||
# entire set of makefiles included in this invocation, looking for lines of the
|
||||
# file as xyz: ## something, and then pretty-format the target and help. Then,
|
||||
# if there's a line with ##@ something, that gets pretty-printed as a category.
|
||||
# More info on the usage of ANSI control characters for terminal formatting:
|
||||
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
|
||||
# More info on the awk command:
|
||||
# http://linuxcommand.org/lc3_adv_awk.php
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
##@ Development
|
||||
|
||||
.PHONY: manifests
|
||||
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
|
||||
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
|
||||
|
||||
.PHONY: generate
|
||||
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
|
||||
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" 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 envtest ## Run tests.
|
||||
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
|
||||
|
||||
##@ Build
|
||||
|
||||
.PHONY: build
|
||||
build: manifests generate fmt vet ## Build manager binary.
|
||||
go build -o bin/manager main.go
|
||||
|
||||
.PHONY: run
|
||||
run: manifests generate fmt vet ## Run a controller from your host.
|
||||
go run ./main.go
|
||||
|
||||
# If you wish built 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/
|
||||
.PHONY: docker-build
|
||||
docker-build: test ## Build docker image with the manager.
|
||||
docker build -t ${IMG} .
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push: ## Push docker image with the manager.
|
||||
docker push ${IMG}
|
||||
|
||||
# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple
|
||||
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
|
||||
# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/
|
||||
# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||
# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
|
||||
# To properly provided solutions that supports more than one platform you should use this option.
|
||||
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
|
||||
.PHONY: docker-buildx
|
||||
docker-buildx: test ## 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
|
||||
- docker buildx create --name project-v3-builder
|
||||
docker buildx use project-v3-builder
|
||||
- docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
|
||||
- docker buildx rm project-v3-builder
|
||||
rm Dockerfile.cross
|
||||
|
||||
##@ 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.
|
||||
$(KUSTOMIZE) build config/crd | kubectl apply -f -
|
||||
|
||||
.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.
|
||||
$(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
|
||||
.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: ## 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 -
|
||||
|
||||
##@ Build Dependencies
|
||||
|
||||
## Location to install dependencies to
|
||||
LOCALBIN ?= $(shell pwd)/bin
|
||||
$(LOCALBIN):
|
||||
mkdir -p $(LOCALBIN)
|
||||
|
||||
## Tool Binaries
|
||||
KUSTOMIZE ?= $(LOCALBIN)/kustomize
|
||||
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
|
||||
ENVTEST ?= $(LOCALBIN)/setup-envtest
|
||||
|
||||
## Tool Versions
|
||||
KUSTOMIZE_VERSION ?= v3.8.7
|
||||
CONTROLLER_TOOLS_VERSION ?= v0.14.0
|
||||
|
||||
KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
|
||||
.PHONY: kustomize
|
||||
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading.
|
||||
$(KUSTOMIZE): $(LOCALBIN)
|
||||
@if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \
|
||||
echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \
|
||||
rm -rf $(LOCALBIN)/kustomize; \
|
||||
fi
|
||||
test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); }
|
||||
|
||||
.PHONY: controller-gen
|
||||
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten.
|
||||
$(CONTROLLER_GEN): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \
|
||||
GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
|
||||
|
||||
.PHONY: envtest
|
||||
envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
|
||||
$(ENVTEST): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
|
||||
|
||||
.PHONY: bundle
|
||||
bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files.
|
||||
operator-sdk generate kustomize manifests -q
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG)
|
||||
$(KUSTOMIZE) build config/manifests | operator-sdk generate bundle $(BUNDLE_GEN_FLAGS)
|
||||
operator-sdk bundle validate ./bundle
|
||||
|
||||
.PHONY: bundle-build
|
||||
bundle-build: ## Build the bundle image.
|
||||
docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
|
||||
|
||||
.PHONY: bundle-push
|
||||
bundle-push: ## Push the bundle image.
|
||||
$(MAKE) docker-push IMG=$(BUNDLE_IMG)
|
||||
|
||||
.PHONY: opm
|
||||
OPM = ./bin/opm
|
||||
opm: ## Download opm locally if necessary.
|
||||
ifeq (,$(wildcard $(OPM)))
|
||||
ifeq (,$(shell which opm 2>/dev/null))
|
||||
@{ \
|
||||
set -e ;\
|
||||
mkdir -p $(dir $(OPM)) ;\
|
||||
OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
|
||||
curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\
|
||||
chmod +x $(OPM) ;\
|
||||
}
|
||||
else
|
||||
OPM = $(shell which opm)
|
||||
endif
|
||||
endif
|
||||
|
||||
# A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0).
|
||||
# These images MUST exist in a registry and be pull-able.
|
||||
BUNDLE_IMGS ?= $(BUNDLE_IMG)
|
||||
|
||||
# The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0).
|
||||
CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION)
|
||||
|
||||
# Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image.
|
||||
ifneq ($(origin CATALOG_BASE_IMG), undefined)
|
||||
FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG)
|
||||
endif
|
||||
|
||||
# Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'.
|
||||
# This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see:
|
||||
# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator
|
||||
.PHONY: catalog-build
|
||||
catalog-build: opm ## Build a catalog image.
|
||||
$(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)
|
||||
|
||||
# Push the catalog image.
|
||||
.PHONY: catalog-push
|
||||
catalog-push: ## Push a catalog image.
|
||||
$(MAKE) docker-push IMG=$(CATALOG_IMG)
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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
|
||||
domain: kube5s.ru
|
||||
layout:
|
||||
- go.kubebuilder.io/v3
|
||||
plugins:
|
||||
manifests.sdk.operatorframework.io/v2: {}
|
||||
scorecard.sdk.operatorframework.io/v2: {}
|
||||
projectName: sless
|
||||
repo: gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless
|
||||
resources:
|
||||
- api:
|
||||
crdVersion: v1
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: kube5s.ru
|
||||
group: sless
|
||||
kind: Function
|
||||
path: gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1
|
||||
version: v1alpha1
|
||||
- api:
|
||||
crdVersion: v1
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: kube5s.ru
|
||||
group: sless
|
||||
kind: Trigger
|
||||
path: gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1
|
||||
version: v1alpha1
|
||||
version: "3"
|
||||
@@ -0,0 +1,94 @@
|
||||
# sless
|
||||
// TODO(user): Add simple overview of use/purpose
|
||||
|
||||
## Description
|
||||
// TODO(user): An in-depth paragraph about your project and overview of use
|
||||
|
||||
## Getting Started
|
||||
You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster.
|
||||
**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows).
|
||||
|
||||
### Running on the cluster
|
||||
1. Install Instances of Custom Resources:
|
||||
|
||||
```sh
|
||||
kubectl apply -f config/samples/
|
||||
```
|
||||
|
||||
2. Build and push your image to the location specified by `IMG`:
|
||||
|
||||
```sh
|
||||
make docker-build docker-push IMG=<some-registry>/sless:tag
|
||||
```
|
||||
|
||||
3. Deploy the controller to the cluster with the image specified by `IMG`:
|
||||
|
||||
```sh
|
||||
make deploy IMG=<some-registry>/sless:tag
|
||||
```
|
||||
|
||||
### Uninstall CRDs
|
||||
To delete the CRDs from the cluster:
|
||||
|
||||
```sh
|
||||
make uninstall
|
||||
```
|
||||
|
||||
### Undeploy controller
|
||||
UnDeploy the controller from the cluster:
|
||||
|
||||
```sh
|
||||
make undeploy
|
||||
```
|
||||
|
||||
## Contributing
|
||||
// TODO(user): Add detailed information on how you would like others to contribute to this project
|
||||
|
||||
### How it works
|
||||
This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/).
|
||||
|
||||
It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/),
|
||||
which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster.
|
||||
|
||||
### Test It Out
|
||||
1. Install the CRDs into the cluster:
|
||||
|
||||
```sh
|
||||
make install
|
||||
```
|
||||
|
||||
2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running):
|
||||
|
||||
```sh
|
||||
make run
|
||||
```
|
||||
|
||||
**NOTE:** You can also run this in one step by running: `make install run`
|
||||
|
||||
### Modifying the API definitions
|
||||
If you are editing the API definitions, generate the manifests such as CRs or CRDs using:
|
||||
|
||||
```sh
|
||||
make manifests
|
||||
```
|
||||
|
||||
**NOTE:** Run `make --help` for more information on all potential `make` targets
|
||||
|
||||
More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2026.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Изменено: 2026-03-07
|
||||
// Описание CRD Function — основной ресурс serverless функции.
|
||||
// Пользователь создаёт Function, контроллер строит образ и деплоит в k8s.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// FunctionSpec — желаемое состояние функции.
|
||||
// Описывает всё необходимое для сборки и запуска пользовательского кода.
|
||||
type FunctionSpec struct {
|
||||
// Runtime — язык и версия выполнения (go1.21, python3.11, nodejs20)
|
||||
// +kubebuilder:validation:Enum=go1.21;python3.11;nodejs20
|
||||
// +kubebuilder:validation:Required
|
||||
Runtime string `json:"runtime"`
|
||||
|
||||
// Entrypoint — точка входа в код функции (например: handler.handle)
|
||||
// +kubebuilder:validation:Required
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
|
||||
// S3Bucket — бакет S3 где хранится zip архив с кодом
|
||||
S3Bucket string `json:"s3Bucket"`
|
||||
|
||||
// S3Key — ключ объекта в S3 (путь до zip архива)
|
||||
S3Key string `json:"s3Key"`
|
||||
|
||||
// MemoryMB — лимит памяти в мегабайтах (default: 128)
|
||||
// +kubebuilder:default=128
|
||||
MemoryMB int32 `json:"memoryMB,omitempty"`
|
||||
|
||||
// TimeoutSec — максимальное время выполнения в секундах (default: 30)
|
||||
// +kubebuilder:default=30
|
||||
TimeoutSec int32 `json:"timeoutSec,omitempty"`
|
||||
|
||||
// Env — переменные окружения, передаются в контейнер функции
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
// FunctionPhase — текущая фаза жизненного цикла функции.
|
||||
// Используем строковый тип чтобы было читаемо в kubectl get functions.
|
||||
type FunctionPhase string
|
||||
|
||||
const (
|
||||
// FunctionPhasePending — функция создана, ожидает сборки образа
|
||||
FunctionPhasePending FunctionPhase = "Pending"
|
||||
// FunctionPhaseBuilding — идёт сборка Docker образа
|
||||
FunctionPhaseBuilding FunctionPhase = "Building"
|
||||
// FunctionPhaseReady — образ собран, функция готова к вызову
|
||||
FunctionPhaseReady FunctionPhase = "Ready"
|
||||
// FunctionPhaseFailed — ошибка при сборке или деплое
|
||||
FunctionPhaseFailed FunctionPhase = "Failed"
|
||||
)
|
||||
|
||||
// FunctionStatus — наблюдаемое состояние функции (заполняет контроллер).
|
||||
type FunctionStatus struct {
|
||||
// Phase — текущая фаза: Pending, Building, Ready, Failed
|
||||
Phase FunctionPhase `json:"phase,omitempty"`
|
||||
|
||||
// ImageRef — полный путь к собранному Docker образу в registry
|
||||
ImageRef string `json:"imageRef,omitempty"`
|
||||
|
||||
// Message — человекочитаемое сообщение об ошибке или статусе
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// Conditions — стандартные k8s conditions для интеграции с инструментами
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
|
||||
// LastBuiltAt — время последней успешной сборки образа
|
||||
LastBuiltAt *metav1.Time `json:"lastBuiltAt,omitempty"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
//+kubebuilder:subresource:status
|
||||
//+kubebuilder:printcolumn:name="Runtime",type=string,JSONPath=`.spec.runtime`
|
||||
//+kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
|
||||
//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
|
||||
// Function — основной ресурс для управления serverless функцией.
|
||||
type Function struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec FunctionSpec `json:"spec,omitempty"`
|
||||
Status FunctionStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
|
||||
// FunctionList contains a list of Function
|
||||
type FunctionList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Function `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&Function{}, &FunctionList{})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2026.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package v1alpha1 contains API Schema definitions for the sless v1alpha1 API group
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=sless.kube5s.ru
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||
)
|
||||
|
||||
var (
|
||||
// GroupVersion is group version used to register these objects
|
||||
GroupVersion = schema.GroupVersion{Group: "sless.kube5s.ru", Version: "v1alpha1"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
|
||||
|
||||
// AddToScheme adds the types in this group-version to the given scheme.
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
// Изменено: 2026-03-08
|
||||
// Описание CRD FunctionJob — одноразовый запуск функции.
|
||||
// Отдельный ресурс (не Trigger) потому что семантика принципиально другая:
|
||||
// - Trigger: постоянно живёт, описывает КАК функцию вызывают (http/cron)
|
||||
// - FunctionJob: запускается один раз, завершается, хранит результат
|
||||
//
|
||||
// Terraform ресурс: sless_job
|
||||
// k8s ресурс: k8s Job в namespace sless-fn-{namespace}
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// FunctionJobSpec — параметры одноразового запуска функции.
|
||||
type FunctionJobSpec struct {
|
||||
// RunID — идентификатор запуска. 0 = не запускать.
|
||||
// Каждое ненулевое значение уникально идентифицирует запуск.
|
||||
// Увеличь RunID (1→2→3) для повторного запуска джоба.
|
||||
// При RunID=0 FunctionJob создаётся в кластере, но k8s Job не запускается.
|
||||
// +kubebuilder:default=0
|
||||
RunID int64 `json:"runId"`
|
||||
|
||||
// FunctionRef — имя Function ресурса в том же namespace
|
||||
// +kubebuilder:validation:Required
|
||||
FunctionRef string `json:"functionRef"`
|
||||
|
||||
// EventJSON — данные передаваемые в handle(event) в JSON формате.
|
||||
// Если не задан — передаётся пустой объект {}.
|
||||
// Пример: {"action": "migrate", "version": "002"}
|
||||
// +kubebuilder:default="{}"
|
||||
EventJSON string `json:"eventJson,omitempty"`
|
||||
}
|
||||
|
||||
// FunctionJobPhase — фаза жизненного цикла одноразового запуска.
|
||||
type FunctionJobPhase string
|
||||
|
||||
const (
|
||||
// FunctionJobPhasePending — ожидает пока Function станет Ready
|
||||
FunctionJobPhasePending FunctionJobPhase = "Pending"
|
||||
// FunctionJobPhaseRunning — k8s Job запущен, функция выполняется
|
||||
FunctionJobPhaseRunning FunctionJobPhase = "Running"
|
||||
// FunctionJobPhaseSucceeded — функция успешно завершилась
|
||||
FunctionJobPhaseSucceeded FunctionJobPhase = "Succeeded"
|
||||
// FunctionJobPhaseFailed — функция завершилась с ошибкой
|
||||
FunctionJobPhaseFailed FunctionJobPhase = "Failed"
|
||||
)
|
||||
|
||||
// FunctionJobStatus — наблюдаемое состояние (заполняет контроллер).
|
||||
type FunctionJobStatus struct {
|
||||
// Phase — текущая фаза: Pending, Running, Succeeded, Failed
|
||||
Phase FunctionJobPhase `json:"phase,omitempty"`
|
||||
|
||||
// JobName — имя созданного k8s Job
|
||||
JobName string `json:"jobName,omitempty"`
|
||||
|
||||
// StartTime — время запуска k8s Job
|
||||
StartTime *metav1.Time `json:"startTime,omitempty"`
|
||||
|
||||
// CompletionTime — время завершения
|
||||
CompletionTime *metav1.Time `json:"completionTime,omitempty"`
|
||||
|
||||
// Message — результат выполнения или сообщение об ошибке
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
//+kubebuilder:subresource:status
|
||||
//+kubebuilder:printcolumn:name="Function",type=string,JSONPath=`.spec.functionRef`
|
||||
//+kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
|
||||
//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
|
||||
// FunctionJob — ресурс для одноразового запуска serverless функции.
|
||||
// Создаётся пользователем через terraform ресурс sless_job.
|
||||
// После завершения (Succeeded/Failed) ресурс остаётся в кластере для аудита.
|
||||
type FunctionJob struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec FunctionJobSpec `json:"spec,omitempty"`
|
||||
Status FunctionJobStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
|
||||
// FunctionJobList contains a list of FunctionJob
|
||||
type FunctionJobList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []FunctionJob `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&FunctionJob{}, &FunctionJobList{})
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Изменено: 2026-03-08
|
||||
// Описание CRD Trigger — триггер для функции (HTTP или Cron).
|
||||
// Один Trigger ссылается на одну Function и определяет способ вызова.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// TriggerType — тип триггера.
|
||||
type TriggerType string
|
||||
|
||||
const (
|
||||
// TriggerTypeHTTP — функция вызывается по HTTP запросу через Ingress
|
||||
TriggerTypeHTTP TriggerType = "http"
|
||||
// TriggerTypeCron — функция вызывается по расписанию (k8s CronJob)
|
||||
TriggerTypeCron TriggerType = "cron"
|
||||
)
|
||||
|
||||
// TriggerSpec — желаемое состояние триггера.
|
||||
type TriggerSpec struct {
|
||||
// Enabled — если false, Deployment функции масштабируется до 0 (функция не принимает запросы).
|
||||
// При true — функция запускается. По умолчанию true.
|
||||
// Позволяет "заморозить" функцию без удаления ресурса.
|
||||
// +kubebuilder:default=true
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// FunctionRef — имя Function ресурса в том же namespace
|
||||
// +kubebuilder:validation:Required
|
||||
FunctionRef string `json:"functionRef"`
|
||||
|
||||
// Type — тип триггера: http или cron
|
||||
// +kubebuilder:validation:Enum=http;cron
|
||||
// +kubebuilder:validation:Required
|
||||
Type TriggerType `json:"type"`
|
||||
|
||||
// Schedule — расписание в формате cron (только для type=cron, например: "0 2 * * *")
|
||||
Schedule string `json:"schedule,omitempty"`
|
||||
|
||||
// PreWarmSeconds — за сколько секунд до запуска прогреть контейнер.
|
||||
// Актуально для cron: запускаем pod заранее чтобы избежать cold start.
|
||||
// +kubebuilder:default=300
|
||||
PreWarmSeconds int32 `json:"preWarmSeconds,omitempty"`
|
||||
}
|
||||
|
||||
// TriggerStatus — наблюдаемое состояние триггера (заполняет контроллер).
|
||||
type TriggerStatus struct {
|
||||
// Active — активен ли триггер в данный момент
|
||||
Active bool `json:"active,omitempty"`
|
||||
|
||||
// URL — публичный URL для вызова функции (только для type=http)
|
||||
URL string `json:"url,omitempty"`
|
||||
|
||||
// LastScheduleTime — время последнего запуска по расписанию (только для type=cron)
|
||||
LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"`
|
||||
|
||||
// Message — человекочитаемое сообщение об ошибке или статусе
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
//+kubebuilder:subresource:status
|
||||
//+kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.type`
|
||||
//+kubebuilder:printcolumn:name="Function",type=string,JSONPath=`.spec.functionRef`
|
||||
//+kubebuilder:printcolumn:name="Active",type=boolean,JSONPath=`.status.active`
|
||||
//+kubebuilder:printcolumn:name="URL",type=string,JSONPath=`.status.url`
|
||||
//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
|
||||
// Trigger — ресурс для управления способом вызова Function.
|
||||
type Trigger struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec TriggerSpec `json:"spec,omitempty"`
|
||||
Status TriggerStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
|
||||
// TriggerList contains a list of Trigger
|
||||
type TriggerList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Trigger `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&Trigger{}, &TriggerList{})
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2026.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Function) DeepCopyInto(out *Function) {
|
||||
*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 Function.
|
||||
func (in *Function) DeepCopy() *Function {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Function)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Function) 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 *FunctionJob) DeepCopyInto(out *FunctionJob) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionJob.
|
||||
func (in *FunctionJob) DeepCopy() *FunctionJob {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FunctionJob)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *FunctionJob) 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 *FunctionJobList) DeepCopyInto(out *FunctionJobList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]FunctionJob, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionJobList.
|
||||
func (in *FunctionJobList) DeepCopy() *FunctionJobList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FunctionJobList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *FunctionJobList) 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 *FunctionJobSpec) DeepCopyInto(out *FunctionJobSpec) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionJobSpec.
|
||||
func (in *FunctionJobSpec) DeepCopy() *FunctionJobSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FunctionJobSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FunctionJobStatus) DeepCopyInto(out *FunctionJobStatus) {
|
||||
*out = *in
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionJobStatus.
|
||||
func (in *FunctionJobStatus) DeepCopy() *FunctionJobStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FunctionJobStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FunctionList) DeepCopyInto(out *FunctionList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Function, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionList.
|
||||
func (in *FunctionList) DeepCopy() *FunctionList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FunctionList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *FunctionList) 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 *FunctionSpec) DeepCopyInto(out *FunctionSpec) {
|
||||
*out = *in
|
||||
if in.Env != nil {
|
||||
in, out := &in.Env, &out.Env
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionSpec.
|
||||
func (in *FunctionSpec) DeepCopy() *FunctionSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FunctionSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FunctionStatus) DeepCopyInto(out *FunctionStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.LastBuiltAt != nil {
|
||||
in, out := &in.LastBuiltAt, &out.LastBuiltAt
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionStatus.
|
||||
func (in *FunctionStatus) DeepCopy() *FunctionStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FunctionStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Trigger) DeepCopyInto(out *Trigger) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Trigger.
|
||||
func (in *Trigger) DeepCopy() *Trigger {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Trigger)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Trigger) 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 *TriggerList) DeepCopyInto(out *TriggerList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Trigger, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriggerList.
|
||||
func (in *TriggerList) DeepCopy() *TriggerList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TriggerList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *TriggerList) 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 *TriggerSpec) DeepCopyInto(out *TriggerSpec) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriggerSpec.
|
||||
func (in *TriggerSpec) DeepCopy() *TriggerSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TriggerSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TriggerStatus) DeepCopyInto(out *TriggerStatus) {
|
||||
*out = *in
|
||||
if in.LastScheduleTime != nil {
|
||||
in, out := &in.LastScheduleTime, &out.LastScheduleTime
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriggerStatus.
|
||||
func (in *TriggerStatus) DeepCopy() *TriggerStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TriggerStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
name: functionjobs.sless.kube5s.ru
|
||||
spec:
|
||||
group: sless.kube5s.ru
|
||||
names:
|
||||
kind: FunctionJob
|
||||
listKind: FunctionJobList
|
||||
plural: functionjobs
|
||||
singular: functionjob
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.functionRef
|
||||
name: Function
|
||||
type: string
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: |-
|
||||
FunctionJob — ресурс для одноразового запуска serverless функции.
|
||||
Создаётся пользователем через terraform ресурс sless_job.
|
||||
После завершения (Succeeded/Failed) ресурс остаётся в кластере для аудита.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: FunctionJobSpec — параметры одноразового запуска функции.
|
||||
properties:
|
||||
eventJson:
|
||||
default: '{}'
|
||||
description: |-
|
||||
EventJSON — данные передаваемые в handle(event) в JSON формате.
|
||||
Если не задан — передаётся пустой объект {}.
|
||||
Пример: {"action": "migrate", "version": "002"}
|
||||
type: string
|
||||
functionRef:
|
||||
description: FunctionRef — имя Function ресурса в том же namespace
|
||||
type: string
|
||||
runId:
|
||||
default: 0
|
||||
description: |-
|
||||
RunID — идентификатор запуска. 0 = не запускать.
|
||||
Каждое ненулевое значение уникально идентифицирует запуск.
|
||||
Увеличь RunID (1→2→3) для повторного запуска джоба.
|
||||
При RunID=0 FunctionJob создаётся в кластере, но k8s Job не запускается.
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- functionRef
|
||||
- runId
|
||||
type: object
|
||||
status:
|
||||
description: FunctionJobStatus — наблюдаемое состояние (заполняет контроллер).
|
||||
properties:
|
||||
completionTime:
|
||||
description: CompletionTime — время завершения
|
||||
format: date-time
|
||||
type: string
|
||||
jobName:
|
||||
description: JobName — имя созданного k8s Job
|
||||
type: string
|
||||
message:
|
||||
description: Message — результат выполнения или сообщение об ошибке
|
||||
type: string
|
||||
phase:
|
||||
description: 'Phase — текущая фаза: Pending, Running, Succeeded, Failed'
|
||||
type: string
|
||||
startTime:
|
||||
description: StartTime — время запуска k8s Job
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -0,0 +1,186 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
name: functions.sless.kube5s.ru
|
||||
spec:
|
||||
group: sless.kube5s.ru
|
||||
names:
|
||||
kind: Function
|
||||
listKind: FunctionList
|
||||
plural: functions
|
||||
singular: function
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.runtime
|
||||
name: Runtime
|
||||
type: string
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Function — основной ресурс для управления serverless функцией.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: |-
|
||||
FunctionSpec — желаемое состояние функции.
|
||||
Описывает всё необходимое для сборки и запуска пользовательского кода.
|
||||
properties:
|
||||
entrypoint:
|
||||
description: 'Entrypoint — точка входа в код функции (например: handler.handle)'
|
||||
type: string
|
||||
env:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Env — переменные окружения, передаются в контейнер функции
|
||||
type: object
|
||||
memoryMB:
|
||||
default: 128
|
||||
description: 'MemoryMB — лимит памяти в мегабайтах (default: 128)'
|
||||
format: int32
|
||||
type: integer
|
||||
runtime:
|
||||
description: Runtime — язык и версия выполнения (go1.21, python3.11,
|
||||
nodejs20)
|
||||
enum:
|
||||
- go1.21
|
||||
- python3.11
|
||||
- nodejs20
|
||||
type: string
|
||||
s3Bucket:
|
||||
description: S3Bucket — бакет S3 где хранится zip архив с кодом
|
||||
type: string
|
||||
s3Key:
|
||||
description: S3Key — ключ объекта в S3 (путь до zip архива)
|
||||
type: string
|
||||
timeoutSec:
|
||||
default: 30
|
||||
description: 'TimeoutSec — максимальное время выполнения в секундах
|
||||
(default: 30)'
|
||||
format: int32
|
||||
type: integer
|
||||
required:
|
||||
- entrypoint
|
||||
- runtime
|
||||
- s3Bucket
|
||||
- s3Key
|
||||
type: object
|
||||
status:
|
||||
description: FunctionStatus — наблюдаемое состояние функции (заполняет
|
||||
контроллер).
|
||||
properties:
|
||||
conditions:
|
||||
description: Conditions — стандартные k8s conditions для интеграции
|
||||
с инструментами
|
||||
items:
|
||||
description: "Condition contains details for one aspect of the current
|
||||
state of this API Resource.\n---\nThis struct is intended for
|
||||
direct use as an array at the field path .status.conditions. For
|
||||
example,\n\n\n\ttype FooStatus struct{\n\t // Represents the
|
||||
observations of a foo's current state.\n\t // Known .status.conditions.type
|
||||
are: \"Available\", \"Progressing\", and \"Degraded\"\n\t //
|
||||
+patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t
|
||||
\ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\"
|
||||
patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
|
||||
\ // other fields\n\t}"
|
||||
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.
|
||||
---
|
||||
Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
|
||||
useful (see .node.status.conditions), the ability to deconflict is important.
|
||||
The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
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
|
||||
imageRef:
|
||||
description: ImageRef — полный путь к собранному Docker образу в registry
|
||||
type: string
|
||||
lastBuiltAt:
|
||||
description: LastBuiltAt — время последней успешной сборки образа
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: Message — человекочитаемое сообщение об ошибке или статусе
|
||||
type: string
|
||||
phase:
|
||||
description: 'Phase — текущая фаза: Pending, Building, Ready, Failed'
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
name: triggers.sless.kube5s.ru
|
||||
spec:
|
||||
group: sless.kube5s.ru
|
||||
names:
|
||||
kind: Trigger
|
||||
listKind: TriggerList
|
||||
plural: triggers
|
||||
singular: trigger
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.type
|
||||
name: Type
|
||||
type: string
|
||||
- jsonPath: .spec.functionRef
|
||||
name: Function
|
||||
type: string
|
||||
- jsonPath: .status.active
|
||||
name: Active
|
||||
type: boolean
|
||||
- jsonPath: .status.url
|
||||
name: URL
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Trigger — ресурс для управления способом вызова Function.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: TriggerSpec — желаемое состояние триггера.
|
||||
properties:
|
||||
enabled:
|
||||
default: true
|
||||
description: |-
|
||||
Enabled — если false, Deployment функции масштабируется до 0 (функция не принимает запросы).
|
||||
При true — функция запускается. По умолчанию true.
|
||||
Позволяет "заморозить" функцию без удаления ресурса.
|
||||
type: boolean
|
||||
functionRef:
|
||||
description: FunctionRef — имя Function ресурса в том же namespace
|
||||
type: string
|
||||
preWarmSeconds:
|
||||
default: 300
|
||||
description: |-
|
||||
PreWarmSeconds — за сколько секунд до запуска прогреть контейнер.
|
||||
Актуально для cron: запускаем pod заранее чтобы избежать cold start.
|
||||
format: int32
|
||||
type: integer
|
||||
schedule:
|
||||
description: 'Schedule — расписание в формате cron (только для type=cron,
|
||||
например: "0 2 * * *")'
|
||||
type: string
|
||||
type:
|
||||
description: 'Type — тип триггера: http или cron'
|
||||
enum:
|
||||
- http
|
||||
- cron
|
||||
type: string
|
||||
required:
|
||||
- enabled
|
||||
- functionRef
|
||||
- type
|
||||
type: object
|
||||
status:
|
||||
description: TriggerStatus — наблюдаемое состояние триггера (заполняет
|
||||
контроллер).
|
||||
properties:
|
||||
active:
|
||||
description: Active — активен ли триггер в данный момент
|
||||
type: boolean
|
||||
lastScheduleTime:
|
||||
description: LastScheduleTime — время последнего запуска по расписанию
|
||||
(только для type=cron)
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: Message — человекочитаемое сообщение об ошибке или статусе
|
||||
type: string
|
||||
url:
|
||||
description: URL — публичный URL для вызова функции (только для type=http)
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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/sless.kube5s.ru_functions.yaml
|
||||
- bases/sless.kube5s.ru_triggers.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizeresource
|
||||
|
||||
patchesStrategicMerge:
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
|
||||
# patches here are for enabling the conversion webhook for each CRD
|
||||
#- patches/webhook_in_functions.yaml
|
||||
#- patches/webhook_in_triggers.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizewebhookpatch
|
||||
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
|
||||
# patches here are for enabling the CA injection for each CRD
|
||||
#- patches/cainjection_in_functions.yaml
|
||||
#- patches/cainjection_in_triggers.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
|
||||
|
||||
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
||||
configurations:
|
||||
- kustomizeconfig.yaml
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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
|
||||
|
||||
namespace:
|
||||
- kind: CustomResourceDefinition
|
||||
version: v1
|
||||
group: apiextensions.k8s.io
|
||||
path: spec/conversion/webhook/clientConfig/service/namespace
|
||||
create: false
|
||||
|
||||
varReference:
|
||||
- path: metadata/annotations
|
||||
@@ -0,0 +1,7 @@
|
||||
# The following patch adds a directive for certmanager to inject CA into the CRD
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
|
||||
name: functions.sless.kube5s.ru
|
||||
@@ -0,0 +1,7 @@
|
||||
# The following patch adds a directive for certmanager to inject CA into the CRD
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
|
||||
name: triggers.sless.kube5s.ru
|
||||
@@ -0,0 +1,16 @@
|
||||
# The following patch enables a conversion webhook for the CRD
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: functions.sless.kube5s.ru
|
||||
spec:
|
||||
conversion:
|
||||
strategy: Webhook
|
||||
webhook:
|
||||
clientConfig:
|
||||
service:
|
||||
namespace: system
|
||||
name: webhook-service
|
||||
path: /convert
|
||||
conversionReviewVersions:
|
||||
- v1
|
||||
@@ -0,0 +1,16 @@
|
||||
# The following patch enables a conversion webhook for the CRD
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: triggers.sless.kube5s.ru
|
||||
spec:
|
||||
conversion:
|
||||
strategy: Webhook
|
||||
webhook:
|
||||
clientConfig:
|
||||
service:
|
||||
namespace: system
|
||||
name: webhook-service
|
||||
path: /convert
|
||||
conversionReviewVersions:
|
||||
- v1
|
||||
@@ -0,0 +1,72 @@
|
||||
# Adds namespace to all resources.
|
||||
namespace: sless-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: sless-
|
||||
|
||||
# Labels to add to all resources and selectors.
|
||||
#commonLabels:
|
||||
# someName: someValue
|
||||
|
||||
bases:
|
||||
- ../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
|
||||
|
||||
patchesStrategicMerge:
|
||||
# Protect the /metrics endpoint by putting it behind auth.
|
||||
# If you want your controller-manager to expose the /metrics
|
||||
# endpoint w/o any authn/z, please comment the following line.
|
||||
- manager_auth_proxy_patch.yaml
|
||||
|
||||
|
||||
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||
# crd/kustomization.yaml
|
||||
#- manager_webhook_patch.yaml
|
||||
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
|
||||
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
|
||||
# 'CERTMANAGER' needs to be enabled to use ca injection
|
||||
#- webhookcainjection_patch.yaml
|
||||
|
||||
# the following config is for teaching kustomize how to do var substitution
|
||||
vars:
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
|
||||
#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
|
||||
# objref:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # this name should match the one in certificate.yaml
|
||||
# fieldref:
|
||||
# fieldpath: metadata.namespace
|
||||
#- name: CERTIFICATE_NAME
|
||||
# objref:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # this name should match the one in certificate.yaml
|
||||
#- name: SERVICE_NAMESPACE # namespace of the service
|
||||
# objref:
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# fieldref:
|
||||
# fieldpath: metadata.namespace
|
||||
#- name: SERVICE_NAME
|
||||
# objref:
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
@@ -0,0 +1,55 @@
|
||||
# This patch inject a sidecar container which is a HTTP proxy for the
|
||||
# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/arch
|
||||
operator: In
|
||||
values:
|
||||
- amd64
|
||||
- arm64
|
||||
- ppc64le
|
||||
- s390x
|
||||
- key: kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
containers:
|
||||
- name: kube-rbac-proxy
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- "ALL"
|
||||
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1
|
||||
args:
|
||||
- "--secure-listen-address=0.0.0.0:8443"
|
||||
- "--upstream=http://127.0.0.1:8080/"
|
||||
- "--logtostderr=true"
|
||||
- "--v=0"
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
protocol: TCP
|
||||
name: https
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 5m
|
||||
memory: 64Mi
|
||||
- name: manager
|
||||
args:
|
||||
- "--health-probe-bind-address=:8081"
|
||||
- "--metrics-bind-address=127.0.0.1:8080"
|
||||
- "--leader-elect"
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: manager
|
||||
@@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- manager.yaml
|
||||
@@ -0,0 +1,102 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: namespace
|
||||
app.kubernetes.io/instance: system
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
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: deployment
|
||||
app.kubernetes.io/instance: controller-manager
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
control-plane: controller-manager
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/default-container: manager
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
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:
|
||||
runAsNonRoot: true
|
||||
# TODO(user): For common cases that do not require escalating privileges
|
||||
# it is recommended to ensure that all your Pods/Containers are restrictive.
|
||||
# More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
|
||||
# Please uncomment the following code if your project does NOT have to work on old Kubernetes
|
||||
# versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
|
||||
# seccompProfile:
|
||||
# type: RuntimeDefault
|
||||
containers:
|
||||
- command:
|
||||
- /manager
|
||||
args:
|
||||
- --leader-elect
|
||||
image: controller:latest
|
||||
name: manager
|
||||
securityContext:
|
||||
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
|
||||
serviceAccountName: controller-manager
|
||||
terminationGracePeriodSeconds: 10
|
||||
@@ -0,0 +1,27 @@
|
||||
# These resources constitute the fully configured set of manifests
|
||||
# used to generate the 'manifests/' directory in a bundle.
|
||||
resources:
|
||||
- bases/sless.clusterserviceversion.yaml
|
||||
- ../default
|
||||
- ../samples
|
||||
- ../scorecard
|
||||
|
||||
# [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix.
|
||||
# Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager.
|
||||
# These patches remove the unnecessary "cert" volume and its manager container volumeMount.
|
||||
#patchesJson6902:
|
||||
#- target:
|
||||
# group: apps
|
||||
# version: v1
|
||||
# kind: Deployment
|
||||
# name: controller-manager
|
||||
# namespace: system
|
||||
# patch: |-
|
||||
# # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs.
|
||||
# # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment.
|
||||
# - op: remove
|
||||
# path: /spec/template/spec/containers/1/volumeMounts/0
|
||||
# # Remove the "cert" volume, since OLM will create and mount a set of certs.
|
||||
# # Update the indices in this path if adding or removing volumes in the manager's Deployment.
|
||||
# - op: remove
|
||||
# path: /spec/template/spec/volumes/0
|
||||
@@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- monitor.yaml
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
# Prometheus Monitor Service (Metrics)
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: servicemonitor
|
||||
app.kubernetes.io/instance: controller-manager-metrics-monitor
|
||||
app.kubernetes.io/component: metrics
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: controller-manager-metrics-monitor
|
||||
namespace: system
|
||||
spec:
|
||||
endpoints:
|
||||
- path: /metrics
|
||||
port: https
|
||||
scheme: https
|
||||
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
tlsConfig:
|
||||
insecureSkipVerify: true
|
||||
selector:
|
||||
matchLabels:
|
||||
control-plane: controller-manager
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: metrics-reader
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: metrics-reader
|
||||
rules:
|
||||
- nonResourceURLs:
|
||||
- "/metrics"
|
||||
verbs:
|
||||
- get
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: proxy-role
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: proxy-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- create
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrolebinding
|
||||
app.kubernetes.io/instance: proxy-rolebinding
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: proxy-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: proxy-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
@@ -0,0 +1,21 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: service
|
||||
app.kubernetes.io/instance: controller-manager-metrics-service
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: controller-manager-metrics-service
|
||||
namespace: system
|
||||
spec:
|
||||
ports:
|
||||
- name: https
|
||||
port: 8443
|
||||
protocol: TCP
|
||||
targetPort: https
|
||||
selector:
|
||||
control-plane: controller-manager
|
||||
@@ -0,0 +1,31 @@
|
||||
# permissions for end users to edit functions.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: function-editor-role
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: function-editor-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- functions
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- functions/status
|
||||
verbs:
|
||||
- get
|
||||
@@ -0,0 +1,27 @@
|
||||
# permissions for end users to view functions.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: function-viewer-role
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: function-viewer-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- functions
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- functions/status
|
||||
verbs:
|
||||
- get
|
||||
@@ -0,0 +1,18 @@
|
||||
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
|
||||
# Comment the following 4 lines if you want to disable
|
||||
# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
|
||||
# which protects your /metrics endpoint.
|
||||
- auth_proxy_service.yaml
|
||||
- auth_proxy_role.yaml
|
||||
- auth_proxy_role_binding.yaml
|
||||
- auth_proxy_client_clusterrole.yaml
|
||||
@@ -0,0 +1,44 @@
|
||||
# permissions to do leader election.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: role
|
||||
app.kubernetes.io/instance: leader-election-role
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: leader-election-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- update
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- update
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- patch
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: rolebinding
|
||||
app.kubernetes.io/instance: leader-election-rolebinding
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: leader-election-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: leader-election-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: manager-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- patch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- namespaces
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- services
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- cronjobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- jobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- functionjobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- functionjobs/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- functionjobs/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- functions
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- functions/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- functions/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- triggers/finalizers
|
||||
verbs:
|
||||
- update
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrolebinding
|
||||
app.kubernetes.io/instance: manager-rolebinding
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: manager-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: manager-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: serviceaccount
|
||||
app.kubernetes.io/instance: controller-manager
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
@@ -0,0 +1,31 @@
|
||||
# permissions for end users to edit triggers.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: trigger-editor-role
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: trigger-editor-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- triggers
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- triggers/status
|
||||
verbs:
|
||||
- get
|
||||
@@ -0,0 +1,27 @@
|
||||
# permissions for end users to view triggers.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: trigger-viewer-role
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: sless
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: trigger-viewer-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- triggers
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- sless.kube5s.ru
|
||||
resources:
|
||||
- triggers/status
|
||||
verbs:
|
||||
- get
|
||||
@@ -0,0 +1,5 @@
|
||||
## Append samples you want in your CSV to this file as resources ##
|
||||
resources:
|
||||
- sless_v1alpha1_function.yaml
|
||||
- sless_v1alpha1_trigger.yaml
|
||||
#+kubebuilder:scaffold:manifestskustomizesamples
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: sless.kube5s.ru/v1alpha1
|
||||
kind: Function
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: function
|
||||
app.kubernetes.io/instance: function-sample
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
app.kubernetes.io/created-by: sless
|
||||
name: function-sample
|
||||
spec:
|
||||
# TODO(user): Add fields here
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: sless.kube5s.ru/v1alpha1
|
||||
kind: Trigger
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: trigger
|
||||
app.kubernetes.io/instance: trigger-sample
|
||||
app.kubernetes.io/part-of: sless
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
app.kubernetes.io/created-by: sless
|
||||
name: trigger-sample
|
||||
spec:
|
||||
# TODO(user): Add fields here
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: scorecard.operatorframework.io/v1alpha3
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: config
|
||||
stages:
|
||||
- parallel: true
|
||||
tests: []
|
||||
@@ -0,0 +1,16 @@
|
||||
resources:
|
||||
- bases/config.yaml
|
||||
patchesJson6902:
|
||||
- path: patches/basic.config.yaml
|
||||
target:
|
||||
group: scorecard.operatorframework.io
|
||||
version: v1alpha3
|
||||
kind: Configuration
|
||||
name: config
|
||||
- path: patches/olm.config.yaml
|
||||
target:
|
||||
group: scorecard.operatorframework.io
|
||||
version: v1alpha3
|
||||
kind: Configuration
|
||||
name: config
|
||||
#+kubebuilder:scaffold:patchesJson6902
|
||||
@@ -0,0 +1,10 @@
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- basic-check-spec
|
||||
image: quay.io/operator-framework/scorecard-test:v1.28.0
|
||||
labels:
|
||||
suite: basic
|
||||
test: basic-check-spec-test
|
||||
@@ -0,0 +1,50 @@
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-bundle-validation
|
||||
image: quay.io/operator-framework/scorecard-test:v1.28.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-bundle-validation-test
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-crds-have-validation
|
||||
image: quay.io/operator-framework/scorecard-test:v1.28.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-crds-have-validation-test
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-crds-have-resources
|
||||
image: quay.io/operator-framework/scorecard-test:v1.28.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-crds-have-resources-test
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-spec-descriptors
|
||||
image: quay.io/operator-framework/scorecard-test:v1.28.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-spec-descriptors-test
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-status-descriptors
|
||||
image: quay.io/operator-framework/scorecard-test:v1.28.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-status-descriptors-test
|
||||
@@ -0,0 +1,366 @@
|
||||
// Изменено: 2026-03-08
|
||||
// FunctionReconciler — основной контроллер оператора.
|
||||
// Следит за CRD Function и управляет lifecycle функции:
|
||||
// Pending → Building (запуск kaniko Job) → Ready (образ собран, Deployment создан) / Failed
|
||||
// Reconcile вызывается k8s при любом изменении Function объекта.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1"
|
||||
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder"
|
||||
)
|
||||
|
||||
// FunctionReconciler reconciles a Function object
|
||||
type FunctionReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
Builder *builder.Builder
|
||||
RegistrySecret string // имя Secret с docker credentials (для imagePullSecrets в подах функций)
|
||||
OperatorNamespace string // namespace оператора — откуда копируем RegistrySecret в sless-fn-*
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functions,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functions/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functions/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch;create
|
||||
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;create
|
||||
//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch
|
||||
|
||||
// Reconcile — главный цикл управления Function.
|
||||
// Логика: читаем текущее состояние → определяем что нужно сделать → делаем.
|
||||
func (r *FunctionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Читаем Function объект из k8s
|
||||
fn := &slessv1alpha1.Function{}
|
||||
if err := r.Get(ctx, req.NamespacedName, fn); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
// Объект удалён — ничего делать не надо, finalizer уже отработал
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("get function: %w", err)
|
||||
}
|
||||
|
||||
// Обрабатываем удаление через finalizer
|
||||
if !fn.DeletionTimestamp.IsZero() {
|
||||
return r.handleDeletion(ctx, fn)
|
||||
}
|
||||
|
||||
// Добавляем finalizer при первом создании чтобы обработать удаление
|
||||
if !containsString(fn.Finalizers, finalizerName) {
|
||||
fn.Finalizers = append(fn.Finalizers, finalizerName)
|
||||
if err := r.Update(ctx, fn); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("add finalizer: %w", err)
|
||||
}
|
||||
return ctrl.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
// Есть новый код (s3Key изменился по сравнению с последним запуском сборки)
|
||||
// и сборка сейчас не идёт — запускаем. Это единственное место где решается "нужна ли сборка".
|
||||
// Почему аннотация а не phase: phase может быть Failed/Ready от прошлого кода;
|
||||
// новый upload меняет Spec.S3Key → контроллер сам понимает что нужно пересобрать.
|
||||
builtKey := fn.Annotations["sless.kube5s.ru/last-built-s3key"]
|
||||
needsBuild := fn.Spec.S3Key != "" && builtKey != fn.Spec.S3Key
|
||||
|
||||
if needsBuild && fn.Status.Phase != slessv1alpha1.FunctionPhaseBuilding {
|
||||
logger.Info("starting build", "function", fn.Name)
|
||||
return r.startBuild(ctx, fn)
|
||||
}
|
||||
|
||||
switch fn.Status.Phase {
|
||||
case slessv1alpha1.FunctionPhaseBuilding:
|
||||
return r.checkBuild(ctx, fn)
|
||||
case slessv1alpha1.FunctionPhaseReady:
|
||||
return r.ensureDeployment(ctx, fn)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
const finalizerName = "sless.kube5s.ru/finalizer"
|
||||
|
||||
// startBuild запускает kaniko Job и помечает функцию как Building.
|
||||
// Критически важно: СНАЧАЛА сохраняем last-built-s3key аннотацию, ПОТОМ status.
|
||||
// Это предотвращает повторный запуск сборки при параллельных reconcile —
|
||||
// следующий reconcile увидит last-built-s3key == spec.S3Key и не войдёт в startBuild.
|
||||
func (r *FunctionReconciler) startBuild(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
||||
jobName, err := r.Builder.Build(ctx, fn.Namespace, fn.Name, fn.Spec.S3Key)
|
||||
if err != nil {
|
||||
return r.setFailed(ctx, fn, fmt.Sprintf("failed to start build: %v", err))
|
||||
}
|
||||
|
||||
// Обновляем аннотации ПЕРВЫМИ — это idempotency guard.
|
||||
// Как только last-built-s3key == spec.S3Key, дальнейшие reconcile не будут
|
||||
// вызывать startBuild снова, даже если status ещё не обновился.
|
||||
if fn.Annotations == nil {
|
||||
fn.Annotations = map[string]string{}
|
||||
}
|
||||
fn.Annotations["sless.kube5s.ru/build-job"] = jobName
|
||||
fn.Annotations["sless.kube5s.ru/last-built-s3key"] = fn.Spec.S3Key
|
||||
if err := r.Update(ctx, fn); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update build annotations: %w", err)
|
||||
}
|
||||
|
||||
// Обновляем статус после аннотаций
|
||||
fn.Status.Phase = slessv1alpha1.FunctionPhaseBuilding
|
||||
fn.Status.Message = "Building image: " + jobName
|
||||
if err := r.Status().Update(ctx, fn); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update status to building: %w", err)
|
||||
}
|
||||
|
||||
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
|
||||
}
|
||||
|
||||
// checkBuild проверяет статус build Job'а.
|
||||
func (r *FunctionReconciler) checkBuild(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
||||
jobName := fn.Annotations["sless.kube5s.ru/build-job"]
|
||||
if jobName == "" {
|
||||
// Аннотация потерялась — перезапускаем сборку
|
||||
fn.Status.Phase = slessv1alpha1.FunctionPhasePending
|
||||
_ = r.Status().Update(ctx, fn)
|
||||
return ctrl.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
status, err := r.Builder.JobStatus(ctx, jobName)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("check build job: %w", err)
|
||||
}
|
||||
|
||||
switch status {
|
||||
case "running":
|
||||
// Ещё идёт — проверим через 10 секунд
|
||||
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
|
||||
|
||||
case "succeeded":
|
||||
imageRef := r.Builder.ImageRef(fn.Namespace, fn.Name, fn.Spec.S3Key)
|
||||
fn.Status.Phase = slessv1alpha1.FunctionPhaseReady
|
||||
fn.Status.ImageRef = imageRef
|
||||
fn.Status.Message = ""
|
||||
now := metav1.Now()
|
||||
fn.Status.LastBuiltAt = &now
|
||||
if err := r.Status().Update(ctx, fn); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update status to ready: %w", err)
|
||||
}
|
||||
// Чистим завершённый Job
|
||||
_ = r.Builder.Cleanup(ctx, jobName)
|
||||
return ctrl.Result{Requeue: true}, nil
|
||||
|
||||
case "failed":
|
||||
return r.setFailed(ctx, fn, "build job failed")
|
||||
}
|
||||
|
||||
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
|
||||
}
|
||||
|
||||
// ensureDeployment создаёт или обновляет Deployment для HTTP функции.
|
||||
// Deployment запускается в отдельном namespace sless-fn-{namespace}.
|
||||
func (r *FunctionReconciler) ensureDeployment(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
||||
deployNS := "sless-fn-" + fn.Namespace
|
||||
// Создаём namespace для функций если не существует
|
||||
ns := &corev1.Namespace{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: deployNS}, ns); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
ns = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: deployNS}}
|
||||
if err := r.Create(ctx, ns); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("create function namespace: %w", err)
|
||||
}
|
||||
} else {
|
||||
return ctrl.Result{}, fmt.Errorf("get function namespace: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Обеспечиваем наличие registry pull-секрета в namespace функций.
|
||||
// Без него kubelet не сможет pull-нуть private образ из Harbor.
|
||||
if r.RegistrySecret != "" && r.OperatorNamespace != "" {
|
||||
if err := r.ensureRegistrySecret(ctx, deployNS); err != nil {
|
||||
// Не фатальная ошибка — логируем, но продолжаем
|
||||
log.FromContext(ctx).Error(err, "failed to ensure registry secret", "ns", deployNS)
|
||||
}
|
||||
}
|
||||
|
||||
desired := r.buildDeployment(fn, deployNS)
|
||||
existing := &appsv1.Deployment{}
|
||||
err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, existing)
|
||||
if errors.IsNotFound(err) {
|
||||
if err := r.Create(ctx, desired); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("create deployment: %w", err)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("get deployment: %w", err)
|
||||
}
|
||||
|
||||
// Обновляем образ, env и imagePullSecrets при пересборке или изменении конфига.
|
||||
// Тег образа уникален per build (sha256 от s3Key) → imagePullPolicy: IfNotPresent
|
||||
// корректно подтягивает новый образ без дополнительных хаков.
|
||||
// Env обновляем целиком — иначе изменение entrypoint/env_vars не применяется.
|
||||
existing.Spec.Template.Spec.Containers[0].Image = fn.Status.ImageRef
|
||||
existing.Spec.Template.Spec.Containers[0].Env = desired.Spec.Template.Spec.Containers[0].Env
|
||||
existing.Spec.Template.Spec.ImagePullSecrets = desired.Spec.Template.Spec.ImagePullSecrets
|
||||
if err := r.Update(ctx, existing); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update deployment: %w", err)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// buildDeployment формирует Deployment манифест для функции.
|
||||
func (r *FunctionReconciler) buildDeployment(fn *slessv1alpha1.Function, namespace string) *appsv1.Deployment {
|
||||
replicas := int32(1)
|
||||
envVars := []corev1.EnvVar{
|
||||
// SLESS_ENTRYPOINT сообщает server.py/server.js какой файл и функцию загружать.
|
||||
// Формат: "module-name.funcName" (например: handler-http.handle)
|
||||
{Name: "SLESS_ENTRYPOINT", Value: fn.Spec.Entrypoint},
|
||||
}
|
||||
for k, v := range fn.Spec.Env {
|
||||
envVars = append(envVars, corev1.EnvVar{Name: k, Value: v})
|
||||
}
|
||||
|
||||
return &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fn.Name,
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{"app": fn.Name, "managed-by": "sless"},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: &replicas,
|
||||
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": fn.Name}},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": fn.Name}},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: fn.Name,
|
||||
Image: fn.Status.ImageRef,
|
||||
Env: envVars,
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{
|
||||
corev1.ResourceMemory: resource.MustParse(fmt.Sprintf("%dMi", fn.Spec.MemoryMB)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ImagePullSecrets: func() []corev1.LocalObjectReference {
|
||||
if r.RegistrySecret != "" {
|
||||
return []corev1.LocalObjectReference{{Name: r.RegistrySecret}}
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ensureRegistrySecret копирует pull-секрет из namespace оператора в namespace функций.
|
||||
// Вызывается при каждом reconcile — если секрет уже есть, ничего не делает.
|
||||
func (r *FunctionReconciler) ensureRegistrySecret(ctx context.Context, targetNS string) error {
|
||||
// Проверяем что секрет уже есть в целевом namespace
|
||||
existing := &corev1.Secret{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: r.RegistrySecret, Namespace: targetNS}, existing); err == nil {
|
||||
return nil // уже есть
|
||||
} else if !errors.IsNotFound(err) {
|
||||
return fmt.Errorf("check secret: %w", err)
|
||||
}
|
||||
|
||||
// Копируем из namespace оператора
|
||||
src := &corev1.Secret{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: r.RegistrySecret, Namespace: r.OperatorNamespace}, src); err != nil {
|
||||
return fmt.Errorf("get source secret from %s: %w", r.OperatorNamespace, err)
|
||||
}
|
||||
|
||||
copy := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: r.RegistrySecret,
|
||||
Namespace: targetNS,
|
||||
},
|
||||
Type: src.Type,
|
||||
Data: src.Data,
|
||||
}
|
||||
if err := r.Create(ctx, copy); err != nil {
|
||||
if !errors.IsAlreadyExists(err) {
|
||||
return fmt.Errorf("create secret in %s: %w", targetNS, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDeletion обрабатывает удаление Function: удаляет Deployment, Service, Ingress и убирает finalizer.
|
||||
func (r *FunctionReconciler) handleDeletion(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
||||
deployNS := "sless-fn-" + fn.Namespace
|
||||
dep := &appsv1.Deployment{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, dep); err == nil {
|
||||
_ = r.Delete(ctx, dep)
|
||||
}
|
||||
|
||||
// Удаляем Service и Ingress — созданы HTTP триггером, но именованы по функции.
|
||||
// Если function_controller не удалит их, Ingress остаётся после destroy → 502.
|
||||
svc := &corev1.Service{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, svc); err == nil {
|
||||
_ = r.Delete(ctx, svc)
|
||||
}
|
||||
ing := &netv1.Ingress{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, ing); err == nil {
|
||||
_ = r.Delete(ctx, ing)
|
||||
}
|
||||
|
||||
fn.Finalizers = removeString(fn.Finalizers, finalizerName)
|
||||
if err := r.Update(ctx, fn); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("remove finalizer: %w", err)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// setFailed переводит функцию в фазу Failed с сообщением об ошибке.
|
||||
func (r *FunctionReconciler) setFailed(ctx context.Context, fn *slessv1alpha1.Function, msg string) (ctrl.Result, error) {
|
||||
fn.Status.Phase = slessv1alpha1.FunctionPhaseFailed
|
||||
fn.Status.Message = msg
|
||||
if err := r.Status().Update(ctx, fn); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update status to failed: %w", err)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func containsString(slice []string, s string) bool {
|
||||
for _, v := range slice {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func removeString(slice []string, s string) []string {
|
||||
var result []string
|
||||
for _, v := range slice {
|
||||
if v != s {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *FunctionReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&slessv1alpha1.Function{}).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
// Изменено: 2026-03-09 (feature B: захват stdout пода Job в status.Message)
|
||||
// FunctionJobReconciler — контроллер одноразовых запусков функций.
|
||||
// При создании FunctionJob:
|
||||
// 1. Ждёт пока Function станет Ready
|
||||
// 2. Создаёт k8s Job который запускает образ функции с CMD runner
|
||||
// 3. Следит за завершением Job → обновляет статус (Succeeded/Failed)
|
||||
//
|
||||
// Почему отдельный ресурс (не Trigger type=job):
|
||||
// Trigger описывает постоянный способ вызова (http endpoint, cron schedule).
|
||||
// FunctionJob — разовое событие с отдельным lifecycle и статусом результата.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1"
|
||||
)
|
||||
|
||||
// FunctionJobReconciler reconciles a FunctionJob object
|
||||
type FunctionJobReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
RegistrySecret string // имя k8s Secret с docker credentials (для imagePullSecrets)
|
||||
KubeClient kubernetes.Interface // typed client для чтения логов подов (logs API недоступен через controller-runtime client)
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functionjobs,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functionjobs/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functionjobs/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
// Reconcile — основной цикл контроллера.
|
||||
func (r *FunctionJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
fj := &slessv1alpha1.FunctionJob{}
|
||||
if err := r.Get(ctx, req.NamespacedName, fj); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("get functionjob: %w", err)
|
||||
}
|
||||
|
||||
// Уже завершён — ничего не делаем
|
||||
if fj.Status.Phase == slessv1alpha1.FunctionJobPhaseSucceeded ||
|
||||
fj.Status.Phase == slessv1alpha1.FunctionJobPhaseFailed {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// RunID=0 означает "не запускать".
|
||||
// Позволяет создать FunctionJob в кластере, но запустить его явно позже
|
||||
// (увеличив RunID > 0 через Terraform или kubectl patch).
|
||||
if fj.Spec.RunID == 0 {
|
||||
if fj.Status.Phase != "Skipped" {
|
||||
fj.Status.Phase = "Skipped"
|
||||
fj.Status.Message = "run_id=0: set run_id>0 to execute"
|
||||
_ = r.Status().Update(ctx, fj)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Проверяем что Function существует и готова
|
||||
fn := &slessv1alpha1.Function{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: fj.Spec.FunctionRef, Namespace: fj.Namespace}, fn); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
fj.Status.Phase = slessv1alpha1.FunctionJobPhasePending
|
||||
fj.Status.Message = "function not found: " + fj.Spec.FunctionRef
|
||||
_ = r.Status().Update(ctx, fj)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("get function: %w", err)
|
||||
}
|
||||
if fn.Status.Phase != slessv1alpha1.FunctionPhaseReady {
|
||||
fj.Status.Phase = slessv1alpha1.FunctionJobPhasePending
|
||||
fj.Status.Message = "waiting for function Ready (current: " + string(fn.Status.Phase) + ")"
|
||||
_ = r.Status().Update(ctx, fj)
|
||||
// RequeueAfter: опрашиваем каждые 15с пока Function не станет Ready.
|
||||
return ctrl.Result{RequeueAfter: 15 * time.Second}, nil
|
||||
}
|
||||
|
||||
deployNS := "sless-fn-" + fj.Namespace
|
||||
jobName := fmt.Sprintf("job-%s-%s", fj.Name, fj.CreationTimestamp.Format("20060102150405"))
|
||||
|
||||
// Если Job уже создан — проверяем его статус
|
||||
existingJob := &batchv1.Job{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: jobName, Namespace: deployNS}, existingJob); err == nil {
|
||||
return r.syncJobStatus(ctx, fj, existingJob)
|
||||
} else if !errors.IsNotFound(err) {
|
||||
return ctrl.Result{}, fmt.Errorf("get job: %w", err)
|
||||
}
|
||||
|
||||
// Создаём k8s Job
|
||||
// Используем образ функции напрямую, переопределяем CMD чтобы запустить runner
|
||||
// вместо server.py/server.js — runner выполняет handle(event) один раз и выходит
|
||||
eventJSON := fj.Spec.EventJSON
|
||||
if eventJSON == "" {
|
||||
eventJSON = "{}"
|
||||
}
|
||||
|
||||
// runner запускается через env var SLESS_EVENT — безопаснее чем передавать в args
|
||||
// (args видны в ps aux, env vars — нет)
|
||||
ttl := int32(600) // автоудаление Job через 10 мин после завершения
|
||||
|
||||
job := &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jobName,
|
||||
Namespace: deployNS,
|
||||
Labels: map[string]string{
|
||||
"managed-by": "sless",
|
||||
"functionjob": fj.Name,
|
||||
"function": fn.Name,
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
// Не перезапускать при ошибке — это одноразовый запуск
|
||||
BackoffLimit: int32Ptr(0),
|
||||
// Автоудаление через 10 мин после завершения — чтобы не засорять кластер
|
||||
TTLSecondsAfterFinished: &ttl,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
// Используем тот же образ что и Deployment функции
|
||||
// runner.py/runner.js переопределяет CMD сервера
|
||||
InitContainers: nil,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "runner",
|
||||
Image: fn.Status.ImageRef,
|
||||
// Переопределяем точку входа: запускаем runner вместо server
|
||||
// runner читает SLESS_EVENT и вызывает handle(event) один раз
|
||||
Command: runtimeRunnerCommand(fn.Spec.Runtime),
|
||||
Env: append(
|
||||
fnEnvVars(fn),
|
||||
corev1.EnvVar{
|
||||
Name: "SLESS_EVENT",
|
||||
Value: eventJSON,
|
||||
},
|
||||
),
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{
|
||||
corev1.ResourceMemory: resource.MustParse(fmt.Sprintf("%dMi", fn.Spec.MemoryMB)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||
{Name: r.RegistrySecret},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, job); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("create job: %w", err)
|
||||
}
|
||||
|
||||
now := metav1.Now()
|
||||
fj.Status.Phase = slessv1alpha1.FunctionJobPhaseRunning
|
||||
fj.Status.JobName = jobName
|
||||
fj.Status.StartTime = &now
|
||||
fj.Status.Message = ""
|
||||
if err := r.Status().Update(ctx, fj); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update functionjob status: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("created job for functionjob", "job", jobName, "functionjob", fj.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// syncJobStatus читает статус k8s Job и обновляет FunctionJob.Status.
|
||||
// Если Job ещё выполняется — запрашивает повторный reconcile через 5 сек
|
||||
// (Owns не работает кросс-неймспейсно, поэтому используем polling).
|
||||
func (r *FunctionJobReconciler) syncJobStatus(ctx context.Context, fj *slessv1alpha1.FunctionJob, job *batchv1.Job) (ctrl.Result, error) {
|
||||
if job.Status.Succeeded > 0 {
|
||||
now := metav1.Now()
|
||||
fj.Status.Phase = slessv1alpha1.FunctionJobPhaseSucceeded
|
||||
fj.Status.CompletionTime = &now
|
||||
// Захватываем stdout пода — это return value функции (runner делает print(json.dumps(result)))
|
||||
fj.Status.Message = getJobPodOutput(ctx, r.KubeClient, job.Namespace, job.Name)
|
||||
} else if job.Status.Failed > 0 {
|
||||
now := metav1.Now()
|
||||
fj.Status.Phase = slessv1alpha1.FunctionJobPhaseFailed
|
||||
fj.Status.CompletionTime = &now
|
||||
fj.Status.Message = "job failed, check pod logs: kubectl logs -n sless-fn-" + fj.Namespace + " -l functionjob=" + fj.Name
|
||||
} else {
|
||||
// Job ещё выполняется — перечитаем через 5 секунд
|
||||
if err := r.Status().Update(ctx, fj); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("sync job status: %w", err)
|
||||
}
|
||||
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||
}
|
||||
if err := r.Status().Update(ctx, fj); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("sync job status: %w", err)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// runtimeRunnerCommand возвращает CMD для запуска одноразового runner вместо HTTP-сервера.
|
||||
// runner читает env SLESS_EVENT и SLESS_ENTRYPOINT, вызывает handle(event) один раз и завершается.
|
||||
func runtimeRunnerCommand(runtime string) []string {
|
||||
switch runtime {
|
||||
case "nodejs20":
|
||||
// inline runner — не требует отдельного файла в образе.
|
||||
// SLESS_ENTRYPOINT="module.func": module=имя файла, func=экспортируемая функция
|
||||
return []string{"node", "-e", `
|
||||
const ep = process.env.SLESS_ENTRYPOINT || 'handler.handle';
|
||||
const dot = ep.lastIndexOf('.');
|
||||
const mod = ep.slice(0, dot >= 0 ? dot : ep.length);
|
||||
const fn = dot >= 0 ? ep.slice(dot + 1) : 'handle';
|
||||
const h = require('/app/function/' + mod);
|
||||
const event = JSON.parse(process.env.SLESS_EVENT || '{}');
|
||||
Promise.resolve(h[fn](event)).then(r => {
|
||||
console.log(JSON.stringify(r));
|
||||
process.exit(0);
|
||||
}).catch(e => {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
});`}
|
||||
default: // python3.11
|
||||
return []string{"python3", "-c", `
|
||||
import os, json, importlib.util
|
||||
ep = os.environ.get("SLESS_ENTRYPOINT", "handler.handle")
|
||||
dot = ep.rfind(".")
|
||||
mod_name = ep[:dot] if dot >= 0 else ep
|
||||
fn_name = ep[dot+1:] if dot >= 0 else "handle"
|
||||
spec = importlib.util.spec_from_file_location(mod_name, "/app/function/" + mod_name + ".py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
event = json.loads(os.environ.get("SLESS_EVENT", "{}"))
|
||||
result = getattr(mod, fn_name)(event)
|
||||
print(json.dumps(result))
|
||||
`}
|
||||
}
|
||||
}
|
||||
|
||||
// fnEnvVars преобразует env vars из FunctionSpec в k8s EnvVar slice.
|
||||
// Включает SLESS_ENTRYPOINT чтобы runner.py/runner.js знал какую функцию вызывать.
|
||||
func fnEnvVars(fn *slessv1alpha1.Function) []corev1.EnvVar {
|
||||
result := []corev1.EnvVar{
|
||||
{Name: "SLESS_ENTRYPOINT", Value: fn.Spec.Entrypoint},
|
||||
}
|
||||
for k, v := range fn.Spec.Env {
|
||||
result = append(result, corev1.EnvVar{Name: k, Value: v})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func int32Ptr(i int32) *int32 { return &i }
|
||||
|
||||
// getJobPodOutput находит под созданный Job-ом и возвращает его stdout (trimmed).
|
||||
// runner.py/runner.js печатают json.dumps(result) в stdout — это и есть return value функции.
|
||||
// Если под не найден или логи недоступны — возвращает "completed successfully" как fallback.
|
||||
func getJobPodOutput(ctx context.Context, kube kubernetes.Interface, namespace, jobName string) string {
|
||||
pods, err := kube.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
|
||||
LabelSelector: "job-name=" + jobName,
|
||||
})
|
||||
if err != nil || len(pods.Items) == 0 {
|
||||
return "completed successfully"
|
||||
}
|
||||
req := kube.CoreV1().Pods(namespace).GetLogs(pods.Items[0].Name, &corev1.PodLogOptions{})
|
||||
stream, err := req.Stream(ctx)
|
||||
if err != nil {
|
||||
return "completed successfully"
|
||||
}
|
||||
defer stream.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
_, _ = io.Copy(buf, stream)
|
||||
out := strings.TrimSpace(buf.String())
|
||||
if out == "" {
|
||||
return "completed successfully"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SetupWithManager регистрирует контроллер.
|
||||
// Owns(&batchv1.Job{}) намеренно убрано: Job создаётся в другом namespace
|
||||
// (sless-fn-*), где OwnerReference кросс-неймспейсно не работают.
|
||||
// Вместо этого используется RequeueAfter-polling в syncJobStatus.
|
||||
func (r *FunctionJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&slessv1alpha1.FunctionJob{}).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2026.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1"
|
||||
//+kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
|
||||
func TestAPIs(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecs(t, "Controller Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
|
||||
ErrorIfCRDPathMissing: true,
|
||||
}
|
||||
|
||||
var err error
|
||||
// cfg is defined in this file globally.
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cfg).NotTo(BeNil())
|
||||
|
||||
err = slessv1alpha1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
//+kubebuilder:scaffold:scheme
|
||||
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient).NotTo(BeNil())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
err := testEnv.Stop()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
@@ -0,0 +1,320 @@
|
||||
// Изменено: 2026-03-10
|
||||
// TriggerReconciler — контроллер триггеров.
|
||||
// HTTP триггер: создаёт Service + Ingress в namespace функции.
|
||||
// Cron триггер: создаёт k8s CronJob который периодически вызывает функцию по внутреннему URL.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1"
|
||||
)
|
||||
|
||||
// TriggerReconciler reconciles a Trigger object
|
||||
type TriggerReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
IngressHost string // базовый домен для http триггеров (fallback): fn.kube5s.ru
|
||||
// ExternalURL — если задан, URL функции = ExternalURL/fn/{namespace}/{name}
|
||||
// (обходит wildcard DNS, трафик идёт через sless-api.kube5s.ru)
|
||||
ExternalURL string
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=triggers/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
const triggerFinalizer = "sless.kube5s.ru/trigger-finalizer"
|
||||
|
||||
// Reconcile — управляет ресурсами для триггера в зависимости от его типа.
|
||||
func (r *TriggerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
tr := &slessv1alpha1.Trigger{}
|
||||
if err := r.Get(ctx, req.NamespacedName, tr); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("get trigger: %w", err)
|
||||
}
|
||||
|
||||
// Обработка удаления
|
||||
if !tr.DeletionTimestamp.IsZero() {
|
||||
return r.handleTriggerDeletion(ctx, tr)
|
||||
}
|
||||
|
||||
// Добавляем finalizer если ещё нет
|
||||
if !containsString(tr.Finalizers, triggerFinalizer) {
|
||||
tr.Finalizers = append(tr.Finalizers, triggerFinalizer)
|
||||
if err := r.Update(ctx, tr); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("add trigger finalizer: %w", err)
|
||||
}
|
||||
return ctrl.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
// Проверяем что Function существует и готова
|
||||
fn := &slessv1alpha1.Function{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: tr.Spec.FunctionRef, Namespace: tr.Namespace}, fn); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
tr.Status.Active = false
|
||||
tr.Status.Message = "function not found: " + tr.Spec.FunctionRef
|
||||
_ = r.Status().Update(ctx, tr)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("get function: %w", err)
|
||||
}
|
||||
if fn.Status.Phase != slessv1alpha1.FunctionPhaseReady {
|
||||
tr.Status.Active = false
|
||||
tr.Status.Message = "waiting for function to be Ready (current: " + string(fn.Status.Phase) + ")"
|
||||
_ = r.Status().Update(ctx, tr)
|
||||
// RequeueAfter: опрашиваем каждые 15с пока Function не станет Ready.
|
||||
// Watch на Function не добавлен, чтобы не усложнять контроллер на этом этапе.
|
||||
return ctrl.Result{RequeueAfter: 15 * time.Second}, nil
|
||||
}
|
||||
|
||||
// Управляем масштабом Deployment функции в зависимости от Enabled.
|
||||
// Deployment находится в namespace sless-fn-{userNS}, имя = имя функции.
|
||||
// Это позволяет "заморозить" функцию без удаления ресурса.
|
||||
deployNS := "sless-fn-" + tr.Namespace
|
||||
dep := &appsv1.Deployment{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, dep); err == nil {
|
||||
var wantReplicas int32
|
||||
if tr.Spec.Enabled {
|
||||
wantReplicas = 1
|
||||
} else {
|
||||
wantReplicas = 0
|
||||
}
|
||||
// Обновляем только если значение изменилось, чтобы не создавать лишних событий
|
||||
if dep.Spec.Replicas == nil || *dep.Spec.Replicas != wantReplicas {
|
||||
dep.Spec.Replicas = &wantReplicas
|
||||
if err := r.Update(ctx, dep); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("scale deployment replicas: %w", err)
|
||||
}
|
||||
logger.Info("scaled deployment", "function", fn.Name, "replicas", wantReplicas)
|
||||
}
|
||||
}
|
||||
|
||||
// Если триггер выключен — останавливаем reconcile здесь, ресурсы не создаём
|
||||
if !tr.Spec.Enabled {
|
||||
tr.Status.Active = false
|
||||
tr.Status.Message = "disabled (enabled=false)"
|
||||
_ = r.Status().Update(ctx, tr)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
switch tr.Spec.Type {
|
||||
case slessv1alpha1.TriggerTypeHTTP:
|
||||
logger.Info("reconcile http trigger", "trigger", tr.Name)
|
||||
return r.reconcileHTTP(ctx, tr, fn)
|
||||
case slessv1alpha1.TriggerTypeCron:
|
||||
logger.Info("reconcile cron trigger", "trigger", tr.Name)
|
||||
return r.reconcileCron(ctx, tr, fn)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// reconcileHTTP создаёт Service для HTTP триггера.
|
||||
// Если ExternalURL задан — URL = {ExternalURL}/fn/{namespace}/{name} (прокси через API).
|
||||
// Иначе (fallback) — URL = https://{funcName}-{namespace}.{IngressHost} + создаёт Ingress.
|
||||
func (r *TriggerReconciler) reconcileHTTP(ctx context.Context, tr *slessv1alpha1.Trigger, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
||||
deployNS := "sless-fn-" + tr.Namespace
|
||||
|
||||
// Service — направляет трафик к Deployment функции (нужен и для прокси, и для Ingress)
|
||||
wantSvc := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: fn.Name, Namespace: deployNS},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Selector: map[string]string{"app": fn.Name},
|
||||
Ports: []corev1.ServicePort{{Port: 8080, Protocol: corev1.ProtocolTCP}},
|
||||
},
|
||||
}
|
||||
existingSvc := &corev1.Service{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, existingSvc); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
if err := r.Create(ctx, wantSvc); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("create service: %w", err)
|
||||
}
|
||||
} else {
|
||||
return ctrl.Result{}, fmt.Errorf("get service: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var funcURL string
|
||||
if r.ExternalURL != "" {
|
||||
// Режим прокси через API сервер — не нужен wildcard DNS
|
||||
funcURL = fmt.Sprintf("%s/fn/%s/%s", r.ExternalURL, tr.Namespace, fn.Name)
|
||||
} else {
|
||||
// Fallback: Ingress с поддоменом (требует wildcard DNS *.IngressHost)
|
||||
host := fmt.Sprintf("%s-%s.%s", fn.Name, tr.Namespace, r.IngressHost)
|
||||
pathType := netv1.PathTypePrefix
|
||||
wantIng := &netv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fn.Name,
|
||||
Namespace: deployNS,
|
||||
Annotations: map[string]string{
|
||||
"kubernetes.io/ingress.class": "nginx",
|
||||
},
|
||||
},
|
||||
Spec: netv1.IngressSpec{
|
||||
Rules: []netv1.IngressRule{
|
||||
{
|
||||
Host: host,
|
||||
IngressRuleValue: netv1.IngressRuleValue{
|
||||
HTTP: &netv1.HTTPIngressRuleValue{
|
||||
Paths: []netv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/",
|
||||
PathType: &pathType,
|
||||
Backend: netv1.IngressBackend{
|
||||
Service: &netv1.IngressServiceBackend{
|
||||
Name: fn.Name,
|
||||
Port: netv1.ServiceBackendPort{Number: 8080},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
existingIng := &netv1.Ingress{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, existingIng); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
if err := r.Create(ctx, wantIng); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("create ingress: %w", err)
|
||||
}
|
||||
} else {
|
||||
return ctrl.Result{}, fmt.Errorf("get ingress: %w", err)
|
||||
}
|
||||
}
|
||||
funcURL = "https://" + host
|
||||
}
|
||||
|
||||
tr.Status.Active = true
|
||||
tr.Status.URL = funcURL
|
||||
tr.Status.Message = ""
|
||||
if err := r.Status().Update(ctx, tr); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update trigger status: %w", err)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// reconcileCron создаёт CronJob который вызывает функцию по HTTP внутри кластера.
|
||||
// curl делает POST на внутренний Service функции — это исключает внешний round-trip.
|
||||
func (r *TriggerReconciler) reconcileCron(ctx context.Context, tr *slessv1alpha1.Trigger, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
||||
deployNS := "sless-fn-" + tr.Namespace
|
||||
// Внутренний URL: Service должен быть создан HTTP триггером или заранее
|
||||
funcURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:8080", fn.Name, deployNS)
|
||||
|
||||
wantCJ := &batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: tr.Name,
|
||||
Namespace: tr.Namespace,
|
||||
Labels: map[string]string{"managed-by": "sless", "trigger": tr.Name},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: tr.Spec.Schedule,
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyOnFailure,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
// curlimages/curl вызывает функцию по внутреннему адресу
|
||||
Name: "invoker",
|
||||
Image: "curlimages/curl:latest",
|
||||
Command: []string{"curl", "-sf", "-X", "POST", funcURL},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
existing := &batchv1.CronJob{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: tr.Name, Namespace: tr.Namespace}, existing); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
if err := r.Create(ctx, wantCJ); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("create cronjob: %w", err)
|
||||
}
|
||||
} else {
|
||||
return ctrl.Result{}, fmt.Errorf("get cronjob: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Синхронизируем расписание если оно изменилось
|
||||
existing.Spec.Schedule = tr.Spec.Schedule
|
||||
if err := r.Update(ctx, existing); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update cronjob schedule: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
tr.Status.Active = true
|
||||
tr.Status.Message = ""
|
||||
if err := r.Status().Update(ctx, tr); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update trigger status: %w", err)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// handleTriggerDeletion удаляет ресурсы триггера и убирает finalizer.
|
||||
func (r *TriggerReconciler) handleTriggerDeletion(ctx context.Context, tr *slessv1alpha1.Trigger) (ctrl.Result, error) {
|
||||
if tr.Spec.Type == slessv1alpha1.TriggerTypeCron {
|
||||
cj := &batchv1.CronJob{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: tr.Name, Namespace: tr.Namespace}, cj); err == nil {
|
||||
_ = r.Delete(ctx, cj)
|
||||
}
|
||||
}
|
||||
|
||||
// Для HTTP триггеров удаляем Service и Ingress из sless-fn-{namespace}.
|
||||
// Имена ресурсов совпадают с именем функции (FunctionRef).
|
||||
// Без этого Ingress остаётся висеть после destroy и endpoint возвращает 502.
|
||||
if tr.Spec.Type == slessv1alpha1.TriggerTypeHTTP {
|
||||
deployNS := "sless-fn-" + tr.Namespace
|
||||
fnName := tr.Spec.FunctionRef
|
||||
|
||||
svc := &corev1.Service{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: fnName, Namespace: deployNS}, svc); err == nil {
|
||||
_ = r.Delete(ctx, svc)
|
||||
}
|
||||
|
||||
ing := &netv1.Ingress{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: fnName, Namespace: deployNS}, ing); err == nil {
|
||||
_ = r.Delete(ctx, ing)
|
||||
}
|
||||
}
|
||||
|
||||
tr.Finalizers = removeString(tr.Finalizers, triggerFinalizer)
|
||||
if err := r.Update(ctx, tr); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("remove trigger finalizer: %w", err)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager регистрирует контроллер.
|
||||
// Watch на Function не добавлен — вместо этого используется RequeueAfter 15s.
|
||||
// Это упрощает код на текущем этапе; при росте нагрузки заменить на Watches.
|
||||
func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&slessv1alpha1.Trigger{}).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
# Изменено: 2026-03-07
|
||||
# Деплой sless оператора в кластер.
|
||||
# Состав:
|
||||
# - ConfigMap: не-секретные env vars (S3_ENDPOINT, REGISTRY_HOST и т.д.)
|
||||
# - Secret: секретные данные (S3 keys, postgres DSN, API token, docker auth)
|
||||
# - Deployment: оператор naeel/sless-operator:v0.1.12 в namespace sless
|
||||
# - Service: ClusterIP :9090 (REST API)
|
||||
# - Ingress: sless-api.kube5s.ru → :9090 (внешний доступ с TLS)
|
||||
#
|
||||
# Перед применением:
|
||||
# kubectl create secret generic sless-operator-secret \
|
||||
# --namespace=sless \
|
||||
# --from-literal=POSTGRES_DSN="..." \
|
||||
# --from-literal=S3_ACCESS_KEY="..." \
|
||||
# --from-literal=S3_SECRET_KEY="..." \
|
||||
# --from-literal=SLESS_API_TOKEN="change-me" \
|
||||
# --dry-run=client -o yaml | kubectl apply -f -
|
||||
#
|
||||
# Применение: kubectl apply -f deployments/k8s/operator.yaml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: sless-operator-config
|
||||
namespace: sless
|
||||
data:
|
||||
S3_ENDPOINT: "s3.msk-1.ngcloud.ru"
|
||||
S3_BUCKET: "sless-functions"
|
||||
S3_USE_SSL: "true"
|
||||
REGISTRY_HOST: "naeel"
|
||||
REGISTRY_SECRET: "sless-registry-auth"
|
||||
API_PORT: "9090"
|
||||
INGRESS_HOST: "fn.kube5s.ru"
|
||||
# EXTERNAL_URL — если задан, URL функции = EXTERNAL_URL/fn/{namespace}/{name}
|
||||
# Позволяет обойтись без wildcard DNS *.fn.kube5s.ru
|
||||
EXTERNAL_URL: "https://sless-api.kube5s.ru"
|
||||
---
|
||||
# Secret создаётся отдельно через kubectl (не коммитить секреты в git!)
|
||||
# Описание ключей:
|
||||
# POSTGRES_DSN — строка подключения к postgres
|
||||
# S3_ACCESS_KEY — ключ доступа к S3/Ceph
|
||||
# S3_SECRET_KEY — секретный ключ S3/Ceph
|
||||
# SLESS_API_TOKEN — токен аутентификации API
|
||||
#
|
||||
# Пример создания:
|
||||
# kubectl create secret generic sless-operator-secret -n sless \
|
||||
# --from-literal=POSTGRES_DSN="postgres://sless:PASSWORD@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" \
|
||||
# --from-literal=S3_ACCESS_KEY="ACCESS_KEY" \
|
||||
# --from-literal=S3_SECRET_KEY="SECRET_KEY" \
|
||||
# --from-literal=SLESS_API_TOKEN="your-token-here"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: sless-operator
|
||||
namespace: sless
|
||||
labels:
|
||||
app: sless-operator
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: sless-operator
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: sless-operator
|
||||
spec:
|
||||
serviceAccountName: sless-operator
|
||||
containers:
|
||||
- name: operator
|
||||
# При обновлении версии оператора — менять тег здесь (не latest!)
|
||||
image: naeel/sless-operator:v0.1.12
|
||||
# Always — чтобы всегда тянуть по точному тегу (не кешировать старый)
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: api
|
||||
containerPort: 9090
|
||||
- name: metrics
|
||||
containerPort: 8080
|
||||
- name: health
|
||||
containerPort: 8081
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: sless-operator-config
|
||||
- secretRef:
|
||||
name: sless-operator-secret
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8081
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8081
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "500m"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: sless-operator
|
||||
namespace: sless
|
||||
spec:
|
||||
selector:
|
||||
app: sless-operator
|
||||
ports:
|
||||
- name: api
|
||||
port: 9090
|
||||
targetPort: 9090
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: sless-operator
|
||||
namespace: sless
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: sless-api.kube5s.ru
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: sless-operator
|
||||
port:
|
||||
number: 9090
|
||||
tls:
|
||||
- hosts:
|
||||
- sless-api.kube5s.ru
|
||||
secretName: sless-operator-tls
|
||||
@@ -0,0 +1,57 @@
|
||||
# Изменено: 2026-03-07
|
||||
# PostgreSQL для хранения логов вызовов функций.
|
||||
# Для dev/test — без PersistentVolume, данные ephemeral.
|
||||
# В prod заменить на managed PostgreSQL от провайдера.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-secret
|
||||
namespace: sless
|
||||
stringData:
|
||||
POSTGRES_PASSWORD: "sless-pg-password"
|
||||
POSTGRES_USER: "sless"
|
||||
POSTGRES_DB: "sless"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: sless
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: postgres-secret
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "500m"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: sless
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
@@ -0,0 +1,72 @@
|
||||
# Изменено: 2026-03-09 (добавлен доступ к pods/log для feature B)
|
||||
# RBAC для sless оператора.
|
||||
# ServiceAccount + ClusterRole + ClusterRoleBinding.
|
||||
# ClusterRole нужен (не namespaced Role) потому что оператор создаёт
|
||||
# ресурсы в нескольких namespace (sless + sless-fn-*)
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: sless-operator
|
||||
namespace: sless
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: sless-operator
|
||||
rules:
|
||||
# Наши CRD
|
||||
- apiGroups: ["sless.kube5s.ru"]
|
||||
resources: ["functions", "triggers", "functionjobs"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["sless.kube5s.ru"]
|
||||
resources: ["functions/status", "triggers/status", "functionjobs/status"]
|
||||
verbs: ["get", "update", "patch"]
|
||||
- apiGroups: ["sless.kube5s.ru"]
|
||||
resources: ["functions/finalizers", "triggers/finalizers", "functionjobs/finalizers"]
|
||||
verbs: ["update"]
|
||||
# Deployments для функций
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
# Jobs для kaniko builder
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs", "cronjobs"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
# Services + Ingress для HTTP триггеров
|
||||
- apiGroups: [""]
|
||||
resources: ["services", "namespaces"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
# Secrets — pull-секрет копируется в sless-fn-* namespace при создании функций
|
||||
# list/watch нужны для controller-runtime cache (информер на secrets)
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch"]
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
# Events для operator-sdk
|
||||
- apiGroups: [""]
|
||||
resources: ["events"]
|
||||
verbs: ["create", "patch"]
|
||||
# Pods + logs — чтение stdout завершённых Job-подов (feature B: status.Message = return value функции)
|
||||
- apiGroups: [""]
|
||||
resources: ["pods", "pods/log"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
# Lease для leader election
|
||||
- apiGroups: ["coordination.k8s.io"]
|
||||
resources: ["leases"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: sless-operator
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: sless-operator
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: sless-operator
|
||||
namespace: sless
|
||||
+39
-4
@@ -3,7 +3,19 @@
|
||||
## Базовый URL
|
||||
|
||||
```
|
||||
https://sless.api.ngcloud.ru/v1
|
||||
http://<operator-host>:9090/v1
|
||||
```
|
||||
|
||||
Локально: `http://localhost:9090/v1`
|
||||
В кластере (будущее): `http://sless-operator.sless.svc.cluster.local:9090/v1`
|
||||
|
||||
**Реализовано** (текущий namespace-aware формат):
|
||||
|
||||
```
|
||||
/v1/namespaces/{namespace}/functions[/{name}]
|
||||
/v1/namespaces/{namespace}/functions/{name}/upload
|
||||
/v1/namespaces/{namespace}/triggers[/{name}]
|
||||
/v1/namespaces/{namespace}/functions/{name}/invocations
|
||||
```
|
||||
|
||||
## Аутентификация
|
||||
@@ -49,11 +61,34 @@ Authorization: Bearer <cloud-token>
|
||||
| GET | /functions/{id}/invocations | История вызовов |
|
||||
| GET | /functions/{id}/invocations/{iid} | Детали вызова + логи |
|
||||
|
||||
## Upload endpoint
|
||||
|
||||
```
|
||||
POST /v1/namespaces/{namespace}/functions/{name}/upload
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer <token>
|
||||
|
||||
field: code = <zip-file>
|
||||
```
|
||||
|
||||
Процесс:
|
||||
1. Принимает zip (max 32MB)
|
||||
2. Распаковывает zip
|
||||
3. Генерирует `Dockerfile` (`FROM naeel/sless-runtime-{runtime}:latest\nCOPY . /app/function/`)
|
||||
4. Перепаковывает в `tar.gz` (kaniko требует tar format)
|
||||
5. Загружает в S3: `contexts/{ns}/{name}/{timestamp}.tar.gz`
|
||||
6. Обновляет `fn.Spec.S3Key` → контроллер видит изменение и запускает kaniko Job
|
||||
|
||||
Ответ `200 OK`:
|
||||
```json
|
||||
{"message": "build queued", "phase": "Pending", "s3_key": "contexts/..."}
|
||||
```
|
||||
|
||||
## Поддерживаемые runtime (v1)
|
||||
|
||||
- `go1.21`
|
||||
- `python3.11`
|
||||
- `nodejs20`
|
||||
- `python3.11` — реализован и протестирован
|
||||
- `go1.21` — планируется
|
||||
- `nodejs20` — планируется
|
||||
|
||||
## Модель Function
|
||||
|
||||
|
||||
@@ -0,0 +1,602 @@
|
||||
# Подробный handoff для следующего агента
|
||||
|
||||
Последнее обновление: 2026-03-10
|
||||
|
||||
## Назначение документа
|
||||
|
||||
Этот документ нужен не для презентации проекта, а для практической передачи контекста следующему агенту.
|
||||
Цель: после чтения файла должно быть понятно:
|
||||
|
||||
1. Что в проекте уже сделано и действительно работает.
|
||||
2. Где архитектура сильная.
|
||||
3. Где есть реальные дефекты, а где просто незавершённые v2-задачи.
|
||||
4. Что нужно делать следующим шагом и в каком порядке.
|
||||
5. Как проверять изменения с учётом ограничений среды (VPN, возможные TLS timeout).
|
||||
|
||||
Документ основан на анализе текущего кода, существующей документации и структуры проекта.
|
||||
Сетевые E2E-проверки намеренно не делались в рамках этого разбора, потому что среда может шуметь из-за VPN и TLS timeout.
|
||||
|
||||
---
|
||||
|
||||
## Краткий вывод
|
||||
|
||||
Проект находится в хорошем состоянии для рабочего MVP managed serverless platform, но ещё не находится в состоянии production-grade managed cloud service.
|
||||
|
||||
Что важно понимать сразу:
|
||||
|
||||
1. Основа выбрана правильно: Kubernetes operator + CRD + build pipeline через S3 + kaniko.
|
||||
2. Пользовательский путь уже частично доказан существующими Terraform examples и E2E заметками.
|
||||
3. Главные проблемы сейчас не в том, что код "вообще не работает", а в том, что системные свойства платформы пока неполные: event lifecycle, observability, auth/tenancy, строгая валидация контрактов, тестовое покрытие.
|
||||
|
||||
Итоговая инженерная оценка:
|
||||
|
||||
1. Как технический фундамент для v1: хорошо.
|
||||
2. Как demo/MVP для внутренней обкатки: хорошо.
|
||||
3. Как настоящий managed cloud service: пока рано, есть несколько архитектурно значимых дыр.
|
||||
|
||||
---
|
||||
|
||||
## Что уже реализовано и выглядит здраво
|
||||
|
||||
### 1. Правильный control plane фундамент
|
||||
|
||||
Текущая модель сервиса строится через CRD и operator-подход:
|
||||
|
||||
1. Function описывает функцию и её runtime/config.
|
||||
2. Trigger описывает способ вызова.
|
||||
3. FunctionJob описывает одноразовый запуск.
|
||||
|
||||
Ключевые файлы:
|
||||
|
||||
1. api/v1alpha1/function_types.go
|
||||
2. api/v1alpha1/trigger_types.go
|
||||
3. api/v1alpha1/job_types.go
|
||||
4. controllers/function_controller.go
|
||||
5. controllers/trigger_controller.go
|
||||
6. controllers/functionjob_controller.go
|
||||
|
||||
Это хорошее решение для системы, которая управляет lifecycle k8s-ресурсов, потому что:
|
||||
|
||||
1. Desired state вынесен в CRD.
|
||||
2. Reconcile loop естественно ложится на build/deploy/cleanup.
|
||||
3. Kubernetes остаётся источником правды по текущему фактическому состоянию ресурсов.
|
||||
|
||||
### 2. Build pipeline собран прагматично и без лишней магии
|
||||
|
||||
Фактически реализована понятная цепочка:
|
||||
|
||||
1. API принимает zip.
|
||||
2. Upload handler генерирует Dockerfile и tar.gz контекст.
|
||||
3. Контекст кладётся в S3.
|
||||
4. Function controller запускает kaniko Job.
|
||||
5. После успешной сборки создаётся/обновляется Deployment функции.
|
||||
|
||||
Ключевые файлы:
|
||||
|
||||
1. internal/api/handler/upload.go
|
||||
2. internal/builder/builder.go
|
||||
3. controllers/function_controller.go
|
||||
|
||||
Сильные стороны текущего решения:
|
||||
|
||||
1. Пользователь не думает про Dockerfile.
|
||||
2. Сборка отделена от API и не делается внутри процесса оператора.
|
||||
3. Используется idempotency guard через аннотацию last-built-s3key.
|
||||
4. Используются version-like image refs через hash от S3 key, а не один общий latest для результата сборки.
|
||||
|
||||
### 3. API и provider уже образуют реальный пользовательский путь
|
||||
|
||||
Есть не только внутренние CRD, но и внешний контракт:
|
||||
|
||||
1. REST API на gorilla/mux.
|
||||
2. Terraform provider в отдельном модуле.
|
||||
3. Набор examples, которые уже реально гонялись.
|
||||
|
||||
Ключевые файлы:
|
||||
|
||||
1. internal/api/router.go
|
||||
2. internal/api/handler/functions.go
|
||||
3. internal/api/handler/triggers.go
|
||||
4. internal/api/handler/jobs.go
|
||||
5. terraform/provider/...
|
||||
6. examples/...
|
||||
|
||||
Это важно: проект уже живёт не только как "внутренний оператор", а как зачаток полноценного managed продукта.
|
||||
|
||||
### 4. Видно, что проект уже проходил через реальные эксплуатационные проблемы
|
||||
|
||||
Это видно по документам:
|
||||
|
||||
1. doc/errors/log.md
|
||||
2. doc/decisions/log.md
|
||||
3. doc/progress.md
|
||||
|
||||
Плюс в том, что проект не застрял на happy path. Уже обнаружены и исправлялись:
|
||||
|
||||
1. бесконечные build loops,
|
||||
2. cleanup после destroy,
|
||||
3. cross-namespace проблемы с owner references,
|
||||
4. image pull / registry проблемы,
|
||||
5. несогласованность Terraform state.
|
||||
|
||||
Это хороший признак инженерной зрелости даже при сырой архитектуре.
|
||||
|
||||
---
|
||||
|
||||
## Текущая фактическая архитектура
|
||||
|
||||
### 1. Один бинарник совмещает operator manager и REST API
|
||||
|
||||
Файл: main.go
|
||||
|
||||
Что происходит:
|
||||
|
||||
1. Загружается env-конфиг.
|
||||
2. Поднимается PostgreSQL store.
|
||||
3. Выполняются миграции.
|
||||
4. Поднимается S3 client.
|
||||
5. Создаётся controller-runtime manager.
|
||||
6. Регистрируются Function, Trigger и FunctionJob reconcilers.
|
||||
7. Параллельно запускается HTTP API.
|
||||
|
||||
Это нормальное решение для раннего v1. Оно упрощает деплой и снижает количество moving parts.
|
||||
|
||||
Ограничение: по мере роста системы API и control plane логика будут мешать друг другу по масштабированию, отказам и ответственности. Но для текущей стадии это приемлемо.
|
||||
|
||||
### 2. Данные хранятся в двух разных источниках правды
|
||||
|
||||
Реально сейчас:
|
||||
|
||||
1. Состояние lifecycle функции и триггеров живёт в Kubernetes CRD/status.
|
||||
2. Invocation history задумана в PostgreSQL.
|
||||
3. Исходный код и build context живут в S3.
|
||||
4. Docker image живёт во внешнем registry.
|
||||
|
||||
Это нормальная модель, но только если границы между источниками правды строго определены.
|
||||
Сейчас эта модель задумана верно, но реализована не до конца, особенно вокруг invocation history.
|
||||
|
||||
### 3. HTTP вызов функции сейчас устроен через публичный прокси /fn/
|
||||
|
||||
Ключевые файлы:
|
||||
|
||||
1. internal/api/router.go
|
||||
2. internal/api/handler/invoke.go
|
||||
3. controllers/trigger_controller.go
|
||||
|
||||
Схема:
|
||||
|
||||
1. Trigger типа http формирует URL вида /fn/{namespace}/{name} через ExternalURL.
|
||||
2. API-сервер принимает запрос.
|
||||
3. InvokeFunction проксирует его во внутренний Service функции.
|
||||
|
||||
Для текущего окружения это разумный обход ограничения wildcard DNS.
|
||||
|
||||
Минус: это превращает API-процесс в data plane proxy для пользовательского трафика. Для MVP годится, для production это создаст узкое место и дополнительные требования к auth, rate limiting, tracing и HA.
|
||||
|
||||
---
|
||||
|
||||
## Подтверждённые проблемы и архитектурные долги
|
||||
|
||||
Ниже перечислены именно подтверждённые проблемы по текущему коду, а не абстрактные придирки.
|
||||
|
||||
### Критичность A — нужно чинить в ближайших итерациях
|
||||
|
||||
#### A1. TriggerReconciler рассчитывает на события Function, но реально не подписан на них
|
||||
|
||||
Файл: controllers/trigger_controller.go
|
||||
|
||||
Проблема:
|
||||
|
||||
1. В Reconcile есть ветка: если Function ещё не Ready, Trigger пишет статус waiting и выходит.
|
||||
2. Комментарий говорит, что повторный reconcile придёт, когда Function изменится.
|
||||
3. Но SetupWithManager регистрирует только For(&Trigger{}), без watch на Function.
|
||||
|
||||
Следствие:
|
||||
|
||||
1. Trigger, созданный раньше готовности Function, может застрять в промежуточном состоянии.
|
||||
2. Пересчёт будет зависеть не от правильного события, а от случайного следующего изменения Trigger.
|
||||
|
||||
Почему это важно:
|
||||
|
||||
Для operator-системы это уже логический дефект event model, а не просто TODO.
|
||||
|
||||
Что делать:
|
||||
|
||||
1. Добавить watch на Function.
|
||||
2. Обеспечить маппинг Function -> Trigger по namespace + FunctionRef.
|
||||
3. Покрыть тестом сценарий "Trigger создан до готовности Function".
|
||||
|
||||
#### A2. FunctionJobReconciler имеет ту же проблему ожидания готовности Function
|
||||
|
||||
Файл: controllers/functionjob_controller.go
|
||||
|
||||
Проблема:
|
||||
|
||||
1. Если Function не Ready, FunctionJob уходит в Pending и выходит.
|
||||
2. В комментарии подразумевается, что reconcile придёт позже.
|
||||
3. Но SetupWithManager также подписан только на FunctionJob.
|
||||
|
||||
Следствие:
|
||||
|
||||
1. Job может зависнуть в Pending без события-пробуждения.
|
||||
2. Фактическое выполнение зависит от внешнего изменения или ручного повторного reconcile.
|
||||
|
||||
Что делать:
|
||||
|
||||
1. Добавить watch на Function.
|
||||
2. Либо ввести RequeueAfter polling для ожидания Function Ready.
|
||||
3. Предпочтительнее watch, если можно аккуратно сматчить зависимости.
|
||||
|
||||
#### A3. Invocation history заявлена, но фактически не пишется
|
||||
|
||||
Файлы:
|
||||
|
||||
1. migrations/001_initial.sql
|
||||
2. internal/storage/postgres/store.go
|
||||
3. internal/api/handler/invocations.go
|
||||
4. internal/api/handler/invoke.go
|
||||
|
||||
Подтверждённый факт:
|
||||
|
||||
1. Есть таблица invocations.
|
||||
2. Есть SaveInvocation и ListInvocations.
|
||||
3. Есть API-эндпоинт чтения истории.
|
||||
4. В текущем коде нет места, где SaveInvocation реально вызывается.
|
||||
|
||||
Следствие:
|
||||
|
||||
1. API истории вызовов обещает поведение, которого фактически нет.
|
||||
2. Документация и UX вводят в заблуждение: кажется, что история есть, но она пустая не потому, что вызовов не было, а потому что они не сохраняются.
|
||||
|
||||
Что делать:
|
||||
|
||||
1. Встроить запись инвокации в HTTP proxy path и, отдельно, в FunctionJob execution path.
|
||||
2. Определить, что именно считается logs/result/status/duration.
|
||||
3. Синхронизировать это решение с API design и examples.
|
||||
|
||||
#### A4. Multi-tenant модель ещё не реализована, а API уже строится вокруг namespace из URL
|
||||
|
||||
Файлы:
|
||||
|
||||
1. internal/api/router.go
|
||||
2. internal/api/middleware/auth.go
|
||||
3. doc/progress.md
|
||||
4. doc/api/design.md
|
||||
|
||||
Проблема:
|
||||
|
||||
1. Namespace передаётся пользователем в URL.
|
||||
2. Auth пока основан на одном статическом токене.
|
||||
3. В документации прямо написано, что настоящая namespace isolation по токену ещё не сделана.
|
||||
|
||||
Следствие:
|
||||
|
||||
1. Контракт API пока demo-only.
|
||||
2. При переходе к реальному tenant-aware auth почти наверняка придётся менять или сильно прятать namespace от клиента.
|
||||
|
||||
Что делать:
|
||||
|
||||
1. Зафиксировать как архитектурное решение: namespace выводится из identity, а не приходит от клиента.
|
||||
2. Добавить whoami/identity resolution слой.
|
||||
3. Планировать миграцию API аккуратно, пока пользователей мало.
|
||||
|
||||
### Критичность B — не ломает demo, но делает платформу хрупкой
|
||||
|
||||
#### B1. Важная часть конфигурации декларативна только на бумаге, но не используется последовательно
|
||||
|
||||
Файлы:
|
||||
|
||||
1. internal/config/config.go
|
||||
2. main.go
|
||||
3. controllers/function_controller.go
|
||||
4. controllers/trigger_controller.go
|
||||
5. controllers/functionjob_controller.go
|
||||
|
||||
Проблема:
|
||||
|
||||
1. Есть FunctionNamespacePrefix в конфиге.
|
||||
2. Но контроллеры и wiring жёстко используют sless и sless-fn-.
|
||||
|
||||
Следствие:
|
||||
|
||||
1. Конфиг частично ложный: кажется, что поведение настраивается, но реально нет.
|
||||
2. Перенос и переиспользование сервиса усложняются.
|
||||
|
||||
Что делать:
|
||||
|
||||
1. Либо реально протащить конфиг до всех мест использования.
|
||||
2. Либо убрать мнимо-настраиваемое поле, если в v1 это сознательный hardcode.
|
||||
|
||||
#### B2. TimeoutSec описан в модели, но по сути не обеспечивается runtime-слоем
|
||||
|
||||
Файлы:
|
||||
|
||||
1. api/v1alpha1/function_types.go
|
||||
2. internal/api/handler/functions.go
|
||||
3. runtimes/python3.11/server.py
|
||||
4. runtimes/nodejs20/server.js
|
||||
|
||||
Проблема:
|
||||
|
||||
1. Поле timeout есть в API/CRD.
|
||||
2. Но runtime-обёртка не ограничивает время выполнения функции.
|
||||
3. HTTP proxy timeout клиента не равен execution timeout функции как платформенной гарантии.
|
||||
|
||||
Следствие:
|
||||
|
||||
1. Контракт платформы частично ложный.
|
||||
2. Длительный обработчик может вести себя непредсказуемо с точки зрения пользователя.
|
||||
|
||||
Что делать:
|
||||
|
||||
1. Либо честно задокументировать, что timeout пока informational.
|
||||
2. Либо реализовать enforcement через runtime/process/k8s job semantics.
|
||||
|
||||
#### B3. Публичный /fn/ endpoint не отделён от control plane auth модели
|
||||
|
||||
Файлы:
|
||||
|
||||
1. internal/api/router.go
|
||||
2. internal/api/middleware/auth.go
|
||||
3. internal/api/handler/invoke.go
|
||||
|
||||
Проблема:
|
||||
|
||||
1. /v1 защищён статическим token.
|
||||
2. /fn/ публичен.
|
||||
3. Никакой tenant-aware authz на invoke path сейчас нет.
|
||||
|
||||
Для MVP это допустимо, но следующий агент должен понимать:
|
||||
|
||||
1. Это не production-ready security model.
|
||||
2. Любая работа по auth должна учитывать отдельно control plane и data plane.
|
||||
|
||||
#### B4. Тестов ядра почти нет
|
||||
|
||||
Файлы:
|
||||
|
||||
1. controllers/suite_test.go
|
||||
2. остальная кодовая база tests почти отсутствуют
|
||||
|
||||
Фактическое состояние:
|
||||
|
||||
1. Есть bootstrap envtest.
|
||||
2. Нет содержательных тестов на reconcile lifecycle, cleanup, event propagation, API handler behaviour.
|
||||
|
||||
Следствие:
|
||||
|
||||
1. Реальная надёжность сейчас в основном держится на ручных E2E и Terraform examples.
|
||||
2. Регрессии в controller logic будут ловиться поздно.
|
||||
|
||||
### Критичность C — скорее незавершённость и техдолг, чем немедленная поломка
|
||||
|
||||
#### C1. Статусная модель богаче реальной логики
|
||||
|
||||
Файлы:
|
||||
|
||||
1. api/v1alpha1/function_types.go
|
||||
2. api/v1alpha1/trigger_types.go
|
||||
|
||||
Замечание:
|
||||
|
||||
1. Conditions объявлены, но не ведутся как системная модель статусов.
|
||||
2. LastScheduleTime есть, но не видно логики обновления.
|
||||
3. PreWarmSeconds есть, но отмечен как нереализованный.
|
||||
|
||||
Это не срочный баг, но следующий агент не должен тратить время, думая что вся эта модель уже рабочая.
|
||||
|
||||
#### C2. Cleanup build contexts и registry artefacts не выглядит завершённым
|
||||
|
||||
Замечание:
|
||||
|
||||
1. При удалении Function чистятся Deployment/Service/Ingress.
|
||||
2. Не видно системного cleanup для S3 contexts и образов registry.
|
||||
|
||||
Для разработки допустимо.
|
||||
Для managed среды это приведёт к накоплению мусора.
|
||||
|
||||
---
|
||||
|
||||
## Что не является проблемой само по себе
|
||||
|
||||
Следующий агент не должен тратить время на преждевременное "улучшательство".
|
||||
|
||||
### 1. Один бинарник для API и operator — нормально для текущей стадии
|
||||
|
||||
Не надо сейчас автоматически распиливать на микросервисы.
|
||||
Это не даст пропорциональной пользы на текущем этапе.
|
||||
|
||||
### 2. Выбор gorilla/mux не является узким местом проекта
|
||||
|
||||
Главные риски проекта не в роутере и не в HTTP фреймворке.
|
||||
|
||||
### 3. Простые runtime wrappers — нормальны для v1
|
||||
|
||||
Python/Node runtime обёртки в текущем виде упрощают систему.
|
||||
Их ограничения понятны, но они не являются главной ближайшей проблемой по сравнению с event model и invocation persistence.
|
||||
|
||||
---
|
||||
|
||||
## Приоритетный план работ для следующего агента
|
||||
|
||||
Ниже порядок работ, который имеет смысл соблюдать.
|
||||
|
||||
### Этап 1. Починить lifecycle зависимости Trigger и FunctionJob от Function
|
||||
|
||||
Цель:
|
||||
|
||||
1. Trigger и Job не должны зависать, если были созданы до готовности Function.
|
||||
|
||||
Задачи:
|
||||
|
||||
1. Добавить watch/mapping из Function в связанные Trigger.
|
||||
2. Добавить watch/mapping из Function в связанные FunctionJob или явный polling для ожидания Ready.
|
||||
3. Убедиться, что reconcile повторяется по правильным событиям, а не случайно.
|
||||
|
||||
Файлы-кандидаты:
|
||||
|
||||
1. controllers/trigger_controller.go
|
||||
2. controllers/functionjob_controller.go
|
||||
|
||||
Критерии готовности:
|
||||
|
||||
1. Trigger, созданный раньше Function Ready, автоматически становится Active после готовности функции.
|
||||
2. FunctionJob, созданный раньше Function Ready, сам продолжает lifecycle без ручного тычка.
|
||||
3. Поведение покрыто тестами или хотя бы воспроизводимым локальным сценарием.
|
||||
|
||||
### Этап 2. Реально включить запись invocation history
|
||||
|
||||
Цель:
|
||||
|
||||
1. Сделать так, чтобы API истории вызовов соответствовал реальному поведению платформы.
|
||||
|
||||
Задачи:
|
||||
|
||||
1. На HTTP invoke path записывать status, duration, http_status и response/error.
|
||||
2. На FunctionJob path записывать результат и статус отдельно как job-triggered invocation либо честно развести две сущности.
|
||||
3. Обновить doc/api/design.md и doc/progress.md по факту.
|
||||
|
||||
Файлы-кандидаты:
|
||||
|
||||
1. internal/api/handler/invoke.go
|
||||
2. internal/storage/postgres/store.go
|
||||
3. controllers/functionjob_controller.go
|
||||
4. internal/api/handler/invocations.go
|
||||
|
||||
Критерии готовности:
|
||||
|
||||
1. После успешного и неуспешного вызова в PostgreSQL появляется запись.
|
||||
2. GET invocations возвращает не пустой декоративный список, а реальные записи.
|
||||
|
||||
### Этап 3. Синхронизировать контракт timeout/status/validation
|
||||
|
||||
Цель:
|
||||
|
||||
1. Убрать ложные обещания платформы и несогласованность API.
|
||||
|
||||
Задачи:
|
||||
|
||||
1. Решить, что означает TimeoutSec прямо сейчас.
|
||||
2. Если не реализуется быстро, явно задокументировать ограничение.
|
||||
3. Выправить Create/Update в functions.go и triggers.go так, чтобы значения по умолчанию и validation были последовательными.
|
||||
|
||||
Файлы-кандидаты:
|
||||
|
||||
1. internal/api/handler/functions.go
|
||||
2. internal/api/handler/triggers.go
|
||||
3. api/v1alpha1/function_types.go
|
||||
4. doc/api/design.md
|
||||
|
||||
Критерии готовности:
|
||||
|
||||
1. API не заставляет клиента задавать поля, которые CRD должен дефолтить.
|
||||
2. Update не может случайно испортить spec нулевыми значениями.
|
||||
3. Документация не обещает больше, чем реально реализовано.
|
||||
|
||||
### Этап 4. Уменьшить ложную конфигурируемость
|
||||
|
||||
Цель:
|
||||
|
||||
1. Либо сделать конфиг реальным, либо убрать фиктивную настраиваемость.
|
||||
|
||||
Задачи:
|
||||
|
||||
1. Разобраться с FunctionNamespacePrefix и хардкодами sless/sless-fn-.
|
||||
2. Принять одно из двух решений:
|
||||
а) протащить конфиг через контроллеры и main;
|
||||
б) удалить поле до тех пор, пока оно реально не нужно.
|
||||
|
||||
Файлы-кандидаты:
|
||||
|
||||
1. internal/config/config.go
|
||||
2. main.go
|
||||
3. controllers/function_controller.go
|
||||
4. controllers/trigger_controller.go
|
||||
5. controllers/functionjob_controller.go
|
||||
|
||||
### Этап 5. Добавить минимально полезные автоматические тесты
|
||||
|
||||
Цель:
|
||||
|
||||
1. Снизить зависимость от ручных прогонов.
|
||||
|
||||
Минимальный полезный набор:
|
||||
|
||||
1. Trigger ждёт Function Ready и потом активируется.
|
||||
2. Function deletion чистит дочерние ресурсы.
|
||||
3. FunctionJob не зависает в Pending/Running навсегда.
|
||||
4. Invoke path корректно пишет invocation history.
|
||||
|
||||
Важно:
|
||||
|
||||
Не надо начинать с больших e2e по сети.
|
||||
Нужны локальные, детерминированные, дешёвые проверки.
|
||||
|
||||
---
|
||||
|
||||
## Как проверять изменения в этой среде
|
||||
|
||||
С учётом замечания пользователя про VPN и возможные TLS timeout, стратегия проверки должна быть осторожной.
|
||||
|
||||
### Что предпочитать
|
||||
|
||||
1. Локальные Go-тесты.
|
||||
2. envtest/контроллерные тесты без внешней сети.
|
||||
3. Анализ кода и существующих E2E результатов в examples/ и doc/progress.md.
|
||||
|
||||
### Что не делать первым шагом
|
||||
|
||||
1. Не начинать с внешних curl/HTTPS smoke test.
|
||||
2. Не считать TLS timeout автоматическим подтверждением бага в сервисе.
|
||||
3. Не делать выводы по сетевому шуму без локального подтверждения.
|
||||
|
||||
### Практический порядок проверки
|
||||
|
||||
1. Сначала локальные unit/envtest проверки.
|
||||
2. Потом, если нужно, ограниченные сценарии через provider/examples.
|
||||
3. Только в самом конце сетевые end-to-end через ingress.
|
||||
|
||||
---
|
||||
|
||||
## Быстрые ориентиры по файлам
|
||||
|
||||
Если следующему агенту нужно быстро войти в проект, читать в таком порядке:
|
||||
|
||||
1. main.go
|
||||
2. api/v1alpha1/function_types.go
|
||||
3. api/v1alpha1/trigger_types.go
|
||||
4. api/v1alpha1/job_types.go
|
||||
5. controllers/function_controller.go
|
||||
6. controllers/trigger_controller.go
|
||||
7. controllers/functionjob_controller.go
|
||||
8. internal/api/router.go
|
||||
9. internal/api/handler/upload.go
|
||||
10. internal/api/handler/invoke.go
|
||||
11. internal/storage/postgres/store.go
|
||||
12. doc/progress.md
|
||||
13. doc/errors/log.md
|
||||
14. doc/decisions/log.md
|
||||
|
||||
---
|
||||
|
||||
## Короткий practical summary для следующего агента
|
||||
|
||||
Если нужно запомнить только главное:
|
||||
|
||||
1. Не рефакторить всё подряд. Основа проекта нормальная.
|
||||
2. Первые реальные проблемы: event model Trigger/FunctionJob и отсутствие записи invocation history.
|
||||
3. Не путать v2-идеи с текущими дефектами: не всё незавершённое надо чинить прямо сейчас.
|
||||
4. Не полагаться в первую очередь на внешнюю сеть и TLS smoke test: среда может шуметь.
|
||||
5. Любые значимые изменения обязательно фиксировать в doc/progress.md и, при необходимости, в doc/errors/log.md или doc/decisions/log.md.
|
||||
|
||||
---
|
||||
|
||||
## Рекомендуемое следующее действие
|
||||
|
||||
Самый рациональный следующий шаг для нового агента:
|
||||
|
||||
1. Сфокусироваться на TriggerReconciler и FunctionJobReconciler.
|
||||
2. Починить повторное пробуждение от изменений Function.
|
||||
3. Добавить под это минимальные локальные тесты.
|
||||
|
||||
Это даст максимальный выигрыш в надёжности control plane без лишнего расширения функциональности.
|
||||
@@ -0,0 +1,613 @@
|
||||
# Claude Opus 4.6: Прагматичный технический обзор sless
|
||||
|
||||
Дата: 2026-03-10
|
||||
Автор: Claude Opus 4.6
|
||||
Контекст: Полный анализ кодовой базы sless + ревью документов GPT-5.4 и Claude Sonnet 4.6
|
||||
|
||||
---
|
||||
|
||||
## Преамбула: для кого этот документ
|
||||
|
||||
Этот документ написан для **небольшого облачного провайдера** (nubes.ru), а не для Amazon или Yandex Cloud.
|
||||
Это значит:
|
||||
|
||||
- Пользователей пока единицы, не тысячи.
|
||||
- Сервис — одна из многих фич облака, не ключевой revenue stream.
|
||||
- Команда маленькая, времени мало, каждый шаг должен давать реальный выход.
|
||||
- Перфекционизм — враг. Прагматизм — друг.
|
||||
|
||||
Все рекомендации ниже даны с учётом этого. Я не буду предлагать gVisor, LLM-validation, image scanning и прочие production-grade вещи на текущем этапе. Подробнее — в разделе "Где я не согласен с коллегами".
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Общая оценка: 7/10 для текущей стадии.**
|
||||
|
||||
Проект **реально работает**. Это главное. Есть:
|
||||
- Полный цикл: код → zip → S3 → kaniko → Docker image → Deployment → HTTP endpoint
|
||||
- Два рантайма (Python 3.11, Node.js 20), оба протестированы
|
||||
- Terraform provider с source_dir, WaitReady, WaitGone
|
||||
- Набор примеров которые работают (hello-node, simple-node, simple-python, notes-python)
|
||||
- Cron триггеры, FunctionJob для разовых запусков
|
||||
- Механизмы lifecycle control (enabled, run_id)
|
||||
|
||||
Проект прошёл через **реальные** проблемы (бесконечные build-циклы, cross-namespace OwnerRef, cleanup bugs, registry нестабильность) и **решил** их все. Это важнее чем идеальная архитектура.
|
||||
|
||||
**Но есть конкретные дефекты, которые реально сломают сервис при первых же пользователях.** Ниже — что именно и в каком порядке чинить.
|
||||
|
||||
---
|
||||
|
||||
## Часть 1: Подтверждённые дефекты — что реально ломается
|
||||
|
||||
### 1.1 Trigger и FunctionJob не реагируют на готовность Function
|
||||
|
||||
**Что происходит:**
|
||||
|
||||
```go
|
||||
// trigger_controller.go, строка ~313
|
||||
func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&slessv1alpha1.Trigger{}). // Только Trigger. Нет watch на Function.
|
||||
Complete(r)
|
||||
}
|
||||
```
|
||||
|
||||
Комментарий в коде (строка ~311) говорит: "настраивает watch на Function". **Это неправда** — watch на Function отсутствует.
|
||||
|
||||
**Когда стреляет:** Terraform создаёт Function и Trigger одновременно (или Trigger чуть раньше). Trigger проверяет Function, видит "не Ready", пишет "waiting" и **выходит без RequeueAfter**. Когда Function станет Ready — Trigger **не узнает об этом**. Trigger может зависнуть навсегда.
|
||||
|
||||
**Аналогичная проблема** в `functionjob_controller.go` — FunctionJob тоже не подписан на изменения Function.
|
||||
|
||||
**Реальный сценарий:** Пользователь делает `terraform apply` с Function + Trigger + Job. Kaniko собирает образ 1-2 минуты. Trigger создаётся за секунды. Trigger застревает в "waiting", пользователь думает что платформа сломалась.
|
||||
|
||||
**Почему это работает сейчас в demo:** Terraform provider делает WaitReady на Function перед созданием Trigger. Но прямое создание через kubectl или API — уязвимо.
|
||||
|
||||
**Как чинить:**
|
||||
|
||||
Вариант A (правильный): Добавить `Watches` на Function с маппингом на связанные Trigger/FunctionJob:
|
||||
```go
|
||||
func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&slessv1alpha1.Trigger{}).
|
||||
Watches(
|
||||
&slessv1alpha1.Function{},
|
||||
handler.EnqueueRequestsFromMapFunc(r.findTriggersForFunction),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
```
|
||||
|
||||
Вариант B (простой, но грубый): Добавить `RequeueAfter: 15 * time.Second` когда Function не Ready. Trigger будет переопрашивать каждые 15 секунд. Грубо, но работает и реализуется за 2 строки.
|
||||
|
||||
**Рекомендация для небольшого провайдера:** Вариант B сейчас, Вариант A — когда появляется хотя бы 10 активных пользователей. RequeueAfter для единиц пользователей вообще не создаёт нагрузки.
|
||||
|
||||
**Оценка трудозатрат:** Вариант B — 15 минут. Вариант A — 1-2 часа.
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Invocation history обещана, но не пишется
|
||||
|
||||
**Факт:** Есть таблица `invocations` в PostgreSQL. Есть метод `SaveInvocation()`. Есть API endpoint GET. Метод **нигде не вызывается**.
|
||||
|
||||
**Файлы:**
|
||||
- `migrations/001_initial.sql` — таблица есть
|
||||
- `internal/storage/postgres/store.go` — SaveInvocation реализован
|
||||
- `internal/api/handler/invocations.go` — ListInvocations вызывается
|
||||
- `internal/api/handler/invoke.go` — SaveInvocation **не вызывается**
|
||||
|
||||
**Когда стреляет:** Пользователь вызывает GET /invocations — всегда пустой список. Он думает что вызовов не было, а они были.
|
||||
|
||||
**Как чинить:**
|
||||
|
||||
В `invoke.go`, после получения ответа от функции, вызвать `SaveInvocation`:
|
||||
```go
|
||||
// После resp, err := httpClient.Do(proxyReq)
|
||||
go func() {
|
||||
_ = h.Store.SaveInvocation(context.Background(), &postgres.Invocation{
|
||||
FunctionName: name,
|
||||
Namespace: ns,
|
||||
HTTPStatus: resp.StatusCode,
|
||||
Duration: duration,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}()
|
||||
```
|
||||
|
||||
**Важно:** Запись в горутине чтобы не замедлять HTTP response. Если Postgres упал — вызов всё равно прошёл, просто не записался. Это нормально для логов, не для критических данных.
|
||||
|
||||
**Альтернатива:** Если invocation history пока никому не нужна — **честно убрать endpoint** из API. Пустой API хуже отсутствующего — он вводит в заблуждение.
|
||||
|
||||
**Рекомендация:** Для началa убрать endpoint или поставить заглушку с 501 Not Implemented. Когда реально понадобится — тогда реализовать нормально с учётом того что писать (статус, duration, размер ответа, ошибка).
|
||||
|
||||
**Оценка трудозатрат:** Заглушка — 5 минут. Реализация — 1-2 часа.
|
||||
|
||||
---
|
||||
|
||||
### 1.3 UpdateFunction молча затирает spec нулевыми значениями
|
||||
|
||||
**Что происходит:**
|
||||
|
||||
```go
|
||||
// functions.go, UpdateFunction
|
||||
fn.Spec.Runtime = req.Runtime // если req.Runtime == "" → затрёт ""
|
||||
fn.Spec.Entrypoint = req.Entrypoint // аналогично
|
||||
fn.Spec.MemoryMB = req.MemoryMB // если не передал → 0, но Create требует >0
|
||||
fn.Spec.TimeoutSec = req.TimeoutSec // если не передал → 0
|
||||
```
|
||||
|
||||
**Когда стреляет:** Пользователь хочет обновить только `env_vars`. Отправляет `{"env_vars": {"DB": "..."}}`. Все остальные поля в req = zero values. MemoryMB становится 0. Runtime становится "". Deployment ломается.
|
||||
|
||||
**Как чинить:**
|
||||
|
||||
Два варианта:
|
||||
1. **PATCH-семантика:** обновлять только ненулевые поля (JSON merge patch)
|
||||
2. **Полная замена:** требовать все поля как в Create (PUT-семантика с валидацией)
|
||||
|
||||
**Рекомендация:** PUT с валидацией — проще и надёжнее. Добавить ту же проверку что в Create:
|
||||
```go
|
||||
if req.Runtime == "" || req.Entrypoint == "" || req.MemoryMB <= 0 {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("runtime, entrypoint and memory_mb are required"))
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
**Оценка трудозатрат:** 15 минут.
|
||||
|
||||
---
|
||||
|
||||
### 1.4 CronJob использует `curlimages/curl:latest` с внешнего DockerHub
|
||||
|
||||
```go
|
||||
// trigger_controller.go, reconcileCron
|
||||
Image: "curlimages/curl:latest",
|
||||
```
|
||||
|
||||
**Проблемы:**
|
||||
1. `:latest` — непредсказуемый тег (вредности этого уже была уроком на этом же проекте)
|
||||
2. Внешняя зависимость — если DockerHub rate-limit или сеть мигнула, cron-триггер не работает
|
||||
3. Минорная: не pinned к дайджесту
|
||||
|
||||
**Как чинить:** Запинить версию: `curlimages/curl:8.5.0` (или какая стабильная). Или использовать `busybox:1.36` с `wget`.
|
||||
|
||||
**Оценка трудозатрат:** 2 минуты.
|
||||
|
||||
---
|
||||
|
||||
## Часть 2: Несогласованность контрактов — не ломает, но путает
|
||||
|
||||
### 2.1 FunctionNamespacePrefix объявлен, но не используется
|
||||
|
||||
**Файлы:**
|
||||
- `internal/config/config.go` — поле `FunctionNamespacePrefix` из env `FUNCTION_NAMESPACE_PREFIX`
|
||||
- `controllers/function_controller.go` — хардкод `"sless-fn-" + fn.Namespace`
|
||||
- `controllers/trigger_controller.go` — хардкод `"sless-fn-" + tr.Namespace`
|
||||
- `controllers/functionjob_controller.go` — хардкод `"sless-fn-" + fj.Namespace`
|
||||
- `internal/api/handler/invoke.go` — хардкод `sless-fn-%s`
|
||||
|
||||
Конфиг-поле создаёт иллюзию настраиваемости. Если кто-то изменит env — ничего не произойдёт.
|
||||
|
||||
**Рекомендация:** Удалить поле из конфига. Хардкод `sless-fn-` это нормально для v1. Если когда-нибудь понадобится менять — тогда и протащить. Сейчас мёртвый конфиг хуже хардкода.
|
||||
|
||||
**Оценка трудозатрат:** 10 минут (удалить поле + обновить комментарий).
|
||||
|
||||
---
|
||||
|
||||
### 2.2 TimeoutSec: есть в CRD и API, не enforcement нигде
|
||||
|
||||
**Файлы:**
|
||||
- `api/v1alpha1/function_types.go` — поле `TimeoutSec int32`
|
||||
- `internal/api/handler/functions.go` — принимается в API
|
||||
- `runtimes/python3.11/server.py` — **нет** ограничения по времени
|
||||
- `runtimes/nodejs20/server.js` — **нет** ограничения по времени
|
||||
- `internal/api/handler/invoke.go` — `httpClient.Timeout = 30 * time.Second` (статический, не из CRD)
|
||||
|
||||
**Рекомендация:** **Не реализовывать** timeout enforcement сейчас. Это потребует:
|
||||
- Передать TimeoutSec через env var в pod
|
||||
- Реализовать в каждом runtime (process timeout, signal handling)
|
||||
- Синхронизировать proxy timeout с pod timeout
|
||||
|
||||
Это значимая работа. Для единиц пользователей — overkill.
|
||||
|
||||
Вместо этого: **задокументировать** в API design что TimeoutSec сейчас informational. Когда появятся реальные long-running проблемы — тогда реализовать.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Conditions в CRD объявлены, но не ведутся
|
||||
|
||||
```go
|
||||
// function_types.go
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
```
|
||||
|
||||
Код обновляет `Phase` и `Message`, но Conditions не заполняет — это мёртвый код в status.
|
||||
|
||||
**Рекомендация:** Не трогать. Conditions — стандартная практика k8s, они пригодятся позже. Пока Phase+Message достаточно. Главное — не строить на них зависимости в контроллере пока не заполняете.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 PreWarmSeconds — объявлен, не реализован
|
||||
|
||||
```go
|
||||
// trigger_types.go
|
||||
PreWarmSeconds int32 `json:"preWarmSeconds,omitempty"`
|
||||
```
|
||||
|
||||
**Рекомендация:** Оставить как есть. Это заготовка для scale-to-zero. Пока нет scale-to-zero — поле бесполезно, но и не мешает.
|
||||
|
||||
---
|
||||
|
||||
## Часть 3: Что реально работает хорошо
|
||||
|
||||
Прежде чем обсуждать проблемы, важно зафиксировать что сделано правильно. Это не "похвала", а правки в оценку рисков: если фундамент плохой — надо переделывать. Фундамент **хороший**.
|
||||
|
||||
### 3.1 Идемпотентность сборки
|
||||
|
||||
```go
|
||||
builtKey := fn.Annotations["sless.kube5s.ru/last-built-s3key"]
|
||||
needsBuild := fn.Spec.S3Key != "" && builtKey != fn.Spec.S3Key
|
||||
```
|
||||
|
||||
Решение с аннотацией `last-built-s3key` как idempotency guard — **отлично**. Это решило реальную проблему (94 параллельных Job-а). Решение простое, понятное, работает. Не надо менять.
|
||||
|
||||
### 3.2 Версионированные теги образов
|
||||
|
||||
Переход от `:latest` к version-based тегам (через hash от S3 key) — правильное решение.
|
||||
Это решило проблему кэширования kaniko и `imagePullPolicy: IfNotPresent`.
|
||||
|
||||
### 3.3 MergePatch вместо Update в upload.go
|
||||
|
||||
```go
|
||||
patch := client.MergeFrom(fn.DeepCopy())
|
||||
fn.Spec.S3Key = s3Key
|
||||
h.K8s.Patch(ctx, fn, patch)
|
||||
```
|
||||
|
||||
Правильно. Прямой `Update` ломался из-за concurrent reconcile. MergePatch — стандартное решение.
|
||||
|
||||
### 3.4 source_dir в Terraform provider
|
||||
|
||||
Убрали зависимость от `hashicorp/archive` — это устранило VPN/registry конфликт. Provider сам создаёт zip, считает SHA256. Независимость от внешних провайдеров = меньше точек отказа.
|
||||
|
||||
### 3.5 Cleanup через finalizer
|
||||
|
||||
Function, Trigger — корректная схема cleanup через finalizer. handleDeletion чистит Deployment + Service + Ingress. handleTriggerDeletion чистит CronJob/Service/Ingress. Terraform provider ждёт WaitGone.
|
||||
|
||||
### 3.6 Решение с cross-namespace polling
|
||||
|
||||
Проблема: OwnerReference кросс-неймспейсно не работают → Owns(&Job{}) бесполезен.
|
||||
Решение: убрать Owns, использовать RequeueAfter polling.
|
||||
Это грубо, но работает и задокументировано в коде с пояснением почему.
|
||||
|
||||
### 3.7 Документация ошибок
|
||||
|
||||
`doc/errors/log.md` — **отличная** инженерная практика. Каждая проблема с причиной, контекстом, решением. Это ценнее тестов на данном этапе, потому что помогает не наступать на те же грабли.
|
||||
|
||||
### 3.8 Один бинарник — правильно для текущего масштаба
|
||||
|
||||
Один процесс = один Deployment, один лог, одна точка мониторинга. Для 1-50 функций в кластере — это идеальное решение. Не надо распиливать.
|
||||
|
||||
---
|
||||
|
||||
## Часть 4: Где я не согласен с коллегами
|
||||
|
||||
### 4.1 С GPT-5.4 (agent-handoff-2026-03-10.md)
|
||||
|
||||
GPT-5.4 провёл качественный технический аудит. Проблемы A1-A4 выявлены точно. Приоритизация разумная. **Но:**
|
||||
|
||||
**Избыточный фокус на config consistency (B1).** GPT-5.4 поставил "убрать FunctionNamespacePrefix" в отдельный этап работ (Этап 4) — это работа на 10 минут, не заслуживает целого этапа. Просто удалить поле заодно при следующем коммите.
|
||||
|
||||
**Недооценка проблемы UpdateFunction.** GPT-5.4 упоминает "Update не может случайно испортить spec" в этапе 3 (validation), но не выделяет как отдельную проблему. Между тем это реально может сломать функцию при первом же `terraform apply` с частичным обновлением.
|
||||
|
||||
**В целом:** GPT-5.4 дал отличную базу. Его 5 этапов — разумный план. Я согласен с приоритизацией примерно на 80%.
|
||||
|
||||
### 4.2 С Claude Sonnet 4.6 (sonnet-review-of-gpt-analysis.md)
|
||||
|
||||
Sonnet выявил что GPT-5.4 "смотрел как на внутренний инструмент". Это верное наблюдение. **Но затем Sonnet ушёл в другую крайность — начал проектировать production security для Amazon-масштаба.**
|
||||
|
||||
Конкретно:
|
||||
|
||||
**gVisor (RuntimeClass)** — для небольшого облачного провайдера с единицами пользователей это **overkill**. gVisor:
|
||||
- Требует установки на каждую ноду кластера
|
||||
- Даёт 10-20% performance overhead
|
||||
- Ломает некоторые syscalls (не все рантаймы работают)
|
||||
- Усложняет debugging
|
||||
- Решает проблему container escape, которая актуальна когда у вас **тысячи** непроверенных пользователей
|
||||
|
||||
**Когда нужен gVisor:** Когда сервис открыт для self-service публичной регистрации. Для invite-only или managed-клиентов небольшого провайдера — NetworkPolicy между namespace достаточно на годы.
|
||||
|
||||
**LLM-валидация кода** — интересная идея, но:
|
||||
- False positives будут ломать developer experience
|
||||
- Задержка сборки +5-15 секунд на каждый деплой
|
||||
- LLM не гарантирует detection rate для malware
|
||||
- Сложно тестировать и поддерживать
|
||||
- Bandit/npm audit покрывают 80% реальных проблем без LLM
|
||||
|
||||
**Когда нужен LLM:** Когда у вас free tier с тысячами анонимных пользователей. Не сейчас.
|
||||
|
||||
**Image scanning (Trivy/Grype)** — полезная вещь, но:
|
||||
- Сканирует base image, не пользовательский код
|
||||
- Можно запустить один раз при обновлении runtime, не при каждом build
|
||||
- Не требует интеграции в pipeline
|
||||
|
||||
**ResourceQuota per tenant** — вот это **полезно** и **просто**:
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: sless-quota
|
||||
namespace: sless-fn-{tenant}
|
||||
spec:
|
||||
hard:
|
||||
pods: "20"
|
||||
requests.memory: 4Gi
|
||||
```
|
||||
Это единственная security-рекомендация Sonnet которую стоит реализовать сейчас. Защищает от fork-бомб и runaway pods без сложностей.
|
||||
|
||||
**NetworkPolicy** — да, стоит добавить. Но простейшую — deny inter-namespace traffic. Не сложную multi-rule систему.
|
||||
|
||||
**Моя оценка Sonnet:** Правильно указал на пробел в security-мышлении. Но рекомендации масштабированы для Amazon, не для nubes.ru. Из его 8-пунктного плана для текущего этапа актуальны только 2: NetworkPolicy (простая) и ResourceQuota.
|
||||
|
||||
---
|
||||
|
||||
## Часть 5: Конкретный план работ — что делать и в каком порядке
|
||||
|
||||
Порядок отсортирован по **отдаче на вложенное время**. Не по "правильности" или "красоте".
|
||||
|
||||
### Этап 0: Быстрые фиксы (1-2 часа суммарно)
|
||||
|
||||
Эти вещи можно и нужно сделать прямо сейчас, одним коммитом:
|
||||
|
||||
| # | Задача | Время | Файлы |
|
||||
|---|--------|-------|-------|
|
||||
| 1 | RequeueAfter: 15s когда Function не Ready в TriggerReconciler | 2 мин | `controllers/trigger_controller.go` строка ~87 |
|
||||
| 2 | RequeueAfter: 15s когда Function не Ready в FunctionJobReconciler | 2 мин | `controllers/functionjob_controller.go` строка ~111 |
|
||||
| 3 | Валидация в UpdateFunction (runtime, entrypoint, memory_mb required) | 15 мин | `internal/api/handler/functions.go` |
|
||||
| 4 | Pinned version curl image: `curlimages/curl:8.5.0` | 2 мин | `controllers/trigger_controller.go` |
|
||||
| 5 | Убрать FunctionNamespacePrefix из config.go | 10 мин | `internal/config/config.go` |
|
||||
| 6 | Invocations endpoint → 501 Not Implemented ИЛИ убрать из API | 10 мин | `internal/api/handler/invocations.go`, `internal/api/router.go` |
|
||||
|
||||
**Почему этап 0:** Каждый из этих фиксов занимает минуты, но закрывает реальный дефект или убирает ложный контракт. Суммарно — 1 час максимум, а сервис становится значительно надёжнее.
|
||||
|
||||
### Этап 1: Реализация invocation history (если нужна) — 2-4 часа
|
||||
|
||||
**Только если** есть реальный use case для истории вызовов (биллинг, дебаг, мониторинг).
|
||||
|
||||
Если нет — **пропустить**. Пустой endpoint хуже отсутствующего, но 501 из этапа 0 честно говорит "не реализовано".
|
||||
|
||||
Если нужна:
|
||||
|
||||
1. Добавить SaveInvocation в invoke.go (асинхронно, в горутине)
|
||||
2. Добавить запись результата в FunctionJobReconciler при завершении Job
|
||||
3. Протестировать через curl + GET /invocations
|
||||
4. Обновить doc/api/design.md
|
||||
|
||||
**Файлы:**
|
||||
- `internal/api/handler/invoke.go`
|
||||
- `controllers/functionjob_controller.go`
|
||||
- `internal/storage/postgres/store.go`
|
||||
- `internal/api/handler/invocations.go`
|
||||
|
||||
### Этап 2: ResourceQuota + NetworkPolicy — 1-2 часа
|
||||
|
||||
Минимальная изоляция tenantов. Даже для единиц пользователей это разумная гигиена.
|
||||
|
||||
### Этап 2.5: LLM-валидация кода при upload — 3-4 часа
|
||||
|
||||
**Обновлено:** Перенесено из "когда-нибудь потом" в активный план.
|
||||
**Причина:** LLM уже в облаке nubes.ru — это dogfooding + маркетинг + реальная загрузка сервиса работой.
|
||||
|
||||
Детали дизайна: `doc/decisions/log.md` → "2026-03-10 — LLM-валидация кода при upload".
|
||||
|
||||
**Суть:** Новый пакет `internal/validator/`. Между распаковкой zip и отправкой в S3 — вызов LLM API.
|
||||
Safe=false → HTTP 400. LLM недоступен → soft-fail (warning, деплой проходит).
|
||||
Выключается через `LLM_ENABLED=false` (default).
|
||||
|
||||
**Файлы:**
|
||||
1. `internal/validator/validator.go` — интерфейс + NoopValidator
|
||||
2. `internal/validator/llm.go` — LLM HTTP client
|
||||
3. `internal/validator/extract.go` — извлечение текстовых файлов из zip
|
||||
4. `internal/config/config.go` — LLM_* env vars
|
||||
5. `internal/api/handler/upload.go` — точка вызова
|
||||
6. `main.go` — wire
|
||||
|
||||
**ResourceQuota:**
|
||||
- `controllers/function_controller.go` → при создании namespace `sless-fn-*` создавать ResourceQuota
|
||||
- Лимиты фиксированные для v1: pods=20, memory=4Gi, cpu=4
|
||||
|
||||
**NetworkPolicy:**
|
||||
- `controllers/function_controller.go` → при создании namespace `sless-fn-*` создавать deny-all NetworkPolicy
|
||||
- Разрешить: egress к DNS (kube-dns), egress к интернету (если нужно), ingress от оператора
|
||||
|
||||
**Файлы для создания:**
|
||||
- Шаблон в контроллере, не отдельный файл
|
||||
|
||||
### Этап 3: Watch на Function для Trigger и FunctionJob — 2-3 часа
|
||||
|
||||
Замена RequeueAfter polling на правильный event-driven watch.
|
||||
|
||||
**Почему не в этапе 0:** RequeueAfter 15s уже закроет проблему для единиц пользователей. Правильный watch нужен когда функций станет сотни и polling начнёт создавать лишнюю нагрузку.
|
||||
|
||||
**Реализация:**
|
||||
```go
|
||||
// trigger_controller.go
|
||||
func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&slessv1alpha1.Trigger{}).
|
||||
Watches(
|
||||
&slessv1alpha1.Function{},
|
||||
handler.EnqueueRequestsFromMapFunc(r.findTriggersForFunction),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *TriggerReconciler) findTriggersForFunction(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
fn := obj.(*slessv1alpha1.Function)
|
||||
var triggers slessv1alpha1.TriggerList
|
||||
_ = r.List(ctx, &triggers, client.InNamespace(fn.Namespace))
|
||||
var requests []reconcile.Request
|
||||
for _, tr := range triggers.Items {
|
||||
if tr.Spec.FunctionRef == fn.Name {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: client.ObjectKeyFromObject(&tr),
|
||||
})
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}
|
||||
```
|
||||
|
||||
Аналогично для FunctionJobReconciler.
|
||||
|
||||
### Этап 4: Минимальные тесты — 3-4 часа
|
||||
|
||||
**Не гнаться за coverage.** Покрыть только то что реально ломалось.
|
||||
|
||||
Тесты через envtest (уже есть bootstrap в `suite_test.go`):
|
||||
|
||||
1. **Trigger ожидает Function Ready:** Create Function (Pending) → Create Trigger → проверить что Trigger в "waiting" → перевести Function в Ready → проверить что Trigger стал Active
|
||||
2. **Cleanup при удалении:** Create Function → Create Deployment в sless-fn- → Delete Function → проверить что Deployment/Service/Ingress удалены
|
||||
3. **FunctionJob не запускается при RunID=0:** Create FunctionJob(RunID=0) → проверить phase=Skipped
|
||||
|
||||
**Не нужны тесты на:**
|
||||
- Авторизацию (один статический токен, тестировать нечего)
|
||||
- S3 upload (внешняя зависимость, лучше E2E)
|
||||
- Kaniko build (внешняя зависимость)
|
||||
|
||||
### Этап 5 (v2): Auth по токену облака — неопределённые сроки
|
||||
|
||||
Это зависит от инфраструктуры nubes.ru:
|
||||
- Нужен endpoint для валидации токена → namespace identity
|
||||
- API router вытаскивает namespace из identity, а не из URL
|
||||
- Обратная совместимость: текущие Terraform-конфиги должны работать
|
||||
|
||||
**Не делать пока не будет:**
|
||||
1. Auth-сервиса/endpoint nubes.ru для валидации токенов
|
||||
2. Хотя бы 3-5 реальных пользователей, которым нужна изоляция
|
||||
|
||||
### Что делать когда-нибудь потом (v2+)
|
||||
|
||||
Эти вещи **не нужны** для текущего масштаба. Помечаю чтобы не забыть, но не тратить время:
|
||||
|
||||
| Тема | Когда реально нужно | Почему не сейчас |
|
||||
|------|---------------------|------------------|
|
||||
| Scale-to-zero (KEDA) | Когда функций >50 и оплата за ресурсы болит | Меняет архитектуру routing, сложно |
|
||||
| gVisor/Kata | Когда открыт self-service signup | Overkill для invite-only |
|
||||
| LLM code validation | **Перенесено в активный план (Этап 2.5)** | Dogfooding облачного LLM |
|
||||
| Image scanning (Trivy) | Когда security-аудит требуется | Можно сканировать base images отдельно |
|
||||
| Go runtime | Когда есть запрос от пользователей | Python + Node покрывают 90% use cases |
|
||||
| Replicas в FunctionSpec | Когда пользователи жалуются на потребление | Пока 1 replica = 1 функция, просто |
|
||||
| Разделение API/Operator | Когда пользователей >100 или нужен HA API | Один бинарник проще |
|
||||
| RabbitMQ event triggers | Когда есть реальный event-driven use case | HTTP + Cron покрывают MVP |
|
||||
| S3/Registry cleanup | Когда 100+ сборок и диск заканчивается | Ручная чистка пока достаточна |
|
||||
| Versions API (rollback) | Когда пользователи просят откат | Текущая модель: re-upload = новая версия |
|
||||
|
||||
---
|
||||
|
||||
## Часть 6: Структурные наблюдения
|
||||
|
||||
### 6.1 Operator namespace hardcode
|
||||
|
||||
`main.go` хардкодит `"sless"` как OperatorNamespace:
|
||||
```go
|
||||
OperatorNamespace: "sless",
|
||||
```
|
||||
|
||||
Это нормально. Namespace оператора не должен быть динамическим — это deployment-level решение. Если переносим в другой namespace, меняем одну строку. Не проблема.
|
||||
|
||||
### 6.2 Ошибки проглатываются через `_ = r.Status().Update(...)`
|
||||
|
||||
Встречается в нескольких местах:
|
||||
```go
|
||||
_ = r.Status().Update(ctx, tr)
|
||||
```
|
||||
|
||||
Это осознанный trade-off: если status update упал, reconcile продолжится, и следующий Requeue перезапишет. Для status-only обновлений это OK. Главное не делать так для spec-изменений.
|
||||
|
||||
### 6.3 handleDeletion не чистит S3 artifacts и registry images
|
||||
|
||||
При удалении Function чистятся: Deployment, Service, Ingress.
|
||||
Не чистятся: S3 build context, Docker image в registry.
|
||||
|
||||
**Для текущего масштаба это не проблема.** При 100+ функциях — начнёт накапливаться мусор. Простое решение — периодический cron-скрипт для чистки orphaned объектов, не встроенная логика в контроллер.
|
||||
|
||||
### 6.4 migrations/001_initial.sql читается из файла на диске
|
||||
|
||||
```go
|
||||
migrationSQL, err := os.ReadFile("migrations/001_initial.sql")
|
||||
```
|
||||
|
||||
Это означает что бинарник зависит от наличия файла в текущей директории. Работает с `go run` и если Docker COPY включает migrations/. Не самый robust подход, но для одного файла — приемлемо.
|
||||
|
||||
**Когда станет проблемой:** Когда будет 5+ миграций. Тогда стоит встроить через `go:embed` или использовать migrate-библиотеку.
|
||||
|
||||
### 6.5 API doc/api/design.md частично устарел
|
||||
|
||||
- Versions API описан, но не реализован
|
||||
- Invocations API описан как рабочий, но не записывает
|
||||
- Runtime list: `go1.21` помечен как "планируется", но решение отложено
|
||||
|
||||
**Рекомендация:** Обновить design.md после этапа 0, зафиксировав что реально работает в v1.
|
||||
|
||||
---
|
||||
|
||||
## Часть 7: Итоговая оценка
|
||||
|
||||
### Что делает этот проект хорошим MVP
|
||||
|
||||
1. **Реально работает end-to-end.** terraform apply → функция доступна по HTTPS.
|
||||
2. **Прошёл через реальные проблемы** и решил их все (не застрял на happy path).
|
||||
3. **Документация ошибок** лучше чем в большинстве проектов.
|
||||
4. **Terraform provider** полностью функционален с source_dir, WaitReady, lifecycle control.
|
||||
5. **Архитектура не over-engineered** — один бинарник, простые контроллеры, minimal dependencies.
|
||||
|
||||
### Что нужно для промышленной эксплуатации (небольшой провайдер)
|
||||
|
||||
1. **Этап 0** — быстрые фиксы (RequeueAfter, validation, pin versions) — **обязательно перед любыми пользователями**
|
||||
2. **ResourceQuota + NetworkPolicy** — минимальная изоляция
|
||||
3. **Auth по облачному токену** — когда инфраструктура nubes.ru будет готова
|
||||
|
||||
### Чего НЕ нужно делать
|
||||
|
||||
- Не распиливать на микросервисы
|
||||
- Не внедрять gVisor/Kata
|
||||
- Не интегрировать LLM-валидацию кода
|
||||
- Не делать scale-to-zero
|
||||
- Не добавлять Go runtime (пока нет запроса)
|
||||
- Не строить versioning/rollback (пока нет запроса)
|
||||
- Не менять gorilla/mux на что-то "модное"
|
||||
- Не добавлять Conditions-логику в контроллеры
|
||||
|
||||
---
|
||||
|
||||
## Файловая карта: что где менять
|
||||
|
||||
### Этап 0 (быстрые фиксы)
|
||||
|
||||
| Файл | Изменение |
|
||||
|------|-----------|
|
||||
| `controllers/trigger_controller.go` ~87 | Добавить `return ctrl.Result{RequeueAfter: 15 * time.Second}, nil` вместо `return ctrl.Result{}, nil` |
|
||||
| `controllers/functionjob_controller.go` ~111 | Аналогично |
|
||||
| `controllers/trigger_controller.go` ~258 | Заменить `curlimages/curl:latest` → `curlimages/curl:8.5.0` |
|
||||
| `internal/api/handler/functions.go` ~UpdateFunction | Добавить валидацию required полей |
|
||||
| `internal/config/config.go` | Удалить FunctionNamespacePrefix |
|
||||
| `internal/api/handler/invocations.go` или `router.go` | Заглушка 501 или убрать endpoint |
|
||||
|
||||
### Этап 2 (security минимум)
|
||||
|
||||
| Файл | Изменение |
|
||||
|------|-----------|
|
||||
| `controllers/function_controller.go` ~ensureDeployment | После создания namespace — создать ResourceQuota и NetworkPolicy |
|
||||
|
||||
### Этап 3 (watches)
|
||||
|
||||
| Файл | Изменение |
|
||||
|------|-----------|
|
||||
| `controllers/trigger_controller.go` ~SetupWithManager | Добавить Watches на Function |
|
||||
| `controllers/functionjob_controller.go` ~SetupWithManager | Добавить Watches на Function |
|
||||
|
||||
---
|
||||
|
||||
## Резюме одной строкой
|
||||
|
||||
**Проект хороший, работает, фундамент правильный. Нужны 1 час быстрых фиксов + ResourceQuota/NetworkPolicy — и можно открывать для первых пользователей.**
|
||||
@@ -0,0 +1,313 @@
|
||||
# Claude Sonnet 4.6: Review of GPT-5.4 Analysis
|
||||
|
||||
Дата: 2026-03-10
|
||||
Автор: Claude Sonnet 4.6
|
||||
Контекст: Анализ документа `agent-handoff-2026-03-10.md`, подготовленного GPT-5.4
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
GPT-5.4 провёл детальный code review и выявил реальные архитектурные проблемы в текущей реализации sless. Анализ качественный, приоритизация разумная, но **фокус исключительно на lifecycle/data consistency**, при этом **полностью упущен security roadmap для production**.
|
||||
|
||||
**Мой вердикт:**
|
||||
- ✅ GPT-5.4 правильно определил проблемы event model (A1, A2)
|
||||
- ✅ Invocation history gap (A3) — точное попадание
|
||||
- ✅ Приоритизация задач логична для текущего MVP
|
||||
- ⚠️ **Критичный пробел:** безопасность multi-tenant serverless платформы вообще не упомянута
|
||||
- ⚠️ Не учтён контекст беседы с пользователем про gVisor, LLM validation, runtime isolation
|
||||
|
||||
---
|
||||
|
||||
## Где GPT-5.4 абсолютно прав
|
||||
|
||||
### 1. Event Model Problems (A1, A2) — критично
|
||||
|
||||
**Проблема:**
|
||||
```go
|
||||
// trigger_controller.go
|
||||
func (r *TriggerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&sllessv1alpha1.Trigger{}). // ❌ нет watch на Function
|
||||
Complete(r)
|
||||
}
|
||||
```
|
||||
|
||||
Trigger может зависнуть в `waiting` если Function не Ready на момент создания. Reconcile не произойдёт автоматически когда Function станет Ready.
|
||||
|
||||
**Почему это важно:** Классическая ошибка в operator patterns. Это не "незавершённая фича" а реальный race condition.
|
||||
|
||||
**Моё мнение:** Приоритет A1/A2 — верный. Это базовая корректность reconciliation loop.
|
||||
|
||||
### 2. Invocation History (A3) — не реализована
|
||||
|
||||
GPT-5.4 правильно выявил: таблица `invocations` есть, SaveInvocation() реализован, но **нигде не вызывается**.
|
||||
|
||||
```go
|
||||
// invoke.go — должно быть, но нет:
|
||||
// store.SaveInvocation(ctx, &storage.Invocation{...})
|
||||
```
|
||||
|
||||
**Моё мнение:** Согласен. Это не мелкая недоработка — это разрыв между API контрактом и реальностью.
|
||||
|
||||
### 3. Multi-tenancy через namespace в URL (A4) — проблема для production
|
||||
|
||||
```go
|
||||
// router.go
|
||||
r.HandleFunc("/v1/namespaces/{namespace}/functions", ...)
|
||||
```
|
||||
|
||||
Пользователь передаёт `namespace` в URL, но auth не проверяет принадлежность. Один токен = все namespace.
|
||||
|
||||
**Моё мнение:** Верно, но это known limitation для MVP. Важно не забыть при переходе к реальному tenant model.
|
||||
|
||||
### 4. Config inconsistency (B1) — мнимая конфигурируемость
|
||||
|
||||
```go
|
||||
// config.go
|
||||
FunctionNamespacePrefix string // есть поле
|
||||
|
||||
// function_controller.go
|
||||
targetNS := "sless-fn-" + fn.Namespace // 🔥 хардкод, игнорирует конфиг
|
||||
```
|
||||
|
||||
**Моё мнение:** Полностью согласен. Либо убрать поле либо использовать везде. Сейчас это вводит в заблуждение.
|
||||
|
||||
---
|
||||
|
||||
## Где GPT-5.4 поверхностен или не учёл контекст
|
||||
|
||||
### 1. Security для multi-tenant serverless — критичный пробел
|
||||
|
||||
GPT-5.4 **вообще не упомянул:**
|
||||
- Container escape protection (gVisor/Kata)
|
||||
- Network isolation между namespace (NetworkPolicy)
|
||||
- Code validation перед деплоем
|
||||
- Resource abuse prevention
|
||||
- Kernel-level isolation
|
||||
|
||||
**Проблема:** Для публичного managed serverless это не "nice to have" — это базовое требование безопасности.
|
||||
|
||||
**Контекст из беседы:**
|
||||
Пользователь спрашивал: "как быть? всё должно быть безопасно". Обсуждали:
|
||||
- gVisor — sandbox runtime для защиты от container escape
|
||||
- NetworkPolicy — изоляция трафика между namespace
|
||||
- LLM validation — проверка кода на malicious patterns
|
||||
- Capsule/vCluster — для усиленной изоляции
|
||||
|
||||
**Мой вывод:** GPT-5.4 дал план для "внутреннего инструмента", но не для "облачного продукта".
|
||||
|
||||
### 2. LLM integration для security — не упомянута
|
||||
|
||||
**Контекст из беседы:**
|
||||
- "LLM скоро запустят в облаке. и он должен себя оправдывать"
|
||||
- Идея: builder вызывает LLM для проверки кода перед сборкой
|
||||
- Защита от криптомайнеров, ботнетов, DDoS-агентов
|
||||
|
||||
GPT-5.4 вообще не включил это в roadmap, хотя пользователь явно видит это частью системы.
|
||||
|
||||
**Мой вывод:** LLM validation должна быть в плане после исправления lifecycle issues, но до production launch.
|
||||
|
||||
### 3. Go runtime — не упомянут
|
||||
|
||||
**Контекст из беседы:**
|
||||
- "пока не надо, но надо golang будет"
|
||||
- Обсуждали двухэтапный Dockerfile (builder → scratch)
|
||||
- Go даёт ~5-10MB бинарь vs ~100MB Python/Node
|
||||
|
||||
GPT-5.4 не включил Go runtime в roadmap, хотя это логичный next step после стабилизации существующих рантаймов.
|
||||
|
||||
### 4. Production deployment model — не рассмотрен
|
||||
|
||||
GPT-5.4 правильно отметил что один бинарь (API + operator) "нормален для v1", но не дал vision когда и как разделять.
|
||||
|
||||
**Что пропущено:**
|
||||
- API становится data plane proxy → bottleneck
|
||||
- HA для operator manager (leader election есть, но не обсуждается)
|
||||
- Separate control/data plane для масштабирования
|
||||
|
||||
**Мой вывод:** Для MVP текущая архитектура OK, но GPT-5.4 мог дать более чёткий trigger point для refactor.
|
||||
|
||||
---
|
||||
|
||||
## Где я частично не согласен
|
||||
|
||||
### 1. Timeout enforcement (B2) — не приоритет сейчас
|
||||
|
||||
GPT-5.4 включил TimeoutSec в "критичность B", но реально это less critical чем security.
|
||||
|
||||
**Моё мнение:**
|
||||
- Runtime timeout — nice to have для MVP
|
||||
- Можно задокументировать как "informational field" и вернуться позже
|
||||
- Важнее NetworkPolicy + gVisor чем timeout enforcement
|
||||
|
||||
### 2. Test coverage (B4) — правильно отмечено, но слишком общо
|
||||
|
||||
GPT-5.4: "Тестов ядра почти нет".
|
||||
|
||||
**Моё мнение:**
|
||||
- Да, это проблема
|
||||
- Но конкретные приоритеты тестов должны быть:
|
||||
1. Event model (Trigger/FunctionJob watch на Function)
|
||||
2. Cleanup finalizers
|
||||
3. Invocation history recording
|
||||
- Не надо гнаться за coverage %, надо покрыть critical paths
|
||||
|
||||
---
|
||||
|
||||
## Production Security Roadmap (пропущен GPT-5.4)
|
||||
|
||||
Это должно быть в handoff, но отсутствует.
|
||||
|
||||
### Immediate (до первых external users)
|
||||
|
||||
1. **gVisor RuntimeClass**
|
||||
```yaml
|
||||
spec:
|
||||
runtimeClassName: gvisor
|
||||
```
|
||||
- Защита от container escape
|
||||
- ~10-20% performance overhead
|
||||
- Установка на ноды + RuntimeClass в k8s
|
||||
|
||||
2. **NetworkPolicy per namespace**
|
||||
```yaml
|
||||
kind: NetworkPolicy
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes: ["Ingress", "Egress"]
|
||||
ingress: [] # Deny all by default
|
||||
```
|
||||
- Запретить трафик между `sless-fn-*` namespace
|
||||
- Разрешить только к external services + internal DNS
|
||||
|
||||
3. **ResourceQuota per tenant**
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "10"
|
||||
requests.memory: 20Gi
|
||||
pods: "50"
|
||||
```
|
||||
|
||||
### Near-term (параллельно с lifecycle fixes)
|
||||
|
||||
4. **Static code analysis в builder**
|
||||
- Python: `bandit` before Docker build
|
||||
- Node.js: `npm audit`
|
||||
- Блокировка деплоя при critical issues
|
||||
|
||||
5. **Image scanning**
|
||||
- Trivy/Grype после kaniko build
|
||||
- Reject образа если critical CVE
|
||||
|
||||
### Medium-term (перед production launch)
|
||||
|
||||
6. **LLM validation hook**
|
||||
- API call к облачному LLM перед сборкой
|
||||
- Детект криптомайнеров, port scanners, DDoS agents
|
||||
- Premium feature или throttled для free tier
|
||||
|
||||
7. **True tenant isolation**
|
||||
- Identity из JWT → namespace mapping
|
||||
- User не может передать namespace в URL
|
||||
- RBAC per tenant
|
||||
|
||||
---
|
||||
|
||||
## Рекомендуемый порядок работ (мой взгляд vs GPT-5.4)
|
||||
|
||||
### GPT-5.4 предложил:
|
||||
1. Event model (Trigger/FunctionJob watch)
|
||||
2. Invocation history
|
||||
3. Timeout/validation sync
|
||||
4. Config consistency
|
||||
5. Tests
|
||||
|
||||
### Я рекомендую:
|
||||
1. ✅ Event model — согласен, приоритет #1
|
||||
2. ✅ Invocation history — согласен, приоритет #2
|
||||
3. ⚠️ **Добавить: gVisor + NetworkPolicy** (до external users)
|
||||
4. ✅ Config consistency
|
||||
5. ✅ Tests (но фокус на critical paths)
|
||||
6. ⚠️ **Добавить: Static analysis в builder**
|
||||
7. Timeout enforcement (lower priority)
|
||||
8. ⚠️ **Добавить: LLM validation (перед production)**
|
||||
|
||||
---
|
||||
|
||||
## Практические дополнения для следующего агента
|
||||
|
||||
### Что делать с GPT-5.4 handoff
|
||||
|
||||
**Использовать как base**, но:
|
||||
1. После этапа 2 (invocation history) → добавить security hardening
|
||||
2. Перед production launch → полный security audit + LLM integration
|
||||
3. Go runtime → после стабилизации Python/Node (не urgent)
|
||||
|
||||
### Конкретные файлы для security work (пропущены GPT-5.4)
|
||||
|
||||
**gVisor:**
|
||||
- `controllers/function_controller.go` — добавить `RuntimeClassName: "gvisor"` в PodSpec
|
||||
- Установка на ноды (вне кода)
|
||||
|
||||
**NetworkPolicy:**
|
||||
- `deployments/k8s/network-policy.yaml` — создать template
|
||||
- `controllers/function_controller.go` — создавать NetworkPolicy per namespace
|
||||
|
||||
**Code validation:**
|
||||
- `internal/builder/builder.go` — добавить pre-build validation hook
|
||||
- `internal/validator/` (новый) — интеграция с bandit/npm audit
|
||||
|
||||
**LLM validation:**
|
||||
- `internal/validator/llm.go` — API client к облачному LLM
|
||||
- `internal/api/handler/upload.go` — вызов перед S3 upload
|
||||
|
||||
---
|
||||
|
||||
## Файлы которые следующий агент должен создать/изучить
|
||||
|
||||
**GPT-5.4 не упомянул, но критичны:**
|
||||
|
||||
1. `doc/security/multi-tenancy-model.md` — дизайн tenant isolation
|
||||
2. `doc/security/runtime-isolation.md` — gVisor/Kata decision + implementation
|
||||
3. `deployments/k8s/network-policy-template.yaml` — NetworkPolicy per tenant
|
||||
4. `deployments/k8s/resource-quota-template.yaml` — ResourceQuota per tenant
|
||||
5. `internal/validator/` — новый пакет для code validation
|
||||
6. `doc/decisions/gvisor-vs-kata.md` — почему gVisor for MVP
|
||||
|
||||
---
|
||||
|
||||
## Окончательный вердикт
|
||||
|
||||
**GPT-5.4 провёл отличный технический аудит кода**, но смотрел через призму "внутреннего инструмента", а не "managed cloud service".
|
||||
|
||||
**Сильные стороны анализа:**
|
||||
- ✅ Детальный разбор lifecycle problems
|
||||
- ✅ Конкретные файлы и строки кода
|
||||
- ✅ Приоритизация по критичности
|
||||
- ✅ Практичный план для MVP stabilization
|
||||
|
||||
**Пробелы:**
|
||||
- ❌ Security roadmap отсутствует
|
||||
- ❌ Не учтён контекст беседы (gVisor, LLM, Go runtime)
|
||||
- ❌ Production deployment model не рассмотрен
|
||||
- ❌ Tenant isolation strategy не обсуждается
|
||||
|
||||
**Рекомендация:**
|
||||
Использовать GPT-5.4 handoff как **техническую базу**, дополнить моими замечаниями по security, затем скомбинировать в единый production roadmap.
|
||||
|
||||
**Для следующего агента:**
|
||||
1. Читать GPT-5.4 документ для lifecycle issues
|
||||
2. Читать этот документ для security roadmap
|
||||
3. Начинать с lifecycle (приоритет GPT-5.4)
|
||||
4. После стабилизации — security hardening (мой приоритет)
|
||||
5. Перед production launch — полный аудит (желательно Claude Opus 4.6)
|
||||
|
||||
---
|
||||
|
||||
## Действия для этой сессии
|
||||
|
||||
Следующий шаг: обновить `doc/progress.md`, зафиксировать оба анализа (GPT-5.4 + мой), чтобы следующий агент видел полную картину.
|
||||
@@ -14,4 +14,436 @@
|
||||
**Решение:** ...
|
||||
```
|
||||
|
||||
## 2026-03-07 — kaniko: unsupported protocol scheme ""
|
||||
|
||||
**Проблема:** kaniko Job падает с `unsupported protocol scheme ""` при доступе к S3.
|
||||
|
||||
**Причина:** В env `S3_ENDPOINT` передавался хост без схемы (`s3.msk-1.ngcloud.ru`), kaniko ожидает полный URL.
|
||||
|
||||
**Решение:** В `builder.go` добавить `"https://" + b.s3Endpoint` при формировании env для kaniko. Также добавить `S3_FORCE_PATH_STYLE=true` — Ceph требует path-style URLs.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — Бесконечный цикл сборки (94 job'а)
|
||||
|
||||
**Проблема:** После upload оператор создавал сотни kaniko Job'ов.
|
||||
|
||||
**Причина:** Upload handler вызывал `Status().Update(phase=Pending)` ПОСЛЕ того как контроллер уже выставил `Building`. Контроллер видел `Pending` → создавал новый Job, upload снова сбрасывал → цикл.
|
||||
|
||||
**Решение:**
|
||||
1. `startBuild()` СНАЧАЛА ставит аннотацию `last-built-s3key = spec.S3Key` (idempotency guard), затем обновляет статус.
|
||||
2. Reconcile запускает сборку только если `spec.S3Key != annotations["last-built-s3key"]`.
|
||||
3. Upload handler убран `Status().Update()` — контроллер сам управляет фазой.
|
||||
4. `IsAlreadyExists` при создании Job не считается ошибкой (parallel reconcile).
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — upload.go: "object has been modified; please apply your changes"
|
||||
|
||||
**Проблема:** `terraform apply` падал при попытке upload кода функции:
|
||||
```
|
||||
status 500: {"error":"update function: Operation cannot be fulfilled on functions.sless.kube5s.ru "pg-query": the object has been modified; please apply your changes to the latest version and try again"}
|
||||
```
|
||||
|
||||
**Причина:** В `upload.go` использовался `h.K8s.Update(ctx, fn)`. Между `Get()` и `Update()` контроллер успевал изменить объект (выставить статус/аннотации) — `resourceVersion` в памяти устаревал, API-сервер отклонял обновление.
|
||||
|
||||
**Решение:** Заменить `Update` на `Patch` (strategic merge patch):
|
||||
```go
|
||||
patch := client.MergeFrom(fn.DeepCopy())
|
||||
fn.Spec.S3Key = s3Key
|
||||
fn.Spec.S3Bucket = h.S3.Bucket()
|
||||
h.K8s.Patch(ctx, fn, patch)
|
||||
```
|
||||
`MergePatch` передаёт только изменённые поля и не требует точного `resourceVersion`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — Terraform Provider: "provider produced inconsistent result" для schedule
|
||||
|
||||
**Проблема:** При создании `sless_trigger` с `type=http` (без поля `schedule`) terraform падал:
|
||||
```
|
||||
.schedule: was null, but now cty.StringVal("")
|
||||
```
|
||||
|
||||
**Причина:** В `trToModel()` провайдера `Schedule` всегда возвращался как `types.StringValue(tr.Schedule)`. Если schedule не задан, API возвращает пустую строку `""`, провайдер сохранял `""` в state. Terraform видел расхождение: в плане было `null` (поле не задано в HCL), в state после apply стало `""`.
|
||||
|
||||
**Решение:** В `trToModel()` возвращать `types.StringNull()` когда schedule пустая строка:
|
||||
```go
|
||||
schedule := types.StringNull()
|
||||
if tr.Schedule != "" {
|
||||
schedule = types.StringValue(tr.Schedule)
|
||||
}
|
||||
```
|
||||
Правило: для `Optional` computed-полей пустая строка из API → `null` в state.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-09 — source_dir: hashicorp/archive недоступен без VPN
|
||||
|
||||
**Проблема:** С VPN недоступен `kube5s.ru` (registry провайдера), без VPN — `registry.terraform.io` (hashicorp/archive). Примеры не работали ни с VPN ни без.
|
||||
|
||||
**Причина:** Зависимость от внешнего провайдера `hashicorp/archive` для создания zip.
|
||||
|
||||
**Решение:** Добавить атрибут `source_dir` в `sless_function`. Провайдер сам упаковывает директорию в zip (`archive/zip` in-memory), считает SHA256 (`crypto/sha256`). Файлы сортируются для детерминированного хэша. `hashicorp/archive` убран из всех примеров.
|
||||
|
||||
**Provider:** v0.1.10.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-09 — Destroy route cleanup bug: endpoint 502 после terraform destroy
|
||||
|
||||
**Проблема:** После `terraform destroy` публичный HTTP-эндпоинт продолжал отвечать — сначала 200, затем 502 `function unreachable`. Тестовый скрипт ждал 120 секунд и падал.
|
||||
|
||||
**Причина:** Три независимые проблемы в связке:
|
||||
1. `trigger_controller.go` `handleTriggerDeletion` намеренно не удалял Service и Ingress (комментарий «оставляем — могут быть нужны другим триггерам»). Неверно: имена Service/Ingress совпадают с именем функции, а не триггера.
|
||||
2. `function_controller.go` `handleDeletion` удалял только Deployment, не трогал Service и Ingress.
|
||||
3. `invoke.go` при DNS NXDOMAIN (`no such host`) возвращал 502, а тестовый скрипт ждёт 404. Итог: Ingress висел вечно → 502 висел 120с.
|
||||
|
||||
**Решение:**
|
||||
1. `handleTriggerDeletion`: для HTTP-триггеров явно удалять `Service` и `Ingress` с именем `fn.Spec.FunctionRef` в `sless-fn-{ns}`.
|
||||
2. `handleDeletion`: добавить удаление `Service` и `Ingress` с именем `fn.Name` в `sless-fn-{ns}` (импорт `netv1`).
|
||||
3. `trigger_resource.go` Delete: polling `GetTrigger` каждые 3с до 90с — провайдер не возвращает успех раньше завершения cleanup.
|
||||
4. `invoke.go`: `no such host` → HTTP 404 вместо 502 — Service удалён, эндпоинт мёртв.
|
||||
|
||||
**Operator:** v0.1.17 (контроллеры), v0.1.18 (invoke.go). **Provider:** v0.1.11.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — Terraform Provider: "Resource Import Not Implemented"
|
||||
|
||||
**Проблема:** После failed `terraform apply` (Function создалась, Trigger упал) state не содержал созданных ресурсов. Повторный apply падал с `status 409: function already exists`. `terraform import` тоже не работал:
|
||||
```
|
||||
This resource does not support import. Please contact the provider developer
|
||||
```
|
||||
|
||||
**Причина:** В провайдере не реализован `ImportState` для ресурса `sless_function`.
|
||||
|
||||
**Решение (краткосрочное):** Удалить ресурсы из кластера вручную (`kubectl delete`) и повторить apply с чистым state.
|
||||
|
||||
**Решение (долгосрочное / TODO):** Реализовать `ImportState` для `sless_function` и `sless_trigger`:
|
||||
```go
|
||||
func (r *FunctionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
// ID формат: "namespace/name"
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — handler.py: column "started_at" does not exist
|
||||
|
||||
**Проблема:** При вызове функции pg-query в логах пода:
|
||||
```
|
||||
psycopg2.errors.UndefinedColumn: column "started_at" does not exist
|
||||
```
|
||||
|
||||
**Причина:** В `examples/pg-query/handler.py` использовалось имя колонки `started_at`, но в схеме `migrations/001_initial.sql` таблицы `invocations` колонка называется `created_at`.
|
||||
|
||||
**Решение:** Исправить `select` и `dict()` в handler.py: `started_at` → `created_at`.
|
||||
|
||||
**Урок:** При написании примеров всегда сверяться со схемой в `migrations/`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — Deployment не перезапустил pod после re-build образа
|
||||
|
||||
**Проблема:** После `terraform apply` (re-upload + kaniko пересборка) pod продолжал использовать старый код — `curl` возвращал ошибку `started_at`.
|
||||
|
||||
**Причина:** Образ тегируется как `:latest`. Kubernetes не перезапускает pod автоматически если тег не изменился — даже если образ на DockerHub обновился. `imagePullPolicy` по умолчанию `IfNotPresent` для non-digest образов, только `Always` гарантирует pull при каждом запуске.
|
||||
|
||||
**Решение (краткосрочное):** Явный `kubectl rollout restart deployment/pg-query -n sless-fn-default`.
|
||||
|
||||
**Решение (долгосрочное / TODO):** В `function_controller.go` после успешной сборки добавить `rollout restart` аннотацию:
|
||||
```go
|
||||
// Форсируем rollout через аннотацию kubectl.kubernetes.io/restartedAt
|
||||
deployment.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
|
||||
```
|
||||
Либо использовать версионированные теги образов вместо `:latest` (`:v{timestamp}`).
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — output_md5 archive провайдера не обновляется при изменении кода
|
||||
|
||||
**Проблема:** `terraform plan` показывал "No changes" даже после изменения JS-файла функции. Функция отдавала старый код.
|
||||
|
||||
**Причина:** `code_hash = data.archive_file.handler_http.output_md5` — атрибут `output_md5` в `hashicorp/archive v2.7.x` возвращает MD5 от **предыдущей** версии zip (баг порядка вычисления). `output_sha` и `output_sha256` от того же файла обновлялись корректно.
|
||||
|
||||
**Решение:** Заменить `output_md5` на `filesha256("${path.module}/code/handler.js")` — хэшируется исходный файл напрямую, минуя archive провайдер.
|
||||
|
||||
**Урок:** Не использовать `output_md5` из `archive_file`. Использовать `filesha256(source_file)`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — FunctionJob зависал в Running навсегда
|
||||
|
||||
**Проблема:** `terraform apply` висел на `sless_job.hello_run: Still creating...` бесконечно. k8s Job давно завершился (Complete), но FunctionJob оставался в `Running`.
|
||||
|
||||
**Причина:** `SetupWithManager` использовал `Owns(&batchv1.Job{})` для watch событий Job. Но Job создавался в namespace `sless-fn-default`, а FunctionJob в `default` — cross-namespace OwnerReference не работают в k8s. Reconcile после завершения Job никогда не вызывался.
|
||||
|
||||
**Решение:** Убрать `Owns(&batchv1.Job{})`. В `syncJobStatus` добавить `return ctrl.Result{RequeueAfter: 5 * time.Second}` когда Job ещё выполняется — polling каждые 5 сек.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — ImagePullBackOff: sless-registry-auth не копировался в sless-fn-*
|
||||
|
||||
**Проблема:** Pod функции падал с `ImagePullBackOff` — не мог скачать образ из registry.
|
||||
|
||||
**Причина:** `buildDeployment` в `function_controller.go` не устанавливал `imagePullSecrets` в PodSpec. Секрет `sless-registry-auth` существовал только в namespace `sless`, но не копировался в `sless-fn-default`.
|
||||
|
||||
**Решение:** Добавить `ensureRegistrySecret()` — копирует секрет из `sless` в `sless-fn-*` при каждом reconcile. В `buildDeployment` добавить `ImagePullSecrets`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — CrashLoopBackOff: Cannot find module handler.js
|
||||
|
||||
**Проблема:** Pod функции падал: `Cannot find module '/app/function/handler.js'`. Entrypoint был `handler-http.handle` (файл `handler-http.js`), а server.js искал хардкоженный `handler.js`.
|
||||
|
||||
**Причина:** `server.js` и `server.py` runtime-образов хардкодили имя модуля `handler`. Поле `SLESS_ENTRYPOINT` env var не использовалось.
|
||||
|
||||
**Решение:** `server.js` и `server.py` переписаны: парсят `SLESS_ENTRYPOINT` (формат `"module.funcName"`), динамически загружают нужный модуль. Runtime пересобран как `v0.1.1`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — Kaniko cache: пересобирал образ со старым server.js
|
||||
|
||||
**Проблема:** После исправления `server.js` kaniko продолжал собирать образы с **старым** server.js из кэша.
|
||||
|
||||
**Причина:** Base image тегирован как `:latest`. Kaniko кэшировал слои — `FROM naeel/sless-runtime-nodejs20:latest` не перечитывал обновлённый образ.
|
||||
|
||||
**Решение:** Новый тег базового образа `v0.1.1`. В `upload.go` изменить `runtimeBaseImage("nodejs20")` → `naeel/sless-runtime-nodejs20:v0.1.1`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — RBAC: secrets is forbidden: cannot list
|
||||
|
||||
**Проблема:** Оператор падал: `secrets is forbidden: User "system:serviceaccount:sless:sless-operator" cannot list resource "secrets"`.
|
||||
|
||||
**Причина:** В `rbac.yaml` для secrets были только `get;create`, нужны также `list;watch` для controller-runtime.
|
||||
|
||||
**Решение:** Добавить `list;watch` в ClusterRole для `resources: ["secrets"]`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — Harbor registry нестабилен (504 Gateway Timeout)
|
||||
|
||||
**Проблема:** Kaniko падал: `GET https://pearlharbor.registryk8s.services.ngcloud.ru/v2/: unexpected status code 504 Gateway Timeout`. Периодически `/v2/` зависал на 10+ секунд или не отвечал вовсе.
|
||||
|
||||
**Причина:** Harbor — внешний сервис облачного провайдера, нестабилен по независящим от нас причинам. Поведение не зависит от VPN.
|
||||
|
||||
**Решение:** Переключить `REGISTRY_HOST` с Harbor на DockerHub (`naeel`). Образы функций теперь пушатся как `naeel/sless-default-{namespace}-{name}:latest`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — Provider: inconsistent result — image_ref изменился после apply
|
||||
|
||||
**Проблема:** После `terraform apply` ошибка:
|
||||
```
|
||||
Provider produced inconsistent result after apply
|
||||
.image_ref: was cty.StringVal("pearlharbor..."), but now cty.StringVal("naeel/...")
|
||||
```
|
||||
|
||||
**Причина:** `image_ref` в схеме провайдера имел `UseStateForUnknown()`. При plan terraform брал значение из state (старый Harbor путь). После apply API возвращал новый DockerHub путь. Расхождение план vs результат.
|
||||
|
||||
**Решение:** Убрать `UseStateForUnknown()` с `image_ref`. Теперь `image_ref` = `(known after apply)` при каждом apply — честно отражает что значение неизвестно до сборки. Провайдер `v0.1.5`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — Provider: inconsistent result — phase Failed→Ready
|
||||
|
||||
**Проблема:**
|
||||
```
|
||||
Provider produced inconsistent result after apply
|
||||
.phase: was cty.StringVal("Failed"), but now cty.StringVal("Ready")
|
||||
```
|
||||
|
||||
**Причина:** При ошибке `WaitReady` (build failed) → `AddError` → `return`. `resp.State.Set` не вызывался → state не обновлялся → в state оставалось старое значение. Затем оператор замечал новый код, пересобирал → функция становилась Ready. Следующий `Read` возвращал `phase=Ready`, terraform видел расхождение.
|
||||
|
||||
**Решение:** Убрать `UseStateForUnknown()` с `image_ref` (связанный фикс). Для `phase` — `UseStateForUnknown()` оставлен, т.к. при нормальном flow `Ready→Ready` предсказуем.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — Стейл-сервис hello-node блокировал proxy запросы
|
||||
|
||||
**Проблема:** `GET /fn/default/hello-node` возвращал `operation not permitted` — `dial tcp 10.100.77.255:8080: connect: operation not permitted`.
|
||||
|
||||
**Причина:** Сервис `hello-node` в `sless-fn-default` существовал 26h с ClusterIP `10.100.77.255` но без Deployment/Pod. iptables дропал пакеты на несуществующий endpoint.
|
||||
|
||||
**Решение:** `kubectl delete svc hello-node -n sless-fn-default`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — Docker credStore: exec "docker-credential-desktop.exe"
|
||||
|
||||
**Проблема:** `docker push` падал с `exec: "docker-credential-desktop.exe": executable file not found`.
|
||||
|
||||
**Причина:** В `~/.docker/config.json` осталась запись `"credsStore": "desktop.exe"` от Docker Desktop.
|
||||
|
||||
**Решение:** Удалить поле `credsStore` из `~/.docker/config.json`, оставить только `auths` с base64 credentials.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — controller-gen v0.11.1 паникует с Go 1.23
|
||||
|
||||
**Проблема:** `operator-sdk create api` падает с panic в `controller-gen` при `make generate`.
|
||||
|
||||
**Причина:** controller-tools v0.11.1 несовместим с Go 1.23 (nil pointer dereference в go/types).
|
||||
|
||||
**Решение:** В `Makefile` заменить `CONTROLLER_TOOLS_VERSION ?= v0.11.1` на `v0.14.0`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — Dockerfile: operator не находит migrations/ при старте
|
||||
|
||||
**Проблема:** Оператор в кластере сразу падает в CrashLoopBackOff:
|
||||
```
|
||||
level=ERROR msg="read migration file" err="open migrations/001_initial.sql: no such file or directory"
|
||||
```
|
||||
**Причина:** Оригинальный `Dockerfile` копировал только `api/`, `controllers/`, `main.go`. Директории `internal/` и `migrations/` в образ не попадали.
|
||||
**Решение:** Добавить в оба stage Dockerfile:
|
||||
```dockerfile
|
||||
COPY internal/ internal/ # в builder stage
|
||||
COPY migrations/ migrations/ # и в builder, и в финальный alpine stage
|
||||
```
|
||||
`migrations/` нужен в финальном образе — оператор читает SQL-файлы в runtime при каждом старте.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — Dockerfile: golang:1.23 несовместим с go.mod (требует 1.25)
|
||||
|
||||
**Проблема:** `docker build` падает на `go mod download`:
|
||||
```
|
||||
go: go.mod requires go >= 1.25 (running go 1.23.12; GOTOOLCHAIN=local)
|
||||
```
|
||||
**Причина:** В `go.mod` указан `go 1.25`, Dockerfile использовал `golang:1.23-alpine`.
|
||||
**Решение:** `FROM golang:1.23-alpine` → `FROM golang:1.25-alpine`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-07 — go.mod: прямые зависимости помечены как indirect
|
||||
|
||||
**Проблема:** gopls показывал ошибки в `go.mod`:
|
||||
```
|
||||
github.com/gorilla/mux should be direct
|
||||
github.com/lib/pq should be direct
|
||||
github.com/minio/minio-go/v7 should be direct
|
||||
k8s.io/api should be direct
|
||||
```
|
||||
**Причина:** Пакеты получили `// indirect` несмотря на прямые импорты — побочный эффект scaffold-генерации operator-sdk.
|
||||
**Решение:** `go mod tidy` автоматически расставил правильные аннотации.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-08 — После kaniko rebuild функция возвращает старый код
|
||||
|
||||
**Проблема:** `terraform apply` с изменённым кодом успешно завершал kaniko build, но функция продолжала возвращать старую версию кода.
|
||||
|
||||
**Причина:** `imagePullPolicy: IfNotPresent` (default в k8s) + `:latest` тег — после успешной сборки оператор обновлял `.spec.template.spec.containers[0].image`, но image tag не менялся (всегда `:latest`). Kubernetes видел что образ уже есть на ноде и не пул-ил новый. Pod оставался запущенным со старым образом.
|
||||
|
||||
**Решение:** В `controllers/function_controller.go`, функция `ensureDeployment`, после обновления image добавлена аннотация:
|
||||
```go
|
||||
existing.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = fn.Status.LastBuiltAt.Time.Format(time.RFC3339)
|
||||
```
|
||||
Аннотация меняется при каждой новой сборке (значение = `lastBuiltAt`) → Kubernetes делает rolling restart → kubelet пул-ит свежий образ.
|
||||
Оператор: **v0.1.11**.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-09 — Негативные тесты API: найденные баги валидации
|
||||
|
||||
### Методология тестирования
|
||||
Тесты запускались через прямые POST-запросы к API (`https://sless-api.kube5s.ru`)
|
||||
со специально некорректными параметрами. Скрипт: `/tmp/sless_negative_tests.sh`.
|
||||
|
||||
### Результаты
|
||||
|
||||
| # | Тест | Ожидание | Факт | Статус |
|
||||
|---|---|---|---|---|
|
||||
| T1 | `runtime="python999"` | 400 ошибка | `Unsupported value: "python999"` | ✅ |
|
||||
| T2 | `memory_mb=-10` | 400 ошибка | **201 Created** — принято без ошибки | ❌ БАГ |
|
||||
| T3 | `name=""` | 400 ошибка | `name and runtime are required` | ✅ |
|
||||
| T4 | `entrypoint=""` (не передан) | 400 ошибка | **201 Created** с `entrypoint:""` | ❌ БАГ |
|
||||
| T5 | trigger → несуществующая fn | 404/400 | `name, type and function are required` | ❌ Неверный текст ошибки |
|
||||
| T6 | job → несуществующая fn | 404/400 | **Создался** (fn не проверяется при создании job) | ⚠️ |
|
||||
| T7 | trigger `type="rabbitmq"` | 400 ошибка | `name, type and function are required` | ❌ Неверный текст ошибки |
|
||||
| T8 | `run_id=0` | job создаётся, не запускается | 201, `run_id:0` | ✅ |
|
||||
| T9 | несуществующий namespace | 404 | `namespaces "nonexistent-ns" not found` | ✅ |
|
||||
| T10 | `memory_mb=999999` | 400 или ограничение | **201, вернул memory_mb=128** (молча обрезан) | ❌ БАГ |
|
||||
|
||||
### Баги для исправления
|
||||
|
||||
**БАГ-1 (API handler/function):** `memory_mb <= 0` не валидируется. Нужно: `if req.MemoryMB <= 0 { return 400 }`.
|
||||
|
||||
**БАГ-2 (API handler/function):** `entrypoint == ""` не валидируется. Нужно: проверять непустой entrypoint.
|
||||
|
||||
**БАГ-3 (API handler/trigger):** При передаче `type="rabbitmq"` или несуществующей `functionRef` ошибка говорит `"name, type and function are required"` — неверный текст. Скорее всего handler проверяет поле `function_ref` (snake_case) а принимает `functionRef` (camelCase) — маппинг не работает.
|
||||
|
||||
**БАГ-4 (API handler/function):** `memory_mb=999999` принято, но возвращён `memory_mb=128` — молчаливое изменение без ошибки.
|
||||
|
||||
### Дополнительно: валидация в Terraform провайдере (plan-time)
|
||||
Провайдер не имел schema-level валидаторов — все ошибки проявлялись только на `apply`.
|
||||
Добавлены validators в `function_resource.go`, `trigger_resource.go`, `job_resource.go`:
|
||||
- `runtime` — только допустимые значения
|
||||
- `memory_mb` — диапазон 64-4096
|
||||
- `timeout_sec` — диапазон 1-900
|
||||
- `trigger.type` — только `http`/`cron`
|
||||
- `run_id` — >= 0
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Исправления (2026-03-08): v0.1.13 оператор + v0.1.7 провайдер
|
||||
|
||||
### API исправления (internal/api/handler/)
|
||||
|
||||
**БАГ-1 FIXED (functions.go):** Добавлена валидация `memory_mb`:
|
||||
```
|
||||
if req.MemoryMB <= 0 || req.MemoryMB > 4096 → 400 "memory_mb must be between 1 and 4096"
|
||||
```
|
||||
|
||||
**БАГ-2 FIXED (functions.go):** Добавлена валидация `entrypoint`:
|
||||
```
|
||||
if req.Entrypoint == "" → 400 "entrypoint is required"
|
||||
```
|
||||
|
||||
**БАГ-3 FIXED (triggers.go):** Добавлена валидация `type`:
|
||||
```
|
||||
if req.Type != "http" && req.Type != "cron" → 400 "type must be \"http\" or \"cron\""
|
||||
```
|
||||
|
||||
**БАГ-4 FIXED (functions.go):** Покрыт случаем T10 — `memory_mb=999999` теперь возвращает 400.
|
||||
|
||||
### Провайдер план-валидаторы (terraform/provider/)
|
||||
|
||||
Добавлен пакет `terraform-plugin-framework-validators v0.19.0`.
|
||||
|
||||
`function_resource.go`:
|
||||
- `runtime` — `stringvalidator.OneOf("nodejs20", "python3.11", "go1.21")`
|
||||
- `memory_mb` — `int64validator.Between(1, 4096)`
|
||||
- `timeout_sec` — `int64validator.Between(1, 900)`
|
||||
|
||||
`trigger_resource.go`:
|
||||
- `type` — `stringvalidator.OneOf("http", "cron")`
|
||||
|
||||
`job_resource.go`:
|
||||
- `run_id` — `int64validator.AtLeast(0)`
|
||||
|
||||
### Подтверждение повторными тестами (API)
|
||||
|
||||
| Тест | Ожидание | Результат |
|
||||
|------|----------|----------|
|
||||
| T2: `memory_mb=-10` | 400 | ✅ `{"error":"memory_mb must be between 1 and 4096"}` |
|
||||
| T4: `entrypoint=""` | 400 | ✅ `{"error":"entrypoint is required"}` |
|
||||
| T7: `type="rabbitmq"` | 400 + правильное сообщение | ✅ `{"error":"type must be \"http\" or \"cron\""}` |
|
||||
| T10: `memory_mb=999999` | 400 | ✅ `{"error":"memory_mb must be between 1 and 4096"}` |
|
||||
|
||||
### Подтверждение план-валидаторов (Terraform)
|
||||
|
||||
Тест: `runtime = "python2.7"` в http.tf →
|
||||
```
|
||||
Error: Invalid Attribute Value Match
|
||||
Attribute runtime value must be one of: ["nodejs20" "python3.11" "go1.21"], got: "python2.7"
|
||||
```
|
||||
|
||||
### Версии
|
||||
- Оператор: `naeel/sless-operator:v0.1.13`
|
||||
- Провайдер: `terra.k8c.ru/naeel/sless v0.1.7`
|
||||
|
||||
@@ -14,20 +14,42 @@
|
||||
|
||||
| Namespace | Что там |
|
||||
|-----------|---------|
|
||||
| `sless` | API сервер, PostgreSQL, Redis |
|
||||
| `sless-fn-{id}` | Функции пользователей (Jobs/Deployments) |
|
||||
| `sless` | PostgreSQL, Secret sless-registry-auth, RBAC |
|
||||
| `sless-fn-{namespace}` | Deployments функций и их Services/Ingress |
|
||||
| `terra` | Terraform registry-server, terraform-operator |
|
||||
|
||||
## Terraform Registry
|
||||
|
||||
- **URL:** `https://terra.k8c.ru`
|
||||
- Image: `naeel/terraform-registry-server:gpg-key-20260208`
|
||||
- Хранилище: S3 `terraform-registry`, путь: `{hostname}/{namespace}/{provider}/{version}/`
|
||||
- Провайдер sless: `terra.k8c.ru/naeel/sless` v0.1.0 опубликован
|
||||
- GPG ключ подписи: `CB3A0DF161ECC416` (файл: `terraform/secrets/private_key.asc`)
|
||||
|
||||
## S3
|
||||
|
||||
- **URL:** `http://ceph.tst.nubes.ru/`
|
||||
- **URL:** `https://s3.msk-1.ngcloud.ru` (HTTPS обязателен — Ceph требует схему)
|
||||
- **Тип:** Ceph S3 compatible
|
||||
- Доступ: внешний, через access/secret key
|
||||
- Бакет для кода функций: `sless-functions`
|
||||
- Бакет для Terraform registry: `terraform-registry`
|
||||
- **Важно:** kaniko требует `S3_FORCE_PATH_STYLE=true` и полный URL с `https://`
|
||||
- Путь для контекстов сборки: `contexts/{namespace}/{name}/{timestamp}.tar.gz`
|
||||
|
||||
## Container Registry
|
||||
|
||||
- Внутренний registry кластера в namespace `registry`
|
||||
- Service: `registry.registry.svc.cluster.local:18080`
|
||||
- Namespace `registry` — это **Apache NiFi Registry** (НЕ Docker registry), порты 18080/18443
|
||||
- Docker registry: **DockerHub**, user `naeel`
|
||||
- Образы функций: `naeel/sless-{namespace}-{name}:latest`
|
||||
- Runtime образы: `naeel/sless-runtime-{runtime}:latest`
|
||||
- Для kaniko: Secret `sless-registry-auth` (docker-registry type) в namespace `sless`
|
||||
|
||||
## DockerHub образы
|
||||
|
||||
| Образ | Назначение |
|
||||
|-------|------------|
|
||||
| `naeel/sless-runtime-python3.11:latest` | Base runtime для python3.11 функций |
|
||||
| `naeel/sless-default-hello:latest` | Пример собранной функции (kaniko) |
|
||||
|
||||
## Мониторинг
|
||||
|
||||
|
||||
+114
-14
@@ -1,31 +1,131 @@
|
||||
# Прогресс разработки
|
||||
|
||||
Последнее обновление: 2026-03-10 (добавлен прагматичный обзор Claude Opus 4.6)
|
||||
|
||||
## Статусы: ✅ готово | 🔄 в процессе | ⏳ не начато
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-10 — Архитектурный разбор и handoff для следующего агента
|
||||
|
||||
| # | Задача | Статус | Заметки |
|
||||
|---|--------|--------|---------|
|
||||
| 1 | GPT-5.4: Подробный разбор кода и lifecycle issues | ✅ | `doc/architecture/agent-handoff-2026-03-10.md` — детальный code review, event model problems, invocation history gap, приоритеты |
|
||||
| 2 | Claude Sonnet 4.6: Review + security roadmap | ✅ | `doc/architecture/sonnet-review-of-gpt-analysis.md` — где GPT-5.4 прав, что пропустил (gVisor, LLM validation, NetworkPolicy), production security roadmap |
|
||||
| 3 | Claude Opus 4.6: Прагматичный обзор для небольшого провайдера | ✅ | `doc/architecture/opus-pragmatic-review-2026-03-10.md` — анализ с учётом масштаба, конкретный план по этапам, разбор где коллеги перемудрили |
|
||||
| 4 | Claude Opus 4.6: Дизайн LLM-валидации кода | ✅ | `doc/decisions/log.md` → "2026-03-10 — LLM-валидация кода при upload" — архитектура, интерфейс, prompt, план файлов |
|
||||
|
||||
---
|
||||
|
||||
## v1 — Базовый сервис
|
||||
|
||||
### Код
|
||||
|
||||
| # | Компонент | Статус | Заметки |
|
||||
|---|-----------|--------|---------|
|
||||
| 1 | Структура проекта, go mod init | ⏳ | |
|
||||
| 2 | internal/config | ⏳ | |
|
||||
| 3 | internal/model | ⏳ | |
|
||||
| 4 | Миграции PostgreSQL | ⏳ | |
|
||||
| 5 | internal/storage/postgres | ⏳ | |
|
||||
| 6 | internal/api — роутер + хендлеры | ⏳ | |
|
||||
| 7 | internal/storage/s3 | ⏳ | |
|
||||
| 8 | internal/builder | ⏳ | |
|
||||
| 9 | internal/runner (k8s) | ⏳ | |
|
||||
| 10 | docker-compose.yml (local dev) | ⏳ | |
|
||||
| 11 | deployments/k8s манифесты | ⏳ | |
|
||||
| 1 | Структура проекта, operator-sdk init | ✅ | module: gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless |
|
||||
| 2 | CRD types: Function, Trigger | ✅ | api/v1alpha1/, zz_generated.deepcopy.go |
|
||||
| 3 | internal/config | ✅ | config.go, Load() из env vars, добавлены IngressHost + APIToken |
|
||||
| 4 | Миграции PostgreSQL | ✅ | migrations/001_initial.sql — таблица invocations |
|
||||
| 5 | internal/storage/postgres | ✅ | store.go: SaveInvocation, ListInvocations, RunMigrations |
|
||||
| 6 | internal/storage/s3 | ✅ | client.go: Upload, Download, Delete, EnsureBucket (Ceph S3) |
|
||||
| 7 | internal/builder | ✅ | builder.go: kaniko Jobs для сборки образов функций |
|
||||
| 8 | controllers/function_controller.go | ✅ | Pending→Building(kaniko)→Ready/Failed, Deployment, finalizer; handleDeletion удаляет Service+Ingress |
|
||||
| 9 | controllers/trigger_controller.go | ✅ | HTTP→Service+Ingress (fallback) или прокси через ExternalURL, Cron→CronJob, finalizer; handleTriggerDeletion удаляет Service+Ingress |
|
||||
| 10 | internal/api/router.go | ✅ | gorilla/mux, /v1/namespaces/{ns}/... (auth), /fn/{ns}/{name} (публичный прокси) |
|
||||
| 11 | internal/api/handler/ | ✅ | functions.go, triggers.go, invocations.go, invoke.go (proxy — no such host → 404), handler.go (slog) |
|
||||
| 12 | internal/api/middleware/ | ✅ | auth.go (Bearer token), logging.go (slog) |
|
||||
| 13 | main.go | ✅ | operator manager + HTTP сервер параллельно, wire всех зависимостей |
|
||||
|
||||
### Кластер (namespace: sless)
|
||||
|
||||
| # | Ресурс | Статус | Заметки |
|
||||
|---|--------|--------|---------|
|
||||
| 1 | namespace sless | ✅ | создан |
|
||||
| 2 | CRD functions.sless.kube5s.ru | ✅ | установлен из config/crd/bases/ |
|
||||
| 3 | CRD triggers.sless.kube5s.ru | ✅ | установлен из config/crd/bases/ |
|
||||
| 4 | ServiceAccount sless-operator | ✅ | deployments/k8s/rbac.yaml |
|
||||
| 5 | ClusterRole + ClusterRoleBinding | ✅ | deployments/k8s/rbac.yaml |
|
||||
| 6 | PostgreSQL Deployment + Service | ✅ | deployments/k8s/postgres.yaml, Running |
|
||||
| 7 | Secret postgres-secret | ✅ | в namespace sless |
|
||||
|
||||
### Runtime image + Upload endpoint (2026-03-07)
|
||||
|
||||
| # | Компонент | Статус | Заметки |
|
||||
|---|-----------|--------|---------|
|
||||
| 1 | runtimes/python3.11/server.py | ✅ | HTTP wrapper :8080, loads /app/function/handler.py |
|
||||
| 2 | runtimes/python3.11/Dockerfile | ✅ | FROM python:3.11-slim, naeel/sless-runtime-python3.11:v0.1.0 |
|
||||
| 3 | Secret sless-registry-auth | ✅ | DockerHub creds для kaniko в namespace sless |
|
||||
| 4 | internal/api/handler/upload.go | ✅ | POST /v1/namespaces/{ns}/functions/{name}/upload |
|
||||
| 5 | Fix: kaniko S3 endpoint | ✅ | https:// + S3_FORCE_PATH_STYLE=true (Ceph) |
|
||||
| 6 | Fix: бесконечный цикл сборки | ✅ | idempotency guard через аннотацию last-built-s3key |
|
||||
|
||||
### E2E тест (2026-03-07)
|
||||
|
||||
| # | Шаг | Результат |
|
||||
|---|-----|-----------|
|
||||
| 1 | Запуск оператора локально | ✅ `source hack/local.env && go run main.go` |
|
||||
| 2 | Создание функции hello + upload handler.zip | ✅ kaniko собрал naeel/sless-default-hello:latest |
|
||||
| 3 | Function phase=Ready, Deployment Running | ✅ namespace sless-fn-default |
|
||||
| 4 | `GET /health` | ✅ `{"status": "ok"}` |
|
||||
| 5 | `POST / {"name":"Naeel"}` | ✅ `{"message": "Hello, Naeel!"}` |
|
||||
| 6 | terraform apply (pg-query python3.11) | ✅ `{"invocations": [], "count": 0}` через https://sless-api.kube5s.ru |
|
||||
| 7 | hello-node (nodejs20) E2E | ✅ | `{"message": "Hello, Naeel! (nodejs20)"}` via https://sless-api.kube5s.ru/fn/default/hello-node |
|
||||
|
||||
### Terraform провайдер (2026-03-07)
|
||||
|
||||
| # | Компонент | Статус | Заметки |
|
||||
|---|-----------|--------|---------|
|
||||
| 1 | terraform/provider/ — независимый Go-модуль | ✅ | module: terraform-provider-sless |
|
||||
| 2 | internal/client/client.go | ✅ | HTTP-клиент sless API, готов к переносу в nubes |
|
||||
| 3 | internal/provider/provider.go | ✅ | паттерн идентичен nubes NubesProvider |
|
||||
| 4 | sless_function resource | ✅ | CRUD + upload zip + WaitReady (kaniko) |
|
||||
| 5 | sless_trigger resource | ✅ | CRUD (http/cron); Delete ждёт исчезновения CR (WaitGone, 90с) |
|
||||
| 6 | hack/build-and-publish.sh | ✅ | GPG-подпись + mc upload в S3 |
|
||||
| 7 | Публикация в terra.k8c.ru | ✅ | v0.1.3, key CB3A0DF161ECC416 |
|
||||
| 8 | terraform init | ✅ | Installed terra.k8c.ru/naeel/sless v0.1.3 |
|
||||
| 9 | sless_job resource | ✅ | одноразовые запуски функции через FunctionJob CRD |
|
||||
| 10 | build_timeout_sec / wait_timeout_sec | ✅ | настраиваемые таймауты в sless_function и sless_job |
|
||||
|
||||
**Примечание:** Код провайдера писался с прицелом на перенос в nubes провайдер.
|
||||
`internal/client/` → `internal/core/` nubes, ресурсы → `internal/resources_gen/`.
|
||||
|
||||
### source_dir + fix destroy cleanup (2026-03-09)
|
||||
|
||||
| # | Компонент | Статус | Заметки |
|
||||
|---|-----------|--------|----------|
|
||||
| 1 | provider: source_dir атрибут | ✅ | zip в памяти, SHA256 автоматически; hashicorp/archive не нужен |
|
||||
| 2 | fix: trigger_controller handleTriggerDeletion | ✅ | удаляет Service+Ingress из sless-fn-{ns} |
|
||||
| 3 | fix: function_controller handleDeletion | ✅ | удаляет Service+Ingress из sless-fn-{ns} |
|
||||
| 4 | fix: trigger_resource.go Delete WaitGone | ✅ | polling GetTrigger до 404, таймаут 90с |
|
||||
| 5 | fix: invoke.go no such host → 404 | ✅ | после destroy DNS NXDOMAIN → 404 вместо 502 |
|
||||
| 6 | E2E тест run_terraform_examples.sh | ✅ | все 4 примера: hello-node, simple-node, simple-python, notes-python |
|
||||
|
||||
### Что ещё не сделано
|
||||
|
||||
| # | Компонент | Статус | Заметки |
|
||||
|---|-----------|--------|---------|
|
||||
| 1 | terraform apply e2e — функция с postgres | ✅ | `{"invocations": [], "count": 0}` — функция подключилась к postgres.sless.svc, таблица пустая |
|
||||
| 2 | HTTP trigger e2e тест | ✅ | прокси /fn/ через sless-api, без wildcard DNS |
|
||||
| 3 | deployments/k8s/operator.yaml | ✅ | Deployment+Service+Ingress, naeel/sless-operator:v0.1.5, Running |
|
||||
| 4 | Внешний доступ к API оператора | ✅ | https://sless-api.kube5s.ru → HTTP/2 200, TLS cert OK |
|
||||
| 5 | DNS запись sless-api.kube5s.ru | ✅ | A → 5.172.178.182, TTL 3600, zone kube5s.ru |
|
||||
| 6 | nodejs20 runtime | ✅ | runtimes/nodejs20/server.js + Dockerfile, naeel/sless-runtime-nodejs20:v0.1.0 |
|
||||
| 7 | upload.go: nodejs20 + package.json | ✅ | runtimeBaseImage switch, hasPackageJSON → npm install |
|
||||
| 8 | go.mod: direct/indirect fix | ✅ | go mod tidy — gorilla/mux, lib/pq, minio-go, k8s.io/api помечены direct |
|
||||
| 9 | Runtime образы: версионированные теги | ✅ | python3.11:v0.1.0, nodejs20:v0.1.0 (убираем :latest) |
|
||||
|
||||
---
|
||||
|
||||
## v2 — Расширения
|
||||
|
||||
| # | Компонент | Статус | Заметки |
|
||||
|---|-----------|--------|---------|
|
||||
| 1 | RabbitMQ event triggers | ⏳ | |
|
||||
| 2 | Keycloak / облачный auth | ⏳ | |
|
||||
| 0 | Изоляция пользователей по namespace | ⏳ | Каждый токен → свой k8s namespace. API: GET /v1/whoami → {namespace}. Провайдер получает ns при Configure(), юзер не видит. Сейчас: хардкод "default" для демо. |
|
||||
| 2 | Облачный auth (nubes.ru token validation) | ⏳ | v1 — статический токен |
|
||||
| 3 | Метрики → Victoria Metrics | ⏳ | |
|
||||
| 4 | Интеграция с облачными PostgreSQL/Redis | ⏳ | |
|
||||
| 5 | Мониторинг UI | ⏳ | |
|
||||
| 4 | Managed PostgreSQL от провайдера | ⏳ | сейчас postgres в k8s |
|
||||
| 5 | Pre-warm для cron триггеров | ⏳ | TriggerSpec.PreWarmSeconds — поле есть, логика не реализована |
|
||||
| 11 | trigger.enabled | ✅ | enabled=false → Deployment replicas=0, in-place update через PATCH |
|
||||
| 12 | job.run_id | ✅ | run_id=0 → skip, run_id>0 → execute. Повторный запуск = увеличить run_id |
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Bug Report: HTTP route is not removed after Terraform destroy
|
||||
|
||||
## Summary
|
||||
|
||||
При удалении примера `hello-node` через Terraform команда `terraform destroy` завершается успешно, но публичный HTTP endpoint не удаляется.
|
||||
|
||||
Фактическое поведение после `destroy` такое:
|
||||
|
||||
1. Сразу после удаления endpoint ещё некоторое время отвечает `HTTP 200` и возвращает корректный ответ функции.
|
||||
2. Затем backend функции действительно исчезает, но публичный маршрут остаётся опубликованным и начинает отвечать `HTTP 502 function unreachable`.
|
||||
3. Даже через 120 секунд endpoint не исчезает.
|
||||
|
||||
Это выглядит как баг cleanup в platform/backend/provider lifecycle для HTTP trigger/route.
|
||||
|
||||
## Affected Example
|
||||
|
||||
- Example: `hello-node`
|
||||
- Terraform files: `hello-node/main.tf`, `hello-node/http.tf`, `hello-node/job.tf`
|
||||
- Public URL: `https://sless-api.kube5s.ru/fn/default/hello-http`
|
||||
- Function name: `hello-http`
|
||||
- Trigger name: `hello-http-trigger`
|
||||
|
||||
## Reproduction
|
||||
|
||||
Использовался репозиторий examples и скрипт:
|
||||
|
||||
- Script: `./run_terraform_examples.sh`
|
||||
|
||||
Шаги воспроизведения:
|
||||
|
||||
1. Выполнить `terraform init` в `hello-node`
|
||||
2. Выполнить `terraform apply`
|
||||
3. Убедиться, что endpoint живой
|
||||
4. Выполнить `terraform destroy`
|
||||
5. Проверять публичный URL после destroy
|
||||
|
||||
Логика проверки встроена в `run_terraform_examples.sh`:
|
||||
|
||||
1. После `apply` endpoint обязан отвечать `200`
|
||||
2. После `destroy` endpoint должен исчезнуть
|
||||
3. Скрипт ждёт до 120 секунд и перепроверяет endpoint каждые 5 секунд
|
||||
|
||||
## Expected Result
|
||||
|
||||
После успешного `terraform destroy`:
|
||||
|
||||
1. Публичный URL должен перестать существовать
|
||||
2. Запрос на URL должен вернуть `404` или другой явный признак отсутствия маршрута
|
||||
3. Provider не должен возвращать успешный destroy раньше, чем cleanup HTTP route завершён
|
||||
|
||||
## Actual Result
|
||||
|
||||
После успешного `terraform destroy`:
|
||||
|
||||
1. Terraform сообщает `Destroy complete! Resources: 4 destroyed.`
|
||||
2. Endpoint `https://sless-api.kube5s.ru/fn/default/hello-http` продолжает отвечать `200`
|
||||
3. Через некоторое время тот же endpoint начинает отвечать `502`
|
||||
4. Тело ответа на `502`:
|
||||
|
||||
```json
|
||||
{"error":"function unreachable: Post \"http://hello-http.sless-fn-default.svc.cluster.local:8080\": dial tcp 10.106.128.167:8080: connect: operation not permitted"}
|
||||
```
|
||||
|
||||
Это означает:
|
||||
|
||||
1. внешний HTTP маршрут всё ещё существует;
|
||||
2. запрос по нему всё ещё направляется внутрь платформы;
|
||||
3. backend функции уже удалён или недоступен;
|
||||
4. cleanup маршрута не завершён.
|
||||
|
||||
## Timeline From Real Run
|
||||
|
||||
Подтверждённая последовательность из фактического прогона:
|
||||
|
||||
1. `terraform destroy` завершился успешно
|
||||
2. первые проверки после destroy возвращали `HTTP 200`
|
||||
3. затем проверки начали возвращать `HTTP 502 function unreachable`
|
||||
4. в течение всех 24 проверок по 5 секунд endpoint не исчез
|
||||
5. итоговое время ожидания: 120 секунд
|
||||
|
||||
Итоговый summary из скрипта:
|
||||
|
||||
```text
|
||||
ERROR SUMMARY
|
||||
example: hello-node
|
||||
step: endpoint cleanup after clean destroy
|
||||
reason: route cleanup bug: public endpoint still exists but backend is already gone (HTTP 502 function unreachable); endpoint was still published after 120s
|
||||
```
|
||||
|
||||
## Why This Is A Real Platform Bug
|
||||
|
||||
Это не похоже на проблему тестового скрипта или Terraform CLI по следующим причинам:
|
||||
|
||||
1. `terraform destroy` завершается без ошибки
|
||||
2. state Terraform очищается как ожидалось
|
||||
3. сначала endpoint отвечает `200`, значит маршрут реально жив после destroy
|
||||
4. потом endpoint отвечает `502 function unreachable`, значит backend уже исчез, но route ещё остался
|
||||
5. скрипт ждёт 120 секунд, то есть это не мгновенная eventual consistency на 1-2 секунды
|
||||
|
||||
Иными словами: удаление backend и удаление публичного маршрута расходятся по времени, а route cleanup либо не выполняется, либо не дожидается завершения.
|
||||
|
||||
## Most Likely Broken Layer
|
||||
|
||||
Наиболее вероятные точки проблемы:
|
||||
|
||||
1. API/backend destroy trigger возвращает success до фактического удаления HTTP route
|
||||
2. Controller удаляет function workload, но не удаляет route/ingress/virtualservice/gateway mapping
|
||||
3. Удаление route запускается асинхронно, но его результат не awaited
|
||||
4. В системе остаётся запись маршрута на имя функции, хотя service/backend уже удалён
|
||||
|
||||
## What To Check In The Development Repo
|
||||
|
||||
Нужно проверить destroy flow именно для HTTP trigger:
|
||||
|
||||
1. Удаляется ли объект trigger только в metadata/storage или реально удаляется и внешний маршрут
|
||||
2. Какие Kubernetes/ingress объекты создаются для HTTP trigger и все ли они удаляются
|
||||
3. Есть ли race condition между удалением function/service и удалением route
|
||||
4. Не возвращает ли provider success раньше, чем backend подтверждает полное удаление маршрута
|
||||
5. Есть ли финальный polling/wait на исчезновение route перед возвратом успешного destroy
|
||||
|
||||
Если архитектура использует отдельные сущности route/service/function, то destroy должен идти в таком порядке:
|
||||
|
||||
1. disable/remove public routing
|
||||
2. дождаться, что endpoint больше не публикуется снаружи
|
||||
3. удалить backend/service/workload
|
||||
4. завершить destroy success
|
||||
|
||||
Сейчас по фактическому поведению порядок либо обратный, либо неполный.
|
||||
|
||||
## Minimal Acceptance Criteria For Fix
|
||||
|
||||
Исправление можно считать рабочим, если после `terraform destroy` для `hello-node` выполняются все условия:
|
||||
|
||||
1. URL `https://sless-api.kube5s.ru/fn/default/hello-http` перестаёт отвечать как живой маршрут
|
||||
2. URL не возвращает `502 function unreachable`
|
||||
3. URL исчезает в разумное время после destroy
|
||||
4. `./run_terraform_examples.sh` проходит шаг `endpoint cleanup after clean destroy`
|
||||
|
||||
## Current Status
|
||||
|
||||
На данный момент массовый прогон examples корректно останавливается на `hello-node`, потому что это первый воспроизводимый failure.
|
||||
|
||||
Дальше прогонять остальные примеры без исправления destroy cleanup смысла нет: тест уже доказал platform bug на базовом HTTP сценарии.
|
||||
@@ -0,0 +1,160 @@
|
||||
# Примеры sless
|
||||
|
||||
## Что такое sless
|
||||
|
||||
**sless** — платформа для запуска serverless-функций в Kubernetes-кластере.
|
||||
|
||||
Код на Python или Node.js загружается в платформу, которая собирает Docker-образ, деплоит его в кластер и публикует HTTP-эндпоинт. Всё управляется через Terraform.
|
||||
|
||||
### Ресурсы
|
||||
|
||||
| Ресурс | Что делает |
|
||||
|---|---|
|
||||
| `sless_function` | Загружает код и собирает Docker-образ. Сама по себе не принимает запросы — нужен триггер или джоб |
|
||||
| `sless_trigger` | Публикует функцию — либо как HTTP-эндпоинт, либо по расписанию (cron) |
|
||||
| `sless_job` | Запускает функцию один раз (например, для инициализации БД) и ждёт результата |
|
||||
|
||||
**Типичный сценарий:** `sless_function` с кодом + `sless_trigger` с `type = "http"` → публичный URL вида `https://sless-api.kube5s.ru/fn/default/имя-функции`.
|
||||
|
||||
---
|
||||
|
||||
Примеры показывают различные сценарии использования serverless функций через Terraform провайдер `terra.k8c.ru/naeel/sless`.
|
||||
|
||||
## Требования
|
||||
|
||||
- Terraform >= 1.0
|
||||
- Доступ к `https://sless-api.kube5s.ru`
|
||||
|
||||
## Провайдер
|
||||
|
||||
Во всех примерах `main.tf` содержит:
|
||||
|
||||
```hcl
|
||||
provider "sless" {
|
||||
endpoint = "https://sless-api.kube5s.ru"
|
||||
token = "dev-token-change-me"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры
|
||||
|
||||
### `simple-python` — джоб передаёт результат в HTTP-функцию (Python)
|
||||
|
||||
При `apply` запускается джоб, его вывод передаётся в HTTP-функцию через `env_vars`.
|
||||
|
||||
```bash
|
||||
cd simple-python
|
||||
terraform init
|
||||
terraform apply -auto-approve
|
||||
|
||||
# Что вернул джоб (время на момент деплоя):
|
||||
terraform output job_result
|
||||
|
||||
# Проверить функцию:
|
||||
curl -s https://sless-api.kube5s.ru/fn/default/simple-py-time-display
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `simple-node` — то же самое, но на Node.js 20
|
||||
|
||||
```bash
|
||||
cd simple-node
|
||||
terraform init
|
||||
terraform apply -auto-approve
|
||||
|
||||
terraform output job_result
|
||||
curl -s https://sless-api.kube5s.ru/fn/default/simple-node-time-display
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `hello-node` — минимальный пример на Node.js
|
||||
|
||||
Две независимые функции: HTTP-функция (возвращает приветствие) и одноразовый джоб (суммирует числа).
|
||||
|
||||
```bash
|
||||
cd hello-node
|
||||
terraform init
|
||||
terraform apply -auto-approve
|
||||
|
||||
# Проверить HTTP-функцию:
|
||||
curl -s -X POST https://sless-api.kube5s.ru/fn/default/hello-http \
|
||||
-H 'Content-Type: application/json' -d '{"name":"World"}'
|
||||
|
||||
# Посмотреть результат джоба:
|
||||
terraform output job_message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `notes-python` — CRUD API на Python + PostgreSQL
|
||||
|
||||
Полноценное приложение: инициализация схемы БД через джобы, CRUD-функция, read-only функция для списка записей.
|
||||
|
||||
**Переменные:**
|
||||
|
||||
| Переменная | Описание | Дефолт |
|
||||
|---|---|---|
|
||||
| `pg_dsn` | DSN для подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` |
|
||||
|
||||
```bash
|
||||
cd notes-python
|
||||
terraform init
|
||||
|
||||
# Опционально — переопределить DSN:
|
||||
# export TF_VAR_pg_dsn="postgres://user:pass@host:5432/db?sslmode=disable"
|
||||
|
||||
terraform apply -auto-approve
|
||||
|
||||
# Проверить инициализацию БД:
|
||||
terraform output db_init_table_status
|
||||
terraform output db_init_index_status
|
||||
|
||||
# URL функций:
|
||||
terraform output notes_url # CRUD
|
||||
terraform output notes_list_url # список всех записей
|
||||
|
||||
# Создать запись:
|
||||
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/add?title=Hello&body=World"
|
||||
|
||||
# Список записей:
|
||||
curl -s https://sless-api.kube5s.ru/fn/default/notes-list
|
||||
|
||||
# Обновить (id из предыдущего ответа):
|
||||
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/update?id=1&title=Updated&body=New+body"
|
||||
|
||||
# Удалить:
|
||||
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/delete?id=1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Общие команды
|
||||
|
||||
```bash
|
||||
# Посмотреть текущее состояние ресурсов:
|
||||
terraform show
|
||||
|
||||
# Пересоздать конкретный ресурс:
|
||||
terraform apply -replace=sless_function.имя -auto-approve
|
||||
|
||||
# Повторно запустить джоб — увеличить run_id в .tf файле, затем:
|
||||
terraform apply -auto-approve
|
||||
|
||||
# Удалить все ресурсы примера:
|
||||
terraform destroy -auto-approve
|
||||
```
|
||||
|
||||
## Структура каждого примера
|
||||
|
||||
```
|
||||
пример/
|
||||
├── main.tf — провайдер
|
||||
├── *.tf — ресурсы (функции, триггеры, джобы)
|
||||
├── outputs.tf — URLs и статусы после apply
|
||||
├── variables.tf — входные переменные (если есть)
|
||||
└── code/ — исходный код функций
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
// 2026-03-08
|
||||
// handler-http.js — HTTP-функция: возвращает приветствие.
|
||||
// Используется с sless_trigger (постоянный эндпоинт).
|
||||
exports.handle = async (event) => {
|
||||
const name = event.name || 'World';
|
||||
return { message: `Hello, ${name}! HTTP !!!` };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// 2026-03-08
|
||||
// handler-job.js — batch-функция: суммирует числа и считает среднее.
|
||||
// Используется с sless_job (одноразовый запуск).
|
||||
// event.numbers — массив чисел, например [1, 2, 3, 4, 5]
|
||||
exports.handle = async (event) => {
|
||||
const numbers = event.numbers || [];
|
||||
const sum = numbers.reduce((acc, n) => acc + n, 0);
|
||||
const avg = numbers.length > 0 ? sum / numbers.length : 0;
|
||||
return { input: numbers, sum, avg, count: numbers.length };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# 2026-03-08 / Изменено: 2026-03-09
|
||||
# http.tf — HTTP-функция: принимает запросы, возвращает приветствие.
|
||||
# Код: code/handler-http.js
|
||||
|
||||
resource "sless_function" "hello_http" {
|
||||
name = "hello-http"
|
||||
runtime = "nodejs20"
|
||||
entrypoint = "handler-http.handle"
|
||||
memory_mb = 128
|
||||
timeout_sec = 30
|
||||
|
||||
source_dir = "${path.module}/code"
|
||||
}
|
||||
|
||||
resource "sless_trigger" "hello_http" {
|
||||
name = "hello-http-trigger"
|
||||
type = "http"
|
||||
function = sless_function.hello_http.name
|
||||
enabled = true
|
||||
}
|
||||
|
||||
output "trigger_url" {
|
||||
value = sless_trigger.hello_http.url
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# 2026-03-08 / Изменено: 2026-03-09
|
||||
# job.tf — одноразовая функция: суммирует числа из переданного массива.
|
||||
# Код: code/handler-job.js
|
||||
|
||||
resource "sless_function" "hello_job" {
|
||||
name = "hello-job"
|
||||
runtime = "nodejs20"
|
||||
entrypoint = "handler-job.handle"
|
||||
memory_mb = 128
|
||||
timeout_sec = 30
|
||||
|
||||
source_dir = "${path.module}/code"
|
||||
}
|
||||
|
||||
# Одноразовый запуск. Для повторного запуска увеличь run_id (1→2→3...).
|
||||
resource "sless_job" "hello_run" {
|
||||
name = "hello-run"
|
||||
function = sless_function.hello_job.name
|
||||
event_json = jsonencode({ numbers = [100, 200, 300] })
|
||||
wait_timeout_sec = 600
|
||||
run_id = 9
|
||||
}
|
||||
|
||||
output "job_phase" {
|
||||
value = sless_job.hello_run.phase
|
||||
}
|
||||
|
||||
output "job_message" {
|
||||
value = sless_job.hello_run.message
|
||||
}
|
||||
|
||||
output "job_completion_time" {
|
||||
value = sless_job.hello_run.completion_time
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# 2026-03-08
|
||||
# main.tf — провайдеры.
|
||||
# Функции и их код определены в отдельных файлах:
|
||||
# http.tf — HTTP-триггер (code/handler-http.js)
|
||||
# job.tf — одноразовый запуск (code/handler-job.js)
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
sless = {
|
||||
source = "terra.k8c.ru/naeel/sless"
|
||||
version = "~> 0.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "sless" {
|
||||
endpoint = "https://sless-api.kube5s.ru"
|
||||
token = "dev-token-change-me"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Временный файл для негативных тестов — не применяется через terraform
|
||||
# Тесты запускаются вручную с временным переименованием в .tf
|
||||
@@ -0,0 +1,31 @@
|
||||
# 2026-03-09
|
||||
# notes_list.py — чтение всех записей из таблицы notes.
|
||||
#
|
||||
# Назначение: отдать полный список заметок одним запросом.
|
||||
# Принимает GET или POST — тело/query параметры игнорируются.
|
||||
# Возвращает JSON-массив, сортировка: новые записи первые (ORDER BY created_at DESC).
|
||||
#
|
||||
# Пример ответа:
|
||||
# [
|
||||
# {"id": 3, "title": "Hello", "body": "World", "created_at": "2026-03-09 ..."},
|
||||
# {"id": 1, "title": "First", "body": "Note", "created_at": "2026-03-08 ..."}
|
||||
# ]
|
||||
import os
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
|
||||
def list_notes(event):
|
||||
dsn = os.environ['PG_DSN']
|
||||
conn = psycopg2.connect(dsn)
|
||||
try:
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute(
|
||||
"SELECT id, title, body, created_at::text FROM notes ORDER BY created_at DESC"
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
@@ -0,0 +1,81 @@
|
||||
# 2026-03-09
|
||||
# notes_crud.py — CRUD роутер для таблицы notes.
|
||||
#
|
||||
# Назначение: единая функция, которая обрабатывает все операции с записями.
|
||||
# Роутинг осуществляется по sub-path URL (event._path), который runtime
|
||||
# берёт из входящего HTTP-запроса и добавляет в event автоматически.
|
||||
#
|
||||
# Доступные маршруты (все POST):
|
||||
# /fn/default/notes/add?title=...&body=... → создать запись
|
||||
# /fn/default/notes/update?id=1&title=...&body=... → обновить запись
|
||||
# /fn/default/notes/delete?id=1 → удалить запись
|
||||
#
|
||||
# Параметры берутся из query string (event._query) или из тела запроса (event).
|
||||
# event._path и event._query добавляет Python runtime (server.py) автоматически.
|
||||
import os
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
|
||||
def crud(event):
|
||||
dsn = os.environ['PG_DSN']
|
||||
# sub-path без ведущего слэша: "add", "update", "delete"
|
||||
action = event.get('_path', '/').strip('/')
|
||||
q = event.get('_query', {})
|
||||
|
||||
conn = psycopg2.connect(dsn)
|
||||
try:
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
|
||||
if action == 'add':
|
||||
title = q.get('title') or event.get('title', '')
|
||||
body = q.get('body') or event.get('body', '')
|
||||
if not title:
|
||||
return {'error': 'title is required'}
|
||||
cur.execute(
|
||||
"INSERT INTO notes (title, body) VALUES (%s, %s)"
|
||||
" RETURNING id, title, body, created_at::text",
|
||||
(title, body)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return dict(row)
|
||||
|
||||
elif action == 'update':
|
||||
id_ = q.get('id') or event.get('id')
|
||||
if not id_:
|
||||
return {'error': 'id is required'}
|
||||
title = q.get('title') or event.get('title', '')
|
||||
body = q.get('body') or event.get('body', '')
|
||||
cur.execute(
|
||||
"UPDATE notes SET title=%s, body=%s WHERE id=%s"
|
||||
" RETURNING id, title, body, created_at::text",
|
||||
(title, body, int(id_))
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return dict(row) if row else {'error': 'not found'}
|
||||
|
||||
elif action == 'delete':
|
||||
id_ = q.get('id') or event.get('id')
|
||||
if not id_:
|
||||
return {'error': 'id is required'}
|
||||
cur.execute(
|
||||
"DELETE FROM notes WHERE id=%s RETURNING id",
|
||||
(int(id_),)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return {'deleted': row['id']} if row else {'error': 'not found'}
|
||||
|
||||
else:
|
||||
return {
|
||||
'error': f'unknown action: /{action}',
|
||||
'hint': 'use /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
return {'error': str(e)}
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
@@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
@@ -0,0 +1,39 @@
|
||||
# 2026-03-09
|
||||
# sql_runner.py — универсальный DDL/SQL исполнитель.
|
||||
#
|
||||
# Назначение: выполнять произвольные SQL запросы переданные через event.
|
||||
# Используется ТОЛЬКО через sless_job (init.tf) — HTTP-триггера нет намеренно,
|
||||
# чтобы никто снаружи не мог выполнить произвольный SQL.
|
||||
#
|
||||
# Входящий event:
|
||||
# {
|
||||
# "statements": [
|
||||
# "CREATE TABLE IF NOT EXISTS ...",
|
||||
# "CREATE INDEX IF NOT EXISTS ..."
|
||||
# ]
|
||||
# }
|
||||
#
|
||||
# Все statements выполняются последовательно в одной транзакции.
|
||||
# Если хотя бы один упал — транзакция откатывается целиком.
|
||||
import os
|
||||
import psycopg2
|
||||
|
||||
|
||||
def run_sql(event):
|
||||
dsn = os.environ['PG_DSN']
|
||||
statements = event.get('statements', [])
|
||||
if not statements:
|
||||
return {'error': 'no statements provided'}
|
||||
|
||||
conn = psycopg2.connect(dsn)
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
for sql in statements:
|
||||
cur.execute(sql)
|
||||
conn.commit()
|
||||
return {'ok': True, 'executed': len(statements)}
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
return {'error': str(e)}
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -0,0 +1,44 @@
|
||||
# 2026-03-09
|
||||
# init.tf — однократная инициализация схемы БД через sless_job.
|
||||
#
|
||||
# Джобы запускаются один раз при terraform apply и ждут завершения.
|
||||
# Использует функцию sql_runner (без HTTP-триггера) для безопасного DDL.
|
||||
#
|
||||
# Порядок выполнения гарантирован через depends_on:
|
||||
# 1. notes_table_init — создаём таблицу
|
||||
# 2. notes_index_init — создаём индекс (требует таблицу)
|
||||
#
|
||||
# Для повторного запуска (например, после DROP TABLE) — увеличь run_id.
|
||||
# run_id отслеживается в state: при изменении terraform перезапустит джоб.
|
||||
|
||||
# Джоб создания таблицы notes.
|
||||
# CREATE TABLE IF NOT EXISTS — безопасно запускать повторно, таблица не пересоздаётся.
|
||||
resource "sless_job" "notes_table_init" {
|
||||
name = "notes-create-table"
|
||||
function = sless_function.sql_runner.name
|
||||
wait_timeout_sec = 120
|
||||
run_id = 1
|
||||
|
||||
event_json = jsonencode({
|
||||
statements = [
|
||||
"CREATE TABLE IF NOT EXISTS notes (id serial PRIMARY KEY, title text NOT NULL, body text, created_at timestamp DEFAULT now())"
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
# Джоб создания индекса для сортировки по дате.
|
||||
# depends_on гарантирует, что таблица уже создана до создания индекса.
|
||||
resource "sless_job" "notes_index_init" {
|
||||
depends_on = [sless_job.notes_table_init]
|
||||
|
||||
name = "notes-create-index"
|
||||
function = sless_function.sql_runner.name
|
||||
wait_timeout_sec = 60
|
||||
run_id = 1
|
||||
|
||||
event_json = jsonencode({
|
||||
statements = [
|
||||
"CREATE INDEX IF NOT EXISTS notes_created_idx ON notes(created_at DESC)"
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# 2026-03-09
|
||||
# main.tf — конфигурация terraform и провайдеров.
|
||||
#
|
||||
# Все ресурсы вынесены в отдельные .tf файлы по назначению:
|
||||
# variables.tf — входные переменные (pg_dsn)
|
||||
# sql-runner.tf — служебная DDL-функция (без HTTP-триггера)
|
||||
# init.tf — однократная инициализация схемы БД
|
||||
# notes.tf — CRUD функция + HTTP-триггер
|
||||
# notes-list.tf — read-only функция + HTTP-триггер
|
||||
# outputs.tf — URLs развёрнутых эндпоинтов
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
# Провайдер для управления serverless функциями через sless API
|
||||
sless = {
|
||||
source = "terra.k8c.ru/naeel/sless"
|
||||
version = "~> 0.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# sless провайдер подключается к API кластера.
|
||||
provider "sless" {
|
||||
endpoint = "https://sless-api.kube5s.ru"
|
||||
token = "dev-token-change-me"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# 2026-03-09
|
||||
# notes-list.tf — read-only эндпоинт: возвращает все заметки, сортировка новые первые.
|
||||
|
||||
resource "sless_function" "notes_list" {
|
||||
name = "notes-list"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "notes_list.list_notes"
|
||||
memory_mb = 128
|
||||
timeout_sec = 30
|
||||
|
||||
env_vars = {
|
||||
PG_DSN = var.pg_dsn
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/notes-list"
|
||||
}
|
||||
|
||||
resource "sless_trigger" "notes_list_http" {
|
||||
name = "notes-list-http"
|
||||
type = "http"
|
||||
function = sless_function.notes_list.name
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# 2026-03-09
|
||||
# notes.tf — CRUD функция для управления заметками (CREATE / UPDATE / DELETE).
|
||||
#
|
||||
# Маршруты (рекомендуется POST):
|
||||
# /fn/default/notes/add?title=...&body=... → INSERT
|
||||
# /fn/default/notes/update?id=1&title=...&body=... → UPDATE
|
||||
# /fn/default/notes/delete?id=1 → DELETE
|
||||
|
||||
resource "sless_function" "notes_crud" {
|
||||
name = "notes"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "notes_crud.crud"
|
||||
memory_mb = 128
|
||||
timeout_sec = 30
|
||||
|
||||
env_vars = {
|
||||
PG_DSN = var.pg_dsn
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/notes"
|
||||
}
|
||||
|
||||
resource "sless_trigger" "notes_crud_http" {
|
||||
name = "notes-http"
|
||||
type = "http"
|
||||
function = sless_function.notes_crud.name
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# 2026-03-09
|
||||
# outputs.tf — публичные URL развёрнутых функций.
|
||||
#
|
||||
# После terraform apply используй эти URLs для тестирования:
|
||||
# terraform output notes_url → базовый URL для CRUD
|
||||
# terraform output notes_list_url → URL для получения всех записей
|
||||
|
||||
# URL CRUD-функции (notes_crud).
|
||||
# Базовый URL — к нему добавляй sub-path:
|
||||
# POST $(terraform output -raw notes_url)/add?title=Hello&body=World
|
||||
# POST $(terraform output -raw notes_url)/update?id=1&title=Updated
|
||||
# POST $(terraform output -raw notes_url)/delete?id=1
|
||||
output "notes_url" {
|
||||
value = sless_trigger.notes_crud_http.url
|
||||
description = "CRUD: /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X"
|
||||
}
|
||||
|
||||
# URL read-only функции (notes_list).
|
||||
# Принимает GET или POST, параметры игнорирует, возвращает все записи.
|
||||
output "notes_list_url" {
|
||||
value = sless_trigger.notes_list_http.url
|
||||
description = "Список всех записей (GET или POST)"
|
||||
}
|
||||
|
||||
# Статус init-джобов — показывает результат инициализации БД.
|
||||
# Если phase="Succeeded" — таблица и индекс созданы успешно.
|
||||
# Если phase="Failed" — смотри message, исправь и увеличь run_id в init.tf.
|
||||
output "db_init_table_status" {
|
||||
value = {
|
||||
phase = sless_job.notes_table_init.phase
|
||||
message = sless_job.notes_table_init.message
|
||||
}
|
||||
description = "Статус джоба создания таблицы notes"
|
||||
}
|
||||
|
||||
output "db_init_index_status" {
|
||||
value = {
|
||||
phase = sless_job.notes_index_init.phase
|
||||
message = sless_job.notes_index_init.message
|
||||
}
|
||||
description = "Статус джоба создания индекса"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# 2026-03-09
|
||||
# sql-runner.tf — служебная DDL-функция для инициализации и миграций БД.
|
||||
#
|
||||
# ВАЖНО: эта функция не имеет HTTP-триггера — только вызов через sless_job.
|
||||
# Это сделано намеренно: функция выполняет произвольный SQL, и открывать её
|
||||
# наружу через HTTP было бы небезопасно.
|
||||
|
||||
resource "sless_function" "sql_runner" {
|
||||
name = "sql-runner"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "sql_runner.run_sql"
|
||||
memory_mb = 128
|
||||
timeout_sec = 30
|
||||
|
||||
env_vars = {
|
||||
PG_DSN = var.pg_dsn
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/sql-runner"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# 2026-03-09
|
||||
# variables.tf — входные переменные для notes-python примера.
|
||||
#
|
||||
# PG_DSN передаётся во все функции через env_vars.
|
||||
# Хранится как sensitive чтобы не светился в terraform output и логах.
|
||||
# В продакшне — не хардкоди DSN здесь, используй TF_VAR_pg_dsn или secrets manager.
|
||||
|
||||
# DSN для подключения к PostgreSQL внутри кластера.
|
||||
# Формат: postgres://user:password@host:port/dbname?sslmode=...
|
||||
variable "pg_dsn" {
|
||||
description = "PostgreSQL DSN для подключения к БД внутри кластера"
|
||||
type = string
|
||||
default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable"
|
||||
sensitive = true
|
||||
}
|
||||
Executable
+333
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOG_DIR="$ROOT_DIR/.test-logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
EXAMPLES=(
|
||||
"hello-node"
|
||||
"simple-node"
|
||||
"simple-python"
|
||||
"notes-python"
|
||||
)
|
||||
|
||||
declare -A CHECK_METHOD=(
|
||||
[hello-node]="POST"
|
||||
[simple-node]="GET"
|
||||
[simple-python]="GET"
|
||||
[notes-python]="GET"
|
||||
)
|
||||
|
||||
declare -A CHECK_URL=(
|
||||
[hello-node]="https://sless-api.kube5s.ru/fn/default/hello-http"
|
||||
[simple-node]="https://sless-api.kube5s.ru/fn/default/simple-node-time-display"
|
||||
[simple-python]="https://sless-api.kube5s.ru/fn/default/simple-py-time-display"
|
||||
[notes-python]="https://sless-api.kube5s.ru/fn/default/notes-list"
|
||||
)
|
||||
|
||||
declare -A CHECK_DATA=(
|
||||
[hello-node]='{"name":"Smoke"}'
|
||||
[simple-node]=''
|
||||
[simple-python]=''
|
||||
[notes-python]=''
|
||||
)
|
||||
|
||||
declare -a PREEXISTING_EXAMPLES=()
|
||||
LAST_LOG_FILE=""
|
||||
CURRENT_EXAMPLE=""
|
||||
CURRENT_STEP=""
|
||||
LAST_ERROR_SUMMARY=""
|
||||
|
||||
fail_run() {
|
||||
local example="$1"
|
||||
local step="$2"
|
||||
local details="$3"
|
||||
|
||||
echo
|
||||
echo "ERROR SUMMARY"
|
||||
echo "example: $example"
|
||||
echo "step: $step"
|
||||
echo "reason: $details"
|
||||
|
||||
if [ -n "$LAST_LOG_FILE" ] && [ -f "$LAST_LOG_FILE" ]; then
|
||||
echo "log: $LAST_LOG_FILE"
|
||||
echo "last log lines:"
|
||||
tail -n 20 "$LAST_LOG_FILE"
|
||||
fi
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_step() {
|
||||
local example="$1"
|
||||
local step="$2"
|
||||
shift 2
|
||||
|
||||
CURRENT_EXAMPLE="$example"
|
||||
CURRENT_STEP="$step"
|
||||
LAST_ERROR_SUMMARY=""
|
||||
|
||||
if ! "$@"; then
|
||||
local details="$LAST_ERROR_SUMMARY"
|
||||
if [ -z "$details" ]; then
|
||||
details="step failed without explicit summary"
|
||||
fi
|
||||
fail_run "$example" "$step" "$details"
|
||||
fi
|
||||
}
|
||||
|
||||
restore_any_backups() {
|
||||
local backup
|
||||
while IFS= read -r backup; do
|
||||
[ -n "$backup" ] || continue
|
||||
if [ -f "$backup" ]; then
|
||||
mv "$backup" "${backup%.copilot.bak}"
|
||||
fi
|
||||
done < <(find "$ROOT_DIR" -name '*.copilot.bak' | sort)
|
||||
}
|
||||
|
||||
trap restore_any_backups EXIT
|
||||
|
||||
clean_local_artifacts() {
|
||||
local example="$1"
|
||||
rm -rf \
|
||||
"$ROOT_DIR/$example/.terraform" \
|
||||
"$ROOT_DIR/$example/.terraform.lock.hcl" \
|
||||
"$ROOT_DIR/$example/terraform.tfstate" \
|
||||
"$ROOT_DIR/$example/terraform.tfstate.backup" \
|
||||
"$ROOT_DIR/$example"/terraform.tfstate.*.backup \
|
||||
"$ROOT_DIR/$example/dist"
|
||||
}
|
||||
|
||||
retry_tf() {
|
||||
local example="$1"
|
||||
local label="$2"
|
||||
shift 2
|
||||
|
||||
local attempt=1
|
||||
while [ "$attempt" -le 3 ]; do
|
||||
LAST_LOG_FILE="$LOG_DIR/${example//\//_}-${label// /_}-${attempt}.log"
|
||||
echo "==> [$example] $label (attempt $attempt/3)"
|
||||
|
||||
(
|
||||
cd "$ROOT_DIR/$example"
|
||||
"$@"
|
||||
) 2>&1 | tee "$LAST_LOG_FILE"
|
||||
|
||||
local status=${PIPESTATUS[0]}
|
||||
if [ "$status" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if grep -Eiq 'Unauthorized|401|403' "$LAST_LOG_FILE"; then
|
||||
LAST_ERROR_SUMMARY="authorization error during $label"
|
||||
echo "[$example] authorization error during $label"
|
||||
return 41
|
||||
fi
|
||||
|
||||
if grep -Eiq 'TLS handshake timeout|tls:.*timeout|i/o timeout|Client\.Timeout exceeded while awaiting headers|context deadline exceeded|unexpected EOF' "$LAST_LOG_FILE" && [ "$attempt" -lt 3 ]; then
|
||||
attempt=$((attempt + 1))
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
if grep -Eiq 'TLS handshake timeout|tls:.*timeout|i/o timeout|Client\.Timeout exceeded while awaiting headers|context deadline exceeded|unexpected EOF' "$LAST_LOG_FILE"; then
|
||||
LAST_ERROR_SUMMARY="network/provider download failure during $label after retries"
|
||||
else
|
||||
LAST_ERROR_SUMMARY="terraform command failed during $label with exit code $status"
|
||||
fi
|
||||
|
||||
return "$status"
|
||||
done
|
||||
|
||||
LAST_ERROR_SUMMARY="terraform command failed during $label after exhausting retries"
|
||||
return 1
|
||||
}
|
||||
|
||||
record_preexisting_if_needed() {
|
||||
local example="$1"
|
||||
if grep -Fq 'No changes. Your infrastructure matches the configuration.' "$LAST_LOG_FILE"; then
|
||||
PREEXISTING_EXAMPLES+=("$example")
|
||||
echo "[$example] detected preexisting remote resources on clean apply"
|
||||
fi
|
||||
}
|
||||
|
||||
probe_endpoint() {
|
||||
local example="$1"
|
||||
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
|
||||
local status_file="$LOG_DIR/${example//\//_}-endpoint-status.txt"
|
||||
local method="${CHECK_METHOD[$example]}"
|
||||
local url="${CHECK_URL[$example]}"
|
||||
local data="${CHECK_DATA[$example]}"
|
||||
|
||||
if [ "$method" = "POST" ]; then
|
||||
curl -sS -X POST -H 'Content-Type: application/json' -d "$data" -o "$body_file" -w '%{http_code}' "$url" > "$status_file"
|
||||
else
|
||||
curl -sS -o "$body_file" -w '%{http_code}' "$url" > "$status_file"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_live_endpoint() {
|
||||
local example="$1"
|
||||
probe_endpoint "$example"
|
||||
|
||||
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
|
||||
local status
|
||||
status="$(cat "$LOG_DIR/${example//\//_}-endpoint-status.txt")"
|
||||
|
||||
if [ "$status" != "200" ]; then
|
||||
LAST_ERROR_SUMMARY="live endpoint check failed with HTTP $status"
|
||||
echo "[$example] live endpoint check failed with HTTP $status"
|
||||
cat "$body_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if grep -Fq 'function unreachable' "$body_file"; then
|
||||
LAST_ERROR_SUMMARY="live endpoint returned function unreachable"
|
||||
echo "[$example] live endpoint check returned unreachable function"
|
||||
cat "$body_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_destroyed_endpoint() {
|
||||
local example="$1"
|
||||
probe_endpoint "$example"
|
||||
|
||||
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
|
||||
local status
|
||||
status="$(cat "$LOG_DIR/${example//\//_}-endpoint-status.txt")"
|
||||
|
||||
if [ "$status" = "404" ] || [ "$status" = "000" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if grep -Eiq 'not found|404 page not found' "$body_file"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$status" = "502" ] && grep -Fq 'function unreachable' "$body_file"; then
|
||||
LAST_ERROR_SUMMARY="route cleanup bug: public endpoint still exists but backend is already gone (HTTP 502 function unreachable)"
|
||||
echo "[$example] route still exists after destroy, but backend is already gone (HTTP 502 function unreachable)"
|
||||
cat "$body_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
LAST_ERROR_SUMMARY="endpoint still responds after destroy with HTTP $status"
|
||||
echo "[$example] endpoint still responds after destroy with HTTP $status"
|
||||
cat "$body_file"
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_destroyed_endpoint() {
|
||||
local example="$1"
|
||||
local attempts=24
|
||||
local sleep_sec=5
|
||||
local try=1
|
||||
|
||||
while [ "$try" -le "$attempts" ]; do
|
||||
if assert_destroyed_endpoint "$example"; then
|
||||
echo "[$example] endpoint disappeared after destroy"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[$example] endpoint still present after destroy, waiting (${try}/${attempts})"
|
||||
try=$((try + 1))
|
||||
sleep "$sleep_sec"
|
||||
done
|
||||
|
||||
echo "[$example] endpoint did not disappear after destroy within $((attempts * sleep_sec))s"
|
||||
if [ -z "$LAST_ERROR_SUMMARY" ]; then
|
||||
LAST_ERROR_SUMMARY="endpoint remained reachable for more than $((attempts * sleep_sec))s after destroy"
|
||||
else
|
||||
LAST_ERROR_SUMMARY="$LAST_ERROR_SUMMARY; endpoint was still published after $((attempts * sleep_sec))s"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
backup_and_modify() {
|
||||
local example="$1"
|
||||
case "$example" in
|
||||
hello-node)
|
||||
cp "$ROOT_DIR/$example/http.tf" "$ROOT_DIR/$example/http.tf.copilot.bak"
|
||||
perl -0pi -e 's/enabled\s+=\s+true/enabled = false/' "$ROOT_DIR/$example/http.tf"
|
||||
;;
|
||||
simple-node)
|
||||
cp "$ROOT_DIR/$example/time-display.tf" "$ROOT_DIR/$example/time-display.tf.copilot.bak"
|
||||
perl -0pi -e 's/memory_mb\s+=\s+64/memory_mb = 96/' "$ROOT_DIR/$example/time-display.tf"
|
||||
;;
|
||||
simple-python)
|
||||
cp "$ROOT_DIR/$example/time-display.tf" "$ROOT_DIR/$example/time-display.tf.copilot.bak"
|
||||
perl -0pi -e 's/memory_mb\s+=\s+64/memory_mb = 96/' "$ROOT_DIR/$example/time-display.tf"
|
||||
;;
|
||||
notes-python)
|
||||
cp "$ROOT_DIR/$example/notes-list.tf" "$ROOT_DIR/$example/notes-list.tf.copilot.bak"
|
||||
perl -0pi -e 's/memory_mb\s+=\s+128/memory_mb = 160/' "$ROOT_DIR/$example/notes-list.tf"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
restore_modified_files() {
|
||||
local example="$1"
|
||||
case "$example" in
|
||||
hello-node)
|
||||
mv "$ROOT_DIR/$example/http.tf.copilot.bak" "$ROOT_DIR/$example/http.tf"
|
||||
;;
|
||||
simple-node)
|
||||
mv "$ROOT_DIR/$example/time-display.tf.copilot.bak" "$ROOT_DIR/$example/time-display.tf"
|
||||
;;
|
||||
simple-python)
|
||||
mv "$ROOT_DIR/$example/time-display.tf.copilot.bak" "$ROOT_DIR/$example/time-display.tf"
|
||||
;;
|
||||
notes-python)
|
||||
mv "$ROOT_DIR/$example/notes-list.tf.copilot.bak" "$ROOT_DIR/$example/notes-list.tf"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_example() {
|
||||
local example="$1"
|
||||
|
||||
echo
|
||||
echo "==== $example ===="
|
||||
|
||||
clean_local_artifacts "$example"
|
||||
run_step "$example" "terraform init" retry_tf "$example" "terraform init" terraform init -input=false -no-color
|
||||
run_step "$example" "terraform apply clean" retry_tf "$example" "terraform apply clean" terraform apply -auto-approve -input=false -no-color
|
||||
record_preexisting_if_needed "$example"
|
||||
run_step "$example" "endpoint check after clean apply" assert_live_endpoint "$example"
|
||||
|
||||
run_step "$example" "terraform destroy clean" retry_tf "$example" "terraform destroy clean" terraform destroy -auto-approve -input=false -no-color
|
||||
run_step "$example" "endpoint cleanup after clean destroy" wait_for_destroyed_endpoint "$example"
|
||||
|
||||
run_step "$example" "terraform apply second" retry_tf "$example" "terraform apply second" terraform apply -auto-approve -input=false -no-color
|
||||
run_step "$example" "endpoint check after second apply" assert_live_endpoint "$example"
|
||||
|
||||
backup_and_modify "$example"
|
||||
run_step "$example" "terraform apply modified" retry_tf "$example" "terraform apply modified" terraform apply -auto-approve -input=false -no-color
|
||||
restore_modified_files "$example"
|
||||
|
||||
run_step "$example" "terraform destroy final" retry_tf "$example" "terraform destroy final" terraform destroy -auto-approve -input=false -no-color
|
||||
run_step "$example" "endpoint cleanup after final destroy" wait_for_destroyed_endpoint "$example"
|
||||
clean_local_artifacts "$example"
|
||||
}
|
||||
|
||||
main() {
|
||||
local example
|
||||
for example in "${EXAMPLES[@]}"; do
|
||||
run_example "$example"
|
||||
done
|
||||
|
||||
if [ "${#PREEXISTING_EXAMPLES[@]}" -gt 0 ]; then
|
||||
echo
|
||||
echo "Preexisting remote resources were detected on first apply for: ${PREEXISTING_EXAMPLES[*]}"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "All Terraform example lifecycles completed successfully."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,20 @@
|
||||
// Создано: 2026-03-09
|
||||
// time_display.js — HTTP-функция (постоянный Deployment + Trigger).
|
||||
// Читает env JOB_TIME, которую terraform передаёт из sless_job.run_getter.message.
|
||||
// Демонстрирует цепочку: Job вычисляет данные → Function использует их через env.
|
||||
|
||||
exports.showTime = function(event) {
|
||||
// JOB_TIME устанавливается terraform из статуса джоба (JSON строка)
|
||||
const jobTimeRaw = process.env.JOB_TIME || '{}';
|
||||
let jobTime;
|
||||
try {
|
||||
const parsed = JSON.parse(jobTimeRaw);
|
||||
jobTime = parsed.time || jobTimeRaw;
|
||||
} catch (e) {
|
||||
jobTime = jobTimeRaw;
|
||||
}
|
||||
return {
|
||||
message: `Сервис запустился в: ${jobTime}`,
|
||||
path: event._path || '/',
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
// Создано: 2026-03-09
|
||||
// time_getter.js — функция запускается как Job (одноразово).
|
||||
// Возвращает JSON со временем запуска. Оператор v0.1.16 захватывает stdout
|
||||
// и записывает его в sless_job.run_getter.message — оттуда terraform передаёт
|
||||
// значение в sless_function.display через env_var JOB_TIME.
|
||||
|
||||
exports.getTime = function(event) {
|
||||
// Возвращаем время в ISO 8601 UTC — без зависимостей, только stdlib
|
||||
return { time: new Date().toISOString() };
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
# Создано: 2026-03-09
|
||||
# main.tf — пример: запустить один раз скрипт при деплое и передать его результат в функцию.
|
||||
# То же самое что simple-python, но на Node.js 20.
|
||||
#
|
||||
# Как это работает:
|
||||
# 1. При «terraform apply» запускается скрипт-джоб (time_getter)
|
||||
# 2. Скрипт возвращает JSON с текущим временем
|
||||
# 3. Terraform подхватывает этот JSON и передаёт в переменную окружения HTTP-функции (time_display)
|
||||
# 4. Функция отдаёт время при каждом запросе
|
||||
#
|
||||
# Зачем такое нужно:
|
||||
# Если данные нужны функции, но считаются один раз при деплое —
|
||||
# напишите логику в джоб, а результат передайте через env_vars.
|
||||
# Например: получить токен, версию схемы БД, время деплоя и т.д.
|
||||
#
|
||||
# namespace захардкодирован внутри провайдера, здесь ничего указывать.
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
sless = {
|
||||
source = "terra.k8c.ru/naeel/sless"
|
||||
version = "~> 0.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "sless" {
|
||||
endpoint = "https://sless-api.kube5s.ru"
|
||||
token = "dev-token-change-me"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Создано: 2026-03-09
|
||||
# outputs.tf — что выводит terraform после apply.
|
||||
|
||||
# Адрес вашей функции — откройте в браузере или вставьте в curl
|
||||
output "display_url" {
|
||||
description = "URL функции time_display"
|
||||
value = sless_trigger.display_http.url
|
||||
}
|
||||
|
||||
# Что вернул скрипт-джоб — именно это передано в функцию как JOB_TIME
|
||||
output "job_result" {
|
||||
description = "Результат выполнения скрипта time_getter"
|
||||
value = sless_job.run_getter.message
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Создано: 2026-03-09 / Изменено: 2026-03-09
|
||||
# time-display.tf — HTTP-функция, доступная по URL после apply.
|
||||
# Получает результат джоба (из time-getter.tf) через переменную окружения JOB_TIME.
|
||||
|
||||
# HTTP-функция — отвечает на запросы по URL из outputs.tf
|
||||
resource "sless_function" "time_display" {
|
||||
name = "simple-node-time-display" # уникальное имя в namespace
|
||||
runtime = "nodejs20"
|
||||
entrypoint = "time_display.showTime" # файл.функция в code/time_display/
|
||||
memory_mb = 64
|
||||
|
||||
# Передаём результат джоба в функцию через переменную окружения.
|
||||
# В коде функции: process.env.JOB_TIME
|
||||
env_vars = {
|
||||
JOB_TIME = sless_job.run_getter.message
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/time_display"
|
||||
|
||||
depends_on = [sless_job.run_getter] # ждём завершения джоба перед деплоем функции
|
||||
}
|
||||
|
||||
# Публикуем функцию по HTTP — URL будет в outputs.tf
|
||||
resource "sless_trigger" "display_http" {
|
||||
name = "simple-node-display-http"
|
||||
type = "http"
|
||||
function = sless_function.time_display.name
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Создано: 2026-03-09 / Изменено: 2026-03-09
|
||||
# time-getter.tf — скрипт который запускается ОДИН РАЗ при terraform apply.
|
||||
# После запуска его результат доступен через: sless_job.run_getter.message
|
||||
# Смотри time-display.tf — там этот результат передаётся в функцию.
|
||||
|
||||
# Функция для скрипта — без HTTP-триггера, вызывается только через джоб ниже
|
||||
resource "sless_function" "time_getter" {
|
||||
name = "simple-node-time-getter" # уникальное имя в namespace
|
||||
runtime = "nodejs20"
|
||||
entrypoint = "time_getter.getTime" # файл.функция в code/time_getter/
|
||||
memory_mb = 64
|
||||
|
||||
source_dir = "${path.module}/code/time_getter"
|
||||
}
|
||||
|
||||
# Джоб — запускает функцию time_getter один раз прямо при apply.
|
||||
# run_id = 1 означает «запустить». Если увеличить (2, 3...) — запустится снова.
|
||||
# После завершения: sless_job.run_getter.message = то что вернула функция
|
||||
resource "sless_job" "run_getter" {
|
||||
name = "simple-node-getter-run"
|
||||
function = sless_function.time_getter.name
|
||||
run_id = 1
|
||||
wait_timeout_sec = 120 # сколько секунд ждать завершения скрипта
|
||||
event_json = "{}" # входные данные для скрипта (пусто — данные не нужны)
|
||||
|
||||
depends_on = [sless_function.time_getter]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Создано: 2026-03-09
|
||||
# time_display.py — HTTP-функция (постоянный Deployment + Trigger).
|
||||
# Читает env JOB_TIME, которую terraform передаёт из sless_job.run_getter.message.
|
||||
# Демонстрирует цепочку: Job вычисляет данные → Function использует их через env.
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
def show_time(event):
|
||||
# JOB_TIME устанавливается terraform из статуса джоба (JSON строка)
|
||||
job_time_raw = os.environ.get("JOB_TIME", "{}")
|
||||
try:
|
||||
job_time = json.loads(job_time_raw).get("time", job_time_raw)
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
job_time = job_time_raw
|
||||
return {
|
||||
"message": f"Сервис запустился в: {job_time}",
|
||||
"path": event.get("_path", "/"),
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Создано: 2026-03-09
|
||||
# time_getter.py — функция запускается как Job (одноразово).
|
||||
# Возвращает JSON со временем запуска. Оператор v0.1.16 захватывает stdout
|
||||
# и записывает его в sless_job.run_getter.message — оттуда terraform передаёт
|
||||
# значение в sless_function.display через env_var JOB_TIME.
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def get_time(event):
|
||||
# Возвращаем время в ISO 8601 UTC — без зависимостей, только stdlib
|
||||
return {"time": datetime.now(timezone.utc).isoformat()}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Для локального тестирования без оператора
|
||||
print(json.dumps(get_time({})))
|
||||
@@ -0,0 +1,29 @@
|
||||
# Создано: 2026-03-09
|
||||
# main.tf — пример: запустить один раз скрипт при деплое и передать его результат в функцию.
|
||||
#
|
||||
# Как это работает:
|
||||
# 1. При «terraform apply» запускается скрипт-джоб (time_getter)
|
||||
# 2. Скрипт возвращает JSON с текущим временем
|
||||
# 3. Terraform подхватывает этот JSON и передаёт в переменную окружения HTTP-функции (time_display)
|
||||
# 4. Функция отдаёт время при каждом запросе
|
||||
#
|
||||
# Зачем такое нужно:
|
||||
# Если данные нужны функции, но считаются один раз при деплое —
|
||||
# напишите логику в джоб, а результат передайте через env_vars.
|
||||
# Например: получить токен, версию схемы БД, время деплоя и т.д.
|
||||
#
|
||||
# namespace захардкодирован внутри провайдера, здесь ничего указывать.
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
sless = {
|
||||
source = "terra.k8c.ru/naeel/sless"
|
||||
version = "~> 0.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "sless" {
|
||||
endpoint = "https://sless-api.kube5s.ru"
|
||||
token = "dev-token-change-me"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Создано: 2026-03-09
|
||||
# outputs.tf — что выводит terraform после apply.
|
||||
|
||||
# Адрес вашей функции — откройте в браузере или вставьте в curl
|
||||
output "display_url" {
|
||||
description = "URL функции time_display"
|
||||
value = sless_trigger.display_http.url
|
||||
}
|
||||
|
||||
# Что вернул скрипт-джоб — именно это передано в функцию как JOB_TIME
|
||||
output "job_result" {
|
||||
description = "Результат выполнения скрипта time_getter"
|
||||
value = sless_job.run_getter.message
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Создано: 2026-03-09 / Изменено: 2026-03-09
|
||||
# time-display.tf — HTTP-функция, доступная по URL после apply.
|
||||
# Получает результат джоба (из time-getter.tf) через переменную окружения JOB_TIME.
|
||||
|
||||
# HTTP-функция — отвечает на запросы по URL из outputs.tf
|
||||
resource "sless_function" "time_display" {
|
||||
name = "simple-py-time-display" # уникальное имя в namespace
|
||||
runtime = "python3.11"
|
||||
entrypoint = "time_display.show_time" # файл.функция в code/time_display/
|
||||
memory_mb = 64
|
||||
|
||||
# Передаём результат джоба в функцию через переменную окружения.
|
||||
# В коде функции: os.environ.get("JOB_TIME")
|
||||
env_vars = {
|
||||
JOB_TIME = sless_job.run_getter.message
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/time_display"
|
||||
|
||||
depends_on = [sless_job.run_getter] # ждём завершения джоба перед деплоем функции
|
||||
}
|
||||
|
||||
# Публикуем функцию по HTTP — URL будет в outputs.tf
|
||||
resource "sless_trigger" "display_http" {
|
||||
name = "simple-py-display-http"
|
||||
type = "http"
|
||||
function = sless_function.time_display.name
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Создано: 2026-03-09 / Изменено: 2026-03-09
|
||||
# time-getter.tf — скрипт который запускается ОДИН РАЗ при terraform apply.
|
||||
# После запуска его результат доступен через: sless_job.run_getter.message
|
||||
# Смотри time-display.tf — там этот результат передаётся в функцию.
|
||||
|
||||
# Функция для скрипта — без HTTP-триггера, вызывается только через джоб ниже
|
||||
resource "sless_function" "time_getter" {
|
||||
name = "simple-py-time-getter" # уникальное имя в namespace
|
||||
runtime = "python3.11"
|
||||
entrypoint = "time_getter.get_time" # файл.функция в code/time_getter/
|
||||
memory_mb = 64
|
||||
|
||||
source_dir = "${path.module}/code/time_getter"
|
||||
}
|
||||
|
||||
# Джоб — запускает функцию time_getter один раз прямо при apply.
|
||||
# run_id = 1 означает «запустить». Если увеличить (2, 3...) — запустится снова.
|
||||
# После завершения: sless_job.run_getter.message = то что вернула функция
|
||||
resource "sless_job" "run_getter" {
|
||||
name = "simple-py-getter-run"
|
||||
function = sless_function.time_getter.name
|
||||
run_id = 1
|
||||
wait_timeout_sec = 120 # сколько секунд ждать завершения скрипта
|
||||
event_json = "{}" # входные данные для скрипта (пусто — данные не нужны)
|
||||
|
||||
depends_on = [sless_function.time_getter]
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
module gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless
|
||||
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/lib/pq v1.11.2
|
||||
github.com/minio/minio-go/v7 v7.0.99
|
||||
github.com/onsi/ginkgo/v2 v2.6.0
|
||||
github.com/onsi/gomega v1.24.1
|
||||
k8s.io/api v0.26.0
|
||||
k8s.io/apimachinery v0.26.0
|
||||
k8s.io/client-go v0.26.0
|
||||
sigs.k8s.io/controller-runtime v0.14.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/zapr v1.2.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/swag v0.19.14 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.26.0 // indirect
|
||||
k8s.io/component-base v0.26.0 // indirect
|
||||
k8s.io/klog/v2 v2.80.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
|
||||
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,649 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
|
||||
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A=
|
||||
github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
|
||||
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
|
||||
github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE=
|
||||
github.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
|
||||
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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc=
|
||||
github.com/onsi/ginkgo/v2 v2.6.0/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc=
|
||||
github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E=
|
||||
github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
|
||||
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
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/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
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.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I=
|
||||
k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg=
|
||||
k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo=
|
||||
k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ=
|
||||
k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg=
|
||||
k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74=
|
||||
k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8=
|
||||
k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg=
|
||||
k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs=
|
||||
k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8=
|
||||
k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=
|
||||
k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E=
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4=
|
||||
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y=
|
||||
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/controller-runtime v0.14.1 h1:vThDes9pzg0Y+UbCPY3Wj34CGIYPgdmspPm2GIpxpzM=
|
||||
sigs.k8s.io/controller-runtime v0.14.1/go.mod h1:GaRkrY8a7UZF0kqFFbUKG7n9ICiTY5T55P1RiE3UZlU=
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
Copyright 2026.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user