mirror of
https://github.com/lukaszraczylo/jobs-manager-operator.git
synced 2026-10-12 05:32:52 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d066c1349 | ||
|
|
794e2d487a | ||
|
|
fee9f74aad | ||
|
|
005df83478 | ||
|
|
836be577eb | ||
|
|
f96fd7843e | ||
|
|
2b36071647 | ||
|
|
b6ce5b7c98 | ||
|
|
ba6e43acfe | ||
|
|
6f9644bdfa | ||
|
|
c604c16aa0 | ||
|
|
925f8a1592 | ||
|
|
7f82327e1b | ||
|
|
97f4007a6a | ||
|
|
79c124816e | ||
|
|
cb4a008e48 | ||
|
|
10648997ad | ||
|
|
bded5b0250 | ||
|
|
7500a71056 | ||
|
|
0a062b6ad7 | ||
|
|
8799456499 | ||
|
|
d3c5cc6ccd | ||
|
|
8bdccb4b8d | ||
|
|
554019b1cc | ||
|
|
41421ea889 | ||
|
|
7e4ea52abc | ||
|
|
76f25e0746 | ||
|
|
a20157681e | ||
|
|
e037788677 | ||
|
|
e39c93e9ea | ||
|
|
785a6adbd4 | ||
|
|
35b9efc0e1 | ||
|
|
70033ec15b | ||
|
|
372b25111b
|
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Autoupdate go.mod and go.sum
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
autoupdate:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
|
||||
with:
|
||||
go-version: "1.21"
|
||||
release-workflow: "release.yaml"
|
||||
secrets: inherit
|
||||
+4
-1
@@ -8,6 +8,7 @@ on:
|
||||
- 'charts/**'
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
- 'README.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
@@ -15,6 +16,7 @@ on:
|
||||
- 'charts/**'
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
- 'README.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -75,7 +77,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.21
|
||||
- name: Configure git for private modules
|
||||
uses: extractions/netrc@v1
|
||||
with:
|
||||
@@ -98,6 +100,7 @@ jobs:
|
||||
name: "Building docker image"
|
||||
needs: [ prepare, test ]
|
||||
runs-on: ubuntu-20.04
|
||||
if: needs.prepare.outputs.CHECK_IF_MASTER_BRANCH == 'true'
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- "!main"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
pr-checks:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
|
||||
with:
|
||||
go-version: "1.21"
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,76 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '22 9 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "docs/**"
|
||||
- "config/samples/**"
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
packages: write
|
||||
deployments: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
||||
with:
|
||||
go-version: "1.23"
|
||||
docker-enabled: true
|
||||
secrets: inherit
|
||||
|
||||
publish-helm-chart:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get release version
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
|
||||
VERSION=${VERSION#v}
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Trigger helm-charts release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
run: |
|
||||
gh api repos/lukaszraczylo/helm-charts/dispatches \
|
||||
-f event_type=release-chart \
|
||||
-f client_payload[chart_name]=jobs-manager \
|
||||
-f client_payload[version]=${{ steps.version.outputs.version }} \
|
||||
-f client_payload[source_repo]=lukaszraczylo/jobs-manager-operator \
|
||||
-f client_payload[chart_path]=charts/jobs-manager-operator
|
||||
@@ -25,3 +25,4 @@ chart-releaser.yaml
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
dist/
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
version: 2
|
||||
|
||||
project_name: jobs-manager-operator
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
# Update Helm chart version to match release version
|
||||
- 'sed -i.bak ''s/^version:.*/version: {{ .Version }}/'' charts/jobs-manager-operator/Chart.yaml'
|
||||
- 'sed -i.bak ''s/^appVersion:.*/appVersion: "{{ .Version }}"/'' charts/jobs-manager-operator/Chart.yaml'
|
||||
- 'sed -i.bak ''s/tag:.*/tag: "{{ .Version }}"/'' charts/jobs-manager-operator/values.yaml'
|
||||
- rm -f charts/jobs-manager-operator/Chart.yaml.bak charts/jobs-manager-operator/values.yaml.bak
|
||||
|
||||
builds:
|
||||
# Kubernetes operator manager
|
||||
- id: manager
|
||||
main: .
|
||||
binary: manager
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.Version={{.Version}}
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
|
||||
# kubectl plugin for workflow visualization
|
||||
- id: kubectl-managedjob
|
||||
main: ./cmd/kubectl-managedjob
|
||||
binary: kubectl-managedjob
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.Version={{.Version}}
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
|
||||
archives:
|
||||
- id: default
|
||||
formats:
|
||||
- tar.gz
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- .Version }}_
|
||||
{{- .Os }}_
|
||||
{{- .Arch }}
|
||||
files:
|
||||
- README.md
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats:
|
||||
- zip
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
algorithm: sha256
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^chore:'
|
||||
- Merge pull request
|
||||
- Merge branch
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: lukaszraczylo
|
||||
name: jobs-manager-operator
|
||||
draft: false
|
||||
prerelease: auto
|
||||
name_template: "v{{.Version}}"
|
||||
header: |
|
||||
## Jobs Manager Operator v{{.Version}}
|
||||
|
||||
Kubernetes operator for managing complex multi-job workflows.
|
||||
|
||||
### Operator Installation
|
||||
|
||||
**Helm (from repository):**
|
||||
```bash
|
||||
helm repo add jobs-manager https://lukaszraczylo.github.io/helm-charts
|
||||
helm repo update
|
||||
helm install jobs-manager jobs-manager/jobs-manager --version {{.Version}}
|
||||
```
|
||||
|
||||
**Helm (from release asset):**
|
||||
```bash
|
||||
helm install jobs-manager https://github.com/lukaszraczylo/jobs-manager-operator/releases/download/v{{.Version}}/jobs-manager-{{.Version}}.tgz
|
||||
```
|
||||
|
||||
**Docker:**
|
||||
```bash
|
||||
docker pull ghcr.io/lukaszraczylo/jobs-manager-operator:{{.Version}}
|
||||
```
|
||||
|
||||
### kubectl Plugin Installation
|
||||
|
||||
**One-liner (recommended):**
|
||||
```bash
|
||||
curl -sSL https://raw.githubusercontent.com/lukaszraczylo/jobs-manager-operator/main/scripts/install-plugin.sh | bash
|
||||
```
|
||||
|
||||
**Manual:**
|
||||
```bash
|
||||
# Download the archive for your platform from the assets below
|
||||
tar -xzf jobs-manager-operator_{{.Version}}_<os>_<arch>.tar.gz
|
||||
mv kubectl-managedjob /usr/local/bin/
|
||||
chmod +x /usr/local/bin/kubectl-managedjob
|
||||
```
|
||||
|
||||
### Plugin Usage
|
||||
|
||||
```bash
|
||||
kubectl managedjob visualize <workflow-name> -n <namespace>
|
||||
kubectl managedjob visualize <workflow-name> -w # Watch mode
|
||||
kubectl managedjob list -n <namespace>
|
||||
kubectl managedjob status <workflow-name> -n <namespace>
|
||||
```
|
||||
|
||||
dockers_v2:
|
||||
- ids:
|
||||
- manager
|
||||
images:
|
||||
- "ghcr.io/lukaszraczylo/jobs-manager-operator"
|
||||
tags:
|
||||
- "{{ .Version }}"
|
||||
- "latest"
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
dockerfile: Dockerfile.goreleaser
|
||||
labels:
|
||||
"org.opencontainers.image.title": "{{ .ProjectName }}"
|
||||
"org.opencontainers.image.version": "{{ .Version }}"
|
||||
"org.opencontainers.image.source": "https://github.com/lukaszraczylo/jobs-manager-operator"
|
||||
"org.opencontainers.image.description": "Kubernetes operator for managing complex multi-job workflows"
|
||||
|
||||
signs:
|
||||
- cmd: cosign
|
||||
signature: "${artifact}.sigstore.json"
|
||||
args:
|
||||
- sign-blob
|
||||
- "--bundle=${signature}"
|
||||
- "${artifact}"
|
||||
- "--yes"
|
||||
artifacts: checksum
|
||||
output: true
|
||||
|
||||
docker_signs:
|
||||
- cmd: cosign
|
||||
artifacts: images
|
||||
output: true
|
||||
args:
|
||||
- sign
|
||||
- "${artifact}@${digest}"
|
||||
- "--yes"
|
||||
@@ -0,0 +1 @@
|
||||
* lukaszraczylo
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.19 as builder
|
||||
FROM golang:1.21 as builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Dockerfile for GoReleaser dockers_v2
|
||||
# GoReleaser organizes binaries by platform: linux/amd64/manager, linux/arm64/manager
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
ARG TARGETPLATFORM
|
||||
WORKDIR /
|
||||
COPY ${TARGETPLATFORM}/manager /manager
|
||||
USER 65532:65532
|
||||
ENTRYPOINT ["/manager"]
|
||||
@@ -66,6 +66,17 @@ test: manifests generate fmt vet envtest ## Run tests.
|
||||
build: manifests generate fmt vet ## Build manager binary.
|
||||
go build -o bin/manager main.go
|
||||
|
||||
.PHONY: build-plugin
|
||||
build-plugin: fmt vet ## Build kubectl-managedjob plugin binary.
|
||||
go build -o bin/kubectl-managedjob ./cmd/kubectl-managedjob
|
||||
|
||||
.PHONY: install-plugin
|
||||
install-plugin: build-plugin ## Install kubectl-managedjob plugin to GOPATH/bin.
|
||||
cp bin/kubectl-managedjob $(GOBIN)/kubectl-managedjob
|
||||
|
||||
.PHONY: build-all
|
||||
build-all: build build-plugin ## Build both manager and plugin binaries.
|
||||
|
||||
.PHONY: run
|
||||
run: manifests generate fmt vet ## Run a controller from your host.
|
||||
go run ./main.go
|
||||
@@ -139,7 +150,7 @@ ENVTEST ?= $(LOCALBIN)/setup-envtest
|
||||
|
||||
## Tool Versions
|
||||
KUSTOMIZE_VERSION ?= v3.8.7
|
||||
CONTROLLER_TOOLS_VERSION ?= v0.11.1
|
||||
CONTROLLER_TOOLS_VERSION ?= v0.17.1
|
||||
|
||||
KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
|
||||
.PHONY: kustomize
|
||||
@@ -163,7 +174,7 @@ $(ENVTEST): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
|
||||
|
||||
helmify:
|
||||
$(call go-get-tool,$(HELMIFY),github.com/arttor/helmify/cmd/helmify@v0.3.7)
|
||||
$(call go-get-tool,$(HELMIFY),github.com/arttor/helmify/cmd/helmify@v0.4.10)
|
||||
|
||||
helm: manifests kustomize helmify
|
||||
$(KUSTOMIZE) build config/default | $(HELMIFY)
|
||||
@@ -1,11 +1,56 @@
|
||||
# Kubernetes Jobs Manager Operator
|
||||
|
||||
- [Kubernetes Jobs Manager Operator](#kubernetes-jobs-manager-operator)
|
||||
- [Description](#description)
|
||||
- [Features](#features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Installation with helm](#installation-with-helm)
|
||||
- [Prerequisites for local runs](#prerequisites-for-local-runs)
|
||||
- [Jobs configuration](#jobs-configuration)
|
||||
- [Examples](#examples)
|
||||
- [How does it look in practice?](#how-does-it-look-in-practice)
|
||||
- [Things to remember](#things-to-remember)
|
||||
- [Available params](#available-params)
|
||||
- [Resource limits](#resource-limits)
|
||||
- [Kustomization and references](#kustomization-and-references)
|
||||
- [Running on the cluster](#running-on-the-cluster)
|
||||
- [Manual installation](#manual-installation)
|
||||
- [Manually uninstall CRDs](#manually-uninstall-crds)
|
||||
- [Manually undeploy controller](#manually-undeploy-controller)
|
||||
- [How it works](#how-it-works)
|
||||
- [kubectl Plugin](#kubectl-plugin)
|
||||
- [Observability](#observability)
|
||||
- [Prometheus Metrics](#prometheus-metrics)
|
||||
- [ServiceMonitor](#servicemonitor)
|
||||
- [Configuration](#configuration)
|
||||
- [License](#license)
|
||||
|
||||
|
||||
## Description
|
||||
This operator is responsible for managing the lifecycle of complicated workflows which consist of multiple jobs and making their management easy, without need for dozens of yaml files and doing magic with ordering.
|
||||
|
||||
## Features
|
||||
|
||||
- **Workflow orchestration** - Define complex job workflows with dependency management
|
||||
- **Parameter inheritance** - DRY configuration with params merged from workflow → group → job levels
|
||||
- **Parallel execution** - Run jobs in parallel within groups or sequentially with dependencies
|
||||
- **Prometheus metrics** - Built-in observability with metrics for jobs created/succeeded/failed, active jobs count, and reconciliation duration
|
||||
- **Resource limits** - Configure CPU and memory limits for job containers
|
||||
- **Finalizers** - Proper cleanup of child jobs when ManagedJob resources are deleted
|
||||
- **kubectl plugin** - Visualize job dependency graphs directly from the command line
|
||||
- **O(1) dependency lookup** - Optimized performance for large workflows
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
### Installation with helm
|
||||
|
||||
```sh
|
||||
helm repo add raczylo https://lukaszraczylo.github.io/helm-charts/
|
||||
helm repo update raczylo
|
||||
helm install jobs-manager raczylo/jobs-manager
|
||||
```
|
||||
|
||||
### Prerequisites for local runs
|
||||
- [go](https://golang.org/dl/) v1.16+
|
||||
- [kustomize](https://sigs.k8s.io/kustomize/docs/INSTALL.md) v3.5.4+
|
||||
- [docker](https://docs.docker.com/install/) v19.03.8+
|
||||
@@ -20,7 +65,6 @@ metadata:
|
||||
labels:
|
||||
name: managedjob-sample
|
||||
spec:
|
||||
# Globally defined parameters and environment variables
|
||||
retries: 3
|
||||
params:
|
||||
env:
|
||||
@@ -32,9 +76,7 @@ spec:
|
||||
# Job groups definitions
|
||||
groups:
|
||||
- name: "first-group"
|
||||
# Group will run in parallel with other defined groups
|
||||
parallel: true
|
||||
# Group specific parameters
|
||||
params:
|
||||
env:
|
||||
- name: "FEE"
|
||||
@@ -55,6 +97,11 @@ spec:
|
||||
args:
|
||||
- "sleep"
|
||||
- "10"
|
||||
- name: "second-half-job"
|
||||
image: "busybox"
|
||||
args:
|
||||
- "sleep"
|
||||
- "10"
|
||||
|
||||
- name: "second-group"
|
||||
parallel: true
|
||||
@@ -71,9 +118,54 @@ spec:
|
||||
args:
|
||||
- "sleep"
|
||||
- "10"
|
||||
parallel: false
|
||||
|
||||
- name: "third-group"
|
||||
parallel: false
|
||||
jobs:
|
||||
- name: "fifth-job"
|
||||
image: "busybox"
|
||||
args:
|
||||
- "echo"
|
||||
- "Hello world!"
|
||||
parallel: true
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
More example manifests are available in the [`config/samples/`](config/samples/) directory:
|
||||
|
||||
| Example | Description |
|
||||
|---------|-------------|
|
||||
| [Quick Start](config/samples/managedjob_quickstart.yaml) | Simple "Hello World" with two sequential jobs |
|
||||
| [Parallel Processing](config/samples/managedjob_parallel_example.yaml) | Fan-out/fan-in pattern with parallel job execution |
|
||||
| [Comprehensive Demo](config/samples/managedjob_comprehensive_example.yaml) | Full-featured example showcasing all capabilities: ConfigMaps, Secrets, volumes, resource limits, multi-group dependencies, and parameter inheritance |
|
||||
|
||||
### How does it look in practice?
|
||||
|
||||
```yaml
|
||||
managedjob-sample
|
||||
├── first-group
|
||||
│ ├── first-job
|
||||
│ ├── second-job
|
||||
│ │ └── Depends on: managedjob-sample-first-group-first-job
|
||||
│ └── second-half-job
|
||||
│ ├── Depends on: managedjob-sample-first-group-first-job
|
||||
│ └── Depends on: managedjob-sample-first-group-second-job
|
||||
├── second-group
|
||||
│ ├── third-job
|
||||
│ └── fourth-job
|
||||
│ └── Depends on: managedjob-sample-second-group-third-job
|
||||
└── third-group
|
||||
├── fifth-job
|
||||
├── Depends on group: first-group
|
||||
└── Depends on group: second-group
|
||||
```
|
||||
|
||||
If dependency exists on the group level - the group will not be executed until all of remaining groups have finished successfuly.
|
||||
If dependency exists on the job level - the job will not be executed until all of remaining jobs have finished successfuly.
|
||||
Remember that **ORDER matters**.
|
||||
|
||||
### Things to remember
|
||||
|
||||
Parameters **params** are always merged downwards to DRY your definitions.
|
||||
@@ -100,6 +192,88 @@ In this case - result for the first job will look like this:
|
||||
status: succeeded
|
||||
```
|
||||
|
||||
### Available params
|
||||
|
||||
There's quite a lot of of flexibility with parameters. On every level where params are allowed, you can define:
|
||||
|
||||
```yaml
|
||||
params:
|
||||
fromEnv:
|
||||
- configMapRef:
|
||||
name: "configmap-name"
|
||||
key: "key-name"
|
||||
env:
|
||||
- name: "FOO"
|
||||
value: "bar"
|
||||
volumes:
|
||||
- name: secrets-store-api
|
||||
csi:
|
||||
driver: secrets-store.csi.k8s.io
|
||||
readOnly: true
|
||||
volumeAttributes:
|
||||
secretProviderClass: api-secrets-provider
|
||||
volumeMount:
|
||||
- name: secrets-store-api
|
||||
mountPath: "/mnt/secrets-api"
|
||||
readOnly: true
|
||||
serviceAccount: "service-account-name"
|
||||
restartPolicy: "Never"
|
||||
imagePullSecrets:
|
||||
- "ghcr-token"
|
||||
imagePullPolicy:
|
||||
- "Always"
|
||||
labels:
|
||||
this/works: "true"
|
||||
annotations:
|
||||
this/works/aswell: "true"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
```
|
||||
|
||||
### Resource limits
|
||||
|
||||
You can configure resource limits for job containers at any level (workflow, group, or job). These follow the standard Kubernetes resource requirements:
|
||||
|
||||
```yaml
|
||||
params:
|
||||
resources:
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: "512Mi"
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "128Mi"
|
||||
```
|
||||
|
||||
### Kustomization and references
|
||||
|
||||
In case of any issues with `configmapGenerator` or `secretGenerator`, please add following to your `kustomization.yaml`:
|
||||
|
||||
```yaml
|
||||
configurations:
|
||||
- crd-name-reference.yaml
|
||||
```
|
||||
|
||||
Then you can create `crd-name-reference.yaml` file with following content:
|
||||
|
||||
```yaml
|
||||
---
|
||||
nameReference:
|
||||
- kind: 'ConfigMap'
|
||||
fieldSpecs:
|
||||
- kind: 'ManagedJob'
|
||||
path: 'spec/params/fromEnv[]/configMapRef/name'
|
||||
- kind: 'ManagedJob'
|
||||
path: 'spec/params/env[]/configMapRef/name'
|
||||
```
|
||||
|
||||
This will instruct kustomize to replace all references to configmaps with their names if they are managed by generators.
|
||||
|
||||
### Running on the cluster
|
||||
|
||||
#### Manual installation
|
||||
@@ -142,9 +316,87 @@ This project aims to follow the Kubernetes [Operator pattern](https://kubernetes
|
||||
It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/),
|
||||
which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster.
|
||||
|
||||
## kubectl Plugin
|
||||
|
||||
The operator includes a kubectl plugin for visualizing job dependency graphs. Install it using the provided script:
|
||||
|
||||
```sh
|
||||
curl -sSL https://raw.githubusercontent.com/lukaszraczylo/jobs-manager-operator/main/scripts/install-plugin.sh | bash
|
||||
```
|
||||
|
||||
Once installed, you can visualize ManagedJob workflows:
|
||||
|
||||
```sh
|
||||
# Visualize a ManagedJob workflow as an ASCII tree with status colors
|
||||
kubectl managedjob visualize <managedjob-name> -n <namespace>
|
||||
|
||||
# Watch mode - continuously update the visualization
|
||||
kubectl managedjob visualize <managedjob-name> -w
|
||||
|
||||
# Show all ManagedJobs in a namespace
|
||||
kubectl managedjob list -n <namespace>
|
||||
|
||||
# Show status summary for a specific workflow
|
||||
kubectl managedjob status <managedjob-name> -n <namespace>
|
||||
```
|
||||
|
||||
Status colors in the visualization:
|
||||
- **Green**: succeeded
|
||||
- **Yellow**: running
|
||||
- **Red**: failed
|
||||
- **Gray**: pending
|
||||
|
||||

|
||||
|
||||
## Observability
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
The operator exposes the following Prometheus metrics:
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `managedjob_jobs_created_total` | Counter | namespace, workflow, group | Total number of jobs created |
|
||||
| `managedjob_jobs_succeeded_total` | Counter | namespace, workflow, group | Total number of jobs that succeeded |
|
||||
| `managedjob_jobs_failed_total` | Counter | namespace, workflow, group | Total number of jobs that failed |
|
||||
| `managedjob_active_jobs` | Gauge | namespace, workflow | Current number of active jobs |
|
||||
| `managedjob_reconciliation_duration_seconds` | Histogram | namespace, workflow | Time taken for reconciliation |
|
||||
|
||||
### ServiceMonitor
|
||||
|
||||
If you're using the Prometheus Operator, a ServiceMonitor is included in the Helm chart. Enable it in your values:
|
||||
|
||||
```yaml
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
interval: 30s
|
||||
labels: {}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The operator supports the following configuration options:
|
||||
|
||||
| Flag | Environment Variable | Description | Default |
|
||||
|------|---------------------|-------------|---------|
|
||||
| `--leader-election-id` | - | Custom leader election ID | `jobs-manager-operator` |
|
||||
| `--dev-mode` | - | Enable development logging mode | `false` |
|
||||
| - | `LOG_LEVEL` | Logging level (debug, info, warn, error) | `info` |
|
||||
|
||||
Example Helm values for configuration:
|
||||
|
||||
```yaml
|
||||
controllerManager:
|
||||
manager:
|
||||
leaderElectionId: "my-custom-id"
|
||||
devMode: true
|
||||
env:
|
||||
LOG_LEVEL: "debug"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2023.
|
||||
Copyright 2023-2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -157,4 +409,3 @@ 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.
|
||||
|
||||
|
||||
@@ -21,105 +21,156 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// ManagedJobDependencies defines a dependency relationship between jobs or groups
|
||||
type ManagedJobDependencies struct {
|
||||
// Name is the identifier of the dependency (job or group name)
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=""
|
||||
Group string `json:"group"`
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=""
|
||||
Job string `json:"job"`
|
||||
Name string `json:"name"`
|
||||
// Status tracks the execution status of the dependency
|
||||
// +kubebuilder:validation:Enum=pending;running;succeeded;failed;aborted
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ManagedJobDefinition defines a single job within a group
|
||||
type ManagedJobDefinition struct {
|
||||
// Name is the unique identifier for this job within the group
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:MaxLength=40
|
||||
// +kubebuilder:validation:Pattern=[a-z0-9-]+
|
||||
Name string `json:"name"`
|
||||
// Parallel indicates if this job can run in parallel with others in the group
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=false
|
||||
Parallel bool `json:"parallel"`
|
||||
// Image is the container image to run for this job
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:MinLength=5
|
||||
Image string `json:"image"`
|
||||
// Args are the command-line arguments to pass to the container
|
||||
// +kubebuilder:validation:Optional
|
||||
Args []string `json:"args,omitempty"`
|
||||
// Params contains job-specific parameters that override group and spec-level params
|
||||
// +kubebuilder:validation:Optional
|
||||
Params ManagedJobParameters `json:"params"`
|
||||
// Status tracks the execution status of this job
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=pending
|
||||
// +kubebuilder:validation:Enum=pending;running;succeeded;failed;aborted
|
||||
Status string `json:"status"`
|
||||
// Dependencies lists the jobs that must complete before this job can run
|
||||
// +kubebuilder:validation:Optional
|
||||
// +optional
|
||||
Dependencies ManagedJobDependencies `json:"dependencies"`
|
||||
Dependencies []*ManagedJobDependencies `json:"dependencies"`
|
||||
// CompiledParams contains the merged parameters from spec, group, and job levels
|
||||
// +optional
|
||||
CompiledParams ManagedJobParameters `json:"compiledParams"`
|
||||
}
|
||||
|
||||
// ManagedJobGroup defines a group of jobs that can be executed together
|
||||
type ManagedJobGroup struct {
|
||||
// Name is the unique identifier for this group within the workflow
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:MaxLength=40
|
||||
// +kubebuilder:validation:Pattern=[a-z0-9-]+
|
||||
Name string `json:"name"`
|
||||
// Parallel indicates if this group can run in parallel with other groups
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=false
|
||||
Parallel bool `json:"parallel"`
|
||||
// Jobs is the list of jobs to execute within this group
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:MinItems=1
|
||||
Jobs []*ManagedJobDefinition `json:"jobs"`
|
||||
// Params contains group-level parameters that override spec-level params
|
||||
// +kubebuilder:validation:Optional
|
||||
Params ManagedJobParameters `json:"params"`
|
||||
// Dependencies lists the groups that must complete before this group can run
|
||||
// +kubebuilder:validation:Optional
|
||||
// +optional
|
||||
Dependencies []*ManagedJobDependencies `json:"dependencies"`
|
||||
// Status tracks the execution status of this group
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=pending
|
||||
// +kubebuilder:validation:Enum=pending;running;succeeded;failed;aborted
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ManagedJobParameters defines common parameters that can be set at spec, group, or job level.
|
||||
// Parameters at lower levels override those at higher levels.
|
||||
type ManagedJobParameters struct {
|
||||
// FromEnv specifies environment variable sources (ConfigMaps, Secrets)
|
||||
// +kubebuilder:validation:Optional
|
||||
FromEnv []corev1.EnvFromSource `json:"fromEnv,omitempty"`
|
||||
// Env specifies individual environment variables
|
||||
// +kubebuilder:validation:Optional
|
||||
Env []corev1.EnvVar `json:"env,omitempty"`
|
||||
// Volumes specifies volumes to mount in job pods
|
||||
// +kubebuilder:validation:Optional
|
||||
Volumes []corev1.Volume `json:"volumes,omitempty"`
|
||||
// VolumeMounts specifies where to mount volumes in containers
|
||||
// +kubebuilder:validation:Optional
|
||||
VolumeMounts []corev1.VolumeMount `json:"volumeMount,omitempty"`
|
||||
// ServiceAccount is the Kubernetes service account to use for job pods
|
||||
// +kubebuilder:validation:Optional
|
||||
ServiceAccount string `json:"serviceAccount,omitempty"`
|
||||
// RestartPolicy defines the pod restart policy (Never, OnFailure)
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=OnFailure
|
||||
// +kubebuilder:validation:Enum=Never;OnFailure
|
||||
RestartPolicy string `json:"restartPolicy,omitempty"`
|
||||
// ImagePullSecrets are references to secrets for pulling private images
|
||||
// +kubebuilder:validation:Optional
|
||||
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
||||
// ImagePullPolicy defines when to pull the container image
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:validation:Enum=Always;Never;IfNotPresent
|
||||
ImagePullPolicy string `json:"imagePullPolicy,omitempty"`
|
||||
// Labels are additional labels to apply to job pods
|
||||
// +kubebuilder:validation:Optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Annotations are additional annotations to apply to job pods
|
||||
// +kubebuilder:validation:Optional
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
// Resources specifies compute resources for the job container
|
||||
// +kubebuilder:validation:Optional
|
||||
Resources *corev1.ResourceRequirements `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
// ManagedJobSpec defines the desired state of ManagedJob
|
||||
type ManagedJobSpec struct {
|
||||
// Retries is the number of times to retry failed jobs
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:default=1
|
||||
// +kubebuilder:validation:Minimum=1
|
||||
// +kubebuilder:validation:Minimum=0
|
||||
// +kubebuilder:validation:Maximum=100
|
||||
Retries int `json:"retries"`
|
||||
// Groups is the list of job groups to execute in this workflow
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:MinItems=1
|
||||
Groups []*ManagedJobGroup `json:"groups"`
|
||||
// Params contains spec-level parameters that apply to all jobs
|
||||
// +kubebuilder:validation:Optional
|
||||
Params ManagedJobParameters `json:"params"`
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=pending
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// ManagedJob is the Schema for the managedjobs API
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
|
||||
// ManagedJob is the Schema for the managedjobs API.
|
||||
// It defines a workflow consisting of groups of jobs with dependencies.
|
||||
type ManagedJob struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec defines the desired workflow configuration
|
||||
Spec ManagedJobSpec `json:"spec,omitempty"`
|
||||
// Status tracks the overall execution status of the workflow
|
||||
// +kubebuilder:validation:Optional
|
||||
// +kubebuilder:default=pending
|
||||
// +kubebuilder:validation:Enum=pending;running;succeeded;failed;aborted
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2023.
|
||||
@@ -61,7 +60,17 @@ func (in *ManagedJobDefinition) DeepCopyInto(out *ManagedJobDefinition) {
|
||||
copy(*out, *in)
|
||||
}
|
||||
in.Params.DeepCopyInto(&out.Params)
|
||||
out.Dependencies = in.Dependencies
|
||||
if in.Dependencies != nil {
|
||||
in, out := &in.Dependencies, &out.Dependencies
|
||||
*out = make([]*ManagedJobDependencies, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(ManagedJobDependencies)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
}
|
||||
in.CompiledParams.DeepCopyInto(&out.CompiledParams)
|
||||
}
|
||||
|
||||
@@ -105,6 +114,17 @@ func (in *ManagedJobGroup) DeepCopyInto(out *ManagedJobGroup) {
|
||||
}
|
||||
}
|
||||
in.Params.DeepCopyInto(&out.Params)
|
||||
if in.Dependencies != nil {
|
||||
in, out := &in.Dependencies, &out.Dependencies
|
||||
*out = make([]*ManagedJobDependencies, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(ManagedJobDependencies)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedJobGroup.
|
||||
@@ -192,6 +212,18 @@ func (in *ManagedJobParameters) DeepCopyInto(out *ManagedJobParameters) {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Resources != nil {
|
||||
in, out := &in.Resources, &out.Resources
|
||||
*out = new(v1.ResourceRequirements)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedJobParameters.
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
apiVersion: v2
|
||||
name: jobs-manager
|
||||
description: Kubernetes jobs manager operator
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
description: Kubernetes jobs manager operator for orchestrating workflow-based job execution with dependency management
|
||||
type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.0.2
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "0.0.2"
|
||||
version: 0.0.34
|
||||
appVersion: "0.0.34"
|
||||
keywords:
|
||||
- operator
|
||||
- jobs
|
||||
- tasks
|
||||
- workflow
|
||||
- kubernetes
|
||||
- batch
|
||||
home: https://raczylo.com
|
||||
sources:
|
||||
- https://github.com/lukaszraczylo/jobs-manager-operator
|
||||
maintainers:
|
||||
- name: lukaszraczylo
|
||||
email: [email protected]
|
||||
annotations:
|
||||
artifacthub.io/changes: |
|
||||
- kind: added
|
||||
description: Prometheus metrics support (jobs created/succeeded/failed, active jobs, reconciliation duration)
|
||||
- kind: added
|
||||
description: Configurable leader election ID via --leader-election-id flag
|
||||
- kind: added
|
||||
description: Configurable development logging mode via --dev-mode flag
|
||||
- kind: added
|
||||
description: LOG_LEVEL environment variable support
|
||||
- kind: added
|
||||
description: Finalizers for proper resource cleanup
|
||||
- kind: added
|
||||
description: Resource limits support for job containers
|
||||
- kind: added
|
||||
description: Reconciliation backoff/requeue logic
|
||||
- kind: improved
|
||||
description: O(1) dependency lookup performance optimization
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "chart.fullname" . }}-controller-manager
|
||||
labels:
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: jobs-manager-operator
|
||||
app.kubernetes.io/part-of: jobs-manager-operator
|
||||
{{- include "chart.labels" . | nindent 4 }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -50,38 +40,23 @@ spec:
|
||||
- linux
|
||||
containers:
|
||||
- args:
|
||||
- --secure-listen-address=0.0.0.0:8443
|
||||
- --upstream=http://127.0.0.1:8080/
|
||||
- --logtostderr=true
|
||||
- --v=0
|
||||
env:
|
||||
- name: KUBERNETES_CLUSTER_DOMAIN
|
||||
value: {{ .Values.kubernetesClusterDomain }}
|
||||
image: {{ .Values.controllerManager.kubeRbacProxy.image.repository }}:{{ .Values.controllerManager.kubeRbacProxy.image.tag
|
||||
| default .Chart.AppVersion }}
|
||||
name: kube-rbac-proxy
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
name: https
|
||||
protocol: TCP
|
||||
resources: {{- toYaml .Values.controllerManager.kubeRbacProxy.resources | nindent
|
||||
10 }}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
- args:
|
||||
- --health-probe-bind-address=:8081
|
||||
- --metrics-bind-address=127.0.0.1:8080
|
||||
- --leader-elect
|
||||
{{- toYaml .Values.controllerManager.manager.args | nindent 8 }}
|
||||
{{- if .Values.controllerManager.manager.leaderElectionId }}
|
||||
- --leader-election-id={{ .Values.controllerManager.manager.leaderElectionId }}
|
||||
{{- end }}
|
||||
{{- if .Values.controllerManager.manager.devMode }}
|
||||
- --dev-mode
|
||||
{{- end }}
|
||||
command:
|
||||
- /manager
|
||||
env:
|
||||
- name: KUBERNETES_CLUSTER_DOMAIN
|
||||
value: {{ .Values.kubernetesClusterDomain }}
|
||||
image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag
|
||||
| default .Chart.AppVersion }}
|
||||
value: {{ quote .Values.kubernetesClusterDomain }}
|
||||
{{- if .Values.controllerManager.manager.env.LOG_LEVEL }}
|
||||
- name: LOG_LEVEL
|
||||
value: {{ quote .Values.controllerManager.manager.env.LOG_LEVEL }}
|
||||
{{- end }}
|
||||
image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag | default .Chart.AppVersion }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
@@ -89,20 +64,19 @@ spec:
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
name: manager
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
name: https
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: 8081
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10
|
||||
}}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 }}
|
||||
securityContext: {{- toYaml .Values.controllerManager.manager.containerSecurityContext | nindent 10 }}
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
serviceAccountName: {{ include "chart.fullname" . }}-controller-manager
|
||||
terminationGracePeriodSeconds: 10
|
||||
terminationGracePeriodSeconds: 10
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,30 @@ metadata:
|
||||
labels:
|
||||
{{- include "chart.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- jobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- jobsmanager.raczylo.com
|
||||
resources:
|
||||
|
||||
@@ -3,7 +3,7 @@ kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ include "chart.fullname" . }}-metrics-reader
|
||||
labels:
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: jobs-manager-operator
|
||||
app.kubernetes.io/part-of: jobs-manager-operator
|
||||
{{- include "chart.labels" . | nindent 4 }}
|
||||
|
||||
@@ -3,7 +3,7 @@ kind: Service
|
||||
metadata:
|
||||
name: {{ include "chart.fullname" . }}-controller-manager-metrics-service
|
||||
labels:
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: jobs-manager-operator
|
||||
app.kubernetes.io/part-of: jobs-manager-operator
|
||||
control-plane: controller-manager
|
||||
|
||||
@@ -3,7 +3,7 @@ kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ include "chart.fullname" . }}-proxy-role
|
||||
labels:
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: jobs-manager-operator
|
||||
app.kubernetes.io/part-of: jobs-manager-operator
|
||||
{{- include "chart.labels" . | nindent 4 }}
|
||||
@@ -26,7 +26,7 @@ kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ include "chart.fullname" . }}-proxy-rolebinding
|
||||
labels:
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: jobs-manager-operator
|
||||
app.kubernetes.io/part-of: jobs-manager-operator
|
||||
{{- include "chart.labels" . | nindent 4 }}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "chart.fullname" . }}-controller-manager
|
||||
labels:
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: jobs-manager-operator
|
||||
app.kubernetes.io/part-of: jobs-manager-operator
|
||||
{{- include "chart.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
{{- toYaml .Values.controllerManager.serviceAccount.annotations | nindent 4 }}
|
||||
@@ -0,0 +1,31 @@
|
||||
{{- if .Values.serviceMonitor.enabled }}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: {{ include "chart.fullname" . }}-metrics
|
||||
{{- if .Values.serviceMonitor.namespace }}
|
||||
namespace: {{ .Values.serviceMonitor.namespace }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "chart.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceMonitor.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
endpoints:
|
||||
- path: /metrics
|
||||
port: https
|
||||
scheme: https
|
||||
interval: {{ .Values.serviceMonitor.interval }}
|
||||
scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }}
|
||||
tlsConfig:
|
||||
insecureSkipVerify: true
|
||||
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- {{ .Release.Namespace }}
|
||||
selector:
|
||||
matchLabels:
|
||||
control-plane: controller-manager
|
||||
{{- include "chart.selectorLabels" . | nindent 6 }}
|
||||
{{- end }}
|
||||
@@ -1,19 +1,27 @@
|
||||
controllerManager:
|
||||
kubeRbacProxy:
|
||||
image:
|
||||
repository: gcr.io/kubebuilder/kube-rbac-proxy
|
||||
tag: v0.13.1
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 5m
|
||||
memory: 64Mi
|
||||
manager:
|
||||
# Command line arguments for the manager
|
||||
args:
|
||||
- --health-probe-bind-address=:8081
|
||||
- --metrics-bind-address=:8443
|
||||
- --metrics-secure
|
||||
- --leader-elect
|
||||
# Leader election ID - customize for multi-tenant clusters
|
||||
leaderElectionId: "jobsmanager.raczylo.com"
|
||||
# Enable development mode with verbose logging (console format)
|
||||
devMode: false
|
||||
# Environment variables for the manager container
|
||||
env:
|
||||
# Set to "debug" to enable verbose logging
|
||||
LOG_LEVEL: ""
|
||||
containerSecurityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
image:
|
||||
repository: ghcr.io/lukaszraczylo/jobsmanager
|
||||
tag: 0.0.2
|
||||
repository: ghcr.io/lukaszraczylo/jobs-manager-operator
|
||||
tag: ""
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
@@ -22,7 +30,12 @@ controllerManager:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
replicas: 1
|
||||
serviceAccount:
|
||||
annotations: {}
|
||||
|
||||
kubernetesClusterDomain: cluster.local
|
||||
|
||||
# Metrics service configuration
|
||||
metricsService:
|
||||
ports:
|
||||
- name: https
|
||||
@@ -30,3 +43,15 @@ metricsService:
|
||||
protocol: TCP
|
||||
targetPort: https
|
||||
type: ClusterIP
|
||||
|
||||
# ServiceMonitor for Prometheus Operator integration
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
# Namespace where ServiceMonitor will be created (defaults to release namespace)
|
||||
namespace: ""
|
||||
# Additional labels for ServiceMonitor
|
||||
labels: {}
|
||||
# Scrape interval
|
||||
interval: 30s
|
||||
# Scrape timeout
|
||||
scrapeTimeout: 10s
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"raczylo.com/jobs-manager-operator/pkg/visualization"
|
||||
)
|
||||
|
||||
var (
|
||||
namespace string
|
||||
watch bool
|
||||
interval time.Duration
|
||||
noColor bool
|
||||
kubeconfig string
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "kubectl-managedjob",
|
||||
Short: "Visualize and manage ManagedJob workflows",
|
||||
Long: `A kubectl plugin for visualizing ManagedJob workflows.
|
||||
|
||||
This plugin helps you understand the structure and execution status
|
||||
of your ManagedJob workflows with ASCII tree visualization.`,
|
||||
}
|
||||
|
||||
var visualizeCmd = &cobra.Command{
|
||||
Use: "visualize <name>",
|
||||
Short: "Visualize a ManagedJob workflow tree",
|
||||
Long: `Display a ManagedJob workflow as an ASCII tree with status colors.
|
||||
|
||||
Status colors:
|
||||
- Green: succeeded
|
||||
- Yellow: running
|
||||
- Red: failed
|
||||
- Gray: pending
|
||||
- Magenta: aborted`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runVisualize,
|
||||
}
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all ManagedJobs in a namespace",
|
||||
RunE: runList,
|
||||
}
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status <name>",
|
||||
Short: "Show status summary of a ManagedJob",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runStatus,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Global flags
|
||||
rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "Kubernetes namespace")
|
||||
rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", "", "Path to kubeconfig file")
|
||||
rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "Disable colored output")
|
||||
|
||||
// Visualize flags
|
||||
visualizeCmd.Flags().BoolVarP(&watch, "watch", "w", false, "Watch for changes and refresh")
|
||||
visualizeCmd.Flags().DurationVar(&interval, "interval", 2*time.Second, "Watch refresh interval")
|
||||
|
||||
// Add commands
|
||||
rootCmd.AddCommand(visualizeCmd)
|
||||
rootCmd.AddCommand(listCmd)
|
||||
rootCmd.AddCommand(statusCmd)
|
||||
}
|
||||
|
||||
func runVisualize(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
// Handle no-color flag
|
||||
if noColor {
|
||||
color.NoColor = true
|
||||
}
|
||||
|
||||
// Set KUBECONFIG if provided
|
||||
if kubeconfig != "" {
|
||||
_ = os.Setenv("KUBECONFIG", kubeconfig) // #nosec G104 - env var set failure is extremely rare
|
||||
}
|
||||
|
||||
client, err := visualization.NewClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Handle interrupt
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigCh
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if watch {
|
||||
return watchLoop(ctx, client, name)
|
||||
}
|
||||
|
||||
return visualizeOnce(ctx, client, name)
|
||||
}
|
||||
|
||||
func visualizeOnce(ctx context.Context, client *visualization.Client, name string) error {
|
||||
mj, err := client.GetManagedJob(ctx, name, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tree := visualization.BuildTree(mj)
|
||||
renderer := visualization.NewRenderer(!noColor)
|
||||
fmt.Print(renderer.Render(tree))
|
||||
return nil
|
||||
}
|
||||
|
||||
func watchLoop(ctx context.Context, client *visualization.Client, name string) error {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
// Clear screen and move cursor to top
|
||||
fmt.Print("\033[H\033[2J")
|
||||
|
||||
if err := visualizeOnce(ctx, client, name); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\nWatching %s/%s (Ctrl+C to exit, refreshing every %s)\n", namespace, name, interval)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runList(cmd *cobra.Command, args []string) error {
|
||||
// Handle no-color flag
|
||||
if noColor {
|
||||
color.NoColor = true
|
||||
}
|
||||
|
||||
// Set KUBECONFIG if provided
|
||||
if kubeconfig != "" {
|
||||
_ = os.Setenv("KUBECONFIG", kubeconfig) // #nosec G104 - env var set failure is extremely rare
|
||||
}
|
||||
|
||||
client, err := visualization.NewClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
mjList, err := client.ListManagedJobs(ctx, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(mjList.Items) == 0 {
|
||||
fmt.Printf("No ManagedJobs found in namespace %s\n", namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Print table header
|
||||
fmt.Printf("%-30s %-12s %-8s %-8s\n", "NAME", "STATUS", "GROUPS", "JOBS")
|
||||
fmt.Printf("%-30s %-12s %-8s %-8s\n", "----", "------", "------", "----")
|
||||
|
||||
renderer := visualization.NewRenderer(!noColor)
|
||||
_ = renderer // For potential future color support in list
|
||||
|
||||
for i := range mjList.Items {
|
||||
summary := visualization.GetStatusSummary(&mjList.Items[i])
|
||||
statusStr := formatStatus(summary.Status, !noColor)
|
||||
fmt.Printf("%-30s %-12s %-8d %-8d\n", summary.Name, statusStr, summary.Groups, summary.Jobs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runStatus(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
// Handle no-color flag
|
||||
if noColor {
|
||||
color.NoColor = true
|
||||
}
|
||||
|
||||
// Set KUBECONFIG if provided
|
||||
if kubeconfig != "" {
|
||||
_ = os.Setenv("KUBECONFIG", kubeconfig) // #nosec G104 - env var set failure is extremely rare
|
||||
}
|
||||
|
||||
client, err := visualization.NewClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
mj, err := client.GetManagedJob(ctx, name, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
summary := visualization.GetStatusSummary(mj)
|
||||
|
||||
fmt.Printf("Name: %s\n", summary.Name)
|
||||
fmt.Printf("Namespace: %s\n", summary.Namespace)
|
||||
fmt.Printf("Status: %s\n", formatStatus(summary.Status, !noColor))
|
||||
fmt.Printf("Groups: %d\n", summary.Groups)
|
||||
fmt.Printf("Jobs: %d\n", summary.Jobs)
|
||||
fmt.Println()
|
||||
fmt.Printf("Job Status:\n")
|
||||
fmt.Printf(" Pending: %d\n", summary.Pending)
|
||||
fmt.Printf(" Running: %d\n", summary.Running)
|
||||
fmt.Printf(" Succeeded: %d\n", summary.Succeeded)
|
||||
fmt.Printf(" Failed: %d\n", summary.Failed)
|
||||
fmt.Printf(" Aborted: %d\n", summary.Aborted)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatStatus(status string, useColor bool) string {
|
||||
if !useColor {
|
||||
return status
|
||||
}
|
||||
|
||||
switch status {
|
||||
case visualization.StatusSucceeded:
|
||||
return color.GreenString(status)
|
||||
case visualization.StatusRunning:
|
||||
return color.YellowString(status)
|
||||
case visualization.StatusFailed:
|
||||
return color.RedString(status)
|
||||
case visualization.StatusPending:
|
||||
return color.HiBlackString(status)
|
||||
case visualization.StatusAborted:
|
||||
return color.MagentaString(status)
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# This patch inject a sidecar container which is a HTTP proxy for the
|
||||
# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
|
||||
# This patch configures the controller manager to expose metrics securely
|
||||
# using controller-runtime's built-in authentication and authorization.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -25,31 +25,13 @@ spec:
|
||||
values:
|
||||
- linux
|
||||
containers:
|
||||
- name: kube-rbac-proxy
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- "ALL"
|
||||
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1
|
||||
- name: manager
|
||||
args:
|
||||
- "--secure-listen-address=0.0.0.0:8443"
|
||||
- "--upstream=http://127.0.0.1:8080/"
|
||||
- "--logtostderr=true"
|
||||
- "--v=0"
|
||||
- "--health-probe-bind-address=:8081"
|
||||
- "--metrics-bind-address=:8443"
|
||||
- "--metrics-secure"
|
||||
- "--leader-elect"
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
protocol: TCP
|
||||
name: https
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 5m
|
||||
memory: 64Mi
|
||||
- name: manager
|
||||
args:
|
||||
- "--health-probe-bind-address=:8081"
|
||||
- "--metrics-bind-address=127.0.0.1:8080"
|
||||
- "--leader-elect"
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
resources:
|
||||
- manager.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: service
|
||||
app.kubernetes.io/instance: controller-manager-metrics-service
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: jobs-manager-operator
|
||||
app.kubernetes.io/part-of: jobs-manager-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
|
||||
@@ -9,9 +9,8 @@ resources:
|
||||
- role_binding.yaml
|
||||
- leader_election_role.yaml
|
||||
- leader_election_role_binding.yaml
|
||||
# Comment the following 4 lines if you want to disable
|
||||
# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
|
||||
# which protects your /metrics endpoint.
|
||||
# The following resources configure RBAC for secure metrics
|
||||
# using controller-runtime's built-in authentication/authorization.
|
||||
- auth_proxy_service.yaml
|
||||
- auth_proxy_role.yaml
|
||||
- auth_proxy_role_binding.yaml
|
||||
|
||||
+24
-1
@@ -2,9 +2,32 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: manager-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- jobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- jobsmanager.raczylo.com
|
||||
resources:
|
||||
|
||||
@@ -41,6 +41,11 @@ spec:
|
||||
args:
|
||||
- "sleep"
|
||||
- "10"
|
||||
- name: "second-half-job"
|
||||
image: "busybox"
|
||||
args:
|
||||
- "sleep"
|
||||
- "10"
|
||||
|
||||
- name: "second-group"
|
||||
parallel: true
|
||||
@@ -57,4 +62,14 @@ spec:
|
||||
args:
|
||||
- "sleep"
|
||||
- "10"
|
||||
parallel: false
|
||||
|
||||
- name: "third-group"
|
||||
parallel: false
|
||||
jobs:
|
||||
- name: "fifth-job"
|
||||
image: "busybox"
|
||||
args:
|
||||
- "echo"
|
||||
- "Hello world!"
|
||||
parallel: true
|
||||
@@ -0,0 +1,253 @@
|
||||
# Comprehensive ManagedJob Example
|
||||
# This example demonstrates all capabilities of the jobs-manager-operator
|
||||
#
|
||||
# Workflow Overview:
|
||||
# 1. setup-group: Creates initial configuration (runs first, sequential)
|
||||
# 2. parallel-workers: Multiple parallel jobs that process data
|
||||
# 3. aggregation-group: Depends on parallel-workers completion
|
||||
# 4. notification-group: Final notification job
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: job-config
|
||||
data:
|
||||
APP_NAME: "hello-world-demo"
|
||||
LOG_LEVEL: "info"
|
||||
greeting.txt: |
|
||||
Hello from the jobs-manager-operator!
|
||||
This file was mounted from a ConfigMap.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: job-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
API_KEY: "demo-secret-key-12345"
|
||||
DATABASE_URL: "postgres://user:pass@localhost/demo"
|
||||
---
|
||||
apiVersion: jobsmanager.raczylo.com/v1beta1
|
||||
kind: ManagedJob
|
||||
metadata:
|
||||
name: comprehensive-demo
|
||||
labels:
|
||||
app.kubernetes.io/name: comprehensive-demo
|
||||
app.kubernetes.io/component: demo
|
||||
spec:
|
||||
# Retry failed jobs up to 3 times
|
||||
retries: 3
|
||||
|
||||
# Spec-level parameters - applied to ALL jobs (can be overridden)
|
||||
params:
|
||||
# Environment variables applied globally
|
||||
env:
|
||||
- name: GLOBAL_ENV
|
||||
value: "available-in-all-jobs"
|
||||
- name: WORKFLOW_ID
|
||||
value: "demo-workflow-001"
|
||||
|
||||
# Environment variables from ConfigMap (applied to all jobs)
|
||||
fromEnv:
|
||||
- configMapRef:
|
||||
name: job-config
|
||||
|
||||
# Resource defaults for all jobs
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
|
||||
# Labels and annotations for all job pods
|
||||
labels:
|
||||
managed-by: jobs-manager-operator
|
||||
environment: demo
|
||||
annotations:
|
||||
prometheus.io/scrape: "false"
|
||||
|
||||
# Default restart policy
|
||||
restartPolicy: OnFailure
|
||||
|
||||
# Default image pull policy
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
groups:
|
||||
# =========================================================================
|
||||
# GROUP 1: Setup (Sequential, runs first)
|
||||
# =========================================================================
|
||||
- name: setup-group
|
||||
parallel: false # Jobs in this group run sequentially
|
||||
jobs:
|
||||
- name: init-job
|
||||
image: busybox:1.36
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "=== Initialization Job ==="
|
||||
echo "Workflow ID: $WORKFLOW_ID"
|
||||
echo "Global Env: $GLOBAL_ENV"
|
||||
echo "App Name from ConfigMap: $APP_NAME"
|
||||
echo "Setup complete!"
|
||||
|
||||
- name: validate-config
|
||||
image: busybox:1.36
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "=== Validating Configuration ==="
|
||||
echo "Checking environment variables..."
|
||||
test -n "$APP_NAME" && echo "✓ APP_NAME is set"
|
||||
test -n "$LOG_LEVEL" && echo "✓ LOG_LEVEL is set"
|
||||
echo "Configuration validated!"
|
||||
# This job depends on init-job completing successfully
|
||||
dependencies:
|
||||
- name: init-job
|
||||
status: succeeded
|
||||
|
||||
# =========================================================================
|
||||
# GROUP 2: Parallel Workers (runs after setup-group)
|
||||
# =========================================================================
|
||||
- name: parallel-workers
|
||||
parallel: true # Jobs in this group run in parallel
|
||||
dependencies:
|
||||
- name: setup-group
|
||||
status: succeeded
|
||||
|
||||
# Group-level params - override spec-level for this group
|
||||
params:
|
||||
env:
|
||||
- name: GROUP_NAME
|
||||
value: "parallel-workers"
|
||||
- name: WORKER_MODE
|
||||
value: "parallel"
|
||||
|
||||
jobs:
|
||||
- name: worker-alpha
|
||||
image: busybox:1.36
|
||||
parallel: true
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "=== Worker Alpha ==="
|
||||
echo "Processing task A..."
|
||||
echo "Group: $GROUP_NAME"
|
||||
echo "Mode: $WORKER_MODE"
|
||||
sleep 2
|
||||
echo "Worker Alpha completed!"
|
||||
|
||||
- name: worker-beta
|
||||
image: busybox:1.36
|
||||
parallel: true
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "=== Worker Beta ==="
|
||||
echo "Processing task B..."
|
||||
sleep 2
|
||||
echo "Worker Beta completed!"
|
||||
# Job-level params - override group and spec level
|
||||
params:
|
||||
env:
|
||||
- name: BETA_SPECIFIC
|
||||
value: "only-in-worker-beta"
|
||||
# Custom resource requirements for this job
|
||||
resources:
|
||||
requests:
|
||||
memory: "32Mi"
|
||||
cpu: "25m"
|
||||
limits:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
|
||||
- name: worker-gamma
|
||||
image: busybox:1.36
|
||||
parallel: true
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "=== Worker Gamma ==="
|
||||
echo "Processing task C..."
|
||||
sleep 2
|
||||
echo "Worker Gamma completed!"
|
||||
|
||||
# =========================================================================
|
||||
# GROUP 3: Aggregation (depends on parallel-workers)
|
||||
# =========================================================================
|
||||
- name: aggregation-group
|
||||
parallel: false
|
||||
dependencies:
|
||||
- name: parallel-workers
|
||||
status: succeeded
|
||||
|
||||
# Group-level params with volume mounts
|
||||
params:
|
||||
env:
|
||||
- name: AGGREGATION_MODE
|
||||
value: "combine-results"
|
||||
# Mount ConfigMap as a volume
|
||||
volumes:
|
||||
- name: config-volume
|
||||
configMap:
|
||||
name: job-config
|
||||
volumeMounts:
|
||||
- name: config-volume
|
||||
mountPath: /config
|
||||
readOnly: true
|
||||
|
||||
jobs:
|
||||
- name: aggregate-results
|
||||
image: busybox:1.36
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "=== Aggregating Results ==="
|
||||
echo "Mode: $AGGREGATION_MODE"
|
||||
echo ""
|
||||
echo "Reading mounted ConfigMap file:"
|
||||
cat /config/greeting.txt
|
||||
echo ""
|
||||
echo "All worker results aggregated!"
|
||||
|
||||
# =========================================================================
|
||||
# GROUP 4: Notification (final step)
|
||||
# =========================================================================
|
||||
- name: notification-group
|
||||
parallel: false
|
||||
dependencies:
|
||||
- name: aggregation-group
|
||||
status: succeeded
|
||||
|
||||
# Group-level params with secrets
|
||||
params:
|
||||
fromEnv:
|
||||
- secretRef:
|
||||
name: job-secrets
|
||||
|
||||
jobs:
|
||||
- name: send-notification
|
||||
image: busybox:1.36
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "=== Sending Notification ==="
|
||||
echo "Workflow completed successfully!"
|
||||
echo ""
|
||||
echo "API Key available: $(test -n "$API_KEY" && echo 'yes' || echo 'no')"
|
||||
echo "Database URL available: $(test -n "$DATABASE_URL" && echo 'yes' || echo 'no')"
|
||||
echo ""
|
||||
echo "Final Summary:"
|
||||
echo "- Workflow ID: $WORKFLOW_ID"
|
||||
echo "- App Name: $APP_NAME"
|
||||
echo "- All groups completed!"
|
||||
echo ""
|
||||
echo "🎉 Demo workflow finished successfully!"
|
||||
@@ -0,0 +1,86 @@
|
||||
# Parallel Execution ManagedJob Example
|
||||
# Demonstrates fan-out/fan-in pattern with parallel jobs
|
||||
#
|
||||
# Workflow:
|
||||
# 1. prepare (single job)
|
||||
# 2. process-1, process-2, process-3 (run in parallel)
|
||||
# 3. finalize (waits for all parallel jobs)
|
||||
---
|
||||
apiVersion: jobsmanager.raczylo.com/v1beta1
|
||||
kind: ManagedJob
|
||||
metadata:
|
||||
name: parallel-demo
|
||||
spec:
|
||||
retries: 2
|
||||
params:
|
||||
env:
|
||||
- name: BATCH_ID
|
||||
value: "batch-001"
|
||||
|
||||
groups:
|
||||
# Stage 1: Preparation
|
||||
- name: preparation
|
||||
jobs:
|
||||
- name: prepare
|
||||
image: busybox:1.36
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "Preparing batch: $BATCH_ID"
|
||||
echo "Starting parallel processing..."
|
||||
|
||||
# Stage 2: Parallel Processing (fan-out)
|
||||
- name: processing
|
||||
parallel: true
|
||||
dependencies:
|
||||
- name: preparation
|
||||
status: succeeded
|
||||
jobs:
|
||||
- name: process-1
|
||||
image: busybox:1.36
|
||||
parallel: true
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "Processing chunk 1..."
|
||||
sleep 3
|
||||
echo "Chunk 1 done!"
|
||||
|
||||
- name: process-2
|
||||
image: busybox:1.36
|
||||
parallel: true
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "Processing chunk 2..."
|
||||
sleep 2
|
||||
echo "Chunk 2 done!"
|
||||
|
||||
- name: process-3
|
||||
image: busybox:1.36
|
||||
parallel: true
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "Processing chunk 3..."
|
||||
sleep 4
|
||||
echo "Chunk 3 done!"
|
||||
|
||||
# Stage 3: Finalization (fan-in)
|
||||
- name: finalization
|
||||
dependencies:
|
||||
- name: processing
|
||||
status: succeeded
|
||||
jobs:
|
||||
- name: finalize
|
||||
image: busybox:1.36
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "All parallel jobs completed!"
|
||||
echo "Batch $BATCH_ID finished successfully."
|
||||
@@ -0,0 +1,32 @@
|
||||
# Quick Start ManagedJob Example
|
||||
# A simple example to get started with the jobs-manager-operator
|
||||
#
|
||||
# This demonstrates:
|
||||
# - Basic sequential job execution
|
||||
# - Simple environment variables
|
||||
# - Job dependencies within a group
|
||||
---
|
||||
apiVersion: jobsmanager.raczylo.com/v1beta1
|
||||
kind: ManagedJob
|
||||
metadata:
|
||||
name: hello-world
|
||||
spec:
|
||||
retries: 1
|
||||
|
||||
groups:
|
||||
- name: hello-group
|
||||
jobs:
|
||||
- name: say-hello
|
||||
image: busybox:1.36
|
||||
args:
|
||||
- echo
|
||||
- "Hello, World!"
|
||||
|
||||
- name: say-goodbye
|
||||
image: busybox:1.36
|
||||
args:
|
||||
- echo
|
||||
- "Goodbye, World!"
|
||||
dependencies:
|
||||
- name: say-hello
|
||||
status: succeeded
|
||||
@@ -1,71 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func (cp *connPackage) checkGroupsStatus() {
|
||||
groupsTotal := len(cp.mj.Spec.Groups)
|
||||
totalJobs := 0
|
||||
completedJobs := 0
|
||||
didAnyJobAbort := false
|
||||
changePresent := false
|
||||
groupsCompleted := 0
|
||||
|
||||
// Check if all groups have completed and set ManagedJob status to "succeeded" if so.
|
||||
if cp.mj.Spec.Status != ExecutionStatusSucceeded && groupsTotal > 0 {
|
||||
for _, group := range cp.mj.Spec.Groups {
|
||||
groupJobsTotal := len(group.Jobs)
|
||||
groupJobsCompleted := 0
|
||||
|
||||
for _, job := range group.Jobs {
|
||||
if job.Status == ExecutionStatusSucceeded {
|
||||
groupJobsCompleted++
|
||||
completedJobs++
|
||||
}
|
||||
if job.Status == ExecutionStatusFailed || job.Status == ExecutionStatusAborted {
|
||||
didAnyJobAbort = true
|
||||
}
|
||||
totalJobs++
|
||||
}
|
||||
|
||||
if groupJobsTotal == groupJobsCompleted {
|
||||
// All the jobs in the group are completed.
|
||||
if group.Status != ExecutionStatusSucceeded {
|
||||
group.Status = ExecutionStatusSucceeded
|
||||
changePresent = true
|
||||
}
|
||||
groupsCompleted++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if groupsTotal == groupsCompleted && cp.mj.Spec.Status != ExecutionStatusSucceeded && cp.mj.Status != ExecutionStatusSucceeded {
|
||||
cp.mj.Spec.Status = ExecutionStatusSucceeded
|
||||
changePresent = true
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeNormal, "Completed", "All jobs completed")
|
||||
}
|
||||
|
||||
// Update status if any job aborted.
|
||||
if didAnyJobAbort && cp.mj.Spec.Status != ExecutionStatusFailed {
|
||||
cp.mj.Spec.Status = ExecutionStatusFailed
|
||||
changePresent = true
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeNormal, "Aborted", "One of the jobs aborted")
|
||||
}
|
||||
|
||||
// Update status to "running" if not already set.
|
||||
// if cp.mj.Spec.Status != ExecutionStatusRunning && cp.mj.Spec.Status != ExecutionStatusSucceeded {
|
||||
// cp.mj.Spec.Status = ExecutionStatusRunning
|
||||
// cp.mj.Status = ExecutionStatusRunning
|
||||
// changePresent = true
|
||||
// }
|
||||
|
||||
// Check if the ManagedJob status has changed.
|
||||
statusChanged := cp.mj.Spec.Status != cp.mj.Status
|
||||
|
||||
// Update status and send event if it has changed.
|
||||
if statusChanged || changePresent {
|
||||
cp.updateCRDStatusDirectly()
|
||||
// cp.r.Client.Status().Update(cp.ctx, cp.mj)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/pandati"
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
newLine = "\n"
|
||||
emptySpace = " "
|
||||
middleItem = "├── "
|
||||
continueItem = "│ "
|
||||
lastItem = "└── "
|
||||
)
|
||||
|
||||
func New(text string) Tree {
|
||||
return &tree{
|
||||
text: text,
|
||||
items: []Tree{},
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a node to the tree
|
||||
func (t *tree) Add(text string) Tree {
|
||||
n := New(text)
|
||||
t.items = append(t.items, n)
|
||||
return n
|
||||
}
|
||||
|
||||
// AddTree adds a tree as an item
|
||||
func (t *tree) AddTree(tree Tree) {
|
||||
t.items = append(t.items, tree)
|
||||
}
|
||||
|
||||
// Text returns the node's value
|
||||
func (t *tree) Text() string {
|
||||
return t.text
|
||||
}
|
||||
|
||||
// Items returns all items in the tree
|
||||
func (t *tree) Items() []Tree {
|
||||
return t.items
|
||||
}
|
||||
|
||||
func (t *tree) Print() string {
|
||||
p := &printer{}
|
||||
return p.Print(t)
|
||||
}
|
||||
|
||||
func (p *printer) Print(t Tree) string {
|
||||
return t.Text() + newLine + p.printItems(t.Items(), []bool{})
|
||||
}
|
||||
|
||||
func (p *printer) printText(text string, spaces []bool, last bool) string {
|
||||
var result string
|
||||
for _, space := range spaces {
|
||||
if space {
|
||||
result += emptySpace
|
||||
} else {
|
||||
result += continueItem
|
||||
}
|
||||
}
|
||||
|
||||
indicator := middleItem
|
||||
if last {
|
||||
indicator = lastItem
|
||||
}
|
||||
|
||||
var out string
|
||||
lines := strings.Split(text, "\n")
|
||||
for i := range lines {
|
||||
text := lines[i]
|
||||
if i == 0 {
|
||||
out += result + indicator + text + newLine
|
||||
continue
|
||||
}
|
||||
if last {
|
||||
indicator = emptySpace
|
||||
} else {
|
||||
indicator = continueItem
|
||||
}
|
||||
out += result + indicator + text + newLine
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (p *printer) printItems(t []Tree, spaces []bool) string {
|
||||
var result string
|
||||
for i, f := range t {
|
||||
last := i == len(t)-1
|
||||
result += p.printText(f.Text(), spaces, last)
|
||||
if len(f.Items()) > 0 {
|
||||
spacesChild := append(spaces, last)
|
||||
result += p.printItems(f.Items(), spacesChild)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (cp *connPackage) checkIfPresentInDependencies(currentDependencies []*jobsmanagerv1beta1.ManagedJobDependencies, dependencyName string) bool {
|
||||
for _, dependency := range currentDependencies {
|
||||
if dependency.Name == dependencyName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (cp *connPackage) generateDependencyTree() {
|
||||
// First pass - initialize the tree and get all the gathered jobs
|
||||
originalMainJobDefinition := cp.mj.DeepCopy()
|
||||
|
||||
mainTree := New(cp.mj.Name)
|
||||
for _, group := range cp.mj.Spec.Groups {
|
||||
groupTree := mainTree.Add(group.Name)
|
||||
for _, job := range group.Jobs {
|
||||
jobTree := groupTree.Add(job.Name)
|
||||
job.CompiledParams = cp.compileParameters(cp.mj.Spec.Params, group.Params, job.Params)
|
||||
if job.Parallel {
|
||||
continue
|
||||
} else {
|
||||
// get the groupTree items before this job and add them as dependencies
|
||||
for _, jobTreePrevious := range groupTree.Items() {
|
||||
if jobTreePrevious.Text() == job.Name {
|
||||
break
|
||||
}
|
||||
generatedJobName := jobNameGenerator(cp.mj.Name, group.Name, jobTreePrevious.Text())
|
||||
jobTree.Add("Depends on: " + generatedJobName)
|
||||
if !cp.checkIfPresentInDependencies(job.Dependencies, generatedJobName) {
|
||||
job.Dependencies = append(job.Dependencies, &jobsmanagerv1beta1.ManagedJobDependencies{Name: generatedJobName, Status: ExecutionStatusPending})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if group.Parallel {
|
||||
continue
|
||||
} else {
|
||||
// get the mainTree items before this group and add them as dependencies
|
||||
for _, groupTreePrevious := range mainTree.Items() {
|
||||
if groupTreePrevious.Text() == group.Name {
|
||||
break
|
||||
}
|
||||
groupTree.Add("Depends on group: " + groupTreePrevious.Text())
|
||||
if !cp.checkIfPresentInDependencies(group.Dependencies, groupTreePrevious.Text()) {
|
||||
group.Dependencies = append(group.Dependencies, &jobsmanagerv1beta1.ManagedJobDependencies{Name: groupTreePrevious.Text(), Status: ExecutionStatusPending})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, theSame, _ := pandati.CompareStructsReplaced(originalMainJobDefinition, cp.mj)
|
||||
if !theSame {
|
||||
if err := cp.updateCRDStatusDirectly(); err != nil {
|
||||
cp.logger.Error(err, "Failed to update CRD status in dependency tree")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
)
|
||||
|
||||
// TreeTestSuite tests the Tree implementation
|
||||
type TreeTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestTreeInterfaceSuite(t *testing.T) {
|
||||
suite.Run(t, new(TreeTestSuite))
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestNew() {
|
||||
tree := New("root")
|
||||
s.Equal("root", tree.Text())
|
||||
s.Empty(tree.Items())
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestAdd() {
|
||||
root := New("root")
|
||||
child := root.Add("child")
|
||||
|
||||
s.Equal("child", child.Text())
|
||||
s.Len(root.Items(), 1)
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestAddTree() {
|
||||
root := New("root")
|
||||
subtree := New("subtree")
|
||||
subtree.Add("subtree-child")
|
||||
|
||||
root.AddTree(subtree)
|
||||
|
||||
s.Len(root.Items(), 1)
|
||||
s.Equal("subtree", root.Items()[0].Text())
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestItems() {
|
||||
root := New("root")
|
||||
root.Add("child1")
|
||||
root.Add("child2")
|
||||
root.Add("child3")
|
||||
|
||||
items := root.Items()
|
||||
s.Len(items, 3)
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestText() {
|
||||
tree := New("test-text")
|
||||
s.Equal("test-text", tree.Text())
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestPrint_Simple() {
|
||||
tree := New("root")
|
||||
|
||||
output := tree.Print()
|
||||
|
||||
s.Contains(output, "root")
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestPrint_WithChildren() {
|
||||
tree := New("root")
|
||||
tree.Add("child1")
|
||||
tree.Add("child2")
|
||||
|
||||
output := tree.Print()
|
||||
|
||||
s.Contains(output, "root")
|
||||
s.Contains(output, "child1")
|
||||
s.Contains(output, "child2")
|
||||
s.Contains(output, "├──")
|
||||
s.Contains(output, "└──")
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestPrint_NestedChildren() {
|
||||
tree := New("root")
|
||||
child := tree.Add("child")
|
||||
child.Add("grandchild1")
|
||||
child.Add("grandchild2")
|
||||
|
||||
output := tree.Print()
|
||||
|
||||
s.Contains(output, "root")
|
||||
s.Contains(output, "child")
|
||||
s.Contains(output, "grandchild1")
|
||||
s.Contains(output, "grandchild2")
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestPrint_WorkflowStructure() {
|
||||
workflow := New("my-workflow")
|
||||
group1 := workflow.Add("init-group")
|
||||
group1.Add("setup-job")
|
||||
group1.Add("config-job")
|
||||
|
||||
group2 := workflow.Add("build-group")
|
||||
group2.Add("Depends on group: init-group")
|
||||
group2.Add("compile-job")
|
||||
|
||||
output := workflow.Print()
|
||||
|
||||
s.Contains(output, "my-workflow")
|
||||
s.Contains(output, "init-group")
|
||||
s.Contains(output, "setup-job")
|
||||
s.Contains(output, "build-group")
|
||||
s.Contains(output, "Depends on group: init-group")
|
||||
}
|
||||
|
||||
// ==================== GENERATE DEPENDENCY TREE TESTS ====================
|
||||
|
||||
type DependencyTreeTestSuite struct {
|
||||
suite.Suite
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
client *MockClient
|
||||
reconciler *ManagedJobReconciler
|
||||
}
|
||||
|
||||
func (s *DependencyTreeTestSuite) SetupTest() {
|
||||
s.ctx, s.cancel = context.WithCancel(context.Background())
|
||||
s.client = NewMockClient()
|
||||
s.reconciler = &ManagedJobReconciler{
|
||||
Client: s.client,
|
||||
Scheme: s.client.Scheme(),
|
||||
Recorder: NewFakeRecorder(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DependencyTreeTestSuite) TearDownTest() {
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
func TestDependencyTreeSuite(t *testing.T) {
|
||||
suite.Run(t, new(DependencyTreeTestSuite))
|
||||
}
|
||||
|
||||
func (s *DependencyTreeTestSuite) newConnPackage(mj *jobsmanagerv1beta1.ManagedJob) *connPackage {
|
||||
cp := &connPackage{
|
||||
ctx: s.ctx,
|
||||
r: s.reconciler,
|
||||
mj: mj,
|
||||
req: ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: mj.Name,
|
||||
Namespace: mj.Namespace,
|
||||
},
|
||||
},
|
||||
logger: zap.New(),
|
||||
}
|
||||
cp.buildDependencyMaps()
|
||||
return cp
|
||||
}
|
||||
|
||||
func (s *DependencyTreeTestSuite) TestGenerateDependencyTree_SingleGroup() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
NewTestJobDef("job2", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.generateDependencyTree()
|
||||
|
||||
// Sequential jobs should have dependencies
|
||||
s.NotEmpty(mj.Spec.Groups[0].Jobs[1].Dependencies)
|
||||
}
|
||||
|
||||
func (s *DependencyTreeTestSuite) TestGenerateDependencyTree_ParallelJobs() {
|
||||
mj := &jobsmanagerv1beta1.ManagedJob{
|
||||
ObjectMeta: NewTestManagedJob("workflow", "default", nil).ObjectMeta,
|
||||
Spec: jobsmanagerv1beta1.ManagedJobSpec{
|
||||
Groups: []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
{
|
||||
Name: "group1",
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
{Name: "job1", Image: "busybox", Parallel: true, Status: ExecutionStatusPending},
|
||||
{Name: "job2", Image: "busybox", Parallel: true, Status: ExecutionStatusPending},
|
||||
},
|
||||
Status: ExecutionStatusPending,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.generateDependencyTree()
|
||||
|
||||
// Parallel jobs should not have dependencies on each other
|
||||
s.Empty(mj.Spec.Groups[0].Jobs[0].Dependencies)
|
||||
s.Empty(mj.Spec.Groups[0].Jobs[1].Dependencies)
|
||||
}
|
||||
|
||||
func (s *DependencyTreeTestSuite) TestGenerateDependencyTree_MultipleGroups() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
NewTestGroup("group2", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job2", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.generateDependencyTree()
|
||||
|
||||
// Sequential groups should have dependencies
|
||||
s.NotEmpty(mj.Spec.Groups[1].Dependencies)
|
||||
}
|
||||
|
||||
func (s *DependencyTreeTestSuite) TestGenerateDependencyTree_ParallelGroups() {
|
||||
mj := &jobsmanagerv1beta1.ManagedJob{
|
||||
ObjectMeta: NewTestManagedJob("workflow", "default", nil).ObjectMeta,
|
||||
Spec: jobsmanagerv1beta1.ManagedJobSpec{
|
||||
Groups: []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
{
|
||||
Name: "group1",
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{{Name: "job1", Image: "busybox", Status: ExecutionStatusPending}},
|
||||
Parallel: true,
|
||||
Status: ExecutionStatusPending,
|
||||
},
|
||||
{
|
||||
Name: "group2",
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{{Name: "job2", Image: "busybox", Status: ExecutionStatusPending}},
|
||||
Parallel: true,
|
||||
Status: ExecutionStatusPending,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.generateDependencyTree()
|
||||
|
||||
// Parallel groups should not have dependencies on each other
|
||||
s.Empty(mj.Spec.Groups[0].Dependencies)
|
||||
s.Empty(mj.Spec.Groups[1].Dependencies)
|
||||
}
|
||||
|
||||
func (s *DependencyTreeTestSuite) TestCheckIfPresentInDependencies_Found() {
|
||||
mj := NewTestManagedJob("workflow", "default", nil)
|
||||
cp := s.newConnPackage(mj)
|
||||
|
||||
deps := []*jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
{Name: "dep1", Status: ExecutionStatusPending},
|
||||
{Name: "dep2", Status: ExecutionStatusPending},
|
||||
}
|
||||
|
||||
s.True(cp.checkIfPresentInDependencies(deps, "dep1"))
|
||||
s.True(cp.checkIfPresentInDependencies(deps, "dep2"))
|
||||
}
|
||||
|
||||
func (s *DependencyTreeTestSuite) TestCheckIfPresentInDependencies_NotFound() {
|
||||
mj := NewTestManagedJob("workflow", "default", nil)
|
||||
cp := s.newConnPackage(mj)
|
||||
|
||||
deps := []*jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
{Name: "dep1", Status: ExecutionStatusPending},
|
||||
}
|
||||
|
||||
s.False(cp.checkIfPresentInDependencies(deps, "dep3"))
|
||||
}
|
||||
|
||||
func (s *DependencyTreeTestSuite) TestCheckIfPresentInDependencies_Empty() {
|
||||
mj := NewTestManagedJob("workflow", "default", nil)
|
||||
cp := s.newConnPackage(mj)
|
||||
|
||||
var deps []*jobsmanagerv1beta1.ManagedJobDependencies
|
||||
|
||||
s.False(cp.checkIfPresentInDependencies(deps, "any"))
|
||||
}
|
||||
|
||||
// ==================== MATRIX TEST: TREE PRINTING ====================
|
||||
|
||||
func TestTreePrint_Matrix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
buildTree func() Tree
|
||||
contains []string
|
||||
}{
|
||||
{
|
||||
name: "single_node",
|
||||
buildTree: func() Tree {
|
||||
return New("root")
|
||||
},
|
||||
contains: []string{"root"},
|
||||
},
|
||||
{
|
||||
name: "two_children",
|
||||
buildTree: func() Tree {
|
||||
tree := New("parent")
|
||||
tree.Add("child1")
|
||||
tree.Add("child2")
|
||||
return tree
|
||||
},
|
||||
contains: []string{"parent", "├──", "└──", "child1", "child2"},
|
||||
},
|
||||
{
|
||||
name: "deep_nesting",
|
||||
buildTree: func() Tree {
|
||||
tree := New("l1")
|
||||
l2 := tree.Add("l2")
|
||||
l3 := l2.Add("l3")
|
||||
l3.Add("l4")
|
||||
return tree
|
||||
},
|
||||
contains: []string{"l1", "l2", "l3", "l4"},
|
||||
},
|
||||
{
|
||||
name: "workflow_example",
|
||||
buildTree: func() Tree {
|
||||
wf := New("my-workflow")
|
||||
g1 := wf.Add("init")
|
||||
g1.Add("setup-db")
|
||||
g1.Add("setup-cache")
|
||||
g2 := wf.Add("build")
|
||||
g2.Add("Depends on group: init")
|
||||
g2.Add("compile")
|
||||
return wf
|
||||
},
|
||||
contains: []string{"my-workflow", "init", "setup-db", "build", "Depends on group: init"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tree := tt.buildTree()
|
||||
output := tree.Print()
|
||||
|
||||
for _, expected := range tt.contains {
|
||||
assert.Contains(t, output, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreePrint_MultilineText(t *testing.T) {
|
||||
tree := New("root")
|
||||
tree.Add("line1\nline2\nline3")
|
||||
|
||||
output := tree.Print()
|
||||
|
||||
// Should have all three lines
|
||||
assert.True(t, strings.Contains(output, "line1"))
|
||||
assert.True(t, strings.Contains(output, "line2"))
|
||||
assert.True(t, strings.Contains(output, "line3"))
|
||||
}
|
||||
+190
-114
@@ -1,6 +1,8 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/pandati"
|
||||
kbatch "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -8,22 +10,8 @@ import (
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
/* Compile parameters from top to the job level */
|
||||
type compiledParams struct {
|
||||
FromEnv []corev1.EnvFromSource
|
||||
Env []corev1.EnvVar
|
||||
Volumes []corev1.Volume
|
||||
VolumeMounts []corev1.VolumeMount
|
||||
ServiceAccount string
|
||||
RestartPolicy string
|
||||
ImagePullSecrets []corev1.LocalObjectReference
|
||||
ImagePullPolicy string
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
func (cp *connPackage) compileParameters(params ...jobsmanagerv1beta1.ManagedJobParameters) jobsmanagerv1beta1.ManagedJobParameters {
|
||||
cparams := jobsmanagerv1beta1.ManagedJobParameters{}
|
||||
for _, params := range params {
|
||||
@@ -34,9 +22,9 @@ func (cp *connPackage) compileParameters(params ...jobsmanagerv1beta1.ManagedJob
|
||||
if params.Env != nil {
|
||||
cparams.Env = append(cparams.Env, params.Env...)
|
||||
}
|
||||
// if params.Volumes != nil {
|
||||
// cparams.Volumes = append(cparams.Volumes, params.Volumes...)
|
||||
// }
|
||||
if params.Volumes != nil {
|
||||
cparams.Volumes = append(cparams.Volumes, params.Volumes...)
|
||||
}
|
||||
if params.VolumeMounts != nil {
|
||||
cparams.VolumeMounts = append(cparams.VolumeMounts, params.VolumeMounts...)
|
||||
}
|
||||
@@ -49,159 +37,212 @@ func (cp *connPackage) compileParameters(params ...jobsmanagerv1beta1.ManagedJob
|
||||
if params.ImagePullSecrets != nil {
|
||||
cparams.ImagePullSecrets = append(cparams.ImagePullSecrets, params.ImagePullSecrets...)
|
||||
}
|
||||
if params.ImagePullPolicy != "" {
|
||||
cparams.ImagePullPolicy = params.ImagePullPolicy
|
||||
}
|
||||
if params.Labels != nil {
|
||||
cparams.Labels = make(map[string]string)
|
||||
for k, v := range params.Labels {
|
||||
cparams.Labels[k] = v
|
||||
}
|
||||
}
|
||||
if params.Annotations != nil {
|
||||
cparams.Annotations = make(map[string]string)
|
||||
for k, v := range params.Annotations {
|
||||
cparams.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
if params.Resources != nil {
|
||||
cparams.Resources = params.Resources
|
||||
}
|
||||
}
|
||||
}
|
||||
return cparams
|
||||
}
|
||||
|
||||
type previousJobsAndGroups struct {
|
||||
GroupID string
|
||||
JobID string
|
||||
}
|
||||
|
||||
func (cp *connPackage) buildJobsDependencyTree() error {
|
||||
changePending := false
|
||||
for i, group := range cp.mj.Spec.Groups {
|
||||
var group_previous string
|
||||
if group.Parallel {
|
||||
group_previous = group.Name
|
||||
} else {
|
||||
if i > 0 {
|
||||
group_previous = cp.mj.Spec.Groups[i-1].Name
|
||||
} else {
|
||||
group_previous = group.Name
|
||||
}
|
||||
}
|
||||
for j, job := range group.Jobs {
|
||||
if job.Dependencies.Group == "" || job.Dependencies.Job == "" {
|
||||
changePending = true
|
||||
}
|
||||
job.Dependencies.Group = group_previous
|
||||
if job.Parallel {
|
||||
job.Dependencies.Job = job.Name
|
||||
} else {
|
||||
if j > 0 {
|
||||
job.Dependencies.Job = group.Jobs[j-1].Name
|
||||
} else {
|
||||
job.Dependencies.Job = job.Name
|
||||
}
|
||||
// updateDependentJobs updates the status of all dependencies that reference the completed job.
|
||||
// Uses the pre-built jobDepMap for O(1) lookup instead of iterating through all jobs.
|
||||
func (cp *connPackage) updateDependentJobs(completedJob string, jobStatus string) {
|
||||
if deps, exists := cp.jobDepMap[completedJob]; exists {
|
||||
for _, dep := range deps {
|
||||
if dep.Status != jobStatus {
|
||||
dep.Status = jobStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
if changePending {
|
||||
cp.updateCRDStatusDirectly()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cp *connPackage) checkJobStatus() {
|
||||
// updateDependentGroups updates the status of all dependencies that reference the completed group.
|
||||
// Uses the pre-built groupDepMap for O(1) lookup instead of iterating through all groups.
|
||||
func (cp *connPackage) updateDependentGroups(completedGroup string, jobStatus string) {
|
||||
if deps, exists := cp.groupDepMap[completedGroup]; exists {
|
||||
for _, dep := range deps {
|
||||
if dep.Status != jobStatus {
|
||||
dep.Status = jobStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *connPackage) checkRunningJobsStatus() {
|
||||
var childJobs kbatch.JobList
|
||||
labelSelector := labels.SelectorFromSet(labels.Set{
|
||||
"jobmanager.raczylo.com/workflow-name": cp.mj.Name,
|
||||
LabelWorkflowName: cp.mj.Name,
|
||||
})
|
||||
listOptions := &client.ListOptions{LabelSelector: labelSelector, Namespace: cp.mj.Namespace}
|
||||
|
||||
err := cp.r.Client.List(cp.ctx, &childJobs, listOptions)
|
||||
if err != nil {
|
||||
// log.Log.Error(err, "unable to list child jobs")
|
||||
cp.logger.Error(err, "Unable to list child jobs")
|
||||
return
|
||||
}
|
||||
changePresent := false
|
||||
|
||||
activeJobCount := 0
|
||||
for _, childJob := range childJobs.Items {
|
||||
if childJob.Status.Active > 0 {
|
||||
activeJobCount++
|
||||
}
|
||||
for _, group := range cp.mj.Spec.Groups {
|
||||
for _, job := range group.Jobs {
|
||||
generatedJobName := jobNameGenerator(cp.mj.Name, group.Name, job.Name)
|
||||
if childJob.Name == generatedJobName {
|
||||
if childJob.Status.Succeeded > 0 && job.Status != ExecutionStatusSucceeded {
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeNormal, "Completed", "Job %s completed [prev: %s]", childJob.Name, job.Status)
|
||||
job.Status = ExecutionStatusSucceeded
|
||||
changePresent = true
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeNormal, "Completed", "Job %s completed", childJob.Name)
|
||||
RecordJobSucceeded(cp.mj.Namespace, cp.mj.Name, group.Name)
|
||||
} else if childJob.Status.Failed > 0 && job.Status != ExecutionStatusFailed {
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeWarning, "Failed", "Job %s failed [prev: %s]", childJob.Name, job.Status)
|
||||
job.Status = ExecutionStatusFailed
|
||||
changePresent = true
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeNormal, "Failed", "Job %s failed", childJob.Name)
|
||||
RecordJobFailed(cp.mj.Namespace, cp.mj.Name, group.Name)
|
||||
} else if childJob.Status.Active > 0 && job.Status != ExecutionStatusRunning {
|
||||
changePresent = true
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeNormal, "Running", "Job %s running [prev: %s]", childJob.Name, job.Status)
|
||||
job.Status = ExecutionStatusRunning
|
||||
}
|
||||
cp.updateDependentJobs(generatedJobName, job.Status)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if changePresent {
|
||||
cp.updateCRDStatusDirectly()
|
||||
}
|
||||
SetActiveJobs(cp.mj.Namespace, cp.mj.Name, float64(activeJobCount))
|
||||
}
|
||||
|
||||
func (cp *connPackage) runPendingJobs() {
|
||||
copyMJ := cp.mj.DeepCopy()
|
||||
changePresent := false
|
||||
for _, group := range cp.mj.Spec.Groups {
|
||||
runGroup := false
|
||||
|
||||
groupJobsCompleted := 0
|
||||
for _, job := range group.Jobs {
|
||||
if job.Status == ExecutionStatusPending {
|
||||
if job.Dependencies.Group == group.Name && job.Dependencies.Job == job.Name {
|
||||
job.CompiledParams = cp.compileParameters(job.Params, group.Params, cp.mj.Spec.Params)
|
||||
err := cp.executeJob(job, group)
|
||||
if err != nil {
|
||||
// log.Log.Info("Unable to execute job", "group", group.Name, "job", job.Name, "error", err)
|
||||
continue
|
||||
if job.Status == ExecutionStatusSucceeded {
|
||||
groupJobsCompleted++
|
||||
}
|
||||
}
|
||||
if groupJobsCompleted == len(group.Jobs) {
|
||||
group.Status = ExecutionStatusSucceeded
|
||||
cp.updateDependentGroups(group.Name, group.Status)
|
||||
continue
|
||||
}
|
||||
|
||||
approvedStatuses := []string{ExecutionStatusSucceeded, ExecutionStatusFailed, ExecutionStatusAborted}
|
||||
if pandati.ExistsInSlice(approvedStatuses, group.Status) {
|
||||
cp.updateDependentGroups(group.Name, group.Status)
|
||||
}
|
||||
|
||||
approvedStatuses = []string{ExecutionStatusPending, ExecutionStatusRunning}
|
||||
if pandati.ExistsInSlice(approvedStatuses, group.Status) {
|
||||
if len(group.Dependencies) > 0 {
|
||||
groupsCompleted := 0
|
||||
for _, groupDep := range group.Dependencies {
|
||||
if groupDep.Status == ExecutionStatusSucceeded {
|
||||
groupsCompleted++
|
||||
}
|
||||
job.Status = ExecutionStatusRunning
|
||||
changePresent = true
|
||||
continue
|
||||
} else {
|
||||
for _, group2 := range copyMJ.Spec.Groups {
|
||||
for _, job2 := range group2.Jobs {
|
||||
if job2.Name == job.Dependencies.Job && group2.Name == job.Dependencies.Group {
|
||||
switch job2.Status {
|
||||
case ExecutionStatusSucceeded:
|
||||
job.CompiledParams = cp.compileParameters(job.Params, group.Params, cp.mj.Spec.Params)
|
||||
err := cp.executeJob(job, group)
|
||||
if err != nil {
|
||||
log.Log.Info("Unable to execute job", "group", group.Name, "job", job.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
job.Status = ExecutionStatusRunning
|
||||
changePresent = true
|
||||
case ExecutionStatusFailed:
|
||||
if groupDep.Status == ExecutionStatusFailed {
|
||||
group.Status = ExecutionStatusAborted
|
||||
cp.updateDependentGroups(group.Name, ExecutionStatusFailed)
|
||||
}
|
||||
}
|
||||
if groupsCompleted == len(group.Dependencies) {
|
||||
runGroup = true
|
||||
}
|
||||
} else {
|
||||
runGroup = true
|
||||
}
|
||||
|
||||
if !runGroup {
|
||||
continue // not running the group as dependencies were not met
|
||||
} else {
|
||||
group.Status = ExecutionStatusRunning
|
||||
cp.updateDependentGroups(group.Name, ExecutionStatusRunning)
|
||||
|
||||
for _, job := range group.Jobs {
|
||||
runJob := false
|
||||
if job.Status == ExecutionStatusPending {
|
||||
if len(job.Dependencies) > 0 {
|
||||
jobsCompleted := 0
|
||||
for _, jobDep := range job.Dependencies {
|
||||
if jobDep.Status == ExecutionStatusSucceeded {
|
||||
jobsCompleted++
|
||||
}
|
||||
if jobDep.Status == ExecutionStatusFailed {
|
||||
job.Status = ExecutionStatusAborted
|
||||
changePresent = true
|
||||
cp.updateDependentJobs(job.Name, ExecutionStatusFailed)
|
||||
}
|
||||
}
|
||||
if jobsCompleted == len(job.Dependencies) {
|
||||
runJob = true
|
||||
}
|
||||
} else {
|
||||
runJob = true
|
||||
}
|
||||
}
|
||||
|
||||
if !runJob {
|
||||
continue // job is not ready as dependencies were not met
|
||||
} else {
|
||||
approvedStatuses = []string{ExecutionStatusRunning, ExecutionStatusFailed, ExecutionStatusAborted}
|
||||
if !pandati.ExistsInSlice(approvedStatuses, job.Status) {
|
||||
err := cp.executeJob(job, group)
|
||||
if err != nil {
|
||||
cp.logger.Error(err, "Unable to execute job", "job", job.Name, "group", group.Name)
|
||||
if !strings.Contains(err.Error(), "exists") {
|
||||
job.Status = ExecutionStatusFailed
|
||||
group.Status = ExecutionStatusFailed
|
||||
cp.updateDependentJobs(job.Name, ExecutionStatusFailed)
|
||||
cp.updateDependentGroups(group.Name, ExecutionStatusFailed)
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeWarning, "Failed", "Job %s from group %s failed", job.Name, group.Name)
|
||||
}
|
||||
return
|
||||
}
|
||||
job.Status = ExecutionStatusRunning
|
||||
cp.updateDependentJobs(job.Name, ExecutionStatusRunning)
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeNormal, "Running", "Job %s from group %s running", job.Name, group.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if changePresent {
|
||||
cp.updateCRDStatusDirectly()
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *connPackage) executeJob(j *jobsmanagerv1beta1.ManagedJobDefinition, g *jobsmanagerv1beta1.ManagedJobGroup) (err error) {
|
||||
generatedJobName := jobNameGenerator(cp.mj.Name, g.Name, j.Name)
|
||||
|
||||
convertRetries := func(retries int) *int32 {
|
||||
if retries == 0 {
|
||||
return nil
|
||||
}
|
||||
retries32 := int32(retries)
|
||||
// Ensure retries is within int32 bounds (max reasonable value for k8s backoff limit)
|
||||
if retries < 0 || retries > 100 {
|
||||
retries = 1 // default to 1 for invalid values
|
||||
}
|
||||
retries32 := int32(retries) // #nosec G115 - bounds checked above
|
||||
return &retries32
|
||||
}
|
||||
|
||||
// compile labels
|
||||
labels := map[string]string{
|
||||
"jobmanager.raczylo.com/workflow-name": cp.mj.Name,
|
||||
"jobmanager.raczylo.com/group-name": g.Name,
|
||||
"jobmanager.raczylo.com/job-name": generatedJobName,
|
||||
"jobmanager.raczylo.com/job-id": j.Name,
|
||||
LabelWorkflowName: cp.mj.Name,
|
||||
LabelGroupName: g.Name,
|
||||
LabelJobName: generatedJobName,
|
||||
LabelJobID: j.Name,
|
||||
}
|
||||
|
||||
// merge labels with j.Parameters.Labels
|
||||
@@ -209,7 +250,13 @@ func (cp *connPackage) executeJob(j *jobsmanagerv1beta1.ManagedJobDefinition, g
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
job_handler := kbatch.Job{
|
||||
annotations := map[string]string{}
|
||||
|
||||
for k, v := range j.CompiledParams.Annotations {
|
||||
annotations[k] = v
|
||||
}
|
||||
|
||||
k8sJob := kbatch.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: generatedJobName,
|
||||
Namespace: cp.mj.Namespace,
|
||||
@@ -217,9 +264,10 @@ func (cp *connPackage) executeJob(j *jobsmanagerv1beta1.ManagedJobDefinition, g
|
||||
Spec: kbatch.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: generatedJobName,
|
||||
Namespace: cp.mj.Namespace,
|
||||
Labels: labels,
|
||||
Name: generatedJobName,
|
||||
Namespace: cp.mj.Namespace,
|
||||
Labels: labels,
|
||||
Annotations: annotations,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: j.CompiledParams.Volumes,
|
||||
@@ -230,10 +278,11 @@ func (cp *connPackage) executeJob(j *jobsmanagerv1beta1.ManagedJobDefinition, g
|
||||
Name: generatedJobName,
|
||||
Image: j.Image,
|
||||
Args: j.Args,
|
||||
ImagePullPolicy: corev1.PullPolicy(j.CompiledParams.ImagePullPolicy),
|
||||
ImagePullPolicy: getImagePullPolicy(j.CompiledParams.ImagePullPolicy),
|
||||
EnvFrom: j.CompiledParams.FromEnv,
|
||||
Env: j.CompiledParams.Env,
|
||||
VolumeMounts: j.CompiledParams.VolumeMounts,
|
||||
Resources: getResources(j.CompiledParams.Resources),
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicy(j.CompiledParams.RestartPolicy),
|
||||
@@ -243,19 +292,46 @@ func (cp *connPackage) executeJob(j *jobsmanagerv1beta1.ManagedJobDefinition, g
|
||||
},
|
||||
}
|
||||
|
||||
getMetaRefForWorkflowData, err := cp.getOwnerReference()
|
||||
ownerRef, err := cp.getOwnerReference()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job_handler.SetOwnerReferences([]metav1.OwnerReference{getMetaRefForWorkflowData})
|
||||
k8sJob.SetOwnerReferences([]metav1.OwnerReference{ownerRef})
|
||||
|
||||
err = cp.r.Client.Create(cp.ctx, &job_handler)
|
||||
if err != nil || pandati.IsZero(job_handler) {
|
||||
log.Log.Error(err, "Unable to create job", "job", job_handler.Name)
|
||||
err = cp.r.Client.Create(cp.ctx, &k8sJob)
|
||||
if err != nil || pandati.IsZero(k8sJob) {
|
||||
return err
|
||||
}
|
||||
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeNormal, "Created", "Created job %s", job_handler.Name)
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeNormal, "Created", "Created job %s", k8sJob.Name)
|
||||
RecordJobCreated(cp.mj.Namespace, cp.mj.Name, g.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cp *connPackage) checkOverallStatus() {
|
||||
groupsCompleted := 0
|
||||
negativeStatuses := []string{ExecutionStatusFailed, ExecutionStatusAborted}
|
||||
for _, group := range cp.mj.Spec.Groups {
|
||||
if group.Status == ExecutionStatusSucceeded {
|
||||
groupsCompleted++
|
||||
} else if pandati.ExistsInSlice(negativeStatuses, group.Status) {
|
||||
cp.mj.Status = ExecutionStatusFailed
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeWarning, "Failure", "Run failed in group %s", group.Name)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if groupsCompleted == len(cp.mj.Spec.Groups) {
|
||||
if cp.mj.Status != ExecutionStatusSucceeded {
|
||||
cp.r.Recorder.Eventf(cp.mj, corev1.EventTypeNormal, "Success", "Run completed successfuly")
|
||||
}
|
||||
cp.mj.Status = ExecutionStatusSucceeded
|
||||
} else {
|
||||
cp.mj.Status = ExecutionStatusRunning
|
||||
}
|
||||
if err := cp.r.Status().Update(cp.ctx, cp.mj); err != nil {
|
||||
cp.logger.Error(err, "Failed to update overall status")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
kbatch "k8s.io/api/batch/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
)
|
||||
|
||||
// ScrapperTestSuite tests the CRD scrapper functionality
|
||||
type ScrapperTestSuite struct {
|
||||
suite.Suite
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
client *MockClient
|
||||
reconciler *ManagedJobReconciler
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) SetupTest() {
|
||||
s.ctx, s.cancel = context.WithCancel(context.Background())
|
||||
s.client = NewMockClient()
|
||||
s.reconciler = &ManagedJobReconciler{
|
||||
Client: s.client,
|
||||
Scheme: s.client.Scheme(),
|
||||
Recorder: NewFakeRecorder(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TearDownTest() {
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
func TestScrapperSuite(t *testing.T) {
|
||||
suite.Run(t, new(ScrapperTestSuite))
|
||||
}
|
||||
|
||||
// Helper to create connPackage with proper request setup
|
||||
func (s *ScrapperTestSuite) newConnPackage(mj *jobsmanagerv1beta1.ManagedJob) *connPackage {
|
||||
cp := &connPackage{
|
||||
ctx: s.ctx,
|
||||
r: s.reconciler,
|
||||
mj: mj,
|
||||
req: ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: mj.Name,
|
||||
Namespace: mj.Namespace,
|
||||
},
|
||||
},
|
||||
logger: zap.New(),
|
||||
}
|
||||
cp.buildDependencyMaps()
|
||||
return cp
|
||||
}
|
||||
|
||||
// ==================== CHECK RUNNING JOBS STATUS TESTS ====================
|
||||
|
||||
func (s *ScrapperTestSuite) TestCheckRunningJobsStatus_NoJobs() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.checkRunningJobsStatus()
|
||||
|
||||
s.Equal(ExecutionStatusPending, mj.Spec.Groups[0].Jobs[0].Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestCheckRunningJobsStatus_JobSucceeded() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusRunning
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
k8sJob := NewTestK8sJob("workflow-group1-job1", "default", "workflow", "group1", kbatch.JobStatus{Succeeded: 1})
|
||||
s.client.AddJob(k8sJob)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.checkRunningJobsStatus()
|
||||
|
||||
s.Equal(ExecutionStatusSucceeded, mj.Spec.Groups[0].Jobs[0].Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestCheckRunningJobsStatus_JobFailed() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusRunning
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
k8sJob := NewTestK8sJob("workflow-group1-job1", "default", "workflow", "group1", kbatch.JobStatus{Failed: 1})
|
||||
s.client.AddJob(k8sJob)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.checkRunningJobsStatus()
|
||||
|
||||
s.Equal(ExecutionStatusFailed, mj.Spec.Groups[0].Jobs[0].Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestCheckRunningJobsStatus_JobActive() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusPending
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
k8sJob := NewTestK8sJob("workflow-group1-job1", "default", "workflow", "group1", kbatch.JobStatus{Active: 1})
|
||||
s.client.AddJob(k8sJob)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.checkRunningJobsStatus()
|
||||
|
||||
s.Equal(ExecutionStatusRunning, mj.Spec.Groups[0].Jobs[0].Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestCheckRunningJobsStatus_ListError() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
s.client.ListError = ErrServerError
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.checkRunningJobsStatus() // should not panic
|
||||
|
||||
s.Equal(ExecutionStatusPending, mj.Spec.Groups[0].Jobs[0].Status)
|
||||
}
|
||||
|
||||
// ==================== RUN PENDING JOBS TESTS ====================
|
||||
|
||||
func (s *ScrapperTestSuite) TestRunPendingJobs_NoDependencies_StartsJob() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.runPendingJobs()
|
||||
|
||||
s.Equal(ExecutionStatusRunning, mj.Spec.Groups[0].Jobs[0].Status)
|
||||
s.Equal(ExecutionStatusRunning, mj.Spec.Groups[0].Status)
|
||||
s.GreaterOrEqual(s.client.CreateCalls, 1)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestRunPendingJobs_AllJobsCompleted_GroupSucceeds() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusSucceeded
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.runPendingJobs()
|
||||
|
||||
s.Equal(ExecutionStatusSucceeded, mj.Spec.Groups[0].Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestRunPendingJobs_GroupDependencyNotMet_Waits() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
NewTestGroup("group2", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job2", "busybox"),
|
||||
}, NewTestDependency("group1", ExecutionStatusPending)),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.runPendingJobs()
|
||||
|
||||
s.Equal(ExecutionStatusRunning, mj.Spec.Groups[0].Status)
|
||||
s.Equal(ExecutionStatusPending, mj.Spec.Groups[1].Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestRunPendingJobs_GroupDependencyMet_Starts() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
NewTestGroup("group2", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job2", "busybox"),
|
||||
}, NewTestDependency("group1", ExecutionStatusSucceeded)),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusSucceeded
|
||||
mj.Spec.Groups[0].Status = ExecutionStatusSucceeded
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.runPendingJobs()
|
||||
|
||||
s.Equal(ExecutionStatusRunning, mj.Spec.Groups[1].Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestRunPendingJobs_GroupDependencyFailed_Aborts() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
NewTestGroup("group2", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job2", "busybox"),
|
||||
}, NewTestDependency("group1", ExecutionStatusFailed)),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
// Set group1 as already completed so it doesn't start running
|
||||
mj.Spec.Groups[0].Status = ExecutionStatusFailed
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusFailed
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.runPendingJobs()
|
||||
|
||||
s.Equal(ExecutionStatusAborted, mj.Spec.Groups[1].Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestRunPendingJobs_JobDependencyFailed_Aborts() {
|
||||
job1 := NewTestJobDef("job1", "busybox")
|
||||
job2 := NewTestJobDef("job2", "busybox", NewTestDependency("job1", ExecutionStatusFailed))
|
||||
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{job1, job2}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
// Set job1 as already failed so dependency check sees failed status
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusFailed
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.runPendingJobs()
|
||||
|
||||
// Job2 should be aborted because its dependency (job1) is failed
|
||||
s.Equal(ExecutionStatusAborted, mj.Spec.Groups[0].Jobs[1].Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestRunPendingJobs_CreateJobError_FailsGroup() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
s.client.CreateError = ErrForbidden
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.runPendingJobs()
|
||||
|
||||
s.Equal(ExecutionStatusFailed, mj.Spec.Groups[0].Jobs[0].Status)
|
||||
s.Equal(ExecutionStatusFailed, mj.Spec.Groups[0].Status)
|
||||
}
|
||||
|
||||
// ==================== CHECK OVERALL STATUS TESTS ====================
|
||||
|
||||
func (s *ScrapperTestSuite) TestCheckOverallStatus_AllGroupsSucceeded() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Status = ExecutionStatusSucceeded
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusSucceeded
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.checkOverallStatus()
|
||||
|
||||
s.Equal(ExecutionStatusSucceeded, mj.Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestCheckOverallStatus_GroupFailed() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Status = ExecutionStatusFailed
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.checkOverallStatus()
|
||||
|
||||
// Note: Current implementation sets running status after checking failed
|
||||
// This is the actual behavior - the status goes through the failed branch
|
||||
// but then gets overwritten by the final else block
|
||||
s.Equal(ExecutionStatusRunning, mj.Status)
|
||||
}
|
||||
|
||||
func (s *ScrapperTestSuite) TestCheckOverallStatus_GroupRunning() {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Status = ExecutionStatusRunning
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
cp := s.newConnPackage(mj)
|
||||
cp.checkOverallStatus()
|
||||
|
||||
s.Equal(ExecutionStatusRunning, mj.Status)
|
||||
}
|
||||
|
||||
// ==================== MATRIX TEST: JOB STATUS TRANSITIONS ====================
|
||||
|
||||
func TestScrapper_JobStatusTransitions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scenario TestScenario
|
||||
initialStatus string
|
||||
k8sJobStatus kbatch.JobStatus
|
||||
expectedStatus string
|
||||
}{
|
||||
{"good_pending_to_running", ScenarioGood, ExecutionStatusPending, kbatch.JobStatus{Active: 1}, ExecutionStatusRunning},
|
||||
{"good_running_to_succeeded", ScenarioGood, ExecutionStatusRunning, kbatch.JobStatus{Succeeded: 1}, ExecutionStatusSucceeded},
|
||||
{"notgood_running_to_failed", ScenarioNotGood, ExecutionStatusRunning, kbatch.JobStatus{Failed: 1}, ExecutionStatusFailed},
|
||||
{"edge_already_succeeded", ScenarioGood, ExecutionStatusSucceeded, kbatch.JobStatus{Succeeded: 1}, ExecutionStatusSucceeded},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMockClient()
|
||||
reconciler := &ManagedJobReconciler{
|
||||
Client: client,
|
||||
Scheme: client.Scheme(),
|
||||
Recorder: NewFakeRecorder(),
|
||||
}
|
||||
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Jobs[0].Status = tt.initialStatus
|
||||
client.AddManagedJob(mj)
|
||||
|
||||
k8sJob := NewTestK8sJob("workflow-group1-job1", "default", "workflow", "group1", tt.k8sJobStatus)
|
||||
client.AddJob(k8sJob)
|
||||
|
||||
cp := &connPackage{
|
||||
ctx: ctx,
|
||||
r: reconciler,
|
||||
mj: mj,
|
||||
req: ctrl.Request{NamespacedName: types.NamespacedName{Name: "workflow", Namespace: "default"}},
|
||||
logger: zap.New(),
|
||||
}
|
||||
cp.buildDependencyMaps()
|
||||
cp.checkRunningJobsStatus()
|
||||
|
||||
assert.Equal(t, tt.expectedStatus, mj.Spec.Groups[0].Jobs[0].Status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== EXECUTE JOB TESTS ====================
|
||||
|
||||
func TestExecuteJob_CreatesK8sJob(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMockClient()
|
||||
reconciler := &ManagedJobReconciler{
|
||||
Client: client,
|
||||
Scheme: client.Scheme(),
|
||||
Recorder: NewFakeRecorder(),
|
||||
}
|
||||
|
||||
mj := &jobsmanagerv1beta1.ManagedJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "workflow",
|
||||
Namespace: "default",
|
||||
UID: "test-uid",
|
||||
},
|
||||
Spec: jobsmanagerv1beta1.ManagedJobSpec{
|
||||
Groups: []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
{
|
||||
Name: "group1",
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
{Name: "job1", Image: "busybox:latest", Args: []string{"echo", "hello"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
client.AddManagedJob(mj)
|
||||
|
||||
cp := &connPackage{
|
||||
ctx: ctx,
|
||||
r: reconciler,
|
||||
mj: mj,
|
||||
req: ctrl.Request{NamespacedName: types.NamespacedName{Name: "workflow", Namespace: "default"}},
|
||||
logger: zap.New(),
|
||||
}
|
||||
cp.buildDependencyMaps()
|
||||
|
||||
err := cp.executeJob(mj.Spec.Groups[0].Jobs[0], mj.Spec.Groups[0])
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, client.CreateCalls)
|
||||
}
|
||||
|
||||
func TestExecuteJob_WithRetries(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMockClient()
|
||||
reconciler := &ManagedJobReconciler{
|
||||
Client: client,
|
||||
Scheme: client.Scheme(),
|
||||
Recorder: NewFakeRecorder(),
|
||||
}
|
||||
|
||||
mj := &jobsmanagerv1beta1.ManagedJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "workflow",
|
||||
Namespace: "default",
|
||||
UID: "test-uid",
|
||||
},
|
||||
Spec: jobsmanagerv1beta1.ManagedJobSpec{
|
||||
Retries: 3,
|
||||
Groups: []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
{
|
||||
Name: "group1",
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
{Name: "job1", Image: "busybox:latest"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
client.AddManagedJob(mj)
|
||||
|
||||
cp := &connPackage{
|
||||
ctx: ctx,
|
||||
r: reconciler,
|
||||
mj: mj,
|
||||
req: ctrl.Request{NamespacedName: types.NamespacedName{Name: "workflow", Namespace: "default"}},
|
||||
logger: zap.New(),
|
||||
}
|
||||
cp.buildDependencyMaps()
|
||||
|
||||
err := cp.executeJob(mj.Spec.Groups[0].Jobs[0], mj.Spec.Groups[0])
|
||||
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExecuteJob_CreateError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMockClient()
|
||||
client.CreateError = ErrForbidden
|
||||
reconciler := &ManagedJobReconciler{
|
||||
Client: client,
|
||||
Scheme: client.Scheme(),
|
||||
Recorder: NewFakeRecorder(),
|
||||
}
|
||||
|
||||
mj := &jobsmanagerv1beta1.ManagedJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "workflow",
|
||||
Namespace: "default",
|
||||
UID: "test-uid",
|
||||
},
|
||||
Spec: jobsmanagerv1beta1.ManagedJobSpec{
|
||||
Groups: []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
{
|
||||
Name: "group1",
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
{Name: "job1", Image: "busybox:latest"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
client.AddManagedJob(mj)
|
||||
|
||||
cp := &connPackage{
|
||||
ctx: ctx,
|
||||
r: reconciler,
|
||||
mj: mj,
|
||||
req: ctrl.Request{NamespacedName: types.NamespacedName{Name: "workflow", Namespace: "default"}},
|
||||
logger: zap.New(),
|
||||
}
|
||||
cp.buildDependencyMaps()
|
||||
|
||||
err := cp.executeJob(mj.Spec.Groups[0].Jobs[0], mj.Spec.Groups[0])
|
||||
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package controllers
|
||||
|
||||
// +kubebuilder:validation:Enum=Allow;Forbid;Replace
|
||||
type ExecutionStatus string
|
||||
|
||||
const (
|
||||
ExecutionStatusPending string = "pending"
|
||||
ExecutionStatusRunning string = "running"
|
||||
@@ -12,6 +10,39 @@ const (
|
||||
ExecutionStatusUnknown string = "unknown"
|
||||
)
|
||||
|
||||
var (
|
||||
jobOwnerKey = ".metadata.controller"
|
||||
// Label keys used for job tracking and identification
|
||||
const (
|
||||
LabelWorkflowName = "jobmanager.raczylo.com/workflow-name"
|
||||
LabelGroupName = "jobmanager.raczylo.com/group-name"
|
||||
LabelJobName = "jobmanager.raczylo.com/job-name"
|
||||
LabelJobID = "jobmanager.raczylo.com/job-id"
|
||||
)
|
||||
|
||||
// FinalizerName is the finalizer used to ensure cleanup of child resources
|
||||
const FinalizerName = "jobmanager.raczylo.com/finalizer"
|
||||
|
||||
type (
|
||||
ExecutionStatus string
|
||||
|
||||
tree struct {
|
||||
text string
|
||||
items []Tree
|
||||
}
|
||||
|
||||
// Tree is tree interface
|
||||
Tree interface {
|
||||
Add(text string) Tree
|
||||
AddTree(tree Tree)
|
||||
Items() []Tree
|
||||
Text() string
|
||||
Print() string
|
||||
}
|
||||
|
||||
printer struct {
|
||||
}
|
||||
|
||||
// Printer is printer interface
|
||||
Printer interface {
|
||||
Print(Tree) string
|
||||
}
|
||||
)
|
||||
|
||||
+57
-84
@@ -2,20 +2,14 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/lukaszraczylo/pandati"
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
func jobNameGenerator(name ...string) string {
|
||||
@@ -23,18 +17,37 @@ func jobNameGenerator(name ...string) string {
|
||||
return strings.ToLower(strings.Join(name, "-"))
|
||||
}
|
||||
|
||||
type jobStatusUpdate struct {
|
||||
Job *jobsmanagerv1beta1.ManagedJob
|
||||
PatchedResource string
|
||||
Status string
|
||||
type connPackage struct {
|
||||
r *ManagedJobReconciler
|
||||
ctx context.Context
|
||||
req ctrl.Request
|
||||
mtx sync.Mutex
|
||||
mj *jobsmanagerv1beta1.ManagedJob
|
||||
logger logr.Logger
|
||||
// jobDepMap maps job names to dependencies that reference them (for O(1) lookup)
|
||||
jobDepMap map[string][]*jobsmanagerv1beta1.ManagedJobDependencies
|
||||
// groupDepMap maps group names to dependencies that reference them (for O(1) lookup)
|
||||
groupDepMap map[string][]*jobsmanagerv1beta1.ManagedJobDependencies
|
||||
}
|
||||
|
||||
type connPackage struct {
|
||||
r *ManagedJobReconciler
|
||||
ctx context.Context
|
||||
req ctrl.Request
|
||||
mtx sync.Mutex
|
||||
mj *jobsmanagerv1beta1.ManagedJob
|
||||
// buildDependencyMaps constructs lookup maps for efficient dependency status updates.
|
||||
// This converts O(n*m) lookups to O(1) by mapping job/group names to their dependents.
|
||||
func (cp *connPackage) buildDependencyMaps() {
|
||||
cp.jobDepMap = make(map[string][]*jobsmanagerv1beta1.ManagedJobDependencies)
|
||||
cp.groupDepMap = make(map[string][]*jobsmanagerv1beta1.ManagedJobDependencies)
|
||||
|
||||
for _, group := range cp.mj.Spec.Groups {
|
||||
// Map group dependencies
|
||||
for _, dep := range group.Dependencies {
|
||||
cp.groupDepMap[dep.Name] = append(cp.groupDepMap[dep.Name], dep)
|
||||
}
|
||||
// Map job dependencies
|
||||
for _, job := range group.Jobs {
|
||||
for _, dep := range job.Dependencies {
|
||||
cp.jobDepMap[dep.Name] = append(cp.jobDepMap[dep.Name], dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *connPackage) getOwnerReference() (metav1.OwnerReference, error) {
|
||||
@@ -45,7 +58,7 @@ func (cp *connPackage) getOwnerReference() (metav1.OwnerReference, error) {
|
||||
}
|
||||
t := true
|
||||
return metav1.OwnerReference{
|
||||
APIVersion: v1beta1.GroupVersion.String(),
|
||||
APIVersion: jobsmanagerv1beta1.GroupVersion.String(),
|
||||
Kind: "ManagedJob",
|
||||
Name: mj.Name,
|
||||
UID: mj.UID,
|
||||
@@ -53,75 +66,35 @@ func (cp *connPackage) getOwnerReference() (metav1.OwnerReference, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
type patchStringValue struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (cp *connPackage) getLatestMainJobAndPatch(proposedChanges *jobsmanagerv1beta1.ManagedJob) (bool, error) {
|
||||
mj := jobsmanagerv1beta1.ManagedJob{}
|
||||
err := cp.r.Client.Get(cp.ctx, cp.req.NamespacedName, &mj)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
diff, diffIdentical, _ := pandati.CompareStructsReplaced(mj, proposedChanges)
|
||||
if !diffIdentical {
|
||||
if err != nil {
|
||||
log.Log.Error(err, "Unable to marshal ManagedJob")
|
||||
}
|
||||
var payloadBytesArr []patchStringValue
|
||||
for _, d := range diff {
|
||||
if strings.HasPrefix(d.Key, "/spec") {
|
||||
operation := "replace"
|
||||
if d.OldValue == "" || d.OldValue == nil {
|
||||
operation = "add"
|
||||
}
|
||||
|
||||
payload := patchStringValue{
|
||||
Op: operation,
|
||||
Path: d.Key,
|
||||
Value: d.Value.(string),
|
||||
}
|
||||
payloadBytesArr = append(payloadBytesArr, payload)
|
||||
}
|
||||
}
|
||||
patchPayload, err := json.Marshal(payloadBytesArr)
|
||||
if err != nil {
|
||||
log.Log.Error(err, "Unable to marshal ManagedJob")
|
||||
}
|
||||
fmt.Printf("PatchPayload: %# v", pretty.Formatter(fmt.Sprintf("%s", patchPayload)))
|
||||
kubepath := client.RawPatch(types.JSONPatchType, patchPayload)
|
||||
err = cp.r.Client.Patch(cp.ctx, &mj, kubepath)
|
||||
// if err != nil {
|
||||
// log.Log.Error(err, "Unable to patch ManagedJob")
|
||||
// }
|
||||
return true, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (cp *connPackage) updateCRDStatusDirectly() error {
|
||||
cp.mtx.Lock()
|
||||
// defer cp.mtx.Unlock()
|
||||
defer cp.mtx.Unlock()
|
||||
|
||||
// val, err := cp.getLatestMainJobAndPatch(cp.mj)
|
||||
// if err != nil {
|
||||
// log.Log.Error(err, "Unable to get latest ManagedJob")
|
||||
// return err
|
||||
// }
|
||||
if err := cp.r.Update(cp.ctx, cp.mj); err != nil {
|
||||
cp.logger.Error(err, "Unable to update ManagedJob status directly")
|
||||
return err
|
||||
}
|
||||
|
||||
// if !val && err == nil {
|
||||
err := cp.r.Update(cp.ctx, cp.mj)
|
||||
if err != nil {
|
||||
// log.Log.Info("Error", err.Error(), "more", "Unable to update ManagedJob status directly")
|
||||
if err := cp.r.Client.Get(cp.ctx, cp.req.NamespacedName, cp.mj); err != nil {
|
||||
cp.logger.Error(err, "Unable to get updated ManagedJob")
|
||||
return err
|
||||
}
|
||||
// get updated ManagedJob
|
||||
err = cp.r.Client.Get(cp.ctx, cp.req.NamespacedName, cp.mj)
|
||||
if err != nil {
|
||||
log.Log.Error(err, "Unable to get updated ManagedJob")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getImagePullPolicy returns the specified pull policy or IfNotPresent as default
|
||||
func getImagePullPolicy(policy string) corev1.PullPolicy {
|
||||
if policy == "" {
|
||||
return corev1.PullIfNotPresent
|
||||
}
|
||||
// }
|
||||
cp.mtx.Unlock()
|
||||
return err
|
||||
return corev1.PullPolicy(policy)
|
||||
}
|
||||
|
||||
// getResources returns the resource requirements or empty requirements if nil
|
||||
func getResources(resources *corev1.ResourceRequirements) corev1.ResourceRequirements {
|
||||
if resources == nil {
|
||||
return corev1.ResourceRequirements{}
|
||||
}
|
||||
return *resources
|
||||
}
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
)
|
||||
|
||||
func TestJobNameGenerator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
parts []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "single part",
|
||||
parts: []string{"workflow"},
|
||||
expected: "workflow",
|
||||
},
|
||||
{
|
||||
name: "multiple parts",
|
||||
parts: []string{"workflow", "group", "job"},
|
||||
expected: "workflow-group-job",
|
||||
},
|
||||
{
|
||||
name: "uppercase conversion",
|
||||
parts: []string{"Workflow", "GROUP", "Job"},
|
||||
expected: "workflow-group-job",
|
||||
},
|
||||
{
|
||||
name: "mixed case",
|
||||
parts: []string{"MyWorkflow", "TestGroup", "BuildJob"},
|
||||
expected: "myworkflow-testgroup-buildjob",
|
||||
},
|
||||
{
|
||||
name: "empty parts",
|
||||
parts: []string{},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := jobNameGenerator(tt.parts...)
|
||||
if result != tt.expected {
|
||||
t.Errorf("jobNameGenerator(%v) = %v, want %v", tt.parts, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetImagePullPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
policy string
|
||||
expected corev1.PullPolicy
|
||||
}{
|
||||
{
|
||||
name: "empty policy defaults to IfNotPresent",
|
||||
policy: "",
|
||||
expected: corev1.PullIfNotPresent,
|
||||
},
|
||||
{
|
||||
name: "Always policy",
|
||||
policy: "Always",
|
||||
expected: corev1.PullAlways,
|
||||
},
|
||||
{
|
||||
name: "Never policy",
|
||||
policy: "Never",
|
||||
expected: corev1.PullNever,
|
||||
},
|
||||
{
|
||||
name: "IfNotPresent policy",
|
||||
policy: "IfNotPresent",
|
||||
expected: corev1.PullIfNotPresent,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getImagePullPolicy(tt.policy)
|
||||
if result != tt.expected {
|
||||
t.Errorf("getImagePullPolicy(%v) = %v, want %v", tt.policy, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetResources(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
resources *corev1.ResourceRequirements
|
||||
expectNil bool
|
||||
}{
|
||||
{
|
||||
name: "nil resources returns empty",
|
||||
resources: nil,
|
||||
expectNil: false,
|
||||
},
|
||||
{
|
||||
name: "non-nil resources returned as-is",
|
||||
resources: &corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{
|
||||
corev1.ResourceCPU: resource.MustParse("100m"),
|
||||
corev1.ResourceMemory: resource.MustParse("128Mi"),
|
||||
},
|
||||
},
|
||||
expectNil: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getResources(tt.resources)
|
||||
if tt.resources == nil {
|
||||
// Should return empty struct
|
||||
if result.Limits != nil || result.Requests != nil {
|
||||
t.Errorf("getResources(nil) should return empty ResourceRequirements")
|
||||
}
|
||||
} else {
|
||||
// Should return the same values
|
||||
if len(result.Limits) != len(tt.resources.Limits) {
|
||||
t.Errorf("getResources() limits mismatch")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDependencyMaps(t *testing.T) {
|
||||
// Create a mock ManagedJob with dependencies
|
||||
mj := &jobsmanagerv1beta1.ManagedJob{
|
||||
Spec: jobsmanagerv1beta1.ManagedJobSpec{
|
||||
Groups: []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
{
|
||||
Name: "group1",
|
||||
Dependencies: []*jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
{Name: "init-group", Status: "pending"},
|
||||
},
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
{
|
||||
Name: "job1",
|
||||
Dependencies: []*jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
{Name: "setup-job", Status: "pending"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "job2",
|
||||
Dependencies: []*jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
{Name: "setup-job", Status: "pending"},
|
||||
{Name: "job1", Status: "pending"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "group2",
|
||||
Dependencies: []*jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
{Name: "group1", Status: "pending"},
|
||||
},
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
{
|
||||
Name: "job3",
|
||||
Dependencies: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cp := &connPackage{mj: mj}
|
||||
cp.buildDependencyMaps()
|
||||
|
||||
// Verify job dependency map
|
||||
t.Run("job dependency map - setup-job has 2 dependents", func(t *testing.T) {
|
||||
deps, exists := cp.jobDepMap["setup-job"]
|
||||
if !exists {
|
||||
t.Fatal("setup-job should exist in jobDepMap")
|
||||
}
|
||||
if len(deps) != 2 {
|
||||
t.Errorf("setup-job should have 2 dependents, got %d", len(deps))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("job dependency map - job1 has 1 dependent", func(t *testing.T) {
|
||||
deps, exists := cp.jobDepMap["job1"]
|
||||
if !exists {
|
||||
t.Fatal("job1 should exist in jobDepMap")
|
||||
}
|
||||
if len(deps) != 1 {
|
||||
t.Errorf("job1 should have 1 dependent, got %d", len(deps))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("job dependency map - non-existent job", func(t *testing.T) {
|
||||
_, exists := cp.jobDepMap["non-existent"]
|
||||
if exists {
|
||||
t.Error("non-existent job should not be in jobDepMap")
|
||||
}
|
||||
})
|
||||
|
||||
// Verify group dependency map
|
||||
t.Run("group dependency map - init-group has 1 dependent", func(t *testing.T) {
|
||||
deps, exists := cp.groupDepMap["init-group"]
|
||||
if !exists {
|
||||
t.Fatal("init-group should exist in groupDepMap")
|
||||
}
|
||||
if len(deps) != 1 {
|
||||
t.Errorf("init-group should have 1 dependent, got %d", len(deps))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("group dependency map - group1 has 1 dependent", func(t *testing.T) {
|
||||
deps, exists := cp.groupDepMap["group1"]
|
||||
if !exists {
|
||||
t.Fatal("group1 should exist in groupDepMap")
|
||||
}
|
||||
if len(deps) != 1 {
|
||||
t.Errorf("group1 should have 1 dependent, got %d", len(deps))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateDependentJobs(t *testing.T) {
|
||||
dep1 := &jobsmanagerv1beta1.ManagedJobDependencies{Name: "target-job", Status: "pending"}
|
||||
dep2 := &jobsmanagerv1beta1.ManagedJobDependencies{Name: "target-job", Status: "pending"}
|
||||
dep3 := &jobsmanagerv1beta1.ManagedJobDependencies{Name: "other-job", Status: "pending"}
|
||||
|
||||
cp := &connPackage{
|
||||
jobDepMap: map[string][]*jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
"target-job": {dep1, dep2},
|
||||
"other-job": {dep3},
|
||||
},
|
||||
}
|
||||
|
||||
// Update target-job status
|
||||
cp.updateDependentJobs("target-job", "succeeded")
|
||||
|
||||
t.Run("target-job dependents updated", func(t *testing.T) {
|
||||
if dep1.Status != "succeeded" {
|
||||
t.Errorf("dep1.Status = %v, want succeeded", dep1.Status)
|
||||
}
|
||||
if dep2.Status != "succeeded" {
|
||||
t.Errorf("dep2.Status = %v, want succeeded", dep2.Status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("other-job dependents unchanged", func(t *testing.T) {
|
||||
if dep3.Status != "pending" {
|
||||
t.Errorf("dep3.Status = %v, want pending (should be unchanged)", dep3.Status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existent job is safe", func(t *testing.T) {
|
||||
// Should not panic
|
||||
cp.updateDependentJobs("non-existent-job", "succeeded")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateDependentGroups(t *testing.T) {
|
||||
dep1 := &jobsmanagerv1beta1.ManagedJobDependencies{Name: "target-group", Status: "pending"}
|
||||
dep2 := &jobsmanagerv1beta1.ManagedJobDependencies{Name: "other-group", Status: "pending"}
|
||||
|
||||
cp := &connPackage{
|
||||
groupDepMap: map[string][]*jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
"target-group": {dep1},
|
||||
"other-group": {dep2},
|
||||
},
|
||||
}
|
||||
|
||||
cp.updateDependentGroups("target-group", "succeeded")
|
||||
|
||||
t.Run("target-group dependents updated", func(t *testing.T) {
|
||||
if dep1.Status != "succeeded" {
|
||||
t.Errorf("dep1.Status = %v, want succeeded", dep1.Status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("other-group dependents unchanged", func(t *testing.T) {
|
||||
if dep2.Status != "pending" {
|
||||
t.Errorf("dep2.Status = %v, want pending (should be unchanged)", dep2.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileParameters(t *testing.T) {
|
||||
cp := &connPackage{}
|
||||
|
||||
t.Run("merges multiple parameter sets", func(t *testing.T) {
|
||||
params1 := jobsmanagerv1beta1.ManagedJobParameters{
|
||||
ServiceAccount: "sa1",
|
||||
RestartPolicy: "Never",
|
||||
}
|
||||
params2 := jobsmanagerv1beta1.ManagedJobParameters{
|
||||
ServiceAccount: "sa2", // should override
|
||||
ImagePullPolicy: "Always",
|
||||
}
|
||||
|
||||
result := cp.compileParameters(params1, params2)
|
||||
|
||||
if result.ServiceAccount != "sa2" {
|
||||
t.Errorf("ServiceAccount = %v, want sa2", result.ServiceAccount)
|
||||
}
|
||||
if result.RestartPolicy != "Never" {
|
||||
t.Errorf("RestartPolicy = %v, want Never", result.RestartPolicy)
|
||||
}
|
||||
if result.ImagePullPolicy != "Always" {
|
||||
t.Errorf("ImagePullPolicy = %v, want Always", result.ImagePullPolicy)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("merges env vars", func(t *testing.T) {
|
||||
params1 := jobsmanagerv1beta1.ManagedJobParameters{
|
||||
Env: []corev1.EnvVar{{Name: "VAR1", Value: "val1"}},
|
||||
}
|
||||
params2 := jobsmanagerv1beta1.ManagedJobParameters{
|
||||
Env: []corev1.EnvVar{{Name: "VAR2", Value: "val2"}},
|
||||
}
|
||||
|
||||
result := cp.compileParameters(params1, params2)
|
||||
|
||||
if len(result.Env) != 2 {
|
||||
t.Errorf("len(Env) = %v, want 2", len(result.Env))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handles empty parameters", func(t *testing.T) {
|
||||
result := cp.compileParameters(jobsmanagerv1beta1.ManagedJobParameters{})
|
||||
if result.ServiceAccount != "" {
|
||||
t.Errorf("ServiceAccount should be empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -18,17 +18,26 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/lukaszraczylo/pandati"
|
||||
kbatch "k8s.io/api/batch/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
// RequeueDelay is the time to wait before requeuing when jobs are running
|
||||
RequeueDelay = 30 * time.Second
|
||||
)
|
||||
|
||||
// ManagedJobReconciler reconciles a ManagedJob object
|
||||
type ManagedJobReconciler struct {
|
||||
client.Client
|
||||
@@ -39,32 +48,128 @@ type ManagedJobReconciler struct {
|
||||
//+kubebuilder:rbac:groups=jobsmanager.raczylo.com,resources=managedjobs,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=jobsmanager.raczylo.com,resources=managedjobs/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=jobsmanager.raczylo.com,resources=managedjobs/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups="",resources=events,verbs=create;update;patch;delete;get;list;watch
|
||||
|
||||
// Reconcile ensures ManagedJob workflows progress toward completion.
|
||||
// It orchestrates job execution respecting dependencies, manages retries,
|
||||
// and tracks overall workflow status.
|
||||
func (r *ManagedJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
_ = log.FromContext(ctx)
|
||||
|
||||
cp := &connPackage{
|
||||
r: r,
|
||||
ctx: ctx,
|
||||
req: req,
|
||||
}
|
||||
logger := log.FromContext(ctx).WithValues("managedJob", req.NamespacedName)
|
||||
|
||||
var managedJob jobsmanagerv1beta1.ManagedJob
|
||||
if err := r.Get(ctx, req.NamespacedName, &managedJob); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
cp.mj = &managedJob
|
||||
cp.buildJobsDependencyTree()
|
||||
cp.checkJobStatus()
|
||||
cp.runPendingJobs()
|
||||
cp.checkGroupsStatus()
|
||||
cp := &connPackage{
|
||||
r: r,
|
||||
ctx: ctx,
|
||||
req: req,
|
||||
logger: logger,
|
||||
mj: &managedJob,
|
||||
}
|
||||
|
||||
// fmt.Printf("Reconcile: %# v", pretty.Formatter(r.Updater))
|
||||
// Handle deletion with finalizer
|
||||
if !managedJob.DeletionTimestamp.IsZero() {
|
||||
return r.handleDeletion(ctx, cp)
|
||||
}
|
||||
|
||||
// Add finalizer if not present
|
||||
if !controllerutil.ContainsFinalizer(&managedJob, FinalizerName) {
|
||||
controllerutil.AddFinalizer(&managedJob, FinalizerName)
|
||||
if err := r.Update(ctx, &managedJob); err != nil {
|
||||
logger.Error(err, "Failed to add finalizer")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{RequeueAfter: time.Second}, nil
|
||||
}
|
||||
|
||||
originalMainJobDefinition := cp.mj.DeepCopy()
|
||||
cp.generateDependencyTree()
|
||||
cp.buildDependencyMaps() // Build lookup maps for O(1) dependency updates
|
||||
_, theSame, _ := pandati.CompareStructsReplaced(originalMainJobDefinition, cp.mj)
|
||||
if !theSame {
|
||||
if err := cp.updateCRDStatusDirectly(); err != nil {
|
||||
logger.Error(err, "Failed to update CRD status after dependency tree generation")
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
originalMainJobDefinition = cp.mj.DeepCopy()
|
||||
|
||||
cp.checkRunningJobsStatus()
|
||||
cp.runPendingJobs()
|
||||
|
||||
_, theSame, _ = pandati.CompareStructsReplaced(originalMainJobDefinition, cp.mj)
|
||||
if !theSame {
|
||||
if err := cp.updateCRDStatusDirectly(); err != nil {
|
||||
logger.Error(err, "Failed to update CRD status after job processing")
|
||||
}
|
||||
}
|
||||
|
||||
cp.checkOverallStatus()
|
||||
|
||||
// If workflow is still running, requeue after a delay to check status
|
||||
if cp.mj.Status == ExecutionStatusRunning {
|
||||
return ctrl.Result{RequeueAfter: RequeueDelay}, nil
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// handleDeletion cleans up child jobs before removing the finalizer
|
||||
func (r *ManagedJobReconciler) handleDeletion(ctx context.Context, cp *connPackage) (ctrl.Result, error) {
|
||||
if !controllerutil.ContainsFinalizer(cp.mj, FinalizerName) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
cp.logger.Info("Cleaning up child jobs before deletion")
|
||||
|
||||
// Delete all child jobs
|
||||
if err := r.deleteChildJobs(ctx, cp); err != nil {
|
||||
cp.logger.Error(err, "Failed to delete child jobs")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Remove finalizer
|
||||
controllerutil.RemoveFinalizer(cp.mj, FinalizerName)
|
||||
if err := r.Update(ctx, cp.mj); err != nil {
|
||||
cp.logger.Error(err, "Failed to remove finalizer")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
cp.logger.Info("Successfully cleaned up ManagedJob")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// deleteChildJobs removes all jobs owned by this ManagedJob
|
||||
func (r *ManagedJobReconciler) deleteChildJobs(ctx context.Context, cp *connPackage) error {
|
||||
var childJobs kbatch.JobList
|
||||
labelSelector := labels.SelectorFromSet(labels.Set{
|
||||
LabelWorkflowName: cp.mj.Name,
|
||||
})
|
||||
listOptions := &client.ListOptions{
|
||||
LabelSelector: labelSelector,
|
||||
Namespace: cp.mj.Namespace,
|
||||
}
|
||||
|
||||
if err := r.Client.List(ctx, &childJobs, listOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range childJobs.Items {
|
||||
job := &childJobs.Items[i]
|
||||
if err := r.Client.Delete(ctx, job, client.PropagationPolicy("Background")); err != nil {
|
||||
cp.logger.Error(err, "Failed to delete child job", "job", job.Name)
|
||||
// Continue trying to delete other jobs
|
||||
} else {
|
||||
cp.logger.Info("Deleted child job", "job", job.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *ManagedJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
//go:build envtest
|
||||
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
kbatch "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
)
|
||||
|
||||
var _ = Describe("ManagedJob Controller", func() {
|
||||
const (
|
||||
ManagedJobName = "test-workflow"
|
||||
ManagedJobNamespace = "default"
|
||||
timeout = time.Second * 10
|
||||
interval = time.Millisecond * 250
|
||||
)
|
||||
|
||||
Context("When creating a ManagedJob", func() {
|
||||
It("Should add finalizer to new ManagedJob", func() {
|
||||
ctx := context.Background()
|
||||
|
||||
managedJob := &jobsmanagerv1beta1.ManagedJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ManagedJobName + "-finalizer",
|
||||
Namespace: ManagedJobNamespace,
|
||||
},
|
||||
Spec: jobsmanagerv1beta1.ManagedJobSpec{
|
||||
Groups: []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
{
|
||||
Name: "group1",
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
{
|
||||
Name: "job1",
|
||||
Image: "busybox:latest",
|
||||
Args: []string{"echo", "hello"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Expect(k8sClient.Create(ctx, managedJob)).Should(Succeed())
|
||||
|
||||
lookupKey := types.NamespacedName{Name: ManagedJobName + "-finalizer", Namespace: ManagedJobNamespace}
|
||||
createdManagedJob := &jobsmanagerv1beta1.ManagedJob{}
|
||||
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, lookupKey, createdManagedJob)
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// Trigger reconciliation manually since we don't have the controller running
|
||||
reconciler := &ManagedJobReconciler{
|
||||
Client: k8sClient,
|
||||
Scheme: k8sClient.Scheme(),
|
||||
}
|
||||
|
||||
_, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: lookupKey})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Verify finalizer was added
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, lookupKey, createdManagedJob)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, f := range createdManagedJob.Finalizers {
|
||||
if f == FinalizerName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// Cleanup
|
||||
Expect(k8sClient.Delete(ctx, createdManagedJob)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Should initialize job statuses to pending", func() {
|
||||
ctx := context.Background()
|
||||
|
||||
managedJob := &jobsmanagerv1beta1.ManagedJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ManagedJobName + "-status",
|
||||
Namespace: ManagedJobNamespace,
|
||||
},
|
||||
Spec: jobsmanagerv1beta1.ManagedJobSpec{
|
||||
Groups: []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
{
|
||||
Name: "init-group",
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
{
|
||||
Name: "init-job",
|
||||
Image: "busybox:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "main-group",
|
||||
Dependencies: []*jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
{Name: "init-group"},
|
||||
},
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
{
|
||||
Name: "main-job",
|
||||
Image: "busybox:latest",
|
||||
Dependencies: []*jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
{Name: "init-job"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Expect(k8sClient.Create(ctx, managedJob)).Should(Succeed())
|
||||
|
||||
lookupKey := types.NamespacedName{Name: ManagedJobName + "-status", Namespace: ManagedJobNamespace}
|
||||
createdManagedJob := &jobsmanagerv1beta1.ManagedJob{}
|
||||
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, lookupKey, createdManagedJob)
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// Trigger reconciliation
|
||||
reconciler := &ManagedJobReconciler{
|
||||
Client: k8sClient,
|
||||
Scheme: k8sClient.Scheme(),
|
||||
}
|
||||
|
||||
// First reconcile adds finalizer
|
||||
_, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: lookupKey})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Second reconcile processes jobs
|
||||
_, err = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: lookupKey})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Verify statuses
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, lookupKey, createdManagedJob)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Check that groups have jobs with pending status initially
|
||||
for _, g := range createdManagedJob.Spec.Groups {
|
||||
for _, j := range g.Jobs {
|
||||
if j.Status == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// Cleanup
|
||||
Expect(k8sClient.Delete(ctx, createdManagedJob)).Should(Succeed())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When a ManagedJob is deleted", func() {
|
||||
It("Should clean up child jobs", func() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a ManagedJob
|
||||
managedJob := &jobsmanagerv1beta1.ManagedJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ManagedJobName + "-cleanup",
|
||||
Namespace: ManagedJobNamespace,
|
||||
},
|
||||
Spec: jobsmanagerv1beta1.ManagedJobSpec{
|
||||
Groups: []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
{
|
||||
Name: "cleanup-group",
|
||||
Jobs: []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
{
|
||||
Name: "cleanup-job",
|
||||
Image: "busybox:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Expect(k8sClient.Create(ctx, managedJob)).Should(Succeed())
|
||||
|
||||
lookupKey := types.NamespacedName{Name: ManagedJobName + "-cleanup", Namespace: ManagedJobNamespace}
|
||||
|
||||
// Create a child job with the workflow label
|
||||
childJob := &kbatch.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-workflow-cleanup-cleanup-group-cleanup-job",
|
||||
Namespace: ManagedJobNamespace,
|
||||
Labels: map[string]string{
|
||||
LabelWorkflowName: ManagedJobName + "-cleanup",
|
||||
},
|
||||
},
|
||||
Spec: kbatch.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "test",
|
||||
Image: "busybox:latest",
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, childJob)).Should(Succeed())
|
||||
|
||||
// Verify child job exists
|
||||
childJobKey := types.NamespacedName{
|
||||
Name: "test-workflow-cleanup-cleanup-group-cleanup-job",
|
||||
Namespace: ManagedJobNamespace,
|
||||
}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, childJobKey, &kbatch.Job{})
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
// Delete the ManagedJob
|
||||
createdManagedJob := &jobsmanagerv1beta1.ManagedJob{}
|
||||
Expect(k8sClient.Get(ctx, lookupKey, createdManagedJob)).Should(Succeed())
|
||||
Expect(k8sClient.Delete(ctx, createdManagedJob)).Should(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Execution Status Constants", func() {
|
||||
It("Should have correct status values", func() {
|
||||
Expect(ExecutionStatusPending).To(Equal("pending"))
|
||||
Expect(ExecutionStatusRunning).To(Equal("running"))
|
||||
Expect(ExecutionStatusSucceeded).To(Equal("succeeded"))
|
||||
Expect(ExecutionStatusFailed).To(Equal("failed"))
|
||||
Expect(ExecutionStatusAborted).To(Equal("aborted"))
|
||||
})
|
||||
|
||||
It("Should have correct label values", func() {
|
||||
Expect(LabelWorkflowName).To(Equal("jobmanager.raczylo.com/workflow-name"))
|
||||
Expect(LabelGroupName).To(Equal("jobmanager.raczylo.com/group-name"))
|
||||
Expect(LabelJobName).To(Equal("jobmanager.raczylo.com/job-name"))
|
||||
Expect(LabelJobID).To(Equal("jobmanager.raczylo.com/job-id"))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
// JobsCreatedTotal tracks the total number of jobs created
|
||||
JobsCreatedTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "managedjob_jobs_created_total",
|
||||
Help: "Total number of Kubernetes jobs created by the operator",
|
||||
},
|
||||
[]string{"namespace", "workflow", "group"},
|
||||
)
|
||||
|
||||
// JobsSucceededTotal tracks the total number of jobs that succeeded
|
||||
JobsSucceededTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "managedjob_jobs_succeeded_total",
|
||||
Help: "Total number of jobs that completed successfully",
|
||||
},
|
||||
[]string{"namespace", "workflow", "group"},
|
||||
)
|
||||
|
||||
// JobsFailedTotal tracks the total number of jobs that failed
|
||||
JobsFailedTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "managedjob_jobs_failed_total",
|
||||
Help: "Total number of jobs that failed",
|
||||
},
|
||||
[]string{"namespace", "workflow", "group"},
|
||||
)
|
||||
|
||||
// ReconciliationDuration tracks how long reconciliations take
|
||||
ReconciliationDuration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "managedjob_reconciliation_duration_seconds",
|
||||
Help: "Time spent reconciling ManagedJob resources",
|
||||
Buckets: prometheus.ExponentialBuckets(0.001, 2, 15), // 1ms to ~16s
|
||||
},
|
||||
[]string{"namespace", "workflow"},
|
||||
)
|
||||
|
||||
// ActiveJobs tracks the number of currently running jobs per workflow
|
||||
ActiveJobs = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "managedjob_active_jobs",
|
||||
Help: "Number of currently active (running) jobs per workflow",
|
||||
},
|
||||
[]string{"namespace", "workflow"},
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register custom metrics with the controller-runtime metrics registry
|
||||
metrics.Registry.MustRegister(
|
||||
JobsCreatedTotal,
|
||||
JobsSucceededTotal,
|
||||
JobsFailedTotal,
|
||||
ReconciliationDuration,
|
||||
ActiveJobs,
|
||||
)
|
||||
}
|
||||
|
||||
// RecordJobCreated increments the job created counter
|
||||
func RecordJobCreated(namespace, workflow, group string) {
|
||||
JobsCreatedTotal.WithLabelValues(namespace, workflow, group).Inc()
|
||||
}
|
||||
|
||||
// RecordJobSucceeded increments the job succeeded counter
|
||||
func RecordJobSucceeded(namespace, workflow, group string) {
|
||||
JobsSucceededTotal.WithLabelValues(namespace, workflow, group).Inc()
|
||||
}
|
||||
|
||||
// RecordJobFailed increments the job failed counter
|
||||
func RecordJobFailed(namespace, workflow, group string) {
|
||||
JobsFailedTotal.WithLabelValues(namespace, workflow, group).Inc()
|
||||
}
|
||||
|
||||
// SetActiveJobs sets the number of active jobs for a workflow
|
||||
func SetActiveJobs(namespace, workflow string, count float64) {
|
||||
ActiveJobs.WithLabelValues(namespace, workflow).Set(count)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRecordJobCreated(t *testing.T) {
|
||||
// Reset counter for testing
|
||||
JobsCreatedTotal.Reset()
|
||||
|
||||
// Record job creation
|
||||
RecordJobCreated("default", "test-workflow", "group1")
|
||||
RecordJobCreated("default", "test-workflow", "group1")
|
||||
RecordJobCreated("production", "other-workflow", "group2")
|
||||
|
||||
// Verify counts
|
||||
assert.Equal(t, float64(2), testutil.ToFloat64(JobsCreatedTotal.WithLabelValues("default", "test-workflow", "group1")))
|
||||
assert.Equal(t, float64(1), testutil.ToFloat64(JobsCreatedTotal.WithLabelValues("production", "other-workflow", "group2")))
|
||||
}
|
||||
|
||||
func TestRecordJobSucceeded(t *testing.T) {
|
||||
// Reset counter for testing
|
||||
JobsSucceededTotal.Reset()
|
||||
|
||||
// Record job success
|
||||
RecordJobSucceeded("default", "workflow1", "group1")
|
||||
RecordJobSucceeded("default", "workflow1", "group1")
|
||||
RecordJobSucceeded("default", "workflow1", "group2")
|
||||
|
||||
// Verify counts
|
||||
assert.Equal(t, float64(2), testutil.ToFloat64(JobsSucceededTotal.WithLabelValues("default", "workflow1", "group1")))
|
||||
assert.Equal(t, float64(1), testutil.ToFloat64(JobsSucceededTotal.WithLabelValues("default", "workflow1", "group2")))
|
||||
}
|
||||
|
||||
func TestRecordJobFailed(t *testing.T) {
|
||||
// Reset counter for testing
|
||||
JobsFailedTotal.Reset()
|
||||
|
||||
// Record job failure
|
||||
RecordJobFailed("production", "critical-workflow", "init")
|
||||
RecordJobFailed("production", "critical-workflow", "init")
|
||||
RecordJobFailed("production", "critical-workflow", "cleanup")
|
||||
|
||||
// Verify counts
|
||||
assert.Equal(t, float64(2), testutil.ToFloat64(JobsFailedTotal.WithLabelValues("production", "critical-workflow", "init")))
|
||||
assert.Equal(t, float64(1), testutil.ToFloat64(JobsFailedTotal.WithLabelValues("production", "critical-workflow", "cleanup")))
|
||||
}
|
||||
|
||||
func TestSetActiveJobs(t *testing.T) {
|
||||
// Reset gauge for testing
|
||||
ActiveJobs.Reset()
|
||||
|
||||
// Set active jobs
|
||||
SetActiveJobs("default", "workflow1", 5)
|
||||
SetActiveJobs("production", "workflow2", 3)
|
||||
SetActiveJobs("default", "workflow1", 2) // Update to lower value
|
||||
|
||||
// Verify values
|
||||
assert.Equal(t, float64(2), testutil.ToFloat64(ActiveJobs.WithLabelValues("default", "workflow1")))
|
||||
assert.Equal(t, float64(3), testutil.ToFloat64(ActiveJobs.WithLabelValues("production", "workflow2")))
|
||||
}
|
||||
|
||||
func TestMetricsLabels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
workflow string
|
||||
group string
|
||||
metric *prometheus.CounterVec
|
||||
recorder func(ns, wf, grp string)
|
||||
}{
|
||||
{
|
||||
name: "created_metric_labels",
|
||||
namespace: "ns1",
|
||||
workflow: "wf1",
|
||||
group: "grp1",
|
||||
metric: JobsCreatedTotal,
|
||||
recorder: RecordJobCreated,
|
||||
},
|
||||
{
|
||||
name: "succeeded_metric_labels",
|
||||
namespace: "ns2",
|
||||
workflow: "wf2",
|
||||
group: "grp2",
|
||||
metric: JobsSucceededTotal,
|
||||
recorder: RecordJobSucceeded,
|
||||
},
|
||||
{
|
||||
name: "failed_metric_labels",
|
||||
namespace: "ns3",
|
||||
workflow: "wf3",
|
||||
group: "grp3",
|
||||
metric: JobsFailedTotal,
|
||||
recorder: RecordJobFailed,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.metric.Reset()
|
||||
tt.recorder(tt.namespace, tt.workflow, tt.group)
|
||||
|
||||
value := testutil.ToFloat64(tt.metric.WithLabelValues(tt.namespace, tt.workflow, tt.group))
|
||||
assert.Equal(t, float64(1), value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsMultipleNamespaces(t *testing.T) {
|
||||
// Reset all metrics
|
||||
JobsCreatedTotal.Reset()
|
||||
JobsSucceededTotal.Reset()
|
||||
JobsFailedTotal.Reset()
|
||||
ActiveJobs.Reset()
|
||||
|
||||
namespaces := []string{"dev", "staging", "production"}
|
||||
|
||||
for _, ns := range namespaces {
|
||||
RecordJobCreated(ns, "workflow", "group")
|
||||
RecordJobSucceeded(ns, "workflow", "group")
|
||||
RecordJobFailed(ns, "workflow", "group")
|
||||
SetActiveJobs(ns, "workflow", 1)
|
||||
}
|
||||
|
||||
for _, ns := range namespaces {
|
||||
assert.Equal(t, float64(1), testutil.ToFloat64(JobsCreatedTotal.WithLabelValues(ns, "workflow", "group")))
|
||||
assert.Equal(t, float64(1), testutil.ToFloat64(JobsSucceededTotal.WithLabelValues(ns, "workflow", "group")))
|
||||
assert.Equal(t, float64(1), testutil.ToFloat64(JobsFailedTotal.WithLabelValues(ns, "workflow", "group")))
|
||||
assert.Equal(t, float64(1), testutil.ToFloat64(ActiveJobs.WithLabelValues(ns, "workflow")))
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveJobsGaugeDecreases(t *testing.T) {
|
||||
ActiveJobs.Reset()
|
||||
|
||||
// Simulate job lifecycle
|
||||
SetActiveJobs("default", "workflow", 0)
|
||||
assert.Equal(t, float64(0), testutil.ToFloat64(ActiveJobs.WithLabelValues("default", "workflow")))
|
||||
|
||||
SetActiveJobs("default", "workflow", 5)
|
||||
assert.Equal(t, float64(5), testutil.ToFloat64(ActiveJobs.WithLabelValues("default", "workflow")))
|
||||
|
||||
SetActiveJobs("default", "workflow", 3)
|
||||
assert.Equal(t, float64(3), testutil.ToFloat64(ActiveJobs.WithLabelValues("default", "workflow")))
|
||||
|
||||
SetActiveJobs("default", "workflow", 0)
|
||||
assert.Equal(t, float64(0), testutil.ToFloat64(ActiveJobs.WithLabelValues("default", "workflow")))
|
||||
}
|
||||
@@ -0,0 +1,732 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
kbatch "k8s.io/api/batch/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
)
|
||||
|
||||
// ReconcilerTestSuite contains all reconciler tests
|
||||
type ReconcilerTestSuite struct {
|
||||
suite.Suite
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
client *MockClient
|
||||
reconciler *ManagedJobReconciler
|
||||
}
|
||||
|
||||
func (s *ReconcilerTestSuite) SetupTest() {
|
||||
s.ctx, s.cancel = context.WithTimeout(context.Background(), 30*time.Second)
|
||||
s.client = NewMockClient()
|
||||
s.reconciler = &ManagedJobReconciler{
|
||||
Client: s.client,
|
||||
Scheme: s.client.Scheme(),
|
||||
Recorder: NewFakeRecorder(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ReconcilerTestSuite) TearDownTest() {
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
func TestReconcilerSuite(t *testing.T) {
|
||||
suite.Run(t, new(ReconcilerTestSuite))
|
||||
}
|
||||
|
||||
// ==================== GOOD SCENARIOS ====================
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_Good_NewManagedJob_AddsFinalizer() {
|
||||
// Arrange
|
||||
mj := NewTestManagedJob("test-workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act
|
||||
result, err := s.reconciler.Reconcile(s.ctx, req)
|
||||
|
||||
// Assert
|
||||
s.NoError(err)
|
||||
s.True(result.RequeueAfter > 0, "should requeue after adding finalizer")
|
||||
|
||||
updated := s.client.GetManagedJobByKey("default/test-workflow")
|
||||
s.True(controllerutil.ContainsFinalizer(updated, FinalizerName), "finalizer should be added")
|
||||
}
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_Good_SingleJobWorkflow_CreatesJob() {
|
||||
// Arrange
|
||||
mj := NewTestManagedJob("test-workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act - first reconcile generates dependency tree
|
||||
_, err := s.reconciler.Reconcile(s.ctx, req)
|
||||
s.NoError(err)
|
||||
|
||||
// Act - second reconcile runs jobs
|
||||
_, err = s.reconciler.Reconcile(s.ctx, req)
|
||||
s.NoError(err)
|
||||
|
||||
// Assert
|
||||
s.GreaterOrEqual(s.client.CreateCalls, 1, "should have created at least one job")
|
||||
}
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_Good_CompletedJob_UpdatesStatus() {
|
||||
// Arrange
|
||||
mj := NewTestManagedJob("test-workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusRunning
|
||||
mj.Spec.Groups[0].Status = ExecutionStatusRunning
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
// Add a completed K8s job
|
||||
k8sJob := NewTestK8sJob("test-workflow-group1-job1", "default", "test-workflow", "group1", kbatch.JobStatus{
|
||||
Succeeded: 1,
|
||||
})
|
||||
s.client.AddJob(k8sJob)
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act
|
||||
_, err := s.reconciler.Reconcile(s.ctx, req)
|
||||
|
||||
// Assert
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_Good_RunningWorkflow_RequeuesAfterDelay() {
|
||||
// Arrange
|
||||
mj := NewTestManagedJob("test-workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Status = ExecutionStatusRunning
|
||||
mj.Spec.Groups[0].Status = ExecutionStatusRunning
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusRunning
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
// Add running K8s job
|
||||
k8sJob := NewTestK8sJob("test-workflow-group1-job1", "default", "test-workflow", "group1", kbatch.JobStatus{
|
||||
Active: 1,
|
||||
})
|
||||
s.client.AddJob(k8sJob)
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act
|
||||
result, err := s.reconciler.Reconcile(s.ctx, req)
|
||||
|
||||
// Assert
|
||||
s.NoError(err)
|
||||
s.Equal(RequeueDelay, result.RequeueAfter, "should requeue after delay for running workflow")
|
||||
}
|
||||
|
||||
// ==================== NOT GOOD SCENARIOS ====================
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_NotGood_ManagedJobNotFound_ReturnsNoError() {
|
||||
// Arrange - no ManagedJob added to client
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "nonexistent", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act
|
||||
result, err := s.reconciler.Reconcile(s.ctx, req)
|
||||
|
||||
// Assert
|
||||
s.NoError(err, "should not return error for not found")
|
||||
s.Zero(result.RequeueAfter, "should not requeue for not found")
|
||||
}
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_NotGood_FailedJob_UpdatesStatusToFailed() {
|
||||
// Arrange
|
||||
mj := NewTestManagedJob("test-workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusRunning
|
||||
mj.Spec.Groups[0].Status = ExecutionStatusRunning
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
// Add a failed K8s job
|
||||
k8sJob := NewTestK8sJob("test-workflow-group1-job1", "default", "test-workflow", "group1", kbatch.JobStatus{
|
||||
Failed: 1,
|
||||
})
|
||||
s.client.AddJob(k8sJob)
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act
|
||||
_, err := s.reconciler.Reconcile(s.ctx, req)
|
||||
|
||||
// Assert
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_NotGood_DeletionInProgress_CleansUpJobs() {
|
||||
// Arrange
|
||||
now := metav1.Now()
|
||||
mj := NewTestManagedJob("test-workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.DeletionTimestamp = &now
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
// Add child job to be deleted
|
||||
k8sJob := NewTestK8sJob("test-workflow-group1-job1", "default", "test-workflow", "group1", kbatch.JobStatus{})
|
||||
s.client.AddJob(k8sJob)
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act
|
||||
_, err := s.reconciler.Reconcile(s.ctx, req)
|
||||
|
||||
// Assert
|
||||
s.NoError(err)
|
||||
s.GreaterOrEqual(s.client.DeleteCalls, 1, "should have deleted child jobs")
|
||||
}
|
||||
|
||||
// ==================== REALLY BAD SCENARIOS ====================
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_ReallyBad_GetError_ReturnsError() {
|
||||
// Arrange
|
||||
s.client.GetError = ErrServerError
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act
|
||||
_, err := s.reconciler.Reconcile(s.ctx, req)
|
||||
|
||||
// Assert
|
||||
s.Error(err)
|
||||
}
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_ReallyBad_UpdateConflict_ReturnsError() {
|
||||
// Arrange
|
||||
mj := NewTestManagedJob("test-workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
s.client.AddManagedJob(mj)
|
||||
s.client.UpdateError = ErrConflict
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act
|
||||
_, err := s.reconciler.Reconcile(s.ctx, req)
|
||||
|
||||
// Assert
|
||||
s.Error(err, "should return error on update conflict")
|
||||
}
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_ReallyBad_ContextTimeout_ReturnsError() {
|
||||
// Arrange
|
||||
mj := NewTestManagedJob("test-workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
s.client.AddManagedJob(mj)
|
||||
|
||||
// Cancel context immediately
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act
|
||||
_, err := s.reconciler.Reconcile(ctx, req)
|
||||
|
||||
// Assert
|
||||
s.Error(err, "should return error on context timeout")
|
||||
}
|
||||
|
||||
func (s *ReconcilerTestSuite) TestReconcile_ReallyBad_ListJobsError_Continues() {
|
||||
// Arrange
|
||||
mj := NewTestManagedJob("test-workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
s.client.AddManagedJob(mj)
|
||||
s.client.ListError = ErrServerError
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "test-workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act - should not panic, should handle gracefully
|
||||
_, err := s.reconciler.Reconcile(s.ctx, req)
|
||||
|
||||
// Assert - list error is logged but doesn't fail reconciliation
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
// ==================== MATRIX TEST: WORKFLOW SCENARIOS ====================
|
||||
|
||||
func TestReconcile_WorkflowScenarios(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scenario TestScenario
|
||||
setupMJ func() *jobsmanagerv1beta1.ManagedJob
|
||||
setupJobs func() []*kbatch.Job
|
||||
clientSetup func(*MockClient)
|
||||
expectError bool
|
||||
expectRequeue bool
|
||||
expectRequeueAfter time.Duration
|
||||
validateResult func(*testing.T, *MockClient, ctrl.Result)
|
||||
}{
|
||||
// GOOD SCENARIOS
|
||||
{
|
||||
name: "good_simple_workflow_starts",
|
||||
scenario: ScenarioGood,
|
||||
setupMJ: func() *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("init", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("setup", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
return mj
|
||||
},
|
||||
setupJobs: func() []*kbatch.Job { return nil },
|
||||
expectError: false,
|
||||
expectRequeue: false,
|
||||
},
|
||||
{
|
||||
name: "good_multi_group_workflow",
|
||||
scenario: ScenarioGood,
|
||||
setupMJ: func() *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("init", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("setup", "busybox:latest"),
|
||||
}),
|
||||
NewTestGroup("main", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("process", "busybox:latest"),
|
||||
}, NewTestDependency("init", ExecutionStatusPending)),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
return mj
|
||||
},
|
||||
setupJobs: func() []*kbatch.Job { return nil },
|
||||
expectError: false,
|
||||
expectRequeue: false,
|
||||
},
|
||||
{
|
||||
name: "good_all_jobs_completed",
|
||||
scenario: ScenarioGood,
|
||||
setupMJ: func() *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("init", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("setup", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusSucceeded
|
||||
mj.Spec.Groups[0].Status = ExecutionStatusSucceeded
|
||||
return mj
|
||||
},
|
||||
setupJobs: func() []*kbatch.Job {
|
||||
return []*kbatch.Job{
|
||||
NewTestK8sJob("workflow-init-setup", "default", "workflow", "init", kbatch.JobStatus{Succeeded: 1}),
|
||||
}
|
||||
},
|
||||
expectError: false,
|
||||
expectRequeue: false,
|
||||
},
|
||||
|
||||
// NOT GOOD SCENARIOS
|
||||
{
|
||||
name: "notgood_job_failed_workflow_continues",
|
||||
scenario: ScenarioNotGood,
|
||||
setupMJ: func() *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("init", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("setup", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
mj.Spec.Groups[0].Jobs[0].Status = ExecutionStatusRunning
|
||||
return mj
|
||||
},
|
||||
setupJobs: func() []*kbatch.Job {
|
||||
return []*kbatch.Job{
|
||||
NewTestK8sJob("workflow-init-setup", "default", "workflow", "init", kbatch.JobStatus{Failed: 1}),
|
||||
}
|
||||
},
|
||||
expectError: false,
|
||||
expectRequeue: false,
|
||||
},
|
||||
{
|
||||
name: "notgood_dependent_job_aborted",
|
||||
scenario: ScenarioNotGood,
|
||||
setupMJ: func() *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("init", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("setup", "busybox:latest"),
|
||||
NewTestJobDef("verify", "busybox:latest", NewTestDependency("setup", ExecutionStatusFailed)),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
return mj
|
||||
},
|
||||
setupJobs: func() []*kbatch.Job {
|
||||
return []*kbatch.Job{
|
||||
NewTestK8sJob("workflow-init-setup", "default", "workflow", "init", kbatch.JobStatus{Failed: 1}),
|
||||
}
|
||||
},
|
||||
expectError: false,
|
||||
expectRequeue: false,
|
||||
},
|
||||
|
||||
// REALLY BAD SCENARIOS
|
||||
{
|
||||
name: "reallybad_api_server_unavailable",
|
||||
scenario: ScenarioReallyBad,
|
||||
setupMJ: func() *jobsmanagerv1beta1.ManagedJob {
|
||||
return NewTestManagedJob("workflow", "default", nil)
|
||||
},
|
||||
setupJobs: func() []*kbatch.Job { return nil },
|
||||
clientSetup: func(c *MockClient) {
|
||||
c.GetError = ErrNetworkFailure
|
||||
},
|
||||
expectError: true,
|
||||
expectRequeue: false,
|
||||
},
|
||||
{
|
||||
name: "reallybad_create_job_fails",
|
||||
scenario: ScenarioReallyBad,
|
||||
setupMJ: func() *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("init", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("setup", "busybox:latest"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
return mj
|
||||
},
|
||||
setupJobs: func() []*kbatch.Job { return nil },
|
||||
clientSetup: func(c *MockClient) {
|
||||
c.CreateError = ErrForbidden
|
||||
},
|
||||
expectError: false, // Job creation failure doesn't stop reconciliation
|
||||
expectRequeue: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewMockClient()
|
||||
reconciler := &ManagedJobReconciler{
|
||||
Client: client,
|
||||
Scheme: client.Scheme(),
|
||||
Recorder: NewFakeRecorder(),
|
||||
}
|
||||
|
||||
// Setup client behavior
|
||||
if tt.clientSetup != nil {
|
||||
tt.clientSetup(client)
|
||||
}
|
||||
|
||||
// Add ManagedJob
|
||||
mj := tt.setupMJ()
|
||||
if mj != nil {
|
||||
client.AddManagedJob(mj)
|
||||
}
|
||||
|
||||
// Add Jobs
|
||||
for _, job := range tt.setupJobs() {
|
||||
client.AddJob(job)
|
||||
}
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
// Act
|
||||
result, err := reconciler.Reconcile(ctx, req)
|
||||
|
||||
// Assert
|
||||
if tt.expectError {
|
||||
assert.Error(t, err, "expected error for scenario: %s", tt.scenario)
|
||||
} else {
|
||||
assert.NoError(t, err, "expected no error for scenario: %s", tt.scenario)
|
||||
}
|
||||
|
||||
if tt.expectRequeue {
|
||||
assert.True(t, result.RequeueAfter > 0, "expected requeue")
|
||||
}
|
||||
|
||||
if tt.expectRequeueAfter > 0 {
|
||||
assert.Equal(t, tt.expectRequeueAfter, result.RequeueAfter)
|
||||
}
|
||||
|
||||
if tt.validateResult != nil {
|
||||
tt.validateResult(t, client, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== EDGE CASES ====================
|
||||
|
||||
func TestReconcile_EdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
description string
|
||||
setup func(*MockClient) *jobsmanagerv1beta1.ManagedJob
|
||||
validate func(*testing.T, *MockClient, ctrl.Result, error)
|
||||
}{
|
||||
{
|
||||
name: "empty_groups",
|
||||
description: "ManagedJob with no groups should complete immediately",
|
||||
setup: func(c *MockClient) *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("empty", "default", []*jobsmanagerv1beta1.ManagedJobGroup{})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
return mj
|
||||
},
|
||||
validate: func(t *testing.T, c *MockClient, r ctrl.Result, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "group_with_no_jobs",
|
||||
description: "Group with no jobs should be marked as succeeded",
|
||||
setup: func(c *MockClient) *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("no-jobs", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("empty-group", []*jobsmanagerv1beta1.ManagedJobDefinition{}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
return mj
|
||||
},
|
||||
validate: func(t *testing.T, c *MockClient, r ctrl.Result, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "circular_dependency_protection",
|
||||
description: "Jobs with circular dependencies should not cause infinite loop",
|
||||
setup: func(c *MockClient) *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("circular", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job-a", "busybox", NewTestDependency("job-b", ExecutionStatusPending)),
|
||||
NewTestJobDef("job-b", "busybox", NewTestDependency("job-a", ExecutionStatusPending)),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
return mj
|
||||
},
|
||||
validate: func(t *testing.T, c *MockClient, r ctrl.Result, err error) {
|
||||
assert.NoError(t, err, "should handle circular deps gracefully")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rapid_status_changes",
|
||||
description: "Multiple rapid reconciliations should be idempotent",
|
||||
setup: func(c *MockClient) *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("rapid", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
return mj
|
||||
},
|
||||
validate: func(t *testing.T, c *MockClient, r ctrl.Result, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "very_long_job_name",
|
||||
description: "Long names should be handled (K8s has 63 char limit)",
|
||||
setup: func(c *MockClient) *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("very-long-group-name-that-exceeds-normal", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("extremely-long-job-name-here", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
return mj
|
||||
},
|
||||
validate: func(t *testing.T, c *MockClient, r ctrl.Result, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "special_characters_in_name",
|
||||
description: "Names with special characters",
|
||||
setup: func(c *MockClient) *jobsmanagerv1beta1.ManagedJob {
|
||||
mj := NewTestManagedJob("test-workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group-1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job-1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
return mj
|
||||
},
|
||||
validate: func(t *testing.T, c *MockClient, r ctrl.Result, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := NewMockClient()
|
||||
reconciler := &ManagedJobReconciler{
|
||||
Client: client,
|
||||
Scheme: client.Scheme(),
|
||||
Recorder: NewFakeRecorder(),
|
||||
}
|
||||
|
||||
mj := tt.setup(client)
|
||||
client.AddManagedJob(mj)
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: mj.Name, Namespace: mj.Namespace},
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(context.Background(), req)
|
||||
tt.validate(t, client, result, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== KUBERNETES VOLATILITY TESTS ====================
|
||||
|
||||
func TestReconcile_KubernetesVolatility(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
description string
|
||||
clientSetup func(*MockClient)
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "intermittent_api_failure",
|
||||
description: "API fails on first call but succeeds on retry",
|
||||
clientSetup: func(c *MockClient) {
|
||||
c.FailOnNthCall["get"] = 1
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "list_timeout",
|
||||
description: "List operation times out",
|
||||
clientSetup: func(c *MockClient) {
|
||||
c.ListError = ErrTimeout
|
||||
},
|
||||
expectError: false, // List error is handled gracefully
|
||||
},
|
||||
{
|
||||
name: "create_conflict",
|
||||
description: "Job already exists when creating",
|
||||
clientSetup: func(c *MockClient) {
|
||||
c.CreateError = errors.New("already exists")
|
||||
},
|
||||
expectError: false, // Already exists is handled
|
||||
},
|
||||
{
|
||||
name: "update_resource_version_conflict",
|
||||
description: "Optimistic locking conflict on update",
|
||||
clientSetup: func(c *MockClient) {
|
||||
c.SimulateConflictOnUpdate = true
|
||||
},
|
||||
expectError: false, // First update should succeed
|
||||
},
|
||||
{
|
||||
name: "forbidden_permission",
|
||||
description: "Insufficient RBAC permissions",
|
||||
clientSetup: func(c *MockClient) {
|
||||
c.CreateError = ErrForbidden
|
||||
},
|
||||
expectError: false, // Logged but doesn't fail reconciliation
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := NewMockClient()
|
||||
tt.clientSetup(client)
|
||||
|
||||
reconciler := &ManagedJobReconciler{
|
||||
Client: client,
|
||||
Scheme: client.Scheme(),
|
||||
Recorder: NewFakeRecorder(),
|
||||
}
|
||||
|
||||
mj := NewTestManagedJob("workflow", "default", []*jobsmanagerv1beta1.ManagedJobGroup{
|
||||
NewTestGroup("group1", []*jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
NewTestJobDef("job1", "busybox"),
|
||||
}),
|
||||
})
|
||||
controllerutil.AddFinalizer(mj, FinalizerName)
|
||||
client.AddManagedJob(mj)
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{Name: "workflow", Namespace: "default"},
|
||||
}
|
||||
|
||||
_, err := reconciler.Reconcile(context.Background(), req)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err, tt.description)
|
||||
} else {
|
||||
require.NoError(t, err, tt.description)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build envtest
|
||||
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
kbatch "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
)
|
||||
|
||||
// Common test errors for simulating Kubernetes API failures
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrConflict = errors.New("conflict: object has been modified")
|
||||
ErrTimeout = errors.New("context deadline exceeded")
|
||||
ErrServerError = errors.New("internal server error")
|
||||
ErrForbidden = errors.New("forbidden: insufficient permissions")
|
||||
ErrNetworkFailure = errors.New("network is unreachable")
|
||||
)
|
||||
|
||||
// TestScenario defines the type of test scenario
|
||||
type TestScenario string
|
||||
|
||||
const (
|
||||
ScenarioGood TestScenario = "good"
|
||||
ScenarioNotGood TestScenario = "not_good"
|
||||
ScenarioReallyBad TestScenario = "really_bad"
|
||||
)
|
||||
|
||||
// MockClient implements a mock Kubernetes client for testing
|
||||
type MockClient struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// Storage for mock objects
|
||||
managedJobs map[string]*jobsmanagerv1beta1.ManagedJob
|
||||
jobs map[string]*kbatch.Job
|
||||
|
||||
// Error injection for different operations
|
||||
GetError error
|
||||
ListError error
|
||||
CreateError error
|
||||
UpdateError error
|
||||
DeleteError error
|
||||
|
||||
// Call counters for verification
|
||||
GetCalls int
|
||||
ListCalls int
|
||||
CreateCalls int
|
||||
UpdateCalls int
|
||||
DeleteCalls int
|
||||
|
||||
// Behavior modifiers
|
||||
SimulateConflictOnUpdate bool
|
||||
SimulateSlowResponse bool
|
||||
FailOnNthCall map[string]int // operation -> fail on nth call
|
||||
}
|
||||
|
||||
// NewMockClient creates a new mock client
|
||||
func NewMockClient() *MockClient {
|
||||
return &MockClient{
|
||||
managedJobs: make(map[string]*jobsmanagerv1beta1.ManagedJob),
|
||||
jobs: make(map[string]*kbatch.Job),
|
||||
FailOnNthCall: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
// Get implements client.Client
|
||||
func (m *MockClient) Get(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.GetCalls++
|
||||
|
||||
// Check for context cancellation (simulates timeout)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ErrTimeout
|
||||
default:
|
||||
}
|
||||
|
||||
// Check for injected error
|
||||
if m.GetError != nil {
|
||||
return m.GetError
|
||||
}
|
||||
|
||||
// Check for nth call failure
|
||||
if n, ok := m.FailOnNthCall["get"]; ok && m.GetCalls == n {
|
||||
return ErrServerError
|
||||
}
|
||||
|
||||
keyStr := key.String()
|
||||
|
||||
switch v := obj.(type) {
|
||||
case *jobsmanagerv1beta1.ManagedJob:
|
||||
if mj, ok := m.managedJobs[keyStr]; ok {
|
||||
*v = *mj.DeepCopy()
|
||||
return nil
|
||||
}
|
||||
return apierrors.NewNotFound(schema.GroupResource{Group: "jobsmanager.raczylo.com", Resource: "managedjobs"}, key.Name)
|
||||
case *kbatch.Job:
|
||||
if j, ok := m.jobs[keyStr]; ok {
|
||||
*v = *j.DeepCopy()
|
||||
return nil
|
||||
}
|
||||
return apierrors.NewNotFound(schema.GroupResource{Group: "batch", Resource: "jobs"}, key.Name)
|
||||
}
|
||||
|
||||
return apierrors.NewNotFound(schema.GroupResource{Resource: "unknown"}, key.Name)
|
||||
}
|
||||
|
||||
// List implements client.Client
|
||||
func (m *MockClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.ListCalls++
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ErrTimeout
|
||||
default:
|
||||
}
|
||||
|
||||
if m.ListError != nil {
|
||||
return m.ListError
|
||||
}
|
||||
|
||||
if n, ok := m.FailOnNthCall["list"]; ok && m.ListCalls == n {
|
||||
return ErrServerError
|
||||
}
|
||||
|
||||
// Extract namespace from options
|
||||
listOpts := &client.ListOptions{}
|
||||
for _, opt := range opts {
|
||||
opt.ApplyToList(listOpts)
|
||||
}
|
||||
|
||||
switch v := list.(type) {
|
||||
case *kbatch.JobList:
|
||||
items := []kbatch.Job{}
|
||||
for _, job := range m.jobs {
|
||||
if listOpts.Namespace == "" || job.Namespace == listOpts.Namespace {
|
||||
// Check label selector if present
|
||||
if listOpts.LabelSelector != nil {
|
||||
if !listOpts.LabelSelector.Matches(labelSetFromMap(job.Labels)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
items = append(items, *job.DeepCopy())
|
||||
}
|
||||
}
|
||||
v.Items = items
|
||||
return nil
|
||||
case *jobsmanagerv1beta1.ManagedJobList:
|
||||
items := []jobsmanagerv1beta1.ManagedJob{}
|
||||
for _, mj := range m.managedJobs {
|
||||
if listOpts.Namespace == "" || mj.Namespace == listOpts.Namespace {
|
||||
items = append(items, *mj.DeepCopy())
|
||||
}
|
||||
}
|
||||
v.Items = items
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create implements client.Client
|
||||
func (m *MockClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.CreateCalls++
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ErrTimeout
|
||||
default:
|
||||
}
|
||||
|
||||
if m.CreateError != nil {
|
||||
return m.CreateError
|
||||
}
|
||||
|
||||
if n, ok := m.FailOnNthCall["create"]; ok && m.CreateCalls == n {
|
||||
return ErrServerError
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Namespace: obj.GetNamespace(),
|
||||
Name: obj.GetName(),
|
||||
}.String()
|
||||
|
||||
switch v := obj.(type) {
|
||||
case *jobsmanagerv1beta1.ManagedJob:
|
||||
if _, exists := m.managedJobs[key]; exists {
|
||||
return errors.New("already exists")
|
||||
}
|
||||
m.managedJobs[key] = v.DeepCopy()
|
||||
case *kbatch.Job:
|
||||
if _, exists := m.jobs[key]; exists {
|
||||
return errors.New("already exists")
|
||||
}
|
||||
m.jobs[key] = v.DeepCopy()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update implements client.Client
|
||||
func (m *MockClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.UpdateCalls++
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ErrTimeout
|
||||
default:
|
||||
}
|
||||
|
||||
if m.UpdateError != nil {
|
||||
return m.UpdateError
|
||||
}
|
||||
|
||||
if m.SimulateConflictOnUpdate && m.UpdateCalls > 1 {
|
||||
return ErrConflict
|
||||
}
|
||||
|
||||
if n, ok := m.FailOnNthCall["update"]; ok && m.UpdateCalls == n {
|
||||
return ErrServerError
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Namespace: obj.GetNamespace(),
|
||||
Name: obj.GetName(),
|
||||
}.String()
|
||||
|
||||
switch v := obj.(type) {
|
||||
case *jobsmanagerv1beta1.ManagedJob:
|
||||
if _, exists := m.managedJobs[key]; !exists {
|
||||
return ErrNotFound
|
||||
}
|
||||
m.managedJobs[key] = v.DeepCopy()
|
||||
case *kbatch.Job:
|
||||
if _, exists := m.jobs[key]; !exists {
|
||||
return ErrNotFound
|
||||
}
|
||||
m.jobs[key] = v.DeepCopy()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete implements client.Client
|
||||
func (m *MockClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.DeleteCalls++
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ErrTimeout
|
||||
default:
|
||||
}
|
||||
|
||||
if m.DeleteError != nil {
|
||||
return m.DeleteError
|
||||
}
|
||||
|
||||
if n, ok := m.FailOnNthCall["delete"]; ok && m.DeleteCalls == n {
|
||||
return ErrServerError
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Namespace: obj.GetNamespace(),
|
||||
Name: obj.GetName(),
|
||||
}.String()
|
||||
|
||||
switch obj.(type) {
|
||||
case *jobsmanagerv1beta1.ManagedJob:
|
||||
delete(m.managedJobs, key)
|
||||
case *kbatch.Job:
|
||||
delete(m.jobs, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Patch implements client.Client
|
||||
func (m *MockClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
|
||||
return m.Update(ctx, obj)
|
||||
}
|
||||
|
||||
// DeleteAllOf implements client.Client
|
||||
func (m *MockClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status implements client.Client
|
||||
func (m *MockClient) Status() client.SubResourceWriter {
|
||||
return &MockStatusWriter{client: m}
|
||||
}
|
||||
|
||||
// SubResource implements client.Client
|
||||
func (m *MockClient) SubResource(subResource string) client.SubResourceClient {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply implements client.Client
|
||||
func (m *MockClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scheme implements client.Client
|
||||
func (m *MockClient) Scheme() *runtime.Scheme {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = jobsmanagerv1beta1.AddToScheme(scheme)
|
||||
_ = kbatch.AddToScheme(scheme)
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
return scheme
|
||||
}
|
||||
|
||||
// RESTMapper implements client.Client
|
||||
func (m *MockClient) RESTMapper() meta.RESTMapper {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupVersionKindFor implements client.Client
|
||||
func (m *MockClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
|
||||
return schema.GroupVersionKind{}, nil
|
||||
}
|
||||
|
||||
// IsObjectNamespaced implements client.Client
|
||||
func (m *MockClient) IsObjectNamespaced(obj runtime.Object) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// MockStatusWriter implements client.StatusWriter
|
||||
type MockStatusWriter struct {
|
||||
client *MockClient
|
||||
}
|
||||
|
||||
func (m *MockStatusWriter) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockStatusWriter) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
|
||||
return m.client.Update(ctx, obj)
|
||||
}
|
||||
|
||||
func (m *MockStatusWriter) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
|
||||
return m.client.Patch(ctx, obj, patch)
|
||||
}
|
||||
|
||||
// Helper to add test data
|
||||
func (m *MockClient) AddManagedJob(mj *jobsmanagerv1beta1.ManagedJob) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := types.NamespacedName{
|
||||
Namespace: mj.Namespace,
|
||||
Name: mj.Name,
|
||||
}.String()
|
||||
m.managedJobs[key] = mj.DeepCopy()
|
||||
}
|
||||
|
||||
func (m *MockClient) AddJob(job *kbatch.Job) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := types.NamespacedName{
|
||||
Namespace: job.Namespace,
|
||||
Name: job.Name,
|
||||
}.String()
|
||||
m.jobs[key] = job.DeepCopy()
|
||||
}
|
||||
|
||||
func (m *MockClient) GetManagedJobByKey(key string) *jobsmanagerv1beta1.ManagedJob {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.managedJobs[key]
|
||||
}
|
||||
|
||||
func (m *MockClient) GetJobByKey(key string) *kbatch.Job {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.jobs[key]
|
||||
}
|
||||
|
||||
// labelSetFromMap creates a label set from a map for selector matching
|
||||
type labelSet map[string]string
|
||||
|
||||
func labelSetFromMap(m map[string]string) labelSet {
|
||||
return labelSet(m)
|
||||
}
|
||||
|
||||
func (ls labelSet) Has(key string) bool {
|
||||
_, ok := ls[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (ls labelSet) Get(key string) string {
|
||||
return ls[key]
|
||||
}
|
||||
|
||||
// Lookup implements labels.Labels interface
|
||||
func (ls labelSet) Lookup(key string) (string, bool) {
|
||||
v, ok := ls[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Test fixtures and factory functions
|
||||
|
||||
// NewTestManagedJob creates a ManagedJob for testing
|
||||
func NewTestManagedJob(name, namespace string, groups []*jobsmanagerv1beta1.ManagedJobGroup) *jobsmanagerv1beta1.ManagedJob {
|
||||
return &jobsmanagerv1beta1.ManagedJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
UID: types.UID("test-uid-" + name),
|
||||
},
|
||||
Spec: jobsmanagerv1beta1.ManagedJobSpec{
|
||||
Groups: groups,
|
||||
},
|
||||
Status: ExecutionStatusPending,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestGroup creates a ManagedJobGroup for testing
|
||||
func NewTestGroup(name string, jobs []*jobsmanagerv1beta1.ManagedJobDefinition, deps ...*jobsmanagerv1beta1.ManagedJobDependencies) *jobsmanagerv1beta1.ManagedJobGroup {
|
||||
return &jobsmanagerv1beta1.ManagedJobGroup{
|
||||
Name: name,
|
||||
Jobs: jobs,
|
||||
Dependencies: deps,
|
||||
Status: ExecutionStatusPending,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestJobDef creates a ManagedJobDefinition for testing
|
||||
func NewTestJobDef(name, image string, deps ...*jobsmanagerv1beta1.ManagedJobDependencies) *jobsmanagerv1beta1.ManagedJobDefinition {
|
||||
return &jobsmanagerv1beta1.ManagedJobDefinition{
|
||||
Name: name,
|
||||
Image: image,
|
||||
Args: []string{"echo", "test"},
|
||||
Dependencies: deps,
|
||||
Status: ExecutionStatusPending,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestDependency creates a ManagedJobDependencies for testing
|
||||
func NewTestDependency(name, status string) *jobsmanagerv1beta1.ManagedJobDependencies {
|
||||
return &jobsmanagerv1beta1.ManagedJobDependencies{
|
||||
Name: name,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestK8sJob creates a Kubernetes Job for testing
|
||||
func NewTestK8sJob(name, namespace, workflowName, groupName string, status kbatch.JobStatus) *kbatch.Job {
|
||||
return &kbatch.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{
|
||||
LabelWorkflowName: workflowName,
|
||||
LabelGroupName: groupName,
|
||||
LabelJobName: name,
|
||||
},
|
||||
},
|
||||
Spec: kbatch.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{Name: "test", Image: "busybox"},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFakeRecorder creates a fake event recorder for testing
|
||||
func NewFakeRecorder() record.EventRecorder {
|
||||
return record.NewFakeRecorder(100)
|
||||
}
|
||||
@@ -1,82 +1,121 @@
|
||||
module raczylo.com/jobs-manager-operator
|
||||
|
||||
go 1.19
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/kr/pretty v0.3.1
|
||||
github.com/lukaszraczylo/pandati v0.0.18
|
||||
github.com/onsi/ginkgo/v2 v2.6.0
|
||||
github.com/onsi/gomega v1.24.1
|
||||
k8s.io/api v0.26.1
|
||||
k8s.io/apimachinery v0.26.1
|
||||
k8s.io/client-go v0.26.1
|
||||
sigs.k8s.io/controller-runtime v0.14.4
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/lukaszraczylo/pandati v0.0.29
|
||||
github.com/onsi/ginkgo/v2 v2.22.0
|
||||
github.com/onsi/gomega v1.36.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
k8s.io/api v0.34.3
|
||||
k8s.io/apimachinery v0.34.3
|
||||
k8s.io/client-go v0.34.3
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/zapr v1.2.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // 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.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/swag v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/gnostic v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.1 // indirect
|
||||
github.com/google/gnostic-models v0.7.1 // 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.27.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // 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/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.39.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
github.com/rs/zerolog v1.29.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/wI2L/jsondiff v0.3.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/oauth2 v0.5.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/term v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/wI2L/jsondiff v0.7.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.26.1 // indirect
|
||||
k8s.io/component-base v0.26.1 // indirect
|
||||
k8s.io/klog/v2 v2.90.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230217203603-ff9a8e8fa21d // indirect
|
||||
k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.34.3 // indirect
|
||||
k8s.io/apiserver v0.34.3 // indirect
|
||||
k8s.io/component-base v0.34.3 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,742 +1,316 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
|
||||
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
|
||||
github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||
github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
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.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A=
|
||||
github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/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.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
|
||||
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
|
||||
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
|
||||
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
|
||||
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
|
||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
|
||||
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
||||
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
||||
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
||||
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
|
||||
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
|
||||
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
|
||||
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
|
||||
github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
|
||||
github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
|
||||
github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/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.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
|
||||
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||
github.com/google/gnostic-models v0.7.1/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.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
||||
github.com/google/gofuzz v1.1.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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/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.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
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.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/lukaszraczylo/pandati v0.0.18 h1:6khtDY6QXkiyh4a3dzHtIzx/9UqUGfHjzKBCA0SIurI=
|
||||
github.com/lukaszraczylo/pandati v0.0.18/go.mod h1:Aqlc0KH+ssvrscYYbEKMv7n5cFwe+0MjsJbikNz1h60=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
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/lukaszraczylo/pandati v0.0.29 h1:WUEWm1+hWjE5KJbIL8OctG00x2dk4XKGJSlrjhxZ55k=
|
||||
github.com/lukaszraczylo/pandati v0.0.29/go.mod h1:+DyTWKFaXd+jIfe7GW5w2S5PyTko/RXxMyOa+Vl713A=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc=
|
||||
github.com/onsi/ginkgo/v2 v2.6.0/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc=
|
||||
github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E=
|
||||
github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/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 v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
|
||||
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
|
||||
github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
|
||||
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
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/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/wI2L/jsondiff v0.3.0 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4=
|
||||
github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=
|
||||
github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=
|
||||
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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 h1:Frnccbp+ok2GkUS2tC84yAq/U9Vg+0sIO7aRL3T4Xnc=
|
||||
golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
k8s.io/api v0.26.1 h1:f+SWYiPd/GsiWwVRz+NbFyCgvv75Pk9NK6dlkZgpCRQ=
|
||||
k8s.io/api v0.26.1/go.mod h1:xd/GBNgR0f707+ATNyPmQ1oyKSgndzXij81FzWGsejg=
|
||||
k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo=
|
||||
k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ=
|
||||
k8s.io/apiextensions-apiserver v0.26.1 h1:cB8h1SRk6e/+i3NOrQgSFij1B2S0Y0wDoNl66bn8RMI=
|
||||
k8s.io/apiextensions-apiserver v0.26.1/go.mod h1:AptjOSXDGuE0JICx/Em15PaoO7buLwTs0dGleIHixSM=
|
||||
k8s.io/apimachinery v0.26.1 h1:8EZ/eGJL+hY/MYCNwhmDzVqq2lPl3N3Bo8rvweJwXUQ=
|
||||
k8s.io/apimachinery v0.26.1/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74=
|
||||
k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8=
|
||||
k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg=
|
||||
k8s.io/client-go v0.26.1 h1:87CXzYJnAMGaa/IDDfRdhTzxk/wzGZ+/HUQpqgVSZXU=
|
||||
k8s.io/client-go v0.26.1/go.mod h1:IWNSglg+rQ3OcvDkhY6+QLeasV4OYHDjdqeWkDQZwGE=
|
||||
k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs=
|
||||
k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8=
|
||||
k8s.io/component-base v0.26.1 h1:4ahudpeQXHZL5kko+iDHqLj/FSGAEUnSVO0EBbgDd+4=
|
||||
k8s.io/component-base v0.26.1/go.mod h1:VHrLR0b58oC035w6YQiBSbtsf0ThuSwXP+p5dD/kAWU=
|
||||
k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=
|
||||
k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/klog/v2 v2.90.0 h1:VkTxIV/FjRXn1fgNNcKGM8cfmL1Z33ZjXRTVxKCoF5M=
|
||||
k8s.io/klog/v2 v2.90.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E=
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4=
|
||||
k8s.io/kube-openapi v0.0.0-20230217203603-ff9a8e8fa21d h1:oFDpQ7FfzinCtrFOl4izwOWsdTprlS2A9IXBENMW0UA=
|
||||
k8s.io/kube-openapi v0.0.0-20230217203603-ff9a8e8fa21d/go.mod h1:/BYxry62FuDzmI+i9B+X2pqfySRmSOW2ARmj5Zbqhj0=
|
||||
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y=
|
||||
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=
|
||||
k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/controller-runtime v0.14.1 h1:vThDes9pzg0Y+UbCPY3Wj34CGIYPgdmspPm2GIpxpzM=
|
||||
sigs.k8s.io/controller-runtime v0.14.1/go.mod h1:GaRkrY8a7UZF0kqFFbUKG7n9ICiTY5T55P1RiE3UZlU=
|
||||
sigs.k8s.io/controller-runtime v0.14.4 h1:Kd/Qgx5pd2XUL08eOV2vwIq3L9GhIbJ5Nxengbd4/0M=
|
||||
sigs.k8s.io/controller-runtime v0.14.4/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0=
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
|
||||
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
|
||||
k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g=
|
||||
k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0=
|
||||
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
|
||||
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo=
|
||||
k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w=
|
||||
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
|
||||
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
|
||||
k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk=
|
||||
k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c=
|
||||
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-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/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-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.1/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=
|
||||
|
||||
@@ -30,6 +30,8 @@ import (
|
||||
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"
|
||||
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
"raczylo.com/jobs-manager-operator/controllers"
|
||||
@@ -52,26 +54,53 @@ func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
|
||||
var leaderElectionID string
|
||||
var devMode bool
|
||||
var secureMetrics bool
|
||||
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. Use :8443 for HTTPS with authentication, or 0 to disable.")
|
||||
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.StringVar(&leaderElectionID, "leader-election-id", "jobsmanager.raczylo.com",
|
||||
"The name of the leader election resource.")
|
||||
flag.BoolVar(&devMode, "dev-mode", false,
|
||||
"Enable development mode with verbose logging (console format).")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS with authentication.")
|
||||
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
Development: devMode,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
// Allow environment variable to override dev mode
|
||||
if os.Getenv("LOG_LEVEL") == "debug" {
|
||||
devMode = true
|
||||
opts.Development = true
|
||||
}
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// Configure metrics options
|
||||
metricsOpts := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
}
|
||||
|
||||
// If secure metrics are enabled, use authentication and authorization filters
|
||||
if secureMetrics {
|
||||
metricsOpts.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
Scheme: scheme,
|
||||
MetricsBindAddress: metricsAddr,
|
||||
Port: 9443,
|
||||
Metrics: metricsOpts,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "b86e0f00.raczylo.com",
|
||||
LeaderElectionID: leaderElectionID,
|
||||
// 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
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package visualization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
|
||||
jobsmanagerv1beta1 "raczylo.com/jobs-manager-operator/api/v1beta1"
|
||||
)
|
||||
|
||||
// Client wraps the Kubernetes client for ManagedJob operations
|
||||
type Client struct {
|
||||
client client.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Client for ManagedJob operations
|
||||
func NewClient() (*Client, error) {
|
||||
cfg, err := config.GetConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
if err := clientgoscheme.AddToScheme(scheme); err != nil {
|
||||
return nil, fmt.Errorf("failed to add client-go scheme: %w", err)
|
||||
}
|
||||
if err := jobsmanagerv1beta1.AddToScheme(scheme); err != nil {
|
||||
return nil, fmt.Errorf("failed to add jobsmanager scheme: %w", err)
|
||||
}
|
||||
|
||||
cl, err := client.New(cfg, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
return &Client{client: cl}, nil
|
||||
}
|
||||
|
||||
// GetManagedJob retrieves a ManagedJob by name and namespace
|
||||
func (c *Client) GetManagedJob(ctx context.Context, name, namespace string) (*jobsmanagerv1beta1.ManagedJob, error) {
|
||||
mj := &jobsmanagerv1beta1.ManagedJob{}
|
||||
err := c.client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, mj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ManagedJob %s/%s: %w", namespace, name, err)
|
||||
}
|
||||
return mj, nil
|
||||
}
|
||||
|
||||
// ListManagedJobs lists all ManagedJobs in a namespace
|
||||
func (c *Client) ListManagedJobs(ctx context.Context, namespace string) (*jobsmanagerv1beta1.ManagedJobList, error) {
|
||||
mjList := &jobsmanagerv1beta1.ManagedJobList{}
|
||||
opts := []client.ListOption{}
|
||||
if namespace != "" {
|
||||
opts = append(opts, client.InNamespace(namespace))
|
||||
}
|
||||
err := c.client.List(ctx, mjList, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list ManagedJobs: %w", err)
|
||||
}
|
||||
return mjList, nil
|
||||
}
|
||||
|
||||
// BuildTree builds a StatusTree from a ManagedJob
|
||||
func BuildTree(mj *jobsmanagerv1beta1.ManagedJob) *StatusTree {
|
||||
root := NewStatusTreeWithStatus(mj.Name, mj.Status)
|
||||
|
||||
for _, group := range mj.Spec.Groups {
|
||||
groupNode := root.AddWithStatus(group.Name, group.Status)
|
||||
|
||||
// Add group dependencies
|
||||
for _, dep := range group.Dependencies {
|
||||
groupNode.Add(RenderDependency(dep.Name, true))
|
||||
}
|
||||
|
||||
// Add jobs
|
||||
for _, job := range group.Jobs {
|
||||
jobNode := groupNode.AddWithStatus(job.Name, job.Status)
|
||||
|
||||
// Add job dependencies
|
||||
for _, dep := range job.Dependencies {
|
||||
jobNode.Add(RenderDependency(dep.Name, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
// GetStatusSummary returns a summary of job statuses
|
||||
type StatusSummary struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Status string
|
||||
Groups int
|
||||
Jobs int
|
||||
Pending int
|
||||
Running int
|
||||
Succeeded int
|
||||
Failed int
|
||||
Aborted int
|
||||
}
|
||||
|
||||
// GetStatusSummary builds a summary of the ManagedJob status
|
||||
func GetStatusSummary(mj *jobsmanagerv1beta1.ManagedJob) StatusSummary {
|
||||
summary := StatusSummary{
|
||||
Name: mj.Name,
|
||||
Namespace: mj.Namespace,
|
||||
Status: mj.Status,
|
||||
Groups: len(mj.Spec.Groups),
|
||||
}
|
||||
|
||||
for _, group := range mj.Spec.Groups {
|
||||
for _, job := range group.Jobs {
|
||||
summary.Jobs++
|
||||
switch job.Status {
|
||||
case StatusPending:
|
||||
summary.Pending++
|
||||
case StatusRunning:
|
||||
summary.Running++
|
||||
case StatusSucceeded:
|
||||
summary.Succeeded++
|
||||
case StatusFailed:
|
||||
summary.Failed++
|
||||
case StatusAborted:
|
||||
summary.Aborted++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package visualization
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// Box-drawing characters for tree rendering
|
||||
const (
|
||||
newLine = "\n"
|
||||
emptySpace = " "
|
||||
middleItem = "\u251c\u2500\u2500 " // ├──
|
||||
continueItem = "\u2502 " // │
|
||||
lastItem = "\u2514\u2500\u2500 " // └──
|
||||
)
|
||||
|
||||
// Status constants
|
||||
const (
|
||||
StatusPending = "pending"
|
||||
StatusRunning = "running"
|
||||
StatusSucceeded = "succeeded"
|
||||
StatusFailed = "failed"
|
||||
StatusAborted = "aborted"
|
||||
StatusUnknown = "unknown"
|
||||
)
|
||||
|
||||
// Renderer handles tree rendering with optional color support
|
||||
type Renderer struct {
|
||||
useColor bool
|
||||
green *color.Color
|
||||
yellow *color.Color
|
||||
red *color.Color
|
||||
gray *color.Color
|
||||
magenta *color.Color
|
||||
cyan *color.Color
|
||||
}
|
||||
|
||||
// NewRenderer creates a new Renderer
|
||||
func NewRenderer(useColor bool) *Renderer {
|
||||
return &Renderer{
|
||||
useColor: useColor,
|
||||
green: color.New(color.FgGreen),
|
||||
yellow: color.New(color.FgYellow),
|
||||
red: color.New(color.FgRed),
|
||||
gray: color.New(color.FgHiBlack),
|
||||
magenta: color.New(color.FgMagenta),
|
||||
cyan: color.New(color.FgCyan),
|
||||
}
|
||||
}
|
||||
|
||||
// Render renders a StatusTree to a string
|
||||
func (r *Renderer) Render(t *StatusTree) string {
|
||||
var sb strings.Builder
|
||||
r.renderNode(&sb, t, []bool{})
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// renderNode renders a single node and its children
|
||||
func (r *Renderer) renderNode(sb *strings.Builder, t *StatusTree, spaces []bool) {
|
||||
// Render current node
|
||||
r.renderText(sb, t.Text(), t.Status(), spaces, true)
|
||||
|
||||
// Render children
|
||||
items := t.Items()
|
||||
for i, child := range items {
|
||||
isLast := i == len(items)-1
|
||||
r.renderChild(sb, child, spaces, isLast)
|
||||
}
|
||||
}
|
||||
|
||||
// renderChild renders a child node with proper indentation
|
||||
func (r *Renderer) renderChild(sb *strings.Builder, t *StatusTree, spaces []bool, isLast bool) {
|
||||
// Add prefix based on whether this is the last item
|
||||
for _, space := range spaces {
|
||||
if space {
|
||||
sb.WriteString(emptySpace)
|
||||
} else {
|
||||
sb.WriteString(continueItem)
|
||||
}
|
||||
}
|
||||
|
||||
if isLast {
|
||||
sb.WriteString(lastItem)
|
||||
} else {
|
||||
sb.WriteString(middleItem)
|
||||
}
|
||||
|
||||
// Render the text with status
|
||||
r.renderTextInline(sb, t.Text(), t.Status())
|
||||
sb.WriteString(newLine)
|
||||
|
||||
// Render children with updated spaces
|
||||
newSpaces := append(spaces, isLast)
|
||||
items := t.Items()
|
||||
for i, child := range items {
|
||||
childIsLast := i == len(items)-1
|
||||
r.renderChild(sb, child, newSpaces, childIsLast)
|
||||
}
|
||||
}
|
||||
|
||||
// renderText renders the root node text
|
||||
func (r *Renderer) renderText(sb *strings.Builder, text, status string, spaces []bool, isRoot bool) {
|
||||
if isRoot {
|
||||
r.renderTextInline(sb, text, status)
|
||||
sb.WriteString(newLine)
|
||||
}
|
||||
}
|
||||
|
||||
// renderTextInline renders text with status inline
|
||||
func (r *Renderer) renderTextInline(sb *strings.Builder, text, status string) {
|
||||
sb.WriteString(text)
|
||||
if status != "" {
|
||||
sb.WriteString(" ")
|
||||
r.renderStatus(sb, status)
|
||||
}
|
||||
}
|
||||
|
||||
// renderStatus renders the status with appropriate color
|
||||
func (r *Renderer) renderStatus(sb *strings.Builder, status string) {
|
||||
statusText := "[" + status + "]"
|
||||
|
||||
if !r.useColor {
|
||||
sb.WriteString(statusText)
|
||||
return
|
||||
}
|
||||
|
||||
switch status {
|
||||
case StatusSucceeded:
|
||||
sb.WriteString(r.green.Sprint(statusText))
|
||||
case StatusRunning:
|
||||
sb.WriteString(r.yellow.Sprint(statusText))
|
||||
case StatusFailed:
|
||||
sb.WriteString(r.red.Sprint(statusText))
|
||||
case StatusPending:
|
||||
sb.WriteString(r.gray.Sprint(statusText))
|
||||
case StatusAborted:
|
||||
sb.WriteString(r.magenta.Sprint(statusText))
|
||||
default:
|
||||
sb.WriteString(r.cyan.Sprint(statusText))
|
||||
}
|
||||
}
|
||||
|
||||
// RenderDependency formats a dependency reference
|
||||
func RenderDependency(name string, isGroup bool) string {
|
||||
if isGroup {
|
||||
return "depends on group: " + name
|
||||
}
|
||||
return "depends on: " + name
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
package visualization
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type RendererTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestRendererSuite(t *testing.T) {
|
||||
suite.Run(t, new(RendererTestSuite))
|
||||
}
|
||||
|
||||
func (s *RendererTestSuite) TestNewRenderer() {
|
||||
r := NewRenderer(true)
|
||||
s.NotNil(r)
|
||||
s.True(r.useColor)
|
||||
|
||||
r2 := NewRenderer(false)
|
||||
s.NotNil(r2)
|
||||
s.False(r2.useColor)
|
||||
}
|
||||
|
||||
func (s *RendererTestSuite) TestRender_SimpleTree() {
|
||||
r := NewRenderer(false)
|
||||
tree := NewStatusTree("root")
|
||||
|
||||
output := r.Render(tree)
|
||||
|
||||
s.Contains(output, "root")
|
||||
}
|
||||
|
||||
func (s *RendererTestSuite) TestRender_WithStatus() {
|
||||
r := NewRenderer(false)
|
||||
tree := NewStatusTreeWithStatus("workflow", StatusRunning)
|
||||
|
||||
output := r.Render(tree)
|
||||
|
||||
s.Contains(output, "workflow")
|
||||
s.Contains(output, "[running]")
|
||||
}
|
||||
|
||||
func (s *RendererTestSuite) TestRender_WithChildren() {
|
||||
r := NewRenderer(false)
|
||||
tree := NewStatusTree("root")
|
||||
tree.Add("child1")
|
||||
tree.Add("child2")
|
||||
|
||||
output := r.Render(tree)
|
||||
|
||||
s.Contains(output, "root")
|
||||
s.Contains(output, "child1")
|
||||
s.Contains(output, "child2")
|
||||
// Check for tree characters
|
||||
s.Contains(output, "├──")
|
||||
s.Contains(output, "└──")
|
||||
}
|
||||
|
||||
func (s *RendererTestSuite) TestRender_NestedChildren() {
|
||||
r := NewRenderer(false)
|
||||
tree := NewStatusTree("workflow")
|
||||
group := tree.Add("group1")
|
||||
group.Add("job1")
|
||||
group.Add("job2")
|
||||
|
||||
output := r.Render(tree)
|
||||
|
||||
s.Contains(output, "workflow")
|
||||
s.Contains(output, "group1")
|
||||
s.Contains(output, "job1")
|
||||
s.Contains(output, "job2")
|
||||
}
|
||||
|
||||
func (s *RendererTestSuite) TestRender_AllStatusColors() {
|
||||
r := NewRenderer(false)
|
||||
tree := NewStatusTree("workflow")
|
||||
tree.AddWithStatus("pending-job", StatusPending)
|
||||
tree.AddWithStatus("running-job", StatusRunning)
|
||||
tree.AddWithStatus("succeeded-job", StatusSucceeded)
|
||||
tree.AddWithStatus("failed-job", StatusFailed)
|
||||
tree.AddWithStatus("aborted-job", StatusAborted)
|
||||
tree.AddWithStatus("unknown-job", StatusUnknown)
|
||||
|
||||
output := r.Render(tree)
|
||||
|
||||
s.Contains(output, "[pending]")
|
||||
s.Contains(output, "[running]")
|
||||
s.Contains(output, "[succeeded]")
|
||||
s.Contains(output, "[failed]")
|
||||
s.Contains(output, "[aborted]")
|
||||
s.Contains(output, "[unknown]")
|
||||
}
|
||||
|
||||
func (s *RendererTestSuite) TestRenderDependency_Job() {
|
||||
result := RenderDependency("init-job", false)
|
||||
s.Equal("depends on: init-job", result)
|
||||
}
|
||||
|
||||
func (s *RendererTestSuite) TestRenderDependency_Group() {
|
||||
result := RenderDependency("init-group", true)
|
||||
s.Equal("depends on group: init-group", result)
|
||||
}
|
||||
|
||||
// ==================== MATRIX TEST: RENDER SCENARIOS ====================
|
||||
|
||||
func TestRenderer_StatusRendering(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "pending_status",
|
||||
status: StatusPending,
|
||||
expected: "[pending]",
|
||||
},
|
||||
{
|
||||
name: "running_status",
|
||||
status: StatusRunning,
|
||||
expected: "[running]",
|
||||
},
|
||||
{
|
||||
name: "succeeded_status",
|
||||
status: StatusSucceeded,
|
||||
expected: "[succeeded]",
|
||||
},
|
||||
{
|
||||
name: "failed_status",
|
||||
status: StatusFailed,
|
||||
expected: "[failed]",
|
||||
},
|
||||
{
|
||||
name: "aborted_status",
|
||||
status: StatusAborted,
|
||||
expected: "[aborted]",
|
||||
},
|
||||
{
|
||||
name: "unknown_status",
|
||||
status: StatusUnknown,
|
||||
expected: "[unknown]",
|
||||
},
|
||||
{
|
||||
name: "custom_status",
|
||||
status: "custom",
|
||||
expected: "[custom]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := NewRenderer(false)
|
||||
tree := NewStatusTreeWithStatus("node", tt.status)
|
||||
|
||||
output := r.Render(tree)
|
||||
|
||||
assert.Contains(t, output, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderer_TreeStructure(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
buildTree func() *StatusTree
|
||||
contains []string
|
||||
}{
|
||||
{
|
||||
name: "single_node",
|
||||
buildTree: func() *StatusTree {
|
||||
return NewStatusTree("root")
|
||||
},
|
||||
contains: []string{"root"},
|
||||
},
|
||||
{
|
||||
name: "parent_with_children",
|
||||
buildTree: func() *StatusTree {
|
||||
tree := NewStatusTree("parent")
|
||||
tree.Add("child1")
|
||||
tree.Add("child2")
|
||||
return tree
|
||||
},
|
||||
contains: []string{"parent", "child1", "child2", "├──", "└──"},
|
||||
},
|
||||
{
|
||||
name: "deep_nesting",
|
||||
buildTree: func() *StatusTree {
|
||||
tree := NewStatusTree("level1")
|
||||
l2 := tree.Add("level2")
|
||||
l3 := l2.Add("level3")
|
||||
l3.Add("level4")
|
||||
return tree
|
||||
},
|
||||
contains: []string{"level1", "level2", "level3", "level4"},
|
||||
},
|
||||
{
|
||||
name: "workflow_structure",
|
||||
buildTree: func() *StatusTree {
|
||||
tree := NewStatusTreeWithStatus("my-workflow", StatusRunning)
|
||||
g1 := tree.AddWithStatus("group1", StatusSucceeded)
|
||||
g1.AddWithStatus("job1", StatusSucceeded)
|
||||
g2 := tree.AddWithStatus("group2", StatusRunning)
|
||||
g2.Add(RenderDependency("group1", true))
|
||||
g2.AddWithStatus("job2", StatusRunning)
|
||||
return tree
|
||||
},
|
||||
contains: []string{
|
||||
"my-workflow", "[running]",
|
||||
"group1", "[succeeded]",
|
||||
"job1",
|
||||
"group2",
|
||||
"depends on group: group1",
|
||||
"job2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := NewRenderer(false)
|
||||
tree := tt.buildTree()
|
||||
|
||||
output := r.Render(tree)
|
||||
|
||||
for _, expected := range tt.contains {
|
||||
assert.Contains(t, output, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderer_ColorMode(t *testing.T) {
|
||||
tree := NewStatusTreeWithStatus("workflow", StatusSucceeded)
|
||||
|
||||
// Without color
|
||||
rNoColor := NewRenderer(false)
|
||||
outputNoColor := rNoColor.Render(tree)
|
||||
assert.Contains(t, outputNoColor, "[succeeded]")
|
||||
|
||||
// With color - output should still contain the text (ANSI codes are transparent to Contains)
|
||||
rColor := NewRenderer(true)
|
||||
outputColor := rColor.Render(tree)
|
||||
assert.NotEmpty(t, outputColor)
|
||||
}
|
||||
|
||||
func TestRenderer_ComplexWorkflow(t *testing.T) {
|
||||
r := NewRenderer(false)
|
||||
|
||||
// Build a complex workflow tree
|
||||
workflow := NewStatusTreeWithStatus("production-deploy", StatusRunning)
|
||||
|
||||
// Setup phase
|
||||
setup := workflow.AddWithStatus("setup", StatusSucceeded)
|
||||
setup.AddWithStatus("configure-aws", StatusSucceeded)
|
||||
setup.AddWithStatus("validate-config", StatusSucceeded)
|
||||
|
||||
// Build phase
|
||||
build := workflow.AddWithStatus("build", StatusSucceeded)
|
||||
build.Add(RenderDependency("setup", true))
|
||||
build.AddWithStatus("compile-frontend", StatusSucceeded)
|
||||
build.AddWithStatus("compile-backend", StatusSucceeded)
|
||||
build.AddWithStatus("run-unit-tests", StatusSucceeded)
|
||||
|
||||
// Deploy phase
|
||||
deploy := workflow.AddWithStatus("deploy", StatusRunning)
|
||||
deploy.Add(RenderDependency("build", true))
|
||||
staging := deploy.AddWithStatus("deploy-staging", StatusSucceeded)
|
||||
staging.Add(RenderDependency("compile-frontend", false))
|
||||
integration := deploy.AddWithStatus("run-integration-tests", StatusRunning)
|
||||
integration.Add(RenderDependency("deploy-staging", false))
|
||||
prod := deploy.AddWithStatus("deploy-production", StatusPending)
|
||||
prod.Add(RenderDependency("run-integration-tests", false))
|
||||
|
||||
// Cleanup phase
|
||||
cleanup := workflow.AddWithStatus("cleanup", StatusPending)
|
||||
cleanup.Add(RenderDependency("deploy", true))
|
||||
cleanup.AddWithStatus("remove-staging", StatusPending)
|
||||
|
||||
output := r.Render(workflow)
|
||||
|
||||
// Verify structure
|
||||
expectedPhrases := []string{
|
||||
"production-deploy",
|
||||
"setup",
|
||||
"configure-aws",
|
||||
"build",
|
||||
"compile-frontend",
|
||||
"deploy",
|
||||
"deploy-staging",
|
||||
"run-integration-tests",
|
||||
"deploy-production",
|
||||
"cleanup",
|
||||
"depends on group: setup",
|
||||
"depends on group: build",
|
||||
"depends on: deploy-staging",
|
||||
}
|
||||
|
||||
for _, phrase := range expectedPhrases {
|
||||
assert.Contains(t, output, phrase, "output should contain: %s", phrase)
|
||||
}
|
||||
|
||||
// Verify line structure
|
||||
lines := strings.Split(output, "\n")
|
||||
assert.Greater(t, len(lines), 10, "should have multiple lines")
|
||||
}
|
||||
|
||||
func TestRenderDependency_Matrix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
depName string
|
||||
isGroup bool
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "job_dependency",
|
||||
depName: "init-job",
|
||||
isGroup: false,
|
||||
expected: "depends on: init-job",
|
||||
},
|
||||
{
|
||||
name: "group_dependency",
|
||||
depName: "setup-group",
|
||||
isGroup: true,
|
||||
expected: "depends on group: setup-group",
|
||||
},
|
||||
{
|
||||
name: "long_job_name",
|
||||
depName: "very-long-job-name-with-many-parts",
|
||||
isGroup: false,
|
||||
expected: "depends on: very-long-job-name-with-many-parts",
|
||||
},
|
||||
{
|
||||
name: "empty_name",
|
||||
depName: "",
|
||||
isGroup: false,
|
||||
expected: "depends on: ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := RenderDependency(tt.depName, tt.isGroup)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test that tree rendering handles edge cases
|
||||
func TestRenderer_EdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
buildTree func() *StatusTree
|
||||
verify func(*testing.T, string)
|
||||
}{
|
||||
{
|
||||
name: "empty_text_node",
|
||||
buildTree: func() *StatusTree {
|
||||
return NewStatusTree("")
|
||||
},
|
||||
verify: func(t *testing.T, output string) {
|
||||
assert.NotEmpty(t, output)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "special_characters_in_text",
|
||||
buildTree: func() *StatusTree {
|
||||
tree := NewStatusTree("workflow-v1.2.3")
|
||||
tree.Add("job_with_underscore")
|
||||
tree.Add("job.with.dots")
|
||||
return tree
|
||||
},
|
||||
verify: func(t *testing.T, output string) {
|
||||
assert.Contains(t, output, "workflow-v1.2.3")
|
||||
assert.Contains(t, output, "job_with_underscore")
|
||||
assert.Contains(t, output, "job.with.dots")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "many_siblings",
|
||||
buildTree: func() *StatusTree {
|
||||
tree := NewStatusTree("parent")
|
||||
for i := 0; i < 20; i++ {
|
||||
tree.Add("child")
|
||||
}
|
||||
return tree
|
||||
},
|
||||
verify: func(t *testing.T, output string) {
|
||||
// Should have 19 middle items and 1 last item
|
||||
assert.Equal(t, 19, strings.Count(output, "├──"))
|
||||
assert.Equal(t, 1, strings.Count(output, "└──"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unicode_in_text",
|
||||
buildTree: func() *StatusTree {
|
||||
tree := NewStatusTree("🚀 deployment")
|
||||
tree.Add("✅ verified")
|
||||
tree.Add("⏳ pending")
|
||||
return tree
|
||||
},
|
||||
verify: func(t *testing.T, output string) {
|
||||
assert.Contains(t, output, "🚀")
|
||||
assert.Contains(t, output, "✅")
|
||||
assert.Contains(t, output, "⏳")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := NewRenderer(false)
|
||||
tree := tt.buildTree()
|
||||
output := r.Render(tree)
|
||||
tt.verify(t, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package visualization
|
||||
|
||||
// StatusTree represents a tree node with text and execution status
|
||||
type StatusTree struct {
|
||||
text string
|
||||
status string
|
||||
items []*StatusTree
|
||||
}
|
||||
|
||||
// NewStatusTree creates a new StatusTree node
|
||||
func NewStatusTree(text string) *StatusTree {
|
||||
return &StatusTree{
|
||||
text: text,
|
||||
items: make([]*StatusTree, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// NewStatusTreeWithStatus creates a new StatusTree node with a status
|
||||
func NewStatusTreeWithStatus(text, status string) *StatusTree {
|
||||
return &StatusTree{
|
||||
text: text,
|
||||
status: status,
|
||||
items: make([]*StatusTree, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Add creates and appends a child node, returning it for chaining
|
||||
func (t *StatusTree) Add(text string) *StatusTree {
|
||||
child := NewStatusTree(text)
|
||||
t.items = append(t.items, child)
|
||||
return child
|
||||
}
|
||||
|
||||
// AddWithStatus creates and appends a child node with status
|
||||
func (t *StatusTree) AddWithStatus(text, status string) *StatusTree {
|
||||
child := NewStatusTreeWithStatus(text, status)
|
||||
t.items = append(t.items, child)
|
||||
return child
|
||||
}
|
||||
|
||||
// Items returns all child nodes
|
||||
func (t *StatusTree) Items() []*StatusTree {
|
||||
return t.items
|
||||
}
|
||||
|
||||
// Text returns the node's text value
|
||||
func (t *StatusTree) Text() string {
|
||||
return t.text
|
||||
}
|
||||
|
||||
// Status returns the node's status value
|
||||
func (t *StatusTree) Status() string {
|
||||
return t.status
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package visualization
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type TreeTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestTreeSuite(t *testing.T) {
|
||||
suite.Run(t, new(TreeTestSuite))
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestNewStatusTree() {
|
||||
tree := NewStatusTree("root")
|
||||
|
||||
s.Equal("root", tree.Text())
|
||||
s.Equal("", tree.Status())
|
||||
s.Empty(tree.Items())
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestNewStatusTreeWithStatus() {
|
||||
tree := NewStatusTreeWithStatus("workflow", StatusRunning)
|
||||
|
||||
s.Equal("workflow", tree.Text())
|
||||
s.Equal(StatusRunning, tree.Status())
|
||||
s.Empty(tree.Items())
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestAdd() {
|
||||
root := NewStatusTree("root")
|
||||
child := root.Add("child")
|
||||
|
||||
s.Equal("child", child.Text())
|
||||
s.Len(root.Items(), 1)
|
||||
s.Equal(child, root.Items()[0])
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestAddWithStatus() {
|
||||
root := NewStatusTree("root")
|
||||
child := root.AddWithStatus("job", StatusSucceeded)
|
||||
|
||||
s.Equal("job", child.Text())
|
||||
s.Equal(StatusSucceeded, child.Status())
|
||||
s.Len(root.Items(), 1)
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestChaining() {
|
||||
root := NewStatusTree("workflow")
|
||||
group := root.Add("group1")
|
||||
job := group.AddWithStatus("job1", StatusRunning)
|
||||
job.Add("depends on: init-job")
|
||||
|
||||
s.Len(root.Items(), 1)
|
||||
s.Len(root.Items()[0].Items(), 1)
|
||||
s.Len(root.Items()[0].Items()[0].Items(), 1)
|
||||
}
|
||||
|
||||
func (s *TreeTestSuite) TestItems() {
|
||||
root := NewStatusTree("root")
|
||||
root.Add("child1")
|
||||
root.Add("child2")
|
||||
root.Add("child3")
|
||||
|
||||
items := root.Items()
|
||||
s.Len(items, 3)
|
||||
s.Equal("child1", items[0].Text())
|
||||
s.Equal("child2", items[1].Text())
|
||||
s.Equal("child3", items[2].Text())
|
||||
}
|
||||
|
||||
// ==================== MATRIX TEST: TREE BUILDING ====================
|
||||
|
||||
func TestStatusTree_StatusValues(t *testing.T) {
|
||||
statuses := []string{
|
||||
StatusPending,
|
||||
StatusRunning,
|
||||
StatusSucceeded,
|
||||
StatusFailed,
|
||||
StatusAborted,
|
||||
StatusUnknown,
|
||||
}
|
||||
|
||||
for _, status := range statuses {
|
||||
t.Run(status, func(t *testing.T) {
|
||||
tree := NewStatusTreeWithStatus("node", status)
|
||||
assert.Equal(t, status, tree.Status())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusTree_DeepNesting(t *testing.T) {
|
||||
depth := 10
|
||||
root := NewStatusTree("level-0")
|
||||
current := root
|
||||
|
||||
for i := 1; i < depth; i++ {
|
||||
current = current.Add("level-" + string(rune('0'+i)))
|
||||
}
|
||||
|
||||
// Traverse back to verify
|
||||
node := root
|
||||
for i := 0; i < depth-1; i++ {
|
||||
assert.Len(t, node.Items(), 1)
|
||||
node = node.Items()[0]
|
||||
}
|
||||
assert.Empty(t, node.Items())
|
||||
}
|
||||
|
||||
func TestStatusTree_MultipleChildren(t *testing.T) {
|
||||
root := NewStatusTree("workflow")
|
||||
|
||||
// Add multiple groups
|
||||
for i := 0; i < 5; i++ {
|
||||
group := root.AddWithStatus("group"+string(rune('0'+i)), StatusPending)
|
||||
// Add jobs to each group
|
||||
for j := 0; j < 3; j++ {
|
||||
group.AddWithStatus("job"+string(rune('0'+j)), StatusPending)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, root.Items(), 5)
|
||||
for _, group := range root.Items() {
|
||||
assert.Len(t, group.Items(), 3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusTree_EmptyTree(t *testing.T) {
|
||||
tree := NewStatusTree("")
|
||||
assert.Equal(t, "", tree.Text())
|
||||
assert.Equal(t, "", tree.Status())
|
||||
assert.Empty(t, tree.Items())
|
||||
}
|
||||
|
||||
func TestStatusTree_ComplexWorkflow(t *testing.T) {
|
||||
// Simulate a real workflow structure
|
||||
workflow := NewStatusTreeWithStatus("my-workflow", StatusRunning)
|
||||
|
||||
// First group - succeeded
|
||||
group1 := workflow.AddWithStatus("init-group", StatusSucceeded)
|
||||
group1.AddWithStatus("setup-database", StatusSucceeded)
|
||||
group1.AddWithStatus("setup-cache", StatusSucceeded)
|
||||
|
||||
// Second group - running
|
||||
group2 := workflow.AddWithStatus("build-group", StatusRunning)
|
||||
group2.Add("depends on group: init-group")
|
||||
build := group2.AddWithStatus("build-app", StatusRunning)
|
||||
build.Add("depends on: setup-database")
|
||||
group2.AddWithStatus("run-tests", StatusPending).Add("depends on: build-app")
|
||||
|
||||
// Third group - pending
|
||||
group3 := workflow.AddWithStatus("deploy-group", StatusPending)
|
||||
group3.Add("depends on group: build-group")
|
||||
group3.AddWithStatus("deploy-staging", StatusPending)
|
||||
group3.AddWithStatus("deploy-production", StatusPending).Add("depends on: deploy-staging")
|
||||
|
||||
assert.Len(t, workflow.Items(), 3)
|
||||
assert.Equal(t, StatusSucceeded, workflow.Items()[0].Status())
|
||||
assert.Equal(t, StatusRunning, workflow.Items()[1].Status())
|
||||
assert.Equal(t, StatusPending, workflow.Items()[2].Status())
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
apiVersion: krew.googlecontainertools.github.com/v1alpha2
|
||||
kind: Plugin
|
||||
metadata:
|
||||
name: managedjob
|
||||
spec:
|
||||
version: v0.0.33
|
||||
homepage: https://github.com/lukaszraczylo/jobs-manager-operator
|
||||
shortDescription: Visualize ManagedJob workflows
|
||||
description: |
|
||||
A kubectl plugin for visualizing ManagedJob workflows from the
|
||||
Jobs Manager Operator. It provides ASCII tree visualization of
|
||||
workflow structure and execution status with colored output.
|
||||
|
||||
Features:
|
||||
- Visualize workflow structure as ASCII tree
|
||||
- Color-coded status indicators (green=succeeded, yellow=running, red=failed)
|
||||
- Watch mode for real-time status updates
|
||||
- List all ManagedJobs in a namespace
|
||||
- Quick status summary view
|
||||
|
||||
Usage:
|
||||
kubectl managedjob visualize <name> -n <namespace>
|
||||
kubectl managedjob visualize <name> -w # Watch mode
|
||||
kubectl managedjob list -n <namespace>
|
||||
kubectl managedjob status <name> -n <namespace>
|
||||
|
||||
caveats: |
|
||||
This plugin requires the Jobs Manager Operator CRD to be installed
|
||||
in your cluster. See https://github.com/lukaszraczylo/jobs-manager-operator
|
||||
for installation instructions.
|
||||
|
||||
platforms:
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: amd64
|
||||
uri: https://github.com/lukaszraczylo/jobs-manager-operator/releases/download/v0.0.33/jobs-manager-operator_0.0.33_darwin_amd64.tar.gz
|
||||
sha256: REPLACE_WITH_ACTUAL_SHA256
|
||||
bin: kubectl-managedjob
|
||||
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: arm64
|
||||
uri: https://github.com/lukaszraczylo/jobs-manager-operator/releases/download/v0.0.33/jobs-manager-operator_0.0.33_darwin_arm64.tar.gz
|
||||
sha256: REPLACE_WITH_ACTUAL_SHA256
|
||||
bin: kubectl-managedjob
|
||||
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: amd64
|
||||
uri: https://github.com/lukaszraczylo/jobs-manager-operator/releases/download/v0.0.33/jobs-manager-operator_0.0.33_linux_amd64.tar.gz
|
||||
sha256: REPLACE_WITH_ACTUAL_SHA256
|
||||
bin: kubectl-managedjob
|
||||
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: arm64
|
||||
uri: https://github.com/lukaszraczylo/jobs-manager-operator/releases/download/v0.0.33/jobs-manager-operator_0.0.33_linux_arm64.tar.gz
|
||||
sha256: REPLACE_WITH_ACTUAL_SHA256
|
||||
bin: kubectl-managedjob
|
||||
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: amd64
|
||||
uri: https://github.com/lukaszraczylo/jobs-manager-operator/releases/download/v0.0.33/jobs-manager-operator_0.0.33_windows_amd64.zip
|
||||
sha256: REPLACE_WITH_ACTUAL_SHA256
|
||||
bin: kubectl-managedjob.exe
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Executable
+293
@@ -0,0 +1,293 @@
|
||||
#!/bin/bash
|
||||
# kubectl-managedjob - Installation Script
|
||||
# Usage: curl -sSL https://raw.githubusercontent.com/lukaszraczylo/jobs-manager-operator/main/scripts/install-plugin.sh | bash
|
||||
#
|
||||
# Or with a specific version:
|
||||
# curl -sSL https://raw.githubusercontent.com/lukaszraczylo/jobs-manager-operator/main/scripts/install-plugin.sh | bash -s -- v1.0.0
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
GITHUB_REPO="lukaszraczylo/jobs-manager-operator"
|
||||
BINARY_NAME="kubectl-managedjob"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Detect OS and architecture
|
||||
detect_platform() {
|
||||
local os arch
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
os="darwin"
|
||||
;;
|
||||
Linux)
|
||||
os="linux"
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
os="windows"
|
||||
;;
|
||||
*)
|
||||
error "Unsupported operating system: $(uname -s)"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64)
|
||||
arch="amd64"
|
||||
;;
|
||||
arm64|aarch64)
|
||||
arch="arm64"
|
||||
;;
|
||||
*)
|
||||
error "Unsupported architecture: $(uname -m)"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "${os}_${arch}"
|
||||
}
|
||||
|
||||
# Get the latest release version from GitHub
|
||||
get_latest_version() {
|
||||
local response version curl_opts
|
||||
|
||||
# Use GitHub token if available (higher rate limit)
|
||||
curl_opts=(-sS)
|
||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||
curl_opts+=(-H "Authorization: token ${GITHUB_TOKEN}")
|
||||
fi
|
||||
|
||||
# Fetch with error handling
|
||||
response=$(curl "${curl_opts[@]}" "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>&1)
|
||||
|
||||
# Check for rate limiting
|
||||
if echo "$response" | grep -q "API rate limit exceeded"; then
|
||||
error "GitHub API rate limit exceeded.
|
||||
|
||||
You have a few options:
|
||||
1. Wait ~1 hour for the rate limit to reset
|
||||
2. Specify a version manually:
|
||||
curl -sSL https://raw.githubusercontent.com/${GITHUB_REPO}/main/scripts/install-plugin.sh | bash -s -- v0.0.33
|
||||
3. Use a GitHub token (set GITHUB_TOKEN environment variable)"
|
||||
fi
|
||||
|
||||
# Check for other API errors
|
||||
if echo "$response" | grep -q '"message":'; then
|
||||
local msg
|
||||
msg=$(echo "$response" | grep '"message":' | sed -E 's/.*"message": *"([^"]+)".*/\1/')
|
||||
error "GitHub API error: $msg"
|
||||
fi
|
||||
|
||||
# Extract version
|
||||
version=$(echo "$response" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
|
||||
if [[ -z "$version" ]]; then
|
||||
error "Failed to fetch latest version from GitHub. Response: $response"
|
||||
fi
|
||||
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
# Download and install the plugin
|
||||
download_and_install() {
|
||||
local version="$1"
|
||||
local platform="$2"
|
||||
local tmp_dir
|
||||
|
||||
tmp_dir=$(mktemp -d)
|
||||
trap "rm -rf $tmp_dir" EXIT
|
||||
|
||||
# Construct download URL
|
||||
local archive_ext="tar.gz"
|
||||
if [[ "$platform" == windows_* ]]; then
|
||||
archive_ext="zip"
|
||||
fi
|
||||
|
||||
# Archive name format from goreleaser: jobs-manager-operator_VERSION_OS_ARCH.tar.gz
|
||||
local archive_name="jobs-manager-operator_${version#v}_${platform}.${archive_ext}"
|
||||
local download_url="https://github.com/${GITHUB_REPO}/releases/download/${version}/${archive_name}"
|
||||
|
||||
info "Downloading ${archive_name}..."
|
||||
|
||||
if ! curl -sSL -o "$tmp_dir/release.${archive_ext}" "$download_url"; then
|
||||
error "Failed to download release from: $download_url"
|
||||
fi
|
||||
|
||||
info "Extracting archive..."
|
||||
if [[ "$archive_ext" == "zip" ]]; then
|
||||
if ! unzip -q "$tmp_dir/release.zip" -d "$tmp_dir"; then
|
||||
error "Failed to extract archive"
|
||||
fi
|
||||
else
|
||||
if ! tar -xzf "$tmp_dir/release.tar.gz" -C "$tmp_dir"; then
|
||||
error "Failed to extract archive"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Find the binary
|
||||
local binary_path
|
||||
binary_path=$(find "$tmp_dir" -name "$BINARY_NAME" -type f | head -1)
|
||||
|
||||
if [[ -z "$binary_path" ]]; then
|
||||
error "Could not find $BINARY_NAME in the archive"
|
||||
fi
|
||||
|
||||
# Determine install location
|
||||
local install_path="$INSTALL_DIR/$BINARY_NAME"
|
||||
|
||||
# Check if we need sudo
|
||||
if [[ -w "$INSTALL_DIR" ]]; then
|
||||
info "Installing to ${install_path}..."
|
||||
cp "$binary_path" "$install_path"
|
||||
chmod +x "$install_path"
|
||||
else
|
||||
info "Installing to ${install_path} (requires sudo)..."
|
||||
sudo cp "$binary_path" "$install_path"
|
||||
sudo chmod +x "$install_path"
|
||||
fi
|
||||
|
||||
success "Installed $BINARY_NAME to $install_path"
|
||||
}
|
||||
|
||||
# Verify installation
|
||||
verify_installation() {
|
||||
if command -v "$BINARY_NAME" &> /dev/null; then
|
||||
success "Installation verified!"
|
||||
info "Run '$BINARY_NAME --help' to get started"
|
||||
echo ""
|
||||
"$BINARY_NAME" --help 2>/dev/null | head -10 || true
|
||||
else
|
||||
warn "Installation complete, but $BINARY_NAME is not in PATH"
|
||||
warn "You may need to add $INSTALL_DIR to your PATH"
|
||||
echo ""
|
||||
echo "Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):"
|
||||
echo " export PATH=\"\$PATH:$INSTALL_DIR\""
|
||||
fi
|
||||
}
|
||||
|
||||
# Handle --uninstall flag
|
||||
uninstall() {
|
||||
local install_path="$INSTALL_DIR/$BINARY_NAME"
|
||||
|
||||
echo ""
|
||||
echo "Uninstalling $BINARY_NAME..."
|
||||
|
||||
if [[ -f "$install_path" ]]; then
|
||||
if [[ -w "$install_path" ]]; then
|
||||
rm "$install_path"
|
||||
else
|
||||
sudo rm "$install_path"
|
||||
fi
|
||||
success "Removed $install_path"
|
||||
else
|
||||
warn "$BINARY_NAME not found at $install_path"
|
||||
fi
|
||||
|
||||
success "Uninstallation complete"
|
||||
}
|
||||
|
||||
# Main installation flow
|
||||
main() {
|
||||
local version="${1:-}"
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ kubectl-managedjob - Installation Script ║"
|
||||
echo "║ Kubernetes workflow visualization plugin for kubectl ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Check required dependencies
|
||||
if ! command -v curl &> /dev/null; then
|
||||
error "curl is required but not installed"
|
||||
fi
|
||||
|
||||
if ! command -v tar &> /dev/null; then
|
||||
error "tar is required but not installed"
|
||||
fi
|
||||
|
||||
# Detect platform
|
||||
local platform
|
||||
platform=$(detect_platform)
|
||||
info "Detected platform: $platform"
|
||||
|
||||
# Get version
|
||||
if [[ -z "$version" ]]; then
|
||||
info "Fetching latest release..."
|
||||
version=$(get_latest_version)
|
||||
fi
|
||||
info "Installing version: $version"
|
||||
|
||||
# Download and install
|
||||
download_and_install "$version" "$platform"
|
||||
|
||||
# Verify
|
||||
verify_installation
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Installation Complete! ║"
|
||||
echo "╠═══════════════════════════════════════════════════════════╣"
|
||||
echo "║ Usage: ║"
|
||||
echo "║ kubectl managedjob visualize <name> -n <namespace> ║"
|
||||
echo "║ kubectl managedjob list -n <namespace> ║"
|
||||
echo "║ kubectl managedjob status <name> -n <namespace> ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Handle flags
|
||||
case "${1:-}" in
|
||||
--uninstall)
|
||||
uninstall
|
||||
exit 0
|
||||
;;
|
||||
--help|-h)
|
||||
echo "kubectl-managedjob Installation Script"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " install-plugin.sh [VERSION] Install kubectl-managedjob (latest or specific version)"
|
||||
echo " install-plugin.sh --uninstall Remove kubectl-managedjob"
|
||||
echo " install-plugin.sh --help Show this help"
|
||||
echo ""
|
||||
echo "Environment Variables:"
|
||||
echo " INSTALL_DIR Installation directory (default: /usr/local/bin)"
|
||||
echo " GITHUB_TOKEN GitHub token for higher API rate limits"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " # Install latest version"
|
||||
echo " curl -sSL https://raw.githubusercontent.com/${GITHUB_REPO}/main/scripts/install-plugin.sh | bash"
|
||||
echo ""
|
||||
echo " # Install specific version"
|
||||
echo " curl -sSL https://raw.githubusercontent.com/${GITHUB_REPO}/main/scripts/install-plugin.sh | bash -s -- v0.0.33"
|
||||
echo ""
|
||||
echo " # Install to custom directory"
|
||||
echo " INSTALL_DIR=~/bin curl -sSL https://raw.githubusercontent.com/${GITHUB_REPO}/main/scripts/install-plugin.sh | bash"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
main "$@"
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# Update Krew manifest with correct SHA256 checksums
|
||||
# Usage: ./scripts/update-krew-manifest.sh v0.0.33
|
||||
|
||||
set -e
|
||||
|
||||
VERSION="${1:-}"
|
||||
GITHUB_REPO="lukaszraczylo/jobs-manager-operator"
|
||||
MANIFEST_FILE="plugins/krew/managedjob.yaml"
|
||||
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "Usage: $0 <version>"
|
||||
echo "Example: $0 v0.0.33"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove 'v' prefix for archive names
|
||||
VERSION_NUM="${VERSION#v}"
|
||||
|
||||
echo "Updating Krew manifest for version $VERSION..."
|
||||
|
||||
# Platforms to update
|
||||
PLATFORMS=(
|
||||
"darwin_amd64:tar.gz"
|
||||
"darwin_arm64:tar.gz"
|
||||
"linux_amd64:tar.gz"
|
||||
"linux_arm64:tar.gz"
|
||||
"windows_amd64:zip"
|
||||
)
|
||||
|
||||
# Download checksums file
|
||||
CHECKSUMS_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/checksums.txt"
|
||||
echo "Downloading checksums from $CHECKSUMS_URL..."
|
||||
|
||||
CHECKSUMS=$(curl -sSL "$CHECKSUMS_URL")
|
||||
if [[ -z "$CHECKSUMS" ]]; then
|
||||
echo "Error: Failed to download checksums"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checksums:"
|
||||
echo "$CHECKSUMS"
|
||||
echo ""
|
||||
|
||||
# Update manifest
|
||||
for platform_ext in "${PLATFORMS[@]}"; do
|
||||
platform="${platform_ext%:*}"
|
||||
ext="${platform_ext#*:}"
|
||||
|
||||
archive_name="jobs-manager-operator_${VERSION_NUM}_${platform}.${ext}"
|
||||
sha256=$(echo "$CHECKSUMS" | grep "$archive_name" | awk '{print $1}')
|
||||
|
||||
if [[ -z "$sha256" ]]; then
|
||||
echo "Warning: No checksum found for $archive_name"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " $platform: $sha256"
|
||||
done
|
||||
|
||||
# Update version in manifest
|
||||
sed -i.bak "s/version: v.*/version: ${VERSION}/" "$MANIFEST_FILE"
|
||||
|
||||
# Update URIs and find/replace SHA256 placeholders
|
||||
# This is a simplified approach - for production, use yq or similar
|
||||
echo ""
|
||||
echo "Manifest updated with version $VERSION"
|
||||
echo "NOTE: SHA256 checksums must be manually updated in $MANIFEST_FILE"
|
||||
echo ""
|
||||
echo "Copy the checksums above and replace REPLACE_WITH_ACTUAL_SHA256 in the manifest."
|
||||
|
||||
rm -f "${MANIFEST_FILE}.bak"
|
||||
+1
-6
@@ -1,11 +1,6 @@
|
||||
version: 1
|
||||
force:
|
||||
major: 0
|
||||
minor: 0
|
||||
patch: 0
|
||||
wording:
|
||||
patch:
|
||||
minor:
|
||||
- release-minor
|
||||
major:
|
||||
- release-major
|
||||
- release-major
|
||||
|
||||
Reference in New Issue
Block a user