This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
# Ignore everything by default and re-include only needed files
|
||||
**
|
||||
|
||||
# Re-include Go source files (but not *_test.go)
|
||||
!**/*.go
|
||||
**/*_test.go
|
||||
|
||||
# Re-include Go module files
|
||||
!go.mod
|
||||
!go.sum
|
||||
39
.gitea/workflows/docker-build.yml
Normal file
39
.gitea/workflows/docker-build.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
name: docker-build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build image
|
||||
runs-on: ubuntu-22.04
|
||||
container: catthehacker/ubuntu:act-latest
|
||||
env:
|
||||
REGISTRY: git.realmanual.ru
|
||||
IMAGE_NAME: ${{ gitea.repository }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.PUSH_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ gitea.ref_name }}
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
bin/*
|
||||
Dockerfile.cross
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Kubernetes Generated files - skip generated files, except for vendored files
|
||||
!vendor/**/zz_generated.*
|
||||
|
||||
# editor and IDE paraphernalia
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Kubeconfig might contain secrets
|
||||
*.kubeconfig
|
||||
52
.golangci.yml
Normal file
52
.golangci.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
version: "2"
|
||||
run:
|
||||
allow-parallel-runners: true
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- copyloopvar
|
||||
- dupl
|
||||
- errcheck
|
||||
- ginkgolinter
|
||||
- goconst
|
||||
- gocyclo
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- revive
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
settings:
|
||||
revive:
|
||||
rules:
|
||||
- name: comment-spacings
|
||||
- name: import-shadowing
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- linters:
|
||||
- lll
|
||||
path: api/*
|
||||
- linters:
|
||||
- dupl
|
||||
- lll
|
||||
path: internal/*
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.24 AS builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /workspace
|
||||
# Copy the Go Modules manifests
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
# cache deps before building and copying source so that we don't need to re-download as much
|
||||
# and so that source changes don't invalidate our downloaded layer
|
||||
RUN go mod download
|
||||
|
||||
# Copy the Go source (relies on .dockerignore to filter)
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
# the GOARCH has no default value to allow the binary to be built according to the host where the command
|
||||
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
|
||||
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
|
||||
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
|
||||
|
||||
# Use distroless as minimal base image to package the manager binary
|
||||
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
WORKDIR /
|
||||
COPY --from=builder /workspace/manager .
|
||||
USER 65532:65532
|
||||
|
||||
ENTRYPOINT ["/manager"]
|
||||
250
Makefile
Normal file
250
Makefile
Normal file
@@ -0,0 +1,250 @@
|
||||
# Image URL to use all building/pushing image targets
|
||||
IMG ?= controller:latest
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
GOBIN=$(shell go env GOPATH)/bin
|
||||
else
|
||||
GOBIN=$(shell go env GOBIN)
|
||||
endif
|
||||
|
||||
# CONTAINER_TOOL defines the container tool to be used for building images.
|
||||
# Be aware that the target commands are only tested with Docker which is
|
||||
# scaffolded by default. However, you might want to replace it to use other
|
||||
# tools. (i.e. podman)
|
||||
CONTAINER_TOOL ?= docker
|
||||
|
||||
# Setting SHELL to bash allows bash commands to be executed by recipes.
|
||||
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
|
||||
SHELL = /usr/bin/env bash -o pipefail
|
||||
.SHELLFLAGS = -ec
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
##@ General
|
||||
|
||||
# The help target prints out all targets with their descriptions organized
|
||||
# beneath their categories. The categories are represented by '##@' and the
|
||||
# target descriptions by '##'. The awk command is responsible for reading the
|
||||
# entire set of makefiles included in this invocation, looking for lines of the
|
||||
# file as xyz: ## something, and then pretty-format the target and help. Then,
|
||||
# if there's a line with ##@ something, that gets pretty-printed as a category.
|
||||
# More info on the usage of ANSI control characters for terminal formatting:
|
||||
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
|
||||
# More info on the awk command:
|
||||
# http://linuxcommand.org/lc3_adv_awk.php
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
##@ Development
|
||||
|
||||
.PHONY: manifests
|
||||
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
|
||||
"$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
|
||||
|
||||
.PHONY: generate
|
||||
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
|
||||
"$(CONTROLLER_GEN)" object: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 setup-envtest ## Run tests.
|
||||
KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out
|
||||
|
||||
# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
|
||||
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
|
||||
# CertManager is installed by default; skip with:
|
||||
# - CERT_MANAGER_INSTALL_SKIP=true
|
||||
KIND_CLUSTER ?= cronjob-restore-test-e2e
|
||||
|
||||
.PHONY: setup-test-e2e
|
||||
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
|
||||
@command -v $(KIND) >/dev/null 2>&1 || { \
|
||||
echo "Kind is not installed. Please install Kind manually."; \
|
||||
exit 1; \
|
||||
}
|
||||
@case "$$($(KIND) get clusters)" in \
|
||||
*"$(KIND_CLUSTER)"*) \
|
||||
echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
|
||||
*) \
|
||||
echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
|
||||
$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
|
||||
esac
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
|
||||
KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v
|
||||
$(MAKE) cleanup-test-e2e
|
||||
|
||||
.PHONY: cleanup-test-e2e
|
||||
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
|
||||
@$(KIND) delete cluster --name $(KIND_CLUSTER)
|
||||
|
||||
.PHONY: lint
|
||||
lint: golangci-lint ## Run golangci-lint linter
|
||||
"$(GOLANGCI_LINT)" run
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
|
||||
"$(GOLANGCI_LINT)" run --fix
|
||||
|
||||
.PHONY: lint-config
|
||||
lint-config: golangci-lint ## Verify golangci-lint linter configuration
|
||||
"$(GOLANGCI_LINT)" config verify
|
||||
|
||||
##@ Build
|
||||
|
||||
.PHONY: build
|
||||
build: manifests generate fmt vet ## Build manager binary.
|
||||
go build -o bin/manager cmd/main.go
|
||||
|
||||
.PHONY: run
|
||||
run: manifests generate fmt vet ## Run a controller from your host.
|
||||
go run ./cmd/main.go
|
||||
|
||||
# If you wish to build the manager image targeting other platforms you can use the --platform flag.
|
||||
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
|
||||
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||
.PHONY: docker-build
|
||||
docker-build: ## Build docker image with the manager.
|
||||
$(CONTAINER_TOOL) build -t ${IMG} .
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push: ## Push docker image with the manager.
|
||||
$(CONTAINER_TOOL) push ${IMG}
|
||||
|
||||
# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
|
||||
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
|
||||
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
|
||||
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||
# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
|
||||
# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
|
||||
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
|
||||
.PHONY: docker-buildx
|
||||
docker-buildx: ## Build and push docker image for the manager for cross-platform support
|
||||
# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
|
||||
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
|
||||
- $(CONTAINER_TOOL) buildx create --name cronjob-restore-builder
|
||||
$(CONTAINER_TOOL) buildx use cronjob-restore-builder
|
||||
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
|
||||
- $(CONTAINER_TOOL) buildx rm cronjob-restore-builder
|
||||
rm Dockerfile.cross
|
||||
|
||||
.PHONY: build-installer
|
||||
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
|
||||
mkdir -p dist
|
||||
cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
|
||||
"$(KUSTOMIZE)" build config/default > dist/install.yaml
|
||||
|
||||
##@ Deployment
|
||||
|
||||
ifndef ignore-not-found
|
||||
ignore-not-found = false
|
||||
endif
|
||||
|
||||
.PHONY: install
|
||||
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
|
||||
@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
|
||||
if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
|
||||
@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
|
||||
if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi
|
||||
|
||||
.PHONY: deploy
|
||||
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
|
||||
cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
|
||||
"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f -
|
||||
|
||||
.PHONY: undeploy
|
||||
undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
|
||||
"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
|
||||
##@ Dependencies
|
||||
|
||||
## Location to install dependencies to
|
||||
LOCALBIN ?= $(shell pwd)/bin
|
||||
$(LOCALBIN):
|
||||
mkdir -p "$(LOCALBIN)"
|
||||
|
||||
## Tool Binaries
|
||||
KUBECTL ?= kubectl
|
||||
KIND ?= kind
|
||||
KUSTOMIZE ?= $(LOCALBIN)/kustomize
|
||||
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
|
||||
ENVTEST ?= $(LOCALBIN)/setup-envtest
|
||||
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
|
||||
|
||||
## Tool Versions
|
||||
KUSTOMIZE_VERSION ?= v5.7.1
|
||||
CONTROLLER_TOOLS_VERSION ?= v0.19.0
|
||||
|
||||
#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
|
||||
ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
|
||||
[ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \
|
||||
printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/')
|
||||
|
||||
#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
|
||||
ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \
|
||||
[ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \
|
||||
printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/')
|
||||
|
||||
GOLANGCI_LINT_VERSION ?= v2.5.0
|
||||
.PHONY: kustomize
|
||||
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
|
||||
$(KUSTOMIZE): $(LOCALBIN)
|
||||
$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
|
||||
|
||||
.PHONY: controller-gen
|
||||
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
|
||||
$(CONTROLLER_GEN): $(LOCALBIN)
|
||||
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
|
||||
|
||||
.PHONY: setup-envtest
|
||||
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
|
||||
@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
|
||||
@"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \
|
||||
echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
.PHONY: envtest
|
||||
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
|
||||
$(ENVTEST): $(LOCALBIN)
|
||||
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
|
||||
|
||||
.PHONY: golangci-lint
|
||||
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
|
||||
$(GOLANGCI_LINT): $(LOCALBIN)
|
||||
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
|
||||
|
||||
# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
|
||||
# $1 - target path with name of binary
|
||||
# $2 - package url which can be installed
|
||||
# $3 - specific version of package
|
||||
define go-install-tool
|
||||
@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
|
||||
set -e; \
|
||||
package=$(2)@$(3) ;\
|
||||
echo "Downloading $${package}" ;\
|
||||
rm -f "$(1)" ;\
|
||||
GOBIN="$(LOCALBIN)" go install $${package} ;\
|
||||
mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
|
||||
} ;\
|
||||
ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
|
||||
endef
|
||||
|
||||
define gomodver
|
||||
$(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null)
|
||||
endef
|
||||
21
PROJECT
Normal file
21
PROJECT
Normal file
@@ -0,0 +1,21 @@
|
||||
# Code generated by tool. DO NOT EDIT.
|
||||
# This file is used to track the info used to scaffold your project
|
||||
# and allow the plugins properly work.
|
||||
# More info: https://book.kubebuilder.io/reference/project-config.html
|
||||
cliVersion: 4.10.1
|
||||
domain: rml.ru
|
||||
layout:
|
||||
- go.kubebuilder.io/v4
|
||||
projectName: gitlab-job-runner
|
||||
repo: github.com/rml/gitlab-job-runner
|
||||
resources:
|
||||
- api:
|
||||
crdVersion: v1
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: rml.ru
|
||||
group: batch
|
||||
kind: GitlabJobRunner
|
||||
path: github.com/rml/gitlab-job-runner/api/v1alpha1
|
||||
version: v1alpha1
|
||||
version: "3"
|
||||
242
README.md
Normal file
242
README.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# GitLab Job Runner Operator
|
||||
|
||||
Kubernetes operator for executing code from GitLab repositories as one-time Jobs or scheduled CronJobs.
|
||||
|
||||
## Features
|
||||
|
||||
- Execute scripts from GitLab repositories as Kubernetes Jobs
|
||||
- Schedule recurring executions using CronJobs
|
||||
- Secure credential management through Kubernetes Secrets
|
||||
- Support for custom container images and service accounts
|
||||
- Automatic cleanup with finalizers
|
||||
|
||||
## Architecture
|
||||
|
||||
The operator watches for `GitlabJobRunner` custom resources and:
|
||||
|
||||
1. Clones the specified GitLab repository using credentials from a Secret
|
||||
2. Creates either a Job (one-time) or CronJob (scheduled) based on the spec
|
||||
3. Executes the specified script from the repository
|
||||
4. Manages lifecycle and status updates
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes cluster (1.24+)
|
||||
- kubectl configured
|
||||
- GitLab repository with scripts
|
||||
- GitLab access token with read permissions
|
||||
|
||||
## Installation
|
||||
|
||||
### Using Helm (Recommended)
|
||||
|
||||
```bash
|
||||
# Install from local chart
|
||||
helm install gitlab-job-runner ./helm/gitlab-job-runner \
|
||||
--namespace gitlab-job-runner-system \
|
||||
--create-namespace
|
||||
|
||||
# Or with custom values
|
||||
helm install gitlab-job-runner ./helm/gitlab-job-runner \
|
||||
--namespace gitlab-job-runner-system \
|
||||
--create-namespace \
|
||||
--set image.repository=your-registry/gitlab-job-runner \
|
||||
--set image.tag=0.1.0
|
||||
```
|
||||
|
||||
### Using Make
|
||||
|
||||
```bash
|
||||
# Apply CRDs
|
||||
make install
|
||||
|
||||
# Deploy operator
|
||||
make deploy IMG=your-registry/gitlab-job-runner:latest
|
||||
```
|
||||
|
||||
### Create GitLab Credentials Secret
|
||||
|
||||
```bash
|
||||
kubectl create secret generic gitlab-credentials \
|
||||
--from-literal=GITLAB_URL=https://gitlab.example.com \
|
||||
--from-literal=GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
### Create Service Account for Script Execution
|
||||
|
||||
If your scripts need to interact with Kubernetes API:
|
||||
|
||||
```bash
|
||||
kubectl apply -f config/samples/example_serviceaccount.yaml
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### One-time Job Example
|
||||
|
||||
```yaml
|
||||
apiVersion: batch.rml.ru/v1alpha1
|
||||
kind: GitlabJobRunner
|
||||
metadata:
|
||||
name: cleanup-job
|
||||
spec:
|
||||
gitlabSecretRef: gitlab-credentials
|
||||
repositoryURL: mygroup/myrepo
|
||||
branch: main
|
||||
scriptPath: scripts/cleanup.sh
|
||||
serviceAccountName: backup-sa
|
||||
```
|
||||
|
||||
### CronJob Example
|
||||
|
||||
```yaml
|
||||
apiVersion: batch.rml.ru/v1alpha1
|
||||
kind: GitlabJobRunner
|
||||
metadata:
|
||||
name: backup-cronjob
|
||||
spec:
|
||||
schedule: "0 2 * * *"
|
||||
gitlabSecretRef: gitlab-credentials
|
||||
repositoryURL: mygroup/myrepo
|
||||
branch: main
|
||||
scriptPath: scripts/backup.sh
|
||||
serviceAccountName: backup-sa
|
||||
suspend: false
|
||||
concurrencyPolicy: Forbid
|
||||
```
|
||||
|
||||
### Apply the Resource
|
||||
|
||||
```bash
|
||||
kubectl apply -f your-gitlabjobrunner.yaml
|
||||
```
|
||||
|
||||
## Custom Resource Specification
|
||||
|
||||
### GitlabJobRunnerSpec
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|----------------------|---------|----------|-------------------------------------------------------------------------------|
|
||||
| `schedule` | string | No | Cron schedule for recurring jobs. If empty, runs as one-time Job |
|
||||
| `gitlabSecretRef` | string | Yes | Name of Secret containing GITLAB_URL and GITLAB_TOKEN |
|
||||
| `repositoryURL` | string | Yes | GitLab repository path (e.g., "group/project") |
|
||||
| `branch` | string | No | Git branch to checkout (default: "main") |
|
||||
| `scriptPath` | string | Yes | Relative path to script in repository |
|
||||
| `image` | string | No | Container image for git clone (default: "bitnami/git:latest") |
|
||||
| `serviceAccountName` | string | No | ServiceAccount for pod execution (default: "default") |
|
||||
| `suspend` | boolean | No | Suspend CronJob execution (CronJobs only) |
|
||||
| `concurrencyPolicy` | string | No | How to handle concurrent executions: Forbid, Allow, Replace (default: Forbid) |
|
||||
|
||||
### GitlabJobRunnerStatus
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------------|-------------|----------------------------------------|
|
||||
| `conditions` | []Condition | Current state conditions |
|
||||
| `lastScheduleTime` | Time | Last time job was scheduled (CronJobs) |
|
||||
| `activeJobName` | string | Name of currently running job |
|
||||
|
||||
## Script Requirements
|
||||
|
||||
Your script in the GitLab repository should:
|
||||
|
||||
- Be executable (`chmod +x`)
|
||||
- Use `#!/bin/sh` or `#!/bin/bash` shebang
|
||||
- Have kubectl binary available (uses `bitnami/kubectl:latest` image)
|
||||
- Use service account permissions for Kubernetes operations
|
||||
|
||||
### Example Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Wait for resource deletion
|
||||
kubectl delete deployment old-app --ignore-not-found=true
|
||||
kubectl wait --for=delete deployment/old-app --timeout=300s
|
||||
|
||||
# Apply new resources
|
||||
kubectl apply -f manifests/new-app.yaml
|
||||
kubectl wait --for=condition=available deployment/new-app --timeout=300s
|
||||
|
||||
echo "Deployment complete"
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.24+
|
||||
- Kubebuilder
|
||||
- Docker
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# Generate CRDs and manifests
|
||||
make generate
|
||||
make manifests
|
||||
|
||||
# Build operator image
|
||||
make docker-build IMG=your-registry/gitlabjobrunner-operator:latest
|
||||
|
||||
# Push image
|
||||
make docker-push IMG=your-registry/gitlabjobrunner-operator:latest
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
make test
|
||||
|
||||
# Run e2e tests
|
||||
make test-e2e
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
The operator exposes metrics on port 8443 (HTTPS) by default:
|
||||
|
||||
- Custom resource reconciliation metrics
|
||||
- Job/CronJob creation status
|
||||
- GitLab access errors
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **GitLab Token**: Store in Kubernetes Secret, use read-only tokens with minimal scope
|
||||
2. **Service Account**: Grant only necessary permissions for script operations
|
||||
3. **Network Policies**: Restrict pod network access if needed
|
||||
4. **Image Security**: Use trusted container images, enable image scanning
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Operator Logs
|
||||
|
||||
```bash
|
||||
kubectl logs -n cronjob-restore-system deployment/cronjob-restore-controller-manager
|
||||
```
|
||||
|
||||
### Check Job/CronJob Status
|
||||
|
||||
```bash
|
||||
kubectl get jobs,cronjobs
|
||||
kubectl describe job <job-name>
|
||||
```
|
||||
|
||||
### Check GitlabJobRunner Status
|
||||
|
||||
```bash
|
||||
kubectl get gitlabjobrunner
|
||||
kubectl describe gitlabjobrunner <name>
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
- **Git clone fails**: Verify GitLab credentials in Secret
|
||||
- **Script not found**: Check repositoryURL, branch, and scriptPath
|
||||
- **Permission denied**: Ensure ServiceAccount has necessary RBAC permissions
|
||||
- **CronJob not triggering**: Verify schedule syntax and suspend field
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0
|
||||
115
api/v1alpha1/gitlabjobrunner_types.go
Normal file
115
api/v1alpha1/gitlabjobrunner_types.go
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
||||
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
||||
|
||||
// GitlabJobRunnerSpec defines the desired state of GitlabJobRunner
|
||||
type GitlabJobRunnerSpec struct {
|
||||
// schedule in Cron format for recurring execution. If empty, runs as one-time Job
|
||||
// +optional
|
||||
Schedule string `json:"schedule,omitempty"`
|
||||
|
||||
// gitlabSecretRef references a Secret containing GitLab credentials
|
||||
// Expected keys: GITLAB_URL, GITLAB_TOKEN
|
||||
// +required
|
||||
GitlabSecretRef string `json:"gitlabSecretRef"`
|
||||
|
||||
// repositoryURL is the GitLab repository to clone
|
||||
// +required
|
||||
RepositoryURL string `json:"repositoryURL"`
|
||||
|
||||
// branch to checkout, defaults to "main"
|
||||
// +optional
|
||||
Branch string `json:"branch,omitempty"`
|
||||
|
||||
// scriptPath is the relative path to the script in the repository
|
||||
// +required
|
||||
ScriptPath string `json:"scriptPath"`
|
||||
|
||||
// image is the container image to use for execution
|
||||
// +optional
|
||||
Image string `json:"image,omitempty"`
|
||||
|
||||
// serviceAccountName to use for the Job/CronJob pods
|
||||
// +optional
|
||||
ServiceAccountName string `json:"serviceAccountName,omitempty"`
|
||||
|
||||
// suspend pauses scheduled execution (only for CronJobs)
|
||||
// +optional
|
||||
Suspend bool `json:"suspend,omitempty"`
|
||||
|
||||
// concurrencyPolicy defines how to handle concurrent executions
|
||||
// +optional
|
||||
// +kubebuilder:default:=Forbid
|
||||
ConcurrencyPolicy string `json:"concurrencyPolicy,omitempty"`
|
||||
}
|
||||
|
||||
// GitlabJobRunnerStatus defines the observed state of GitlabJobRunner.
|
||||
type GitlabJobRunnerStatus struct {
|
||||
// conditions represent the current state of the GitlabJobRunner resource.
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
|
||||
// lastScheduleTime is the last time a Job was scheduled
|
||||
// +optional
|
||||
LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"`
|
||||
|
||||
// activeJobName is the name of the currently running Job
|
||||
// +optional
|
||||
ActiveJobName string `json:"activeJobName,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
|
||||
// GitlabJobRunner is the Schema for the gitlabjobrunners API
|
||||
type GitlabJobRunner struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// metadata is a standard object metadata
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||
|
||||
// spec defines the desired state of GitlabJobRunner
|
||||
// +required
|
||||
Spec GitlabJobRunnerSpec `json:"spec"`
|
||||
|
||||
// status defines the observed state of GitlabJobRunner
|
||||
// +optional
|
||||
Status GitlabJobRunnerStatus `json:"status,omitzero"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// GitlabJobRunnerList contains a list of GitlabJobRunner
|
||||
type GitlabJobRunnerList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitzero"`
|
||||
Items []GitlabJobRunner `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&GitlabJobRunner{}, &GitlabJobRunnerList{})
|
||||
}
|
||||
36
api/v1alpha1/groupversion_info.go
Normal file
36
api/v1alpha1/groupversion_info.go
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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 batch v1alpha1 API group.
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=batch.rml.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: "batch.rml.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
|
||||
)
|
||||
126
api/v1alpha1/zz_generated.deepcopy.go
Normal file
126
api/v1alpha1/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,126 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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 *GitlabJobRunner) DeepCopyInto(out *GitlabJobRunner) {
|
||||
*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 GitlabJobRunner.
|
||||
func (in *GitlabJobRunner) DeepCopy() *GitlabJobRunner {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GitlabJobRunner)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *GitlabJobRunner) 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 *GitlabJobRunnerList) DeepCopyInto(out *GitlabJobRunnerList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]GitlabJobRunner, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabJobRunnerList.
|
||||
func (in *GitlabJobRunnerList) DeepCopy() *GitlabJobRunnerList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GitlabJobRunnerList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *GitlabJobRunnerList) 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 *GitlabJobRunnerSpec) DeepCopyInto(out *GitlabJobRunnerSpec) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabJobRunnerSpec.
|
||||
func (in *GitlabJobRunnerSpec) DeepCopy() *GitlabJobRunnerSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GitlabJobRunnerSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GitlabJobRunnerStatus) DeepCopyInto(out *GitlabJobRunnerStatus) {
|
||||
*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.LastScheduleTime != nil {
|
||||
in, out := &in.LastScheduleTime, &out.LastScheduleTime
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitlabJobRunnerStatus.
|
||||
func (in *GitlabJobRunnerStatus) DeepCopy() *GitlabJobRunnerStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GitlabJobRunnerStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
204
cmd/main.go
Normal file
204
cmd/main.go
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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 main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
batchv1alpha1 "github.com/rml/gitlab-job-runner/api/v1alpha1"
|
||||
"github.com/rml/gitlab-job-runner/internal/controller"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(batchv1alpha1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var metricsCertPath, metricsCertName, metricsCertKey string
|
||||
var webhookCertPath, webhookCertName, webhookCertKey string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
|
||||
flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
|
||||
flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
|
||||
flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
|
||||
"The directory that contains the metrics server certificate.")
|
||||
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
|
||||
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
}
|
||||
|
||||
// Initial webhook TLS options
|
||||
webhookTLSOpts := tlsOpts
|
||||
webhookServerOptions := webhook.Options{
|
||||
TLSOpts: webhookTLSOpts,
|
||||
}
|
||||
|
||||
if len(webhookCertPath) > 0 {
|
||||
setupLog.Info("Initializing webhook certificate watcher using provided certificates",
|
||||
"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
|
||||
|
||||
webhookServerOptions.CertDir = webhookCertPath
|
||||
webhookServerOptions.CertName = webhookCertName
|
||||
webhookServerOptions.KeyName = webhookCertKey
|
||||
}
|
||||
|
||||
webhookServer := webhook.NewServer(webhookServerOptions)
|
||||
|
||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||
// More info:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.22.4/pkg/metrics/server
|
||||
// - https://book.kubebuilder.io/reference/metrics.html
|
||||
metricsServerOptions := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.22.4/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
}
|
||||
|
||||
// If the certificate is not specified, controller-runtime will automatically
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
//
|
||||
// TODO(user): If you enable certManager, uncomment the following lines:
|
||||
// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
|
||||
// managed by cert-manager for the metrics server.
|
||||
// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
|
||||
if len(metricsCertPath) > 0 {
|
||||
setupLog.Info("Initializing metrics certificate watcher using provided certificates",
|
||||
"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
|
||||
|
||||
metricsServerOptions.CertDir = metricsCertPath
|
||||
metricsServerOptions.CertName = metricsCertName
|
||||
metricsServerOptions.KeyName = metricsCertKey
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
WebhookServer: webhookServer,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "3665dcc6.rml.ru",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||
// LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := (&controller.GitlabJobRunnerReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "GitlabJobRunner")
|
||||
os.Exit(1)
|
||||
}
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
157
config/crd/bases/batch.rml.ru_gitlabjobrunners.yaml
Normal file
157
config/crd/bases/batch.rml.ru_gitlabjobrunners.yaml
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.19.0
|
||||
name: gitlabjobrunners.batch.rml.ru
|
||||
spec:
|
||||
group: batch.rml.ru
|
||||
names:
|
||||
kind: GitlabJobRunner
|
||||
listKind: GitlabJobRunnerList
|
||||
plural: gitlabjobrunners
|
||||
singular: gitlabjobrunner
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: GitlabJobRunner is the Schema for the gitlabjobrunners API
|
||||
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: spec defines the desired state of GitlabJobRunner
|
||||
properties:
|
||||
branch:
|
||||
description: branch to checkout, defaults to "main"
|
||||
type: string
|
||||
concurrencyPolicy:
|
||||
default: Forbid
|
||||
description: concurrencyPolicy defines how to handle concurrent executions
|
||||
type: string
|
||||
gitlabSecretRef:
|
||||
description: |-
|
||||
gitlabSecretRef references a Secret containing GitLab credentials
|
||||
Expected keys: GITLAB_URL, GITLAB_TOKEN
|
||||
type: string
|
||||
image:
|
||||
description: image is the container image to use for execution
|
||||
type: string
|
||||
repositoryURL:
|
||||
description: repositoryURL is the GitLab repository to clone
|
||||
type: string
|
||||
schedule:
|
||||
description: schedule in Cron format for recurring execution. If empty,
|
||||
runs as one-time Job
|
||||
type: string
|
||||
scriptPath:
|
||||
description: scriptPath is the relative path to the script in the
|
||||
repository
|
||||
type: string
|
||||
serviceAccountName:
|
||||
description: serviceAccountName to use for the Job/CronJob pods
|
||||
type: string
|
||||
suspend:
|
||||
description: suspend pauses scheduled execution (only for CronJobs)
|
||||
type: boolean
|
||||
required:
|
||||
- gitlabSecretRef
|
||||
- repositoryURL
|
||||
- scriptPath
|
||||
type: object
|
||||
status:
|
||||
description: status defines the observed state of GitlabJobRunner
|
||||
properties:
|
||||
activeJobName:
|
||||
description: activeJobName is the name of the currently running Job
|
||||
type: string
|
||||
conditions:
|
||||
description: conditions represent the current state of the GitlabJobRunner
|
||||
resource.
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
lastScheduleTime:
|
||||
description: lastScheduleTime is the last time a Job was scheduled
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
16
config/crd/kustomization.yaml
Normal file
16
config/crd/kustomization.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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/batch.rml.ru_gitlabjobrunners.yaml
|
||||
# +kubebuilder:scaffold:crdkustomizeresource
|
||||
|
||||
patches:
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
|
||||
# patches here are for enabling the conversion webhook for each CRD
|
||||
# +kubebuilder:scaffold:crdkustomizewebhookpatch
|
||||
|
||||
# [WEBHOOK] To enable webhook, uncomment the following section
|
||||
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
||||
#configurations:
|
||||
#- kustomizeconfig.yaml
|
||||
19
config/crd/kustomizeconfig.yaml
Normal file
19
config/crd/kustomizeconfig.yaml
Normal file
@@ -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
|
||||
30
config/default/cert_metrics_manager_patch.yaml
Normal file
30
config/default/cert_metrics_manager_patch.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
# This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs.
|
||||
|
||||
# Add the volumeMount for the metrics-server certs
|
||||
- op: add
|
||||
path: /spec/template/spec/containers/0/volumeMounts/-
|
||||
value:
|
||||
mountPath: /tmp/k8s-metrics-server/metrics-certs
|
||||
name: metrics-certs
|
||||
readOnly: true
|
||||
|
||||
# Add the --metrics-cert-path argument for the metrics server
|
||||
- op: add
|
||||
path: /spec/template/spec/containers/0/args/-
|
||||
value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs
|
||||
|
||||
# Add the metrics-server certs volume configuration
|
||||
- op: add
|
||||
path: /spec/template/spec/volumes/-
|
||||
value:
|
||||
name: metrics-certs
|
||||
secret:
|
||||
secretName: metrics-server-cert
|
||||
optional: false
|
||||
items:
|
||||
- key: ca.crt
|
||||
path: ca.crt
|
||||
- key: tls.crt
|
||||
path: tls.crt
|
||||
- key: tls.key
|
||||
path: tls.key
|
||||
234
config/default/kustomization.yaml
Normal file
234
config/default/kustomization.yaml
Normal file
@@ -0,0 +1,234 @@
|
||||
# Adds namespace to all resources.
|
||||
namespace: gitlab-job-runner-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: gitlab-job-runner-
|
||||
|
||||
# Labels to add to all resources and selectors.
|
||||
#labels:
|
||||
#- includeSelectors: true
|
||||
# pairs:
|
||||
# someName: someValue
|
||||
|
||||
resources:
|
||||
- ../crd
|
||||
- ../rbac
|
||||
- ../manager
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||
# crd/kustomization.yaml
|
||||
#- ../webhook
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
|
||||
#- ../certmanager
|
||||
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
||||
#- ../prometheus
|
||||
# [METRICS] Expose the controller manager metrics service.
|
||||
- metrics_service.yaml
|
||||
# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
|
||||
# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
|
||||
# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
|
||||
# be able to communicate with the Webhook Server.
|
||||
#- ../network-policy
|
||||
|
||||
# Uncomment the patches line if you enable Metrics
|
||||
patches:
|
||||
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
|
||||
# More info: https://book.kubebuilder.io/reference/metrics
|
||||
- path: manager_metrics_patch.yaml
|
||||
target:
|
||||
kind: Deployment
|
||||
|
||||
# Uncomment the patches line if you enable Metrics and CertManager
|
||||
# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line.
|
||||
# This patch will protect the metrics with certManager self-signed certs.
|
||||
#- path: cert_metrics_manager_patch.yaml
|
||||
# target:
|
||||
# kind: Deployment
|
||||
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||
# crd/kustomization.yaml
|
||||
#- path: manager_webhook_patch.yaml
|
||||
# target:
|
||||
# kind: Deployment
|
||||
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
|
||||
# Uncomment the following replacements to add the cert-manager CA injection annotations
|
||||
#replacements:
|
||||
# - source: # Uncomment the following block to enable certificates for metrics
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: controller-manager-metrics-service
|
||||
# fieldPath: metadata.name
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: metrics-certs
|
||||
# fieldPaths:
|
||||
# - spec.dnsNames.0
|
||||
# - spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor
|
||||
# kind: ServiceMonitor
|
||||
# group: monitoring.coreos.com
|
||||
# version: v1
|
||||
# name: controller-manager-metrics-monitor
|
||||
# fieldPaths:
|
||||
# - spec.endpoints.0.tlsConfig.serverName
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 0
|
||||
# create: true
|
||||
|
||||
# - source:
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: controller-manager-metrics-service
|
||||
# fieldPath: metadata.namespace
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: metrics-certs
|
||||
# fieldPaths:
|
||||
# - spec.dnsNames.0
|
||||
# - spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 1
|
||||
# create: true
|
||||
# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor
|
||||
# kind: ServiceMonitor
|
||||
# group: monitoring.coreos.com
|
||||
# version: v1
|
||||
# name: controller-manager-metrics-monitor
|
||||
# fieldPaths:
|
||||
# - spec.endpoints.0.tlsConfig.serverName
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 1
|
||||
# create: true
|
||||
|
||||
# - source: # Uncomment the following block if you have any webhook
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# fieldPath: .metadata.name # Name of the service
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPaths:
|
||||
# - .spec.dnsNames.0
|
||||
# - .spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# fieldPath: .metadata.namespace # Namespace of the service
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPaths:
|
||||
# - .spec.dnsNames.0
|
||||
# - .spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 1
|
||||
# create: true
|
||||
|
||||
# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # This name should match the one in certificate.yaml
|
||||
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: ValidatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPath: .metadata.name
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: ValidatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
|
||||
# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: MutatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPath: .metadata.name
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: MutatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
|
||||
# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||
# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
|
||||
# +kubebuilder:scaffold:crdkustomizecainjectionns
|
||||
# - source:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPath: .metadata.name
|
||||
# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
|
||||
# +kubebuilder:scaffold:crdkustomizecainjectionname
|
||||
4
config/default/manager_metrics_patch.yaml
Normal file
4
config/default/manager_metrics_patch.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
|
||||
- op: add
|
||||
path: /spec/template/spec/containers/0/args/0
|
||||
value: --metrics-bind-address=:8443
|
||||
18
config/default/metrics_service.yaml
Normal file
18
config/default/metrics_service.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: controller-manager-metrics-service
|
||||
namespace: system
|
||||
spec:
|
||||
ports:
|
||||
- name: https
|
||||
port: 8443
|
||||
protocol: TCP
|
||||
targetPort: 8443
|
||||
selector:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
8
config/manager/kustomization.yaml
Normal file
8
config/manager/kustomization.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
resources:
|
||||
- manager.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
images:
|
||||
- name: controller
|
||||
newName: controller
|
||||
newTag: latest
|
||||
99
config/manager/manager.yaml
Normal file
99
config/manager/manager.yaml
Normal file
@@ -0,0 +1,99 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
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: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/default-container: manager
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
spec:
|
||||
# TODO(user): Uncomment the following code to configure the nodeAffinity expression
|
||||
# according to the platforms which are supported by your solution.
|
||||
# It is considered best practice to support multiple architectures. You can
|
||||
# build your manager image using the makefile target docker-buildx.
|
||||
# affinity:
|
||||
# nodeAffinity:
|
||||
# requiredDuringSchedulingIgnoredDuringExecution:
|
||||
# nodeSelectorTerms:
|
||||
# - matchExpressions:
|
||||
# - key: kubernetes.io/arch
|
||||
# operator: In
|
||||
# values:
|
||||
# - amd64
|
||||
# - arm64
|
||||
# - ppc64le
|
||||
# - s390x
|
||||
# - key: kubernetes.io/os
|
||||
# operator: In
|
||||
# values:
|
||||
# - linux
|
||||
securityContext:
|
||||
# Projects are configured by default to adhere to the "restricted" Pod Security Standards.
|
||||
# This ensures that deployments meet the highest security requirements for Kubernetes.
|
||||
# For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- command:
|
||||
- /manager
|
||||
args:
|
||||
- --leader-elect
|
||||
- --health-probe-bind-address=:8081
|
||||
image: controller:latest
|
||||
name: manager
|
||||
ports: []
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- "ALL"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8081
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: 8081
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
# TODO(user): Configure the resources accordingly based on the project requirements.
|
||||
# More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
volumeMounts: []
|
||||
volumes: []
|
||||
serviceAccountName: controller-manager
|
||||
terminationGracePeriodSeconds: 10
|
||||
27
config/network-policy/allow-metrics-traffic.yaml
Normal file
27
config/network-policy/allow-metrics-traffic.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
# This NetworkPolicy allows ingress traffic
|
||||
# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those
|
||||
# namespaces are able to gather data from the metrics endpoint.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: allow-metrics-traffic
|
||||
namespace: system
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# This allows ingress traffic from any namespace with the label metrics: enabled
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
metrics: enabled # Only from namespaces with this label
|
||||
ports:
|
||||
- port: 8443
|
||||
protocol: TCP
|
||||
2
config/network-policy/kustomization.yaml
Normal file
2
config/network-policy/kustomization.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- allow-metrics-traffic.yaml
|
||||
11
config/prometheus/kustomization.yaml
Normal file
11
config/prometheus/kustomization.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
resources:
|
||||
- monitor.yaml
|
||||
|
||||
# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus
|
||||
# to securely reference certificates created and managed by cert-manager.
|
||||
# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml
|
||||
# to mount the "metrics-server-cert" secret in the Manager Deployment.
|
||||
#patches:
|
||||
# - path: monitor_tls_patch.yaml
|
||||
# target:
|
||||
# kind: ServiceMonitor
|
||||
27
config/prometheus/monitor.yaml
Normal file
27
config/prometheus/monitor.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
# Prometheus Monitor Service (Metrics)
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: controller-manager-metrics-monitor
|
||||
namespace: system
|
||||
spec:
|
||||
endpoints:
|
||||
- path: /metrics
|
||||
port: https # Ensure this is the name of the port that exposes HTTPS metrics
|
||||
scheme: https
|
||||
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
tlsConfig:
|
||||
# TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
|
||||
# certificate verification, exposing the system to potential man-in-the-middle attacks.
|
||||
# For production environments, it is recommended to use cert-manager for automatic TLS certificate management.
|
||||
# To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml,
|
||||
# which securely references the certificate from the 'metrics-server-cert' secret.
|
||||
insecureSkipVerify: true
|
||||
selector:
|
||||
matchLabels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
19
config/prometheus/monitor_tls_patch.yaml
Normal file
19
config/prometheus/monitor_tls_patch.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
# Patch for Prometheus ServiceMonitor to enable secure TLS configuration
|
||||
# using certificates managed by cert-manager
|
||||
- op: replace
|
||||
path: /spec/endpoints/0/tlsConfig
|
||||
value:
|
||||
# SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
|
||||
serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc
|
||||
insecureSkipVerify: false
|
||||
ca:
|
||||
secret:
|
||||
name: metrics-server-cert
|
||||
key: ca.crt
|
||||
cert:
|
||||
secret:
|
||||
name: metrics-server-cert
|
||||
key: tls.crt
|
||||
keySecret:
|
||||
name: metrics-server-cert
|
||||
key: tls.key
|
||||
27
config/rbac/gitlabjobrunner_admin_role.yaml
Normal file
27
config/rbac/gitlabjobrunner_admin_role.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
# This rule is not used by the project cronjob-restore itself.
|
||||
# It is provided to allow the cluster admin to help manage permissions for users.
|
||||
#
|
||||
# Grants full permissions ('*') over batch.rml.ru.
|
||||
# This role is intended for users authorized to modify roles and bindings within the cluster,
|
||||
# enabling them to delegate specific permissions to other users or groups as needed.
|
||||
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: gitlabjobrunner-admin-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners
|
||||
verbs:
|
||||
- '*'
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners/status
|
||||
verbs:
|
||||
- get
|
||||
33
config/rbac/gitlabjobrunner_editor_role.yaml
Normal file
33
config/rbac/gitlabjobrunner_editor_role.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
# This rule is not used by the project cronjob-restore itself.
|
||||
# It is provided to allow the cluster admin to help manage permissions for users.
|
||||
#
|
||||
# Grants permissions to create, update, and delete resources within the batch.rml.ru.
|
||||
# This role is intended for users who need to manage these resources
|
||||
# but should not control RBAC or manage permissions for others.
|
||||
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: gitlabjobrunner-editor-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners/status
|
||||
verbs:
|
||||
- get
|
||||
29
config/rbac/gitlabjobrunner_viewer_role.yaml
Normal file
29
config/rbac/gitlabjobrunner_viewer_role.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
# This rule is not used by the project cronjob-restore itself.
|
||||
# It is provided to allow the cluster admin to help manage permissions for users.
|
||||
#
|
||||
# Grants read-only access to batch.rml.ru resources.
|
||||
# This role is intended for users who need visibility into these resources
|
||||
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
|
||||
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: gitlabjobrunner-viewer-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners/status
|
||||
verbs:
|
||||
- get
|
||||
27
config/rbac/kustomization.yaml
Normal file
27
config/rbac/kustomization.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
resources:
|
||||
# All RBAC will be applied under this service account in
|
||||
# the deployment namespace. You may comment out this resource
|
||||
# if your manager will use a service account that exists at
|
||||
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
|
||||
# subjects if changing service account names.
|
||||
- service_account.yaml
|
||||
- role.yaml
|
||||
- role_binding.yaml
|
||||
- leader_election_role.yaml
|
||||
- leader_election_role_binding.yaml
|
||||
# The following RBAC configurations are used to protect
|
||||
# the metrics endpoint with authn/authz. These configurations
|
||||
# ensure that only authorized users and service accounts
|
||||
# can access the metrics endpoint. Comment the following
|
||||
# permissions if you want to disable this protection.
|
||||
# More info: https://book.kubebuilder.io/reference/metrics.html
|
||||
- metrics_auth_role.yaml
|
||||
- metrics_auth_role_binding.yaml
|
||||
- metrics_reader_role.yaml
|
||||
# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
|
||||
# default, aiding admins in cluster management. Those roles are
|
||||
# not used by the gitlab-job-runner itself. You can comment the following lines
|
||||
# if you do not want those helpers be installed with your Project.
|
||||
- gitlabjobrunner_admin_role.yaml
|
||||
- gitlabjobrunner_editor_role.yaml
|
||||
- gitlabjobrunner_viewer_role.yaml
|
||||
40
config/rbac/leader_election_role.yaml
Normal file
40
config/rbac/leader_election_role.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
# permissions to do leader election.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
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
|
||||
15
config/rbac/leader_election_role_binding.yaml
Normal file
15
config/rbac/leader_election_role_binding.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: leader-election-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: leader-election-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
17
config/rbac/metrics_auth_role.yaml
Normal file
17
config/rbac/metrics_auth_role.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: metrics-auth-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- create
|
||||
12
config/rbac/metrics_auth_role_binding.yaml
Normal file
12
config/rbac/metrics_auth_role_binding.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: metrics-auth-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: metrics-auth-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
9
config/rbac/metrics_reader_role.yaml
Normal file
9
config/rbac/metrics_reader_role.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: metrics-reader
|
||||
rules:
|
||||
- nonResourceURLs:
|
||||
- "/metrics"
|
||||
verbs:
|
||||
- get
|
||||
53
config/rbac/role.yaml
Normal file
53
config/rbac/role.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: manager-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- cronjobs
|
||||
- jobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
15
config/rbac/role_binding.yaml
Normal file
15
config/rbac/role_binding.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: manager-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: manager-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
8
config/rbac/service_account.yaml
Normal file
8
config/rbac/service_account.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: controller-manager
|
||||
namespace: system
|
||||
44
config/samples/batch_v1alpha1_gitlabjobrunner.yaml
Normal file
44
config/samples/batch_v1alpha1_gitlabjobrunner.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
apiVersion: batch.rml.ru/v1alpha1
|
||||
kind: GitlabJobRunner
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: gitlabjobrunner-sample-job
|
||||
spec:
|
||||
gitlabSecretRef: gitlab-credentials
|
||||
repositoryURL: mygroup/myrepo
|
||||
branch: main
|
||||
scriptPath: scripts/cleanup.sh
|
||||
image: bitnami/git:latest
|
||||
serviceAccountName: default
|
||||
|
||||
---
|
||||
apiVersion: batch.rml.ru/v1alpha1
|
||||
kind: GitlabJobRunner
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gitlab-job-runner
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: gitlabjobrunner-sample-cronjob
|
||||
spec:
|
||||
schedule: "0 2 * * *"
|
||||
gitlabSecretRef: gitlab-credentials
|
||||
repositoryURL: mygroup/myrepo
|
||||
branch: main
|
||||
scriptPath: scripts/backup.sh
|
||||
image: bitnami/git:latest
|
||||
serviceAccountName: backup-sa
|
||||
suspend: false
|
||||
concurrencyPolicy: Forbid
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: gitlab-credentials
|
||||
type: Opaque
|
||||
stringData:
|
||||
GITLAB_URL: "https://gitlab.example.com"
|
||||
GITLAB_TOKEN: "glpat-xxxxxxxxxxxxxxxxxxxx"
|
||||
9
config/samples/example_secret.yaml
Normal file
9
config/samples/example_secret.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: gitlab-credentials
|
||||
namespace: default
|
||||
type: Opaque
|
||||
stringData:
|
||||
GITLAB_URL: "https://gitlab.example.com"
|
||||
GITLAB_TOKEN: "glpat-xxxxxxxxxxxxxxxxxxxx"
|
||||
27
config/samples/example_serviceaccount.yaml
Normal file
27
config/samples/example_serviceaccount.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: backup-sa
|
||||
namespace: default
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: backup-role
|
||||
rules:
|
||||
- apiGroups: ["*"]
|
||||
resources: ["*"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: backup-role-binding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: backup-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: backup-sa
|
||||
namespace: default
|
||||
4
config/samples/kustomization.yaml
Normal file
4
config/samples/kustomization.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
## Append samples of your project ##
|
||||
resources:
|
||||
- batch_v1alpha1_gitlabjobrunner.yaml
|
||||
# +kubebuilder:scaffold:manifestskustomizesamples
|
||||
29
examples/backup.sh
Executable file
29
examples/backup.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# Example backup script that uses kubectl
|
||||
|
||||
set -e
|
||||
|
||||
BACKUP_DIR="/tmp/backup-$(date +%Y%m%d-%H%M%S)"
|
||||
NAMESPACE="${NAMESPACE:-default}"
|
||||
|
||||
echo "Starting backup process..."
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
# Backup all deployments
|
||||
echo "Backing up deployments..."
|
||||
kubectl get deployments -n "${NAMESPACE}" -o yaml > "${BACKUP_DIR}/deployments.yaml"
|
||||
|
||||
# Backup all services
|
||||
echo "Backing up services..."
|
||||
kubectl get services -n "${NAMESPACE}" -o yaml > "${BACKUP_DIR}/services.yaml"
|
||||
|
||||
# Backup all configmaps
|
||||
echo "Backing up configmaps..."
|
||||
kubectl get configmaps -n "${NAMESPACE}" -o yaml > "${BACKUP_DIR}/configmaps.yaml"
|
||||
|
||||
# Backup all secrets (be careful with this in production)
|
||||
echo "Backing up secrets..."
|
||||
kubectl get secrets -n "${NAMESPACE}" -o yaml > "${BACKUP_DIR}/secrets.yaml"
|
||||
|
||||
echo "Backup completed: ${BACKUP_DIR}"
|
||||
ls -lh "${BACKUP_DIR}"
|
||||
48
examples/cleanup-and-recreate.sh
Executable file
48
examples/cleanup-and-recreate.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# Example script for cleanup and recreation of Kubernetes resources
|
||||
|
||||
set -e
|
||||
|
||||
RESOURCE_NAME="${RESOURCE_NAME:-example-deployment}"
|
||||
NAMESPACE="${NAMESPACE:-default}"
|
||||
|
||||
echo "Starting cleanup and recreation process..."
|
||||
|
||||
# Step 1: Delete old resource
|
||||
echo "Deleting old deployment: ${RESOURCE_NAME}"
|
||||
kubectl delete deployment "${RESOURCE_NAME}" -n "${NAMESPACE}" --ignore-not-found=true
|
||||
|
||||
# Step 2: Wait for complete deletion
|
||||
echo "Waiting for deployment deletion..."
|
||||
kubectl wait --for=delete deployment/"${RESOURCE_NAME}" -n "${NAMESPACE}" --timeout=300s 2>/dev/null || true
|
||||
|
||||
# Step 3: Create new resource from manifest
|
||||
echo "Creating new deployment..."
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ${RESOURCE_NAME}
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ${RESOURCE_NAME}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ${RESOURCE_NAME}
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- containerPort: 80
|
||||
EOF
|
||||
|
||||
# Step 4: Wait for new deployment to be ready
|
||||
echo "Waiting for deployment to be ready..."
|
||||
kubectl wait --for=condition=available deployment/"${RESOURCE_NAME}" -n "${NAMESPACE}" --timeout=300s
|
||||
|
||||
echo "Deployment ${RESOURCE_NAME} successfully recreated!"
|
||||
100
go.mod
Normal file
100
go.mod
Normal file
@@ -0,0 +1,100 @@
|
||||
module github.com/rml/gitlab-job-runner
|
||||
|
||||
go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/onsi/ginkgo/v2 v2.22.0
|
||||
github.com/onsi/gomega v1.36.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/client-go v0.34.1
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-logr/zapr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.34.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.34.1 // indirect
|
||||
k8s.io/apiserver v0.34.1 // indirect
|
||||
k8s.io/component-base v0.34.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
259
go.sum
Normal file
259
go.sum
Normal file
@@ -0,0 +1,259 @@
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/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/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
|
||||
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
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 v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
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.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
|
||||
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
|
||||
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
|
||||
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
|
||||
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
|
||||
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
|
||||
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
|
||||
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
|
||||
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
15
hack/boilerplate.go.txt
Normal file
15
hack/boilerplate.go.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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.
|
||||
*/
|
||||
23
helm/gitlab-job-runner/.helmignore
Normal file
23
helm/gitlab-job-runner/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
17
helm/gitlab-job-runner/Chart.yaml
Normal file
17
helm/gitlab-job-runner/Chart.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: v2
|
||||
name: gitlab-job-runner
|
||||
description: Kubernetes operator for executing code from GitLab repositories as Jobs or CronJobs
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
keywords:
|
||||
- gitlab
|
||||
- job
|
||||
- cronjob
|
||||
- operator
|
||||
- automation
|
||||
maintainers:
|
||||
- name: RML Team
|
||||
home: https://git.realmanual.ru/realmanual/gitlab-job-runner.git
|
||||
sources:
|
||||
- https://git.realmanual.ru/realmanual/gitlab-job-runner.git
|
||||
26
helm/gitlab-job-runner/QUICKSTART.md
Normal file
26
helm/gitlab-job-runner/QUICKSTART.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# GitLab Job Runner
|
||||
|
||||
Kubernetes operator for executing code from GitLab repositories as Jobs or CronJobs.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install with Helm
|
||||
helm install gitlab-job-runner ./helm/gitlab-job-runner \
|
||||
--namespace gitlab-job-runner-system \
|
||||
--create-namespace
|
||||
|
||||
# Create GitLab credentials
|
||||
kubectl create secret generic gitlab-credentials \
|
||||
--from-literal=GITLAB_URL=https://gitlab.example.com \
|
||||
--from-literal=GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Deploy example job
|
||||
kubectl apply -f config/samples/batch_v1alpha1_gitlabjobrunner.yaml
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Full Documentation](README.md)
|
||||
- [Helm Chart Documentation](helm/gitlab-job-runner/README.md)
|
||||
- [Examples](examples/)
|
||||
151
helm/gitlab-job-runner/README.md
Normal file
151
helm/gitlab-job-runner/README.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# GitLab Job Runner Helm Chart
|
||||
|
||||
Kubernetes operator for executing code from GitLab repositories as Jobs or CronJobs.
|
||||
|
||||
## Installation
|
||||
|
||||
### Add Helm Repository
|
||||
|
||||
```bash
|
||||
helm repo add gitlab-job-runner https://your-repo-url
|
||||
helm repo update
|
||||
```
|
||||
|
||||
### Install Chart
|
||||
|
||||
```bash
|
||||
helm install gitlab-job-runner gitlab-job-runner/gitlab-job-runner \
|
||||
--namespace gitlab-job-runner-system \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
### Install from Local
|
||||
|
||||
```bash
|
||||
helm install gitlab-job-runner ./helm/gitlab-job-runner \
|
||||
--namespace gitlab-job-runner-system \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```yaml
|
||||
# values.yaml
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: your-registry/gitlab-job-runner
|
||||
tag: "0.1.0"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
```
|
||||
|
||||
### Enable Leader Election
|
||||
|
||||
For high availability with multiple replicas:
|
||||
|
||||
```yaml
|
||||
replicaCount: 3
|
||||
|
||||
leaderElection:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### Metrics Configuration
|
||||
|
||||
```yaml
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 8443
|
||||
secure: true
|
||||
```
|
||||
|
||||
### RBAC Configuration
|
||||
|
||||
```yaml
|
||||
rbac:
|
||||
create: true
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: ""
|
||||
annotations: {}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------------------------|-------------------------|----------------|
|
||||
| `replicaCount` | Number of replicas | `1` |
|
||||
| `image.repository` | Image repository | `controller` |
|
||||
| `image.tag` | Image tag | `latest` |
|
||||
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
|
||||
| `serviceAccount.create` | Create service account | `true` |
|
||||
| `serviceAccount.name` | Service account name | `""` |
|
||||
| `rbac.create` | Create RBAC resources | `true` |
|
||||
| `leaderElection.enabled` | Enable leader election | `false` |
|
||||
| `metrics.enabled` | Enable metrics | `true` |
|
||||
| `metrics.port` | Metrics port | `8443` |
|
||||
| `metrics.secure` | Secure metrics endpoint | `true` |
|
||||
| `healthProbe.port` | Health probe port | `8081` |
|
||||
| `resources.limits.cpu` | CPU limit | `500m` |
|
||||
| `resources.limits.memory` | Memory limit | `128Mi` |
|
||||
| `resources.requests.cpu` | CPU request | `10m` |
|
||||
| `resources.requests.memory` | Memory request | `64Mi` |
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, create GitLab credentials and GitlabJobRunner resources:
|
||||
|
||||
### Create GitLab Secret
|
||||
|
||||
```bash
|
||||
kubectl create secret generic gitlab-credentials \
|
||||
--from-literal=GITLAB_URL=https://gitlab.example.com \
|
||||
--from-literal=GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx \
|
||||
-n default
|
||||
```
|
||||
|
||||
### Create GitlabJobRunner
|
||||
|
||||
```yaml
|
||||
apiVersion: batch.rml.ru/v1alpha1
|
||||
kind: GitlabJobRunner
|
||||
metadata:
|
||||
name: example-job
|
||||
spec:
|
||||
gitlabSecretRef: gitlab-credentials
|
||||
repositoryURL: mygroup/myrepo
|
||||
branch: main
|
||||
scriptPath: scripts/deploy.sh
|
||||
```
|
||||
|
||||
## Upgrade
|
||||
|
||||
```bash
|
||||
helm upgrade gitlab-job-runner gitlab-job-runner/gitlab-job-runner \
|
||||
--namespace gitlab-job-runner-system
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
helm uninstall gitlab-job-runner \
|
||||
--namespace gitlab-job-runner-system
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
helm test gitlab-job-runner \
|
||||
--namespace gitlab-job-runner-system
|
||||
```
|
||||
42
helm/gitlab-job-runner/templates/NOTES.txt
Normal file
42
helm/gitlab-job-runner/templates/NOTES.txt
Normal file
@@ -0,0 +1,42 @@
|
||||
NOTES:
|
||||
Thank you for installing {{ .Chart.Name }}.
|
||||
|
||||
Your release is named {{ .Release.Name }}.
|
||||
|
||||
To learn more about the release, try:
|
||||
|
||||
$ helm status {{ .Release.Name }} -n {{ .Release.Namespace }}
|
||||
$ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }}
|
||||
|
||||
The GitLab Job Runner operator has been deployed successfully!
|
||||
|
||||
Controller manager deployment: {{ include "gitlab-job-runner.fullname" . }}-controller-manager
|
||||
|
||||
Next steps:
|
||||
|
||||
1. Create a Secret with GitLab credentials:
|
||||
|
||||
kubectl create secret generic gitlab-credentials \
|
||||
--from-literal=GITLAB_URL=https://gitlab.example.com \
|
||||
--from-literal=GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
2. Create a GitlabJobRunner resource:
|
||||
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: batch.rml.ru/v1alpha1
|
||||
kind: GitlabJobRunner
|
||||
metadata:
|
||||
name: example-job
|
||||
spec:
|
||||
gitlabSecretRef: gitlab-credentials
|
||||
repositoryURL: mygroup/myrepo
|
||||
branch: main
|
||||
scriptPath: scripts/deploy.sh
|
||||
EOF
|
||||
|
||||
3. Check the status:
|
||||
|
||||
kubectl get gitlabjobrunner
|
||||
kubectl describe gitlabjobrunner example-job
|
||||
|
||||
For more information, visit: https://github.com/rml/gitlab-job-runner
|
||||
60
helm/gitlab-job-runner/templates/_helpers.tpl
Normal file
60
helm/gitlab-job-runner/templates/_helpers.tpl
Normal file
@@ -0,0 +1,60 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "gitlab-job-runner.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "gitlab-job-runner.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "gitlab-job-runner.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "gitlab-job-runner.labels" -}}
|
||||
helm.sh/chart: {{ include "gitlab-job-runner.chart" . }}
|
||||
{{ include "gitlab-job-runner.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "gitlab-job-runner.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "gitlab-job-runner.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "gitlab-job-runner.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "gitlab-job-runner.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
157
helm/gitlab-job-runner/templates/crd.yaml
Normal file
157
helm/gitlab-job-runner/templates/crd.yaml
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.19.0
|
||||
name: gitlabjobrunners.batch.rml.ru
|
||||
spec:
|
||||
group: batch.rml.ru
|
||||
names:
|
||||
kind: GitlabJobRunner
|
||||
listKind: GitlabJobRunnerList
|
||||
plural: gitlabjobrunners
|
||||
singular: gitlabjobrunner
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: GitlabJobRunner is the Schema for the gitlabjobrunners API
|
||||
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: spec defines the desired state of GitlabJobRunner
|
||||
properties:
|
||||
branch:
|
||||
description: branch to checkout, defaults to "main"
|
||||
type: string
|
||||
concurrencyPolicy:
|
||||
default: Forbid
|
||||
description: concurrencyPolicy defines how to handle concurrent executions
|
||||
type: string
|
||||
gitlabSecretRef:
|
||||
description: |-
|
||||
gitlabSecretRef references a Secret containing GitLab credentials
|
||||
Expected keys: GITLAB_URL, GITLAB_TOKEN
|
||||
type: string
|
||||
image:
|
||||
description: image is the container image to use for execution
|
||||
type: string
|
||||
repositoryURL:
|
||||
description: repositoryURL is the GitLab repository to clone
|
||||
type: string
|
||||
schedule:
|
||||
description: schedule in Cron format for recurring execution. If empty,
|
||||
runs as one-time Job
|
||||
type: string
|
||||
scriptPath:
|
||||
description: scriptPath is the relative path to the script in the
|
||||
repository
|
||||
type: string
|
||||
serviceAccountName:
|
||||
description: serviceAccountName to use for the Job/CronJob pods
|
||||
type: string
|
||||
suspend:
|
||||
description: suspend pauses scheduled execution (only for CronJobs)
|
||||
type: boolean
|
||||
required:
|
||||
- gitlabSecretRef
|
||||
- repositoryURL
|
||||
- scriptPath
|
||||
type: object
|
||||
status:
|
||||
description: status defines the observed state of GitlabJobRunner
|
||||
properties:
|
||||
activeJobName:
|
||||
description: activeJobName is the name of the currently running Job
|
||||
type: string
|
||||
conditions:
|
||||
description: conditions represent the current state of the GitlabJobRunner
|
||||
resource.
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
lastScheduleTime:
|
||||
description: lastScheduleTime is the last time a Job was scheduled
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
79
helm/gitlab-job-runner/templates/deployment.yaml
Normal file
79
helm/gitlab-job-runner/templates/deployment.yaml
Normal file
@@ -0,0 +1,79 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "gitlab-job-runner.fullname" . }}-controller-manager
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gitlab-job-runner.labels" . | nindent 4 }}
|
||||
control-plane: controller-manager
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gitlab-job-runner.selectorLabels" . | nindent 6 }}
|
||||
control-plane: controller-manager
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "gitlab-job-runner.selectorLabels" . | nindent 8 }}
|
||||
control-plane: controller-manager
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "gitlab-job-runner.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: manager
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
command:
|
||||
- /manager
|
||||
args:
|
||||
{{- if .Values.leaderElection.enabled }}
|
||||
- --leader-elect
|
||||
{{- end }}
|
||||
{{- if .Values.metrics.enabled }}
|
||||
- --metrics-bind-address=:{{ .Values.metrics.port }}
|
||||
{{- if .Values.metrics.secure }}
|
||||
- --metrics-secure=true
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- --metrics-bind-address=0
|
||||
{{- end }}
|
||||
- --health-probe-bind-address=:{{ .Values.healthProbe.port }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: {{ .Values.healthProbe.port }}
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: {{ .Values.healthProbe.port }}
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
terminationGracePeriodSeconds: 10
|
||||
57
helm/gitlab-job-runner/templates/leader-election-rbac.yaml
Normal file
57
helm/gitlab-job-runner/templates/leader-election-rbac.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
{{- if .Values.leaderElection.enabled -}}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: {{ include "gitlab-job-runner.fullname" . }}-leader-election-role
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gitlab-job-runner.labels" . | nindent 4 }}
|
||||
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
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: {{ include "gitlab-job-runner.fullname" . }}-leader-election-rolebinding
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gitlab-job-runner.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: {{ include "gitlab-job-runner.fullname" . }}-leader-election-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "gitlab-job-runner.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
20
helm/gitlab-job-runner/templates/metrics-service.yaml
Normal file
20
helm/gitlab-job-runner/templates/metrics-service.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
{{- if .Values.metrics.enabled -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "gitlab-job-runner.fullname" . }}-metrics
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gitlab-job-runner.labels" . | nindent 4 }}
|
||||
control-plane: controller-manager
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "gitlab-job-runner.selectorLabels" . | nindent 4 }}
|
||||
control-plane: controller-manager
|
||||
ports:
|
||||
- name: https
|
||||
port: {{ .Values.metrics.port }}
|
||||
protocol: TCP
|
||||
targetPort: {{ .Values.metrics.port }}
|
||||
{{- end }}
|
||||
56
helm/gitlab-job-runner/templates/role.yaml
Normal file
56
helm/gitlab-job-runner/templates/role.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
{{- if .Values.rbac.create -}}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ include "gitlab-job-runner.fullname" . }}-manager-role
|
||||
labels:
|
||||
{{- include "gitlab-job-runner.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- cronjobs
|
||||
- jobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- batch.rml.ru
|
||||
resources:
|
||||
- gitlabjobrunners/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
{{- end }}
|
||||
16
helm/gitlab-job-runner/templates/rolebinding.yaml
Normal file
16
helm/gitlab-job-runner/templates/rolebinding.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
{{- if .Values.rbac.create -}}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ include "gitlab-job-runner.fullname" . }}-manager-rolebinding
|
||||
labels:
|
||||
{{- include "gitlab-job-runner.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ include "gitlab-job-runner.fullname" . }}-manager-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "gitlab-job-runner.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
13
helm/gitlab-job-runner/templates/serviceaccount.yaml
Normal file
13
helm/gitlab-job-runner/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "gitlab-job-runner.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gitlab-job-runner.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
15
helm/gitlab-job-runner/templates/tests/test-connection.yaml
Normal file
15
helm/gitlab-job-runner/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "gitlab-job-runner.fullname" . }}-test
|
||||
namespace: {{ .Release.Namespace }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
|
||||
data:
|
||||
run.sh: |-
|
||||
#!/bin/sh
|
||||
set -e
|
||||
kubectl get crd gitlabjobrunners.batch.rml.ru
|
||||
kubectl get deployment {{ include "gitlab-job-runner.fullname" . }}-controller-manager -n {{ .Release.Namespace }}
|
||||
echo "GitLab Job Runner operator is deployed successfully!"
|
||||
56
helm/gitlab-job-runner/values.yaml
Normal file
56
helm/gitlab-job-runner/values.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: controller
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 8443
|
||||
secure: true
|
||||
|
||||
healthProbe:
|
||||
port: 8081
|
||||
|
||||
leaderElection:
|
||||
enabled: false
|
||||
|
||||
rbac:
|
||||
create: true
|
||||
432
internal/controller/gitlabjobrunner_controller.go
Normal file
432
internal/controller/gitlabjobrunner_controller.go
Normal file
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
batchv1alpha1 "github.com/rml/gitlab-job-runner/api/v1alpha1"
|
||||
)
|
||||
|
||||
// GitlabJobRunnerReconciler reconciles a GitlabJobRunner object
|
||||
type GitlabJobRunnerReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
const (
|
||||
finalizerName = "batch.rml.ru/finalizer"
|
||||
defaultImage = "bitnami/git:latest"
|
||||
defaultBranch = "main"
|
||||
)
|
||||
|
||||
// +kubebuilder:rbac:groups=batch.rml.ru,resources=gitlabjobrunners,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=batch.rml.ru,resources=gitlabjobrunners/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=batch.rml.ru,resources=gitlabjobrunners/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
func (r *GitlabJobRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := logf.FromContext(ctx)
|
||||
|
||||
// Fetch the GitlabJobRunner instance
|
||||
runner := &batchv1alpha1.GitlabJobRunner{}
|
||||
if err := r.Get(ctx, req.NamespacedName, runner); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
log.Error(err, "unable to fetch GitlabJobRunner")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Handle finalizer
|
||||
if runner.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
if !controllerutil.ContainsFinalizer(runner, finalizerName) {
|
||||
controllerutil.AddFinalizer(runner, finalizerName)
|
||||
if err := r.Update(ctx, runner); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if controllerutil.ContainsFinalizer(runner, finalizerName) {
|
||||
if err := r.cleanupResources(ctx, runner); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
controllerutil.RemoveFinalizer(runner, finalizerName)
|
||||
if err := r.Update(ctx, runner); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Verify GitLab secret exists
|
||||
secret := &corev1.Secret{}
|
||||
if err := r.Get(ctx, types.NamespacedName{
|
||||
Name: runner.Spec.GitlabSecretRef,
|
||||
Namespace: runner.Namespace,
|
||||
}, secret); err != nil {
|
||||
log.Error(err, "unable to fetch GitLab secret")
|
||||
r.updateStatus(ctx, runner, "Degraded", metav1.ConditionFalse, "SecretNotFound", "GitLab secret not found")
|
||||
return ctrl.Result{RequeueAfter: time.Minute}, err
|
||||
}
|
||||
|
||||
// Determine if this should be a Job or CronJob
|
||||
if runner.Spec.Schedule != "" {
|
||||
return r.reconcileCronJob(ctx, runner)
|
||||
}
|
||||
return r.reconcileJob(ctx, runner)
|
||||
}
|
||||
|
||||
// reconcileJob handles one-time Job creation
|
||||
func (r *GitlabJobRunnerReconciler) reconcileJob(ctx context.Context, runner *batchv1alpha1.GitlabJobRunner) (ctrl.Result, error) {
|
||||
log := logf.FromContext(ctx)
|
||||
|
||||
jobName := runner.Name
|
||||
job := &batchv1.Job{}
|
||||
err := r.Get(ctx, types.NamespacedName{Name: jobName, Namespace: runner.Namespace}, job)
|
||||
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
job = r.constructJob(runner)
|
||||
if err := controllerutil.SetControllerReference(runner, job, r.Scheme); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
if err := r.Create(ctx, job); err != nil {
|
||||
log.Error(err, "unable to create Job")
|
||||
r.updateStatus(ctx, runner, "Degraded", metav1.ConditionFalse, "JobCreationFailed", err.Error())
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
log.Info("created Job", "job", jobName)
|
||||
r.updateStatus(ctx, runner, "Progressing", metav1.ConditionTrue, "JobCreated", "Job created successfully")
|
||||
return ctrl.Result{}, nil
|
||||
} else if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Job exists, update status based on its state
|
||||
if job.Status.Succeeded > 0 {
|
||||
r.updateStatus(ctx, runner, "Available", metav1.ConditionTrue, "JobCompleted", "Job completed successfully")
|
||||
} else if job.Status.Failed > 0 {
|
||||
r.updateStatus(ctx, runner, "Degraded", metav1.ConditionFalse, "JobFailed", "Job failed")
|
||||
} else {
|
||||
r.updateStatus(ctx, runner, "Progressing", metav1.ConditionTrue, "JobRunning", "Job is running")
|
||||
runner.Status.ActiveJobName = jobName
|
||||
r.Status().Update(ctx, runner)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// reconcileCronJob handles CronJob creation and management
|
||||
func (r *GitlabJobRunnerReconciler) reconcileCronJob(ctx context.Context, runner *batchv1alpha1.GitlabJobRunner) (ctrl.Result, error) {
|
||||
log := logf.FromContext(ctx)
|
||||
|
||||
cronJobName := runner.Name
|
||||
cronJob := &batchv1.CronJob{}
|
||||
err := r.Get(ctx, types.NamespacedName{Name: cronJobName, Namespace: runner.Namespace}, cronJob)
|
||||
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
cronJob = r.constructCronJob(runner)
|
||||
if err := controllerutil.SetControllerReference(runner, cronJob, r.Scheme); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
if err := r.Create(ctx, cronJob); err != nil {
|
||||
log.Error(err, "unable to create CronJob")
|
||||
r.updateStatus(ctx, runner, "Degraded", metav1.ConditionFalse, "CronJobCreationFailed", err.Error())
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
log.Info("created CronJob", "cronjob", cronJobName)
|
||||
r.updateStatus(ctx, runner, "Available", metav1.ConditionTrue, "CronJobCreated", "CronJob created successfully")
|
||||
return ctrl.Result{}, nil
|
||||
} else if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// CronJob exists, check if update is needed
|
||||
needsUpdate := false
|
||||
if cronJob.Spec.Schedule != runner.Spec.Schedule {
|
||||
cronJob.Spec.Schedule = runner.Spec.Schedule
|
||||
needsUpdate = true
|
||||
}
|
||||
if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend != runner.Spec.Suspend {
|
||||
cronJob.Spec.Suspend = &runner.Spec.Suspend
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
if err := r.Update(ctx, cronJob); err != nil {
|
||||
log.Error(err, "unable to update CronJob")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
log.Info("updated CronJob", "cronjob", cronJobName)
|
||||
}
|
||||
|
||||
// Update status with last schedule time
|
||||
if cronJob.Status.LastScheduleTime != nil {
|
||||
runner.Status.LastScheduleTime = cronJob.Status.LastScheduleTime
|
||||
r.Status().Update(ctx, runner)
|
||||
}
|
||||
|
||||
r.updateStatus(ctx, runner, "Available", metav1.ConditionTrue, "CronJobActive", "CronJob is active")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// constructJob creates a Job specification
|
||||
func (r *GitlabJobRunnerReconciler) constructJob(runner *batchv1alpha1.GitlabJobRunner) *batchv1.Job {
|
||||
image := runner.Spec.Image
|
||||
if image == "" {
|
||||
image = defaultImage
|
||||
}
|
||||
|
||||
branch := runner.Spec.Branch
|
||||
if branch == "" {
|
||||
branch = defaultBranch
|
||||
}
|
||||
|
||||
backoffLimit := int32(3)
|
||||
ttlSecondsAfterFinished := int32(3600)
|
||||
|
||||
job := &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: runner.Name,
|
||||
Namespace: runner.Namespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/name": "gitlabjobrunner",
|
||||
"app.kubernetes.io/instance": runner.Name,
|
||||
"app.kubernetes.io/managed-by": "gitlabjobrunner-controller",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
BackoffLimit: &backoffLimit,
|
||||
TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: r.constructPodSpec(runner, image, branch),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
// constructCronJob creates a CronJob specification
|
||||
func (r *GitlabJobRunnerReconciler) constructCronJob(runner *batchv1alpha1.GitlabJobRunner) *batchv1.CronJob {
|
||||
image := runner.Spec.Image
|
||||
if image == "" {
|
||||
image = defaultImage
|
||||
}
|
||||
|
||||
branch := runner.Spec.Branch
|
||||
if branch == "" {
|
||||
branch = defaultBranch
|
||||
}
|
||||
|
||||
concurrencyPolicy := batchv1.ForbidConcurrent
|
||||
if runner.Spec.ConcurrencyPolicy != "" {
|
||||
switch runner.Spec.ConcurrencyPolicy {
|
||||
case "Allow":
|
||||
concurrencyPolicy = batchv1.AllowConcurrent
|
||||
case "Replace":
|
||||
concurrencyPolicy = batchv1.ReplaceConcurrent
|
||||
}
|
||||
}
|
||||
|
||||
successfulJobsHistoryLimit := int32(3)
|
||||
failedJobsHistoryLimit := int32(1)
|
||||
|
||||
cronJob := &batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: runner.Name,
|
||||
Namespace: runner.Namespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/name": "gitlabjobrunner",
|
||||
"app.kubernetes.io/instance": runner.Name,
|
||||
"app.kubernetes.io/managed-by": "gitlabjobrunner-controller",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: runner.Spec.Schedule,
|
||||
ConcurrencyPolicy: concurrencyPolicy,
|
||||
Suspend: &runner.Spec.Suspend,
|
||||
SuccessfulJobsHistoryLimit: &successfulJobsHistoryLimit,
|
||||
FailedJobsHistoryLimit: &failedJobsHistoryLimit,
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: r.constructPodSpec(runner, image, branch),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return cronJob
|
||||
}
|
||||
|
||||
// constructPodSpec creates the pod specification for Job/CronJob
|
||||
func (r *GitlabJobRunnerReconciler) constructPodSpec(runner *batchv1alpha1.GitlabJobRunner, image, branch string) corev1.PodSpec {
|
||||
serviceAccount := runner.Spec.ServiceAccountName
|
||||
if serviceAccount == "" {
|
||||
serviceAccount = "default"
|
||||
}
|
||||
|
||||
return corev1.PodSpec{
|
||||
ServiceAccountName: serviceAccount,
|
||||
RestartPolicy: corev1.RestartPolicyOnFailure,
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "git-clone",
|
||||
Image: image,
|
||||
Command: []string{
|
||||
"sh",
|
||||
"-c",
|
||||
fmt.Sprintf(`
|
||||
git clone -b %s --depth 1 https://oauth2:$GITLAB_TOKEN@$(echo $GITLAB_URL | sed 's|https://||')/%s /workspace
|
||||
`, branch, runner.Spec.RepositoryURL),
|
||||
},
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: "GITLAB_URL",
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: runner.Spec.GitlabSecretRef,
|
||||
},
|
||||
Key: "GITLAB_URL",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "GITLAB_TOKEN",
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: runner.Spec.GitlabSecretRef,
|
||||
},
|
||||
Key: "GITLAB_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "workspace",
|
||||
MountPath: "/workspace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "executor",
|
||||
Image: "bitnami/kubectl:latest",
|
||||
Command: []string{
|
||||
"sh",
|
||||
"-c",
|
||||
fmt.Sprintf("cd /workspace && sh %s", runner.Spec.ScriptPath),
|
||||
},
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "workspace",
|
||||
MountPath: "/workspace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "workspace",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// updateStatus updates the status conditions
|
||||
func (r *GitlabJobRunnerReconciler) updateStatus(ctx context.Context, runner *batchv1alpha1.GitlabJobRunner, condType string, status metav1.ConditionStatus, reason, message string) {
|
||||
condition := metav1.Condition{
|
||||
Type: condType,
|
||||
Status: status,
|
||||
ObservedGeneration: runner.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
}
|
||||
|
||||
// Remove existing condition of the same type
|
||||
for i, c := range runner.Status.Conditions {
|
||||
if c.Type == condType {
|
||||
runner.Status.Conditions = append(runner.Status.Conditions[:i], runner.Status.Conditions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
runner.Status.Conditions = append(runner.Status.Conditions, condition)
|
||||
r.Status().Update(ctx, runner)
|
||||
}
|
||||
|
||||
// cleanupResources handles cleanup when GitlabJobRunner is deleted
|
||||
func (r *GitlabJobRunnerReconciler) cleanupResources(ctx context.Context, runner *batchv1alpha1.GitlabJobRunner) error {
|
||||
log := logf.FromContext(ctx)
|
||||
|
||||
// Delete associated Job if exists
|
||||
job := &batchv1.Job{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: runner.Name, Namespace: runner.Namespace}, job); err == nil {
|
||||
if err := r.Delete(ctx, job); err != nil {
|
||||
log.Error(err, "unable to delete Job")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Delete associated CronJob if exists
|
||||
cronJob := &batchv1.CronJob{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: runner.Name, Namespace: runner.Namespace}, cronJob); err == nil {
|
||||
if err := r.Delete(ctx, cronJob); err != nil {
|
||||
log.Error(err, "unable to delete CronJob")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *GitlabJobRunnerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&batchv1alpha1.GitlabJobRunner{}).
|
||||
Owns(&batchv1.Job{}).
|
||||
Owns(&batchv1.CronJob{}).
|
||||
Named("gitlabjobrunner").
|
||||
Complete(r)
|
||||
}
|
||||
84
internal/controller/gitlabjobrunner_controller_test.go
Normal file
84
internal/controller/gitlabjobrunner_controller_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
batchv1alpha1 "github.com/rml/gitlab-job-runner/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ = Describe("GitlabJobRunner Controller", func() {
|
||||
Context("When reconciling a resource", func() {
|
||||
const resourceName = "test-resource"
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
typeNamespacedName := types.NamespacedName{
|
||||
Name: resourceName,
|
||||
Namespace: "default", // TODO(user):Modify as needed
|
||||
}
|
||||
gitlabjobrunner := &batchv1alpha1.GitlabJobRunner{}
|
||||
|
||||
BeforeEach(func() {
|
||||
By("creating the custom resource for the Kind GitlabJobRunner")
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, gitlabjobrunner)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
resource := &batchv1alpha1.GitlabJobRunner{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resourceName,
|
||||
Namespace: "default",
|
||||
},
|
||||
// TODO(user): Specify other spec details if needed.
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
||||
resource := &batchv1alpha1.GitlabJobRunner{}
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Cleanup the specific resource instance GitlabJobRunner")
|
||||
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||
})
|
||||
It("should successfully reconcile the resource", func() {
|
||||
By("Reconciling the created resource")
|
||||
controllerReconciler := &GitlabJobRunnerReconciler{
|
||||
Client: k8sClient,
|
||||
Scheme: k8sClient.Scheme(),
|
||||
}
|
||||
|
||||
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
|
||||
// Example: If you expect a certain status condition after reconciliation, verify it here.
|
||||
})
|
||||
})
|
||||
})
|
||||
116
internal/controller/suite_test.go
Normal file
116
internal/controller/suite_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"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"
|
||||
|
||||
batchv1alpha1 "github.com/rml/gitlab-job-runner/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 (
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
testEnv *envtest.Environment
|
||||
cfg *rest.Config
|
||||
k8sClient client.Client
|
||||
)
|
||||
|
||||
func TestControllers(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecs(t, "Controller Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
|
||||
|
||||
ctx, cancel = context.WithCancel(context.TODO())
|
||||
|
||||
var err error
|
||||
err = batchv1alpha1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// +kubebuilder:scaffold:scheme
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
|
||||
ErrorIfCRDPathMissing: true,
|
||||
}
|
||||
|
||||
// Retrieve the first found binary directory to allow running tests from IDEs
|
||||
if getFirstFoundEnvTestBinaryDir() != "" {
|
||||
testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
|
||||
}
|
||||
|
||||
// cfg is defined in this file globally.
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cfg).NotTo(BeNil())
|
||||
|
||||
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")
|
||||
cancel()
|
||||
err := testEnv.Stop()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
|
||||
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
|
||||
// controller-runtime. When running tests directly (e.g., via an IDE) without using
|
||||
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
|
||||
//
|
||||
// This function streamlines the process by finding the required binaries, similar to
|
||||
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
|
||||
// properly set up, run 'make setup-envtest' beforehand.
|
||||
func getFirstFoundEnvTestBinaryDir() string {
|
||||
basePath := filepath.Join("..", "..", "bin", "k8s")
|
||||
entries, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
logf.Log.Error(err, "Failed to read directory", "path", basePath)
|
||||
return ""
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
return filepath.Join(basePath, entry.Name())
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
92
test/e2e/e2e_suite_test.go
Normal file
92
test/e2e/e2e_suite_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
//go:build e2e
|
||||
// +build e2e
|
||||
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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 e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/rml/gitlab-job-runner/test/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
// Optional Environment Variables:
|
||||
// - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup.
|
||||
// These variables are useful if CertManager is already installed, avoiding
|
||||
// re-installation and conflicts.
|
||||
skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true"
|
||||
// isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster
|
||||
isCertManagerAlreadyInstalled = false
|
||||
|
||||
// projectImage is the name of the image which will be build and loaded
|
||||
// with the code source changes to be tested.
|
||||
projectImage = "example.com/cronjob-restore:v0.0.1"
|
||||
)
|
||||
|
||||
// TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated,
|
||||
// temporary environment to validate project changes with the purpose of being used in CI jobs.
|
||||
// The default setup requires Kind, builds/loads the Manager Docker image locally, and installs
|
||||
// CertManager.
|
||||
func TestE2E(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "Starting cronjob-restore integration test suite\n")
|
||||
RunSpecs(t, "e2e suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
By("building the manager(Operator) image")
|
||||
cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectImage))
|
||||
_, err := utils.Run(cmd)
|
||||
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager(Operator) image")
|
||||
|
||||
// TODO(user): If you want to change the e2e test vendor from Kind, ensure the image is
|
||||
// built and available before running the tests. Also, remove the following block.
|
||||
By("loading the manager(Operator) image on Kind")
|
||||
err = utils.LoadImageToKindClusterWithName(projectImage)
|
||||
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind")
|
||||
|
||||
// The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing.
|
||||
// To prevent errors when tests run in environments with CertManager already installed,
|
||||
// we check for its presence before execution.
|
||||
// Setup CertManager before the suite if not skipped and if not already installed
|
||||
if !skipCertManagerInstall {
|
||||
By("checking if cert manager is installed already")
|
||||
isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled()
|
||||
if !isCertManagerAlreadyInstalled {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n")
|
||||
Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager")
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
// Teardown CertManager after the suite if not skipped and if it was not already installed
|
||||
if !skipCertManagerInstall && !isCertManagerAlreadyInstalled {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n")
|
||||
utils.UninstallCertManager()
|
||||
}
|
||||
})
|
||||
337
test/e2e/e2e_test.go
Normal file
337
test/e2e/e2e_test.go
Normal file
@@ -0,0 +1,337 @@
|
||||
//go:build e2e
|
||||
// +build e2e
|
||||
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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 e2e
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/rml/gitlab-job-runner/test/utils"
|
||||
)
|
||||
|
||||
// namespace where the project is deployed in
|
||||
const namespace = "cronjob-restore-system"
|
||||
|
||||
// serviceAccountName created for the project
|
||||
const serviceAccountName = "cronjob-restore-controller-manager"
|
||||
|
||||
// metricsServiceName is the name of the metrics service of the project
|
||||
const metricsServiceName = "cronjob-restore-controller-manager-metrics-service"
|
||||
|
||||
// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
|
||||
const metricsRoleBindingName = "cronjob-restore-metrics-binding"
|
||||
|
||||
var _ = Describe("Manager", Ordered, func() {
|
||||
var controllerPodName string
|
||||
|
||||
// Before running the tests, set up the environment by creating the namespace,
|
||||
// enforce the restricted security policy to the namespace, installing CRDs,
|
||||
// and deploying the controller.
|
||||
BeforeAll(func() {
|
||||
By("creating manager namespace")
|
||||
cmd := exec.Command("kubectl", "create", "ns", namespace)
|
||||
_, err := utils.Run(cmd)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create namespace")
|
||||
|
||||
By("labeling the namespace to enforce the restricted security policy")
|
||||
cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace,
|
||||
"pod-security.kubernetes.io/enforce=restricted")
|
||||
_, err = utils.Run(cmd)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy")
|
||||
|
||||
By("installing CRDs")
|
||||
cmd = exec.Command("make", "install")
|
||||
_, err = utils.Run(cmd)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs")
|
||||
|
||||
By("deploying the controller-manager")
|
||||
cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage))
|
||||
_, err = utils.Run(cmd)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager")
|
||||
})
|
||||
|
||||
// After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
|
||||
// and deleting the namespace.
|
||||
AfterAll(func() {
|
||||
By("cleaning up the curl pod for metrics")
|
||||
cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
|
||||
_, _ = utils.Run(cmd)
|
||||
|
||||
By("undeploying the controller-manager")
|
||||
cmd = exec.Command("make", "undeploy")
|
||||
_, _ = utils.Run(cmd)
|
||||
|
||||
By("uninstalling CRDs")
|
||||
cmd = exec.Command("make", "uninstall")
|
||||
_, _ = utils.Run(cmd)
|
||||
|
||||
By("removing manager namespace")
|
||||
cmd = exec.Command("kubectl", "delete", "ns", namespace)
|
||||
_, _ = utils.Run(cmd)
|
||||
})
|
||||
|
||||
// After each test, check for failures and collect logs, events,
|
||||
// and pod descriptions for debugging.
|
||||
AfterEach(func() {
|
||||
specReport := CurrentSpecReport()
|
||||
if specReport.Failed() {
|
||||
By("Fetching controller manager pod logs")
|
||||
cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
|
||||
controllerLogs, err := utils.Run(cmd)
|
||||
if err == nil {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
|
||||
}
|
||||
|
||||
By("Fetching Kubernetes events")
|
||||
cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp")
|
||||
eventsOutput, err := utils.Run(cmd)
|
||||
if err == nil {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err)
|
||||
}
|
||||
|
||||
By("Fetching curl-metrics logs")
|
||||
cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
|
||||
metricsOutput, err := utils.Run(cmd)
|
||||
if err == nil {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err)
|
||||
}
|
||||
|
||||
By("Fetching controller manager pod description")
|
||||
cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace)
|
||||
podDescription, err := utils.Run(cmd)
|
||||
if err == nil {
|
||||
fmt.Println("Pod description:\n", podDescription)
|
||||
} else {
|
||||
fmt.Println("Failed to describe controller pod")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
SetDefaultEventuallyTimeout(2 * time.Minute)
|
||||
SetDefaultEventuallyPollingInterval(time.Second)
|
||||
|
||||
Context("Manager", func() {
|
||||
It("should run successfully", func() {
|
||||
By("validating that the controller-manager pod is running as expected")
|
||||
verifyControllerUp := func(g Gomega) {
|
||||
// Get the name of the controller-manager pod
|
||||
cmd := exec.Command("kubectl", "get",
|
||||
"pods", "-l", "control-plane=controller-manager",
|
||||
"-o", "go-template={{ range .items }}"+
|
||||
"{{ if not .metadata.deletionTimestamp }}"+
|
||||
"{{ .metadata.name }}"+
|
||||
"{{ \"\\n\" }}{{ end }}{{ end }}",
|
||||
"-n", namespace,
|
||||
)
|
||||
|
||||
podOutput, err := utils.Run(cmd)
|
||||
g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
|
||||
podNames := utils.GetNonEmptyLines(podOutput)
|
||||
g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
|
||||
controllerPodName = podNames[0]
|
||||
g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))
|
||||
|
||||
// Validate the pod's status
|
||||
cmd = exec.Command("kubectl", "get",
|
||||
"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
|
||||
"-n", namespace,
|
||||
)
|
||||
output, err := utils.Run(cmd)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status")
|
||||
}
|
||||
Eventually(verifyControllerUp).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should ensure the metrics endpoint is serving metrics", func() {
|
||||
By("creating a ClusterRoleBinding for the service account to allow access to metrics")
|
||||
cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
|
||||
"--clusterrole=cronjob-restore-metrics-reader",
|
||||
fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
|
||||
)
|
||||
_, err := utils.Run(cmd)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")
|
||||
|
||||
By("validating that the metrics service is available")
|
||||
cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
|
||||
_, err = utils.Run(cmd)
|
||||
Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")
|
||||
|
||||
By("getting the service account token")
|
||||
token, err := serviceAccountToken()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(token).NotTo(BeEmpty())
|
||||
|
||||
By("ensuring the controller pod is ready")
|
||||
verifyControllerPodReady := func(g Gomega) {
|
||||
cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace,
|
||||
"-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}")
|
||||
output, err := utils.Run(cmd)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(output).To(Equal("True"), "Controller pod not ready")
|
||||
}
|
||||
Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed())
|
||||
|
||||
By("verifying that the controller manager is serving the metrics server")
|
||||
verifyMetricsServerStarted := func(g Gomega) {
|
||||
cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
|
||||
output, err := utils.Run(cmd)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(output).To(ContainSubstring("Serving metrics server"),
|
||||
"Metrics server not yet started")
|
||||
}
|
||||
Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed())
|
||||
|
||||
// +kubebuilder:scaffold:e2e-metrics-webhooks-readiness
|
||||
|
||||
By("creating the curl-metrics pod to access the metrics endpoint")
|
||||
cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
|
||||
"--namespace", namespace,
|
||||
"--image=curlimages/curl:latest",
|
||||
"--overrides",
|
||||
fmt.Sprintf(`{
|
||||
"spec": {
|
||||
"containers": [{
|
||||
"name": "curl",
|
||||
"image": "curlimages/curl:latest",
|
||||
"command": ["/bin/sh", "-c"],
|
||||
"args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"],
|
||||
"securityContext": {
|
||||
"readOnlyRootFilesystem": true,
|
||||
"allowPrivilegeEscalation": false,
|
||||
"capabilities": {
|
||||
"drop": ["ALL"]
|
||||
},
|
||||
"runAsNonRoot": true,
|
||||
"runAsUser": 1000,
|
||||
"seccompProfile": {
|
||||
"type": "RuntimeDefault"
|
||||
}
|
||||
}
|
||||
}],
|
||||
"serviceAccountName": "%s"
|
||||
}
|
||||
}`, token, metricsServiceName, namespace, serviceAccountName))
|
||||
_, err = utils.Run(cmd)
|
||||
Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")
|
||||
|
||||
By("waiting for the curl-metrics pod to complete.")
|
||||
verifyCurlUp := func(g Gomega) {
|
||||
cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
|
||||
"-o", "jsonpath={.status.phase}",
|
||||
"-n", namespace)
|
||||
output, err := utils.Run(cmd)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status")
|
||||
}
|
||||
Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed())
|
||||
|
||||
By("getting the metrics by checking curl-metrics logs")
|
||||
verifyMetricsAvailable := func(g Gomega) {
|
||||
metricsOutput, err := getMetricsOutput()
|
||||
g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
|
||||
g.Expect(metricsOutput).NotTo(BeEmpty())
|
||||
g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
|
||||
}
|
||||
Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed())
|
||||
})
|
||||
|
||||
// +kubebuilder:scaffold:e2e-webhooks-checks
|
||||
|
||||
// TODO: Customize the e2e test suite with scenarios specific to your project.
|
||||
// Consider applying sample/CR(s) and check their status and/or verifying
|
||||
// the reconciliation by using the metrics, i.e.:
|
||||
// metricsOutput, err := getMetricsOutput()
|
||||
// Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
|
||||
// Expect(metricsOutput).To(ContainSubstring(
|
||||
// fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
|
||||
// strings.ToLower(<Kind>),
|
||||
// ))
|
||||
})
|
||||
})
|
||||
|
||||
// serviceAccountToken returns a token for the specified service account in the given namespace.
|
||||
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
|
||||
// and parsing the resulting token from the API response.
|
||||
func serviceAccountToken() (string, error) {
|
||||
const tokenRequestRawString = `{
|
||||
"apiVersion": "authentication.k8s.io/v1",
|
||||
"kind": "TokenRequest"
|
||||
}`
|
||||
|
||||
// Temporary file to store the token request
|
||||
secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
|
||||
tokenRequestFile := filepath.Join("/tmp", secretName)
|
||||
err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var out string
|
||||
verifyTokenCreation := func(g Gomega) {
|
||||
// Execute kubectl command to create the token
|
||||
cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
|
||||
"/api/v1/namespaces/%s/serviceaccounts/%s/token",
|
||||
namespace,
|
||||
serviceAccountName,
|
||||
), "-f", tokenRequestFile)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Parse the JSON output to extract the token
|
||||
var token tokenRequest
|
||||
err = json.Unmarshal(output, &token)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
out = token.Status.Token
|
||||
}
|
||||
Eventually(verifyTokenCreation).Should(Succeed())
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
|
||||
func getMetricsOutput() (string, error) {
|
||||
By("getting the curl-metrics logs")
|
||||
cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
|
||||
return utils.Run(cmd)
|
||||
}
|
||||
|
||||
// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
|
||||
// containing only the token field that we need to extract.
|
||||
type tokenRequest struct {
|
||||
Status struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"status"`
|
||||
}
|
||||
226
test/utils/utils.go
Normal file
226
test/utils/utils.go
Normal file
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
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 utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck
|
||||
)
|
||||
|
||||
const (
|
||||
certmanagerVersion = "v1.19.1"
|
||||
certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"
|
||||
|
||||
defaultKindBinary = "kind"
|
||||
defaultKindCluster = "kind"
|
||||
)
|
||||
|
||||
func warnError(err error) {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
|
||||
}
|
||||
|
||||
// Run executes the provided command within this context
|
||||
func Run(cmd *exec.Cmd) (string, error) {
|
||||
dir, _ := GetProjectDir()
|
||||
cmd.Dir = dir
|
||||
|
||||
if err := os.Chdir(cmd.Dir); err != nil {
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err)
|
||||
}
|
||||
|
||||
cmd.Env = append(os.Environ(), "GO111MODULE=on")
|
||||
command := strings.Join(cmd.Args, " ")
|
||||
_, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err)
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// UninstallCertManager uninstalls the cert manager
|
||||
func UninstallCertManager() {
|
||||
url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
|
||||
cmd := exec.Command("kubectl", "delete", "-f", url)
|
||||
if _, err := Run(cmd); err != nil {
|
||||
warnError(err)
|
||||
}
|
||||
|
||||
// Delete leftover leases in kube-system (not cleaned by default)
|
||||
kubeSystemLeases := []string{
|
||||
"cert-manager-cainjector-leader-election",
|
||||
"cert-manager-controller",
|
||||
}
|
||||
for _, lease := range kubeSystemLeases {
|
||||
cmd = exec.Command("kubectl", "delete", "lease", lease,
|
||||
"-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0")
|
||||
if _, err := Run(cmd); err != nil {
|
||||
warnError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InstallCertManager installs the cert manager bundle.
|
||||
func InstallCertManager() error {
|
||||
url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
|
||||
cmd := exec.Command("kubectl", "apply", "-f", url)
|
||||
if _, err := Run(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
// Wait for cert-manager-webhook to be ready, which can take time if cert-manager
|
||||
// was re-installed after uninstalling on a cluster.
|
||||
cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
|
||||
"--for", "condition=Available",
|
||||
"--namespace", "cert-manager",
|
||||
"--timeout", "5m",
|
||||
)
|
||||
|
||||
_, err := Run(cmd)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed
|
||||
// by verifying the existence of key CRDs related to Cert Manager.
|
||||
func IsCertManagerCRDsInstalled() bool {
|
||||
// List of common Cert Manager CRDs
|
||||
certManagerCRDs := []string{
|
||||
"certificates.cert-manager.io",
|
||||
"issuers.cert-manager.io",
|
||||
"clusterissuers.cert-manager.io",
|
||||
"certificaterequests.cert-manager.io",
|
||||
"orders.acme.cert-manager.io",
|
||||
"challenges.acme.cert-manager.io",
|
||||
}
|
||||
|
||||
// Execute the kubectl command to get all CRDs
|
||||
cmd := exec.Command("kubectl", "get", "crds")
|
||||
output, err := Run(cmd)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if any of the Cert Manager CRDs are present
|
||||
crdList := GetNonEmptyLines(output)
|
||||
for _, crd := range certManagerCRDs {
|
||||
for _, line := range crdList {
|
||||
if strings.Contains(line, crd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// LoadImageToKindClusterWithName loads a local docker image to the kind cluster
|
||||
func LoadImageToKindClusterWithName(name string) error {
|
||||
cluster := defaultKindCluster
|
||||
if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
|
||||
cluster = v
|
||||
}
|
||||
kindOptions := []string{"load", "docker-image", name, "--name", cluster}
|
||||
kindBinary := defaultKindBinary
|
||||
if v, ok := os.LookupEnv("KIND"); ok {
|
||||
kindBinary = v
|
||||
}
|
||||
cmd := exec.Command(kindBinary, kindOptions...)
|
||||
_, err := Run(cmd)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetNonEmptyLines converts given command output string into individual objects
|
||||
// according to line breakers, and ignores the empty elements in it.
|
||||
func GetNonEmptyLines(output string) []string {
|
||||
var res []string
|
||||
elements := strings.Split(output, "\n")
|
||||
for _, element := range elements {
|
||||
if element != "" {
|
||||
res = append(res, element)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// GetProjectDir will return the directory where the project is
|
||||
func GetProjectDir() (string, error) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return wd, fmt.Errorf("failed to get current working directory: %w", err)
|
||||
}
|
||||
wd = strings.ReplaceAll(wd, "/test/e2e", "")
|
||||
return wd, nil
|
||||
}
|
||||
|
||||
// UncommentCode searches for target in the file and remove the comment prefix
|
||||
// of the target content. The target content may span multiple lines.
|
||||
func UncommentCode(filename, target, prefix string) error {
|
||||
// false positive
|
||||
// nolint:gosec
|
||||
content, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file %q: %w", filename, err)
|
||||
}
|
||||
strContent := string(content)
|
||||
|
||||
idx := strings.Index(strContent, target)
|
||||
if idx < 0 {
|
||||
return fmt.Errorf("unable to find the code %q to be uncomment", target)
|
||||
}
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
_, err = out.Write(content[:idx])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to output: %w", err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewBufferString(target))
|
||||
if !scanner.Scan() {
|
||||
return nil
|
||||
}
|
||||
for {
|
||||
if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil {
|
||||
return fmt.Errorf("failed to write to output: %w", err)
|
||||
}
|
||||
// Avoid writing a newline in case the previous line was the last in target.
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
if _, err = out.WriteString("\n"); err != nil {
|
||||
return fmt.Errorf("failed to write to output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = out.Write(content[idx+len(target):]); err != nil {
|
||||
return fmt.Errorf("failed to write to output: %w", err)
|
||||
}
|
||||
|
||||
// false positive
|
||||
// nolint:gosec
|
||||
if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write file %q: %w", filename, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user