mirror of
https://github.com/lukaszraczylo/kubernetes-images-sync-operator.git
synced 2026-07-11 04:11:44 +00:00
new year update (#4)
* Bring operator to the brand new world of build and deployments. * Clean up the code and basic improvements. * More fixes, moving from python to golang worker. * fixup! More fixes, moving from python to golang worker. * fixup! fixup! More fixes, moving from python to golang worker. * fixup! fixup! fixup! More fixes, moving from python to golang worker. * fixup! fixup! fixup! fixup! More fixes, moving from python to golang worker. * fixup! fixup! fixup! fixup! fixup! More fixes, moving from python to golang worker. * fixup! fixup! fixup! fixup! fixup! fixup! More fixes, moving from python to golang worker.
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
name: Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
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.25"
|
||||||
|
secrets: inherit
|
||||||
@@ -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.25"
|
||||||
|
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]=kube-images-sync \
|
||||||
|
-f client_payload[version]=${{ steps.version.outputs.version }} \
|
||||||
|
-f client_payload[source_repo]=lukaszraczylo/kubernetes-images-sync-operator \
|
||||||
|
-f client_payload[chart_path]=charts/kube-images-sync-operator
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||||
|
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
project_name: kubernetes-images-sync-operator
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
# Update Helm chart version to match release version
|
||||||
|
- 'sed -i.bak ''s/^version:.*/version: {{ .Version }}/'' charts/kube-images-sync-operator/Chart.yaml'
|
||||||
|
- 'sed -i.bak ''s/^appVersion:.*/appVersion: "{{ .Version }}"/'' charts/kube-images-sync-operator/Chart.yaml'
|
||||||
|
- 'sed -i.bak ''s/tag:.*/tag: "{{ .Version }}"/'' charts/kube-images-sync-operator/values.yaml'
|
||||||
|
- 'sed -i.bak ''s/workerImage:.*/workerImage: ghcr.io\/lukaszraczylo\/kubernetes-images-sync-worker:{{ .Version }}/'' charts/kube-images-sync-operator/values.yaml'
|
||||||
|
- rm -f charts/kube-images-sync-operator/Chart.yaml.bak charts/kube-images-sync-operator/values.yaml.bak
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- id: manager
|
||||||
|
main: ./cmd
|
||||||
|
binary: manager
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X main.Version={{.Version}}
|
||||||
|
- -X github.com/lukaszraczylo/kubernetes-images-sync-operator/internal/shared.BACKUP_JOB_IMAGE=ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:{{.Version}}
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- darwin
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
|
||||||
|
- id: worker
|
||||||
|
main: ./cmd/worker
|
||||||
|
binary: worker
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X main.Version={{.Version}}
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- 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: kubernetes-images-sync-operator
|
||||||
|
draft: false
|
||||||
|
prerelease: auto
|
||||||
|
name_template: "v{{.Version}}"
|
||||||
|
header: |
|
||||||
|
## Kubernetes Images Sync Operator v{{.Version}}
|
||||||
|
|
||||||
|
Kubernetes operator for backing up and syncing container images across environments.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
**Helm (from repository):**
|
||||||
|
```bash
|
||||||
|
helm repo add kube-images-sync https://lukaszraczylo.github.io/helm-charts
|
||||||
|
helm repo update
|
||||||
|
helm install kube-images-sync kube-images-sync/kube-images-sync --version {{.Version}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Helm (from release asset):**
|
||||||
|
```bash
|
||||||
|
helm install kube-images-sync https://github.com/lukaszraczylo/kubernetes-images-sync-operator/releases/download/v{{.Version}}/kube-images-sync-{{.Version}}.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker:**
|
||||||
|
```bash
|
||||||
|
docker pull ghcr.io/lukaszraczylo/kubernetes-images-sync-operator:{{.Version}}
|
||||||
|
docker pull ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:{{.Version}}
|
||||||
|
```
|
||||||
|
|
||||||
|
dockers_v2:
|
||||||
|
- id: operator
|
||||||
|
ids:
|
||||||
|
- manager
|
||||||
|
images:
|
||||||
|
- "ghcr.io/lukaszraczylo/kubernetes-images-sync-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/kubernetes-images-sync-operator"
|
||||||
|
"org.opencontainers.image.description": "Kubernetes operator for backing up and syncing container images"
|
||||||
|
|
||||||
|
- id: worker
|
||||||
|
ids:
|
||||||
|
- worker
|
||||||
|
images:
|
||||||
|
- "ghcr.io/lukaszraczylo/kubernetes-images-sync-worker"
|
||||||
|
tags:
|
||||||
|
- "{{ .Version }}"
|
||||||
|
- "latest"
|
||||||
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
dockerfile: Dockerfile.worker
|
||||||
|
extra_files:
|
||||||
|
- docker-image-worker/storage.conf
|
||||||
|
- docker-image-worker/containers.conf
|
||||||
|
- docker-image-worker/registries.conf
|
||||||
|
- docker-image-worker/podman-preauth.sh
|
||||||
|
labels:
|
||||||
|
"org.opencontainers.image.title": "kubernetes-images-sync-worker"
|
||||||
|
"org.opencontainers.image.version": "{{ .Version }}"
|
||||||
|
"org.opencontainers.image.source": "https://github.com/lukaszraczylo/kubernetes-images-sync-operator"
|
||||||
|
"org.opencontainers.image.description": "Worker image for backing up container images to S3 or local storage"
|
||||||
|
|
||||||
|
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,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"]
|
||||||
@@ -7,7 +7,6 @@ ARG TARGETARCH
|
|||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
gnupg2 \
|
gnupg2 \
|
||||||
python3-pip \
|
|
||||||
sudo \
|
sudo \
|
||||||
jq \
|
jq \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -30,11 +29,18 @@ RUN adduser --disabled-password --gecos "" --uid 1001 runner \
|
|||||||
|
|
||||||
WORKDIR /home/runner
|
WORKDIR /home/runner
|
||||||
|
|
||||||
COPY storage.conf containers.conf registries.conf /home/runner/.config/containers/
|
# Copy container configuration files
|
||||||
COPY requirements.txt export.py cleanup.py s3_utils.py podman-preauth.sh ./
|
COPY docker-image-worker/storage.conf docker-image-worker/containers.conf docker-image-worker/registries.conf /home/runner/.config/containers/
|
||||||
|
|
||||||
|
# Copy the entrypoint script
|
||||||
|
COPY docker-image-worker/podman-preauth.sh ./
|
||||||
|
|
||||||
|
# Copy the worker binary (from goreleaser build context)
|
||||||
|
COPY $TARGETPLATFORM/worker ./
|
||||||
|
|
||||||
USER runner
|
USER runner
|
||||||
RUN sudo chown -R runner:runner /home/runner/.config \
|
RUN sudo chown -R runner:runner /home/runner/.config \
|
||||||
&& python3 -m pip install --no-cache-dir --only-binary=:all: -r requirements.txt \
|
&& sudo chmod +x podman-preauth.sh worker
|
||||||
&& sudo chmod +x podman-preauth.sh
|
|
||||||
ENTRYPOINT ["/home/runner/podman-preauth.sh"]
|
ENTRYPOINT ["/home/runner/podman-preauth.sh"]
|
||||||
CMD ["bash", "-c"]
|
CMD ["bash", "-c"]
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
# Image URL to use all building/pushing image targets
|
# Image URL to use all building/pushing image targets
|
||||||
IMG ?= ghcr.io/lukaszraczylo/kubernetes-images-sync-operator
|
IMG ?= ghcr.io/lukaszraczylo/kubernetes-images-sync-operator
|
||||||
IMG_WORKER ?= ghcr.io/lukaszraczylo/kubernetes-images-sync-worker
|
IMG_WORKER ?= ghcr.io/lukaszraczylo/kubernetes-images-sync-worker
|
||||||
CHART_NAME = kube-images-sync-operator
|
CHART_NAME = kube-images-sync
|
||||||
|
CHART_DIR = charts/kube-images-sync-operator
|
||||||
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
|
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
|
||||||
ENVTEST_K8S_VERSION = 1.31.0
|
ENVTEST_K8S_VERSION = 1.31.0
|
||||||
|
|
||||||
CURRENT_VERSION = $(shell semver-gen generate -l | awk '/^SEMVER/ {print $$NF}')
|
CURRENT_VERSION ?= $(shell semver-gen generate -l 2>/dev/null | awk '/^SEMVER/ {print $$NF}')
|
||||||
ifeq ($(CURRENT_VERSION),)
|
ifeq ($(CURRENT_VERSION),)
|
||||||
$(error Failed to extract version number)
|
CURRENT_VERSION = 0.5.54
|
||||||
endif
|
endif
|
||||||
|
|
||||||
IMAGE_VERSION_TAG ?= $(CURRENT_VERSION)
|
IMAGE_VERSION_TAG ?= $(CURRENT_VERSION)
|
||||||
@@ -173,9 +174,9 @@ HELMIFY ?= helmify
|
|||||||
|
|
||||||
## Tool Versions
|
## Tool Versions
|
||||||
KUSTOMIZE_VERSION ?= v5.4.3
|
KUSTOMIZE_VERSION ?= v5.4.3
|
||||||
CONTROLLER_TOOLS_VERSION ?= v0.16.1
|
CONTROLLER_TOOLS_VERSION ?= v0.17.1
|
||||||
ENVTEST_VERSION ?= release-0.19
|
ENVTEST_VERSION ?= release-0.19
|
||||||
GOLANGCI_LINT_VERSION ?= v1.59.1
|
GOLANGCI_LINT_VERSION ?= v1.62.2
|
||||||
|
|
||||||
.PHONY: print-version
|
.PHONY: print-version
|
||||||
print-version:
|
print-version:
|
||||||
@@ -223,7 +224,7 @@ release-chart:
|
|||||||
@test -d ../helm-charts || exit 1
|
@test -d ../helm-charts || exit 1
|
||||||
rm -fr ../helm-charts/charts/${CHART_NAME} || true
|
rm -fr ../helm-charts/charts/${CHART_NAME} || true
|
||||||
mkdir -p ../helm-charts/charts/${CHART_NAME}
|
mkdir -p ../helm-charts/charts/${CHART_NAME}
|
||||||
cp -R chart/* ../helm-charts/charts/${CHART_NAME}
|
cp -R ${CHART_DIR}/* ../helm-charts/charts/${CHART_NAME}
|
||||||
cd ../helm-charts/charts/${CHART_NAME} && \
|
cd ../helm-charts/charts/${CHART_NAME} && \
|
||||||
cr package --config ../../chart-releaser.yaml
|
cr package --config ../../chart-releaser.yaml
|
||||||
cd ../helm-charts && git add -A charts/packages && git commit -m "Add packaged charts" && git push
|
cd ../helm-charts && git add -A charts/packages && git commit -m "Add packaged charts" && git push
|
||||||
|
|||||||
@@ -24,12 +24,13 @@ helm install raczylo/kube-images-sync
|
|||||||
|
|
||||||
Please remember that backups are triggered whenever the new object appears
|
Please remember that backups are triggered whenever the new object appears
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
apiVersion: raczylo.com/v1
|
apiVersion: raczylo.com/v1
|
||||||
kind: ClusterImageExport
|
kind: ClusterImageExport
|
||||||
metadata:
|
metadata:
|
||||||
name: backup-20240901
|
name: backup-20240901
|
||||||
spec:
|
spec:
|
||||||
|
name: backup-20240901
|
||||||
jobAnnotations:
|
jobAnnotations:
|
||||||
my-fancy-export: 11-09-2024
|
my-fancy-export: 11-09-2024
|
||||||
# Excludes will remove all images with listed wording from the backup list
|
# Excludes will remove all images with listed wording from the backup list
|
||||||
@@ -68,6 +69,88 @@ spec:
|
|||||||
maxConcurrentJobs: 1
|
maxConcurrentJobs: 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Automatic Cleanup (TTL & Retention)
|
||||||
|
|
||||||
|
To prevent old exports from accumulating, you can configure automatic cleanup using TTL (time-based) or retention policies (count-based).
|
||||||
|
|
||||||
|
> **WARNING**: When a ClusterImageExport is deleted, the actual backed up images in storage are also deleted. Make sure your retention settings align with your backup requirements.
|
||||||
|
|
||||||
|
### TTL-based cleanup
|
||||||
|
|
||||||
|
Delete exports after a specified number of days:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: raczylo.com/v1
|
||||||
|
kind: ClusterImageExport
|
||||||
|
metadata:
|
||||||
|
name: daily-backup-2024-12-18
|
||||||
|
spec:
|
||||||
|
name: daily-backup
|
||||||
|
basePath: /backups/daily
|
||||||
|
storage:
|
||||||
|
target: S3
|
||||||
|
s3:
|
||||||
|
bucket: my-backup-bucket
|
||||||
|
region: eu-west-1
|
||||||
|
useRole: true
|
||||||
|
maxConcurrentJobs: 5
|
||||||
|
# Delete this backup 30 days after completion
|
||||||
|
ttlDaysAfterFinished: 30
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retention-based cleanup
|
||||||
|
|
||||||
|
Keep only the last N successful/failed exports per base path:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: raczylo.com/v1
|
||||||
|
kind: ClusterImageExport
|
||||||
|
metadata:
|
||||||
|
name: weekly-backup-2024-w51
|
||||||
|
spec:
|
||||||
|
name: weekly-backup
|
||||||
|
basePath: /backups/weekly
|
||||||
|
storage:
|
||||||
|
target: S3
|
||||||
|
s3:
|
||||||
|
bucket: my-backup-bucket
|
||||||
|
region: eu-west-1
|
||||||
|
useRole: true
|
||||||
|
maxConcurrentJobs: 5
|
||||||
|
# Keep the last 12 successful backups (3 months of weekly backups)
|
||||||
|
# Keep only the last 2 failed backups for debugging
|
||||||
|
retention:
|
||||||
|
maxSuccessful: 12
|
||||||
|
maxFailed: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined TTL + Retention
|
||||||
|
|
||||||
|
You can use both policies together. The export will be deleted when either condition is met:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: raczylo.com/v1
|
||||||
|
kind: ClusterImageExport
|
||||||
|
metadata:
|
||||||
|
name: monthly-backup-2024-12
|
||||||
|
spec:
|
||||||
|
name: monthly-backup
|
||||||
|
basePath: /backups/monthly
|
||||||
|
storage:
|
||||||
|
target: S3
|
||||||
|
s3:
|
||||||
|
bucket: my-backup-bucket
|
||||||
|
region: eu-west-1
|
||||||
|
useRole: true
|
||||||
|
maxConcurrentJobs: 10
|
||||||
|
# Keep backups for up to 1 year
|
||||||
|
ttlDaysAfterFinished: 365
|
||||||
|
# But also limit to last 12 monthly backups
|
||||||
|
retention:
|
||||||
|
maxSuccessful: 12
|
||||||
|
maxFailed: 1
|
||||||
|
```
|
||||||
|
|
||||||
## Worth knowing
|
## Worth knowing
|
||||||
|
|
||||||
* If you provide roleARN, you also need to set the useRole to true.
|
* If you provide roleARN, you also need to set the useRole to true.
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RetentionPolicy defines how many completed ClusterImageExport resources to keep
|
||||||
|
type RetentionPolicy struct {
|
||||||
|
// Maximum number of successful exports to keep
|
||||||
|
// +kubebuilder:validation:Minimum=0
|
||||||
|
// +kubebuilder:default=3
|
||||||
|
MaxSuccessful *int32 `json:"maxSuccessful,omitempty"`
|
||||||
|
// Maximum number of failed exports to keep
|
||||||
|
// +kubebuilder:validation:Minimum=0
|
||||||
|
// +kubebuilder:default=1
|
||||||
|
MaxFailed *int32 `json:"maxFailed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ClusterImageStorageS3 struct {
|
type ClusterImageStorageS3 struct {
|
||||||
// Bucket name
|
// Bucket name
|
||||||
Bucket string `json:"bucket"`
|
Bucket string `json:"bucket"`
|
||||||
@@ -40,7 +52,7 @@ type ClusterImageStorageS3 struct {
|
|||||||
|
|
||||||
// ClusterImageStorageSpec defines the desired state of ClusterImageStorage
|
// ClusterImageStorageSpec defines the desired state of ClusterImageStorage
|
||||||
type ClusterImageStorageSpec struct {
|
type ClusterImageStorageSpec struct {
|
||||||
// +kubebuilder:validation:Enum=file;S3
|
// +kubebuilder:validation:Enum=FILE;S3
|
||||||
StorageTarget string `json:"target"`
|
StorageTarget string `json:"target"`
|
||||||
S3 ClusterImageStorageS3 `json:"s3,omitempty"`
|
S3 ClusterImageStorageS3 `json:"s3,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -67,10 +79,22 @@ type ClusterImageExportSpec struct {
|
|||||||
JobAnnotations map[string]string `json:"jobAnnotations,omitempty"`
|
JobAnnotations map[string]string `json:"jobAnnotations,omitempty"`
|
||||||
// +kubebuilder:validation:Optional
|
// +kubebuilder:validation:Optional
|
||||||
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
||||||
// +kubebuilder:validation.Minimum=1
|
// +kubebuilder:validation:Minimum=1
|
||||||
// +kubebuilder:validation.Maximum=100
|
// +kubebuilder:validation:Maximum=100
|
||||||
|
// +kubebuilder:default=5
|
||||||
MaxConcurrentJobs int `json:"maxConcurrentJobs"`
|
MaxConcurrentJobs int `json:"maxConcurrentJobs"`
|
||||||
AdditionalImages []string `json:"additionalImages,omitempty"`
|
AdditionalImages []string `json:"additionalImages,omitempty"`
|
||||||
|
// TTLDaysAfterFinished specifies how many days to keep completed exports.
|
||||||
|
// If set, the export (and its backed up images) will be deleted after this many days.
|
||||||
|
// WARNING: Deletion removes both the CRD and the actual backed up images from storage.
|
||||||
|
// +kubebuilder:validation:Minimum=1
|
||||||
|
// +kubebuilder:validation:Optional
|
||||||
|
TTLDaysAfterFinished *int32 `json:"ttlDaysAfterFinished,omitempty"`
|
||||||
|
// Retention specifies how many completed exports to keep per base path.
|
||||||
|
// Oldest exports beyond this limit will be deleted (including their backed up images).
|
||||||
|
// WARNING: Deletion removes both the CRD and the actual backed up images from storage.
|
||||||
|
// +kubebuilder:validation:Optional
|
||||||
|
Retention *RetentionPolicy `json:"retention,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClusterImageExportStatus defines the observed state of ClusterImageExport
|
// ClusterImageExportStatus defines the observed state of ClusterImageExport
|
||||||
@@ -80,6 +104,8 @@ type ClusterImageExportStatus struct {
|
|||||||
TotalImages int `json:"totalImages,omitempty"`
|
TotalImages int `json:"totalImages,omitempty"`
|
||||||
// Number of images that have completed export
|
// Number of images that have completed export
|
||||||
CompletedImages int `json:"completedImages,omitempty"`
|
CompletedImages int `json:"completedImages,omitempty"`
|
||||||
|
// CompletedAt is the timestamp when the export completed (SUCCESS or FAILED)
|
||||||
|
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
|
|||||||
+4
-4
@@ -3,16 +3,16 @@ name: kube-images-sync
|
|||||||
description: |
|
description: |
|
||||||
A Helm chart for Kubernetes Images Sync Operator.
|
A Helm chart for Kubernetes Images Sync Operator.
|
||||||
Kubernetes Images Sync Operator is responsible for backing up and restoring images from a Kubernetes cluster.
|
Kubernetes Images Sync Operator is responsible for backing up and restoring images from a Kubernetes cluster.
|
||||||
It's ultimate goal is to provide synchonization of images between multiple environments, quite often air-gapped.
|
Its ultimate goal is to provide synchronization of images between multiple environments, quite often air-gapped.
|
||||||
It compiles the list of images currently present in the cluster and uploads them to the specified storage.
|
It compiles the list of images currently present in the cluster and uploads them to the specified storage.
|
||||||
Whenever new CRD is created - it will try to figure out which images were already uploaded and which are new and
|
Whenever a new CRD is created - it will try to figure out which images were already uploaded and which are new and
|
||||||
upload only the new ones to avoid repetition.
|
upload only the new ones to avoid repetition.
|
||||||
|
|
||||||
type: application
|
type: application
|
||||||
|
|
||||||
version: 0.5.53
|
version: 0.5.54
|
||||||
|
|
||||||
appVersion: "0.5.53"
|
appVersion: "0.5.54"
|
||||||
|
|
||||||
home: https://github.com/lukaszraczylo/kubernetes-images-sync-operator
|
home: https://github.com/lukaszraczylo/kubernetes-images-sync-operator
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -3,6 +3,7 @@ sa:
|
|||||||
manager:
|
manager:
|
||||||
args:
|
args:
|
||||||
- --metrics-bind-address=:8443
|
- --metrics-bind-address=:8443
|
||||||
|
- --metrics-secure
|
||||||
- --leader-elect
|
- --leader-elect
|
||||||
- --health-probe-bind-address=:8081
|
- --health-probe-bind-address=:8081
|
||||||
containerSecurityContext:
|
containerSecurityContext:
|
||||||
@@ -11,10 +12,10 @@ sa:
|
|||||||
drop:
|
drop:
|
||||||
- ALL
|
- ALL
|
||||||
env:
|
env:
|
||||||
workerImage: ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:0.5.53
|
workerImage: ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:0.5.54
|
||||||
image:
|
image:
|
||||||
repository: ghcr.io/lukaszraczylo/kubernetes-images-sync-operator
|
repository: ghcr.io/lukaszraczylo/kubernetes-images-sync-operator
|
||||||
tag: 0.5.53
|
tag: "0.5.54"
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 500m
|
cpu: 500m
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: kube-images-sync
|
||||||
|
description: |
|
||||||
|
A Helm chart for Kubernetes Images Sync Operator.
|
||||||
|
Kubernetes Images Sync Operator is responsible for backing up and restoring images from a Kubernetes cluster.
|
||||||
|
Its ultimate goal is to provide synchronization of images between multiple environments, quite often air-gapped.
|
||||||
|
It compiles the list of images currently present in the cluster and uploads them to the specified storage.
|
||||||
|
Whenever a new CRD is created - it will try to figure out which images were already uploaded and which are new and
|
||||||
|
upload only the new ones to avoid repetition.
|
||||||
|
|
||||||
|
type: application
|
||||||
|
|
||||||
|
version: 0.5.54
|
||||||
|
|
||||||
|
appVersion: "0.5.54"
|
||||||
|
|
||||||
|
home: https://github.com/lukaszraczylo/kubernetes-images-sync-operator
|
||||||
|
|
||||||
|
maintainers:
|
||||||
|
- name: lukaszraczylo
|
||||||
|
email: github-enquiries@raczylo.com
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "chart.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "chart.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "chart.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "chart.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "chart.chart" . }}
|
||||||
|
{{ include "chart.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "chart.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "chart.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Get the worker image
|
||||||
|
*/}}
|
||||||
|
{{- define "chart.workerImage" -}}
|
||||||
|
{{- printf "%s:%s" .Values.images.worker.repository .Values.images.worker.tag }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: clusterimages.raczylo.com
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.17.1
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
group: raczylo.com
|
||||||
|
names:
|
||||||
|
kind: ClusterImage
|
||||||
|
listKind: ClusterImageList
|
||||||
|
plural: clusterimages
|
||||||
|
singular: clusterimage
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.exportName
|
||||||
|
name: Ref
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.image
|
||||||
|
name: Image
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.tag
|
||||||
|
name: Tag
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.sha
|
||||||
|
name: SHA
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.storage
|
||||||
|
name: Storage
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.exportPath
|
||||||
|
name: Path
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.progress
|
||||||
|
name: Progress
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.retryCount
|
||||||
|
name: Retries
|
||||||
|
type: integer
|
||||||
|
- jsonPath: .metadata.creationTimestamp
|
||||||
|
name: Age
|
||||||
|
type: date
|
||||||
|
name: v1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: ClusterImage is the Schema for the clusterimages API
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: ClusterImageSpec defines the desired state of ClusterImage
|
||||||
|
properties:
|
||||||
|
exportName:
|
||||||
|
type: string
|
||||||
|
exportPath:
|
||||||
|
type: string
|
||||||
|
fullName:
|
||||||
|
type: string
|
||||||
|
image:
|
||||||
|
type: string
|
||||||
|
imageNamespace:
|
||||||
|
type: string
|
||||||
|
imagePullSecrets:
|
||||||
|
items:
|
||||||
|
description: |-
|
||||||
|
LocalObjectReference contains enough information to let you locate the
|
||||||
|
referenced object inside the same namespace.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
default: ""
|
||||||
|
description: |-
|
||||||
|
Name of the referent.
|
||||||
|
This field is effectively required, but due to backwards compatibility is
|
||||||
|
allowed to be empty. Instances of this type with an empty value here are
|
||||||
|
almost certainly wrong.
|
||||||
|
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
|
type: array
|
||||||
|
jobAnnotations:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
sha:
|
||||||
|
type: string
|
||||||
|
storage:
|
||||||
|
type: string
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- exportName
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: ClusterImageStatus defines the observed state of ClusterImage
|
||||||
|
properties:
|
||||||
|
progress:
|
||||||
|
type: string
|
||||||
|
retryCount:
|
||||||
|
default: 0
|
||||||
|
description: default value is 0
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: clusterimageexports.raczylo.com
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.17.1
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
group: raczylo.com
|
||||||
|
names:
|
||||||
|
kind: ClusterImageExport
|
||||||
|
listKind: ClusterImageExportList
|
||||||
|
plural: clusterimageexports
|
||||||
|
singular: clusterimageexport
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.basePath
|
||||||
|
name: BasePath
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.storage.target
|
||||||
|
name: Storage
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.progress
|
||||||
|
name: Progress
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.completedImages
|
||||||
|
name: Images
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.totalImages
|
||||||
|
name: Total
|
||||||
|
type: string
|
||||||
|
- jsonPath: .metadata.creationTimestamp
|
||||||
|
name: Age
|
||||||
|
type: date
|
||||||
|
name: v1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: ClusterImageExport is the Schema for the clusterimageexports
|
||||||
|
API
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: ClusterImageExportSpec defines the desired state of ClusterImageExport
|
||||||
|
properties:
|
||||||
|
additionalImages:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
basePath:
|
||||||
|
description: Base path for the export - both file and S3
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
createdAt:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
excludedNamespaces:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
excludes:
|
||||||
|
description: Exclude images which contain these strings
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
imagePullSecrets:
|
||||||
|
items:
|
||||||
|
description: |-
|
||||||
|
LocalObjectReference contains enough information to let you locate the
|
||||||
|
referenced object inside the same namespace.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
default: ""
|
||||||
|
description: |-
|
||||||
|
Name of the referent.
|
||||||
|
This field is effectively required, but due to backwards compatibility is
|
||||||
|
allowed to be empty. Instances of this type with an empty value here are
|
||||||
|
almost certainly wrong.
|
||||||
|
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
|
type: array
|
||||||
|
includes:
|
||||||
|
description: Include only images which contain these strings
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
jobAnnotations:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
maxConcurrentJobs:
|
||||||
|
default: 5
|
||||||
|
maximum: 100
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
namespaces:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
storage:
|
||||||
|
description: ClusterImageStorageSpec defines the desired state of
|
||||||
|
ClusterImageStorage
|
||||||
|
properties:
|
||||||
|
s3:
|
||||||
|
properties:
|
||||||
|
accessKey:
|
||||||
|
description: S3 bucket credentials
|
||||||
|
type: string
|
||||||
|
bucket:
|
||||||
|
description: Bucket name
|
||||||
|
type: string
|
||||||
|
endpoint:
|
||||||
|
description: |-
|
||||||
|
Defines the endpoint for the S3 storage
|
||||||
|
If none specified - default AWS endpoint will be used
|
||||||
|
type: string
|
||||||
|
region:
|
||||||
|
type: string
|
||||||
|
roleARN:
|
||||||
|
description: RoleARN is the ARN of the role to be used for
|
||||||
|
the deployment
|
||||||
|
type: string
|
||||||
|
secretKey:
|
||||||
|
type: string
|
||||||
|
secretName:
|
||||||
|
description: Defines the secret name for credentials
|
||||||
|
type: string
|
||||||
|
useRole:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- bucket
|
||||||
|
- region
|
||||||
|
type: object
|
||||||
|
target:
|
||||||
|
enum:
|
||||||
|
- FILE
|
||||||
|
- S3
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- target
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- basePath
|
||||||
|
- maxConcurrentJobs
|
||||||
|
- name
|
||||||
|
- storage
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: ClusterImageExportStatus defines the observed state of ClusterImageExport
|
||||||
|
properties:
|
||||||
|
completedImages:
|
||||||
|
description: Number of images that have completed export
|
||||||
|
type: integer
|
||||||
|
progress:
|
||||||
|
type: string
|
||||||
|
totalImages:
|
||||||
|
description: Total number of images to be exported
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-controller-manager
|
||||||
|
labels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.controllerManager.replicas }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
{{- include "chart.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
{{- include "chart.selectorLabels" . | nindent 8 }}
|
||||||
|
annotations:
|
||||||
|
kubectl.kubernetes.io/default-container: manager
|
||||||
|
spec:
|
||||||
|
affinity:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: kubernetes.io/arch
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- key: kubernetes.io/os
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- linux
|
||||||
|
containers:
|
||||||
|
- args:
|
||||||
|
{{- toYaml .Values.controllerManager.manager.args | nindent 8 }}
|
||||||
|
command:
|
||||||
|
- /manager
|
||||||
|
env:
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: POD_SERVICE_ACCOUNT
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.serviceAccountName
|
||||||
|
- name: WORKER_IMAGE
|
||||||
|
value: {{ quote .Values.controllerManager.manager.env.workerImage }}
|
||||||
|
- name: KUBERNETES_CLUSTER_DOMAIN
|
||||||
|
value: {{ quote .Values.kubernetesClusterDomain }}
|
||||||
|
image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag | default .Chart.AppVersion }}
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 8081
|
||||||
|
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:
|
||||||
|
{{- toYaml .Values.controllerManager.manager.containerSecurityContext | nindent 10 }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.controllerManager.podSecurityContext | nindent 8 }}
|
||||||
|
serviceAccountName: {{ include "chart.fullname" . }}-controller-manager
|
||||||
|
terminationGracePeriodSeconds: 10
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-impex-mgr
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- daemonsets
|
||||||
|
- deployments
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- batch
|
||||||
|
resources:
|
||||||
|
- cronjobs
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- batch
|
||||||
|
resources:
|
||||||
|
- jobs
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- raczylo.com
|
||||||
|
resources:
|
||||||
|
- '*'
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- raczylo.com
|
||||||
|
resources:
|
||||||
|
- '*/finalizers'
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- raczylo.com
|
||||||
|
resources:
|
||||||
|
- '*/status'
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-impex-mgrbinding
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: '{{ include "chart.fullname" . }}-impex-mgr'
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: '{{ include "chart.fullname" . }}-controller-manager'
|
||||||
|
namespace: '{{ .Release.Namespace }}'
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-metrics-auth-raczylo
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- authentication.k8s.io
|
||||||
|
resources:
|
||||||
|
- tokenreviews
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- apiGroups:
|
||||||
|
- authorization.k8s.io
|
||||||
|
resources:
|
||||||
|
- subjectaccessreviews
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-metrics-auth-raczylobinding
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: '{{ include "chart.fullname" . }}-metrics-auth-raczylo'
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: '{{ include "chart.fullname" . }}-controller-manager'
|
||||||
|
namespace: '{{ .Release.Namespace }}'
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-metrics-raczylo
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
- nonResourceURLs:
|
||||||
|
- /metrics
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-raczylo-com-leader
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- coordination.k8s.io
|
||||||
|
resources:
|
||||||
|
- leases
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- events
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-raczylo-com-leaderbinding
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: '{{ include "chart.fullname" . }}-raczylo-com-leader'
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: '{{ include "chart.fullname" . }}-controller-manager'
|
||||||
|
namespace: '{{ .Release.Namespace }}'
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-raczylo.com-clusterimage-editor-role
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- raczylo.com
|
||||||
|
resources:
|
||||||
|
- clusterimages
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- raczylo.com
|
||||||
|
resources:
|
||||||
|
- clusterimages/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-raczylo.com-clusterimage-viewer-role
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- raczylo.com
|
||||||
|
resources:
|
||||||
|
- clusterimages
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- raczylo.com
|
||||||
|
resources:
|
||||||
|
- clusterimages/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- watch
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-raczylo.com-clusterimageexport-editor-role
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- raczylo.com
|
||||||
|
resources:
|
||||||
|
- clusterimageexports
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- raczylo.com
|
||||||
|
resources:
|
||||||
|
- clusterimageexports/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-raczylo.com-clusterimageexport-viewer-role
|
||||||
|
labels:
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- raczylo.com
|
||||||
|
resources:
|
||||||
|
- clusterimageexports
|
||||||
|
- clusterimageexports/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-metrics-service
|
||||||
|
labels:
|
||||||
|
control-plane: controller-manager
|
||||||
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.metricsService.type }}
|
||||||
|
selector:
|
||||||
|
control-plane: controller-manager
|
||||||
|
{{- include "chart.selectorLabels" . | nindent 4 }}
|
||||||
|
ports:
|
||||||
|
{{- .Values.metricsService.ports | toYaml | nindent 2 }}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}-controller-manager
|
||||||
|
labels:
|
||||||
|
{{- 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 }}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
kubernetesClusterDomain: cluster.local
|
||||||
|
|
||||||
|
controllerManager:
|
||||||
|
manager:
|
||||||
|
# Command line arguments for the manager
|
||||||
|
args:
|
||||||
|
- --metrics-bind-address=:8443
|
||||||
|
- --metrics-secure
|
||||||
|
- --leader-elect
|
||||||
|
- --health-probe-bind-address=:8081
|
||||||
|
containerSecurityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
env:
|
||||||
|
workerImage: ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:0.5.54
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/lukaszraczylo/kubernetes-images-sync-operator
|
||||||
|
tag: "0.5.54"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 128Mi
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 64Mi
|
||||||
|
podSecurityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
replicas: 1
|
||||||
|
serviceAccount:
|
||||||
|
annotations: {}
|
||||||
|
|
||||||
|
# Metrics service configuration
|
||||||
|
metricsService:
|
||||||
|
ports:
|
||||||
|
- name: https
|
||||||
|
port: 8443
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 8443
|
||||||
|
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,369 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Global flags
|
||||||
|
useRole bool
|
||||||
|
useCurrentRole bool
|
||||||
|
roleName string
|
||||||
|
awsAccessKeyID string
|
||||||
|
awsSecretKey string
|
||||||
|
endpointURL string
|
||||||
|
region string
|
||||||
|
maxRetries int
|
||||||
|
retryDelay time.Duration
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rootCmd := &cobra.Command{
|
||||||
|
Use: "worker",
|
||||||
|
Short: "Kubernetes Images Sync Worker",
|
||||||
|
Long: "Worker for backing up container images to S3 or local storage",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add global flags
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&useRole, "use_role", false, "Use IAM role for authentication")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&useCurrentRole, "use_current_role", false, "Use current AWS role (e.g., from Kubernetes service account)")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&roleName, "role_name", "", "The name of the IAM role to assume (only when --use_role is set)")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&awsAccessKeyID, "aws_access_key_id", "", "AWS access key ID")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&awsSecretKey, "aws_secret_access_key", "", "AWS secret access key")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&endpointURL, "endpoint_url", "", "S3-compatible endpoint URL")
|
||||||
|
rootCmd.PersistentFlags().StringVar(®ion, "region", "", "AWS region")
|
||||||
|
rootCmd.PersistentFlags().IntVar(&maxRetries, "max_retries", 5, "Maximum number of retries")
|
||||||
|
rootCmd.PersistentFlags().DurationVar(&retryDelay, "retry_delay", 5*time.Second, "Delay between retries")
|
||||||
|
|
||||||
|
// Add commands
|
||||||
|
rootCmd.AddCommand(exportCmd())
|
||||||
|
rootCmd.AddCommand(cleanupCmd())
|
||||||
|
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "export <source> <destination>",
|
||||||
|
Short: "Export a file to S3 or local destination",
|
||||||
|
Long: "Transfer a file from a local source to either a local destination or an S3 bucket",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
source := args[0]
|
||||||
|
destination := args[1]
|
||||||
|
return runExport(source, destination)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "cleanup <destination>",
|
||||||
|
Short: "Remove a directory from S3 or local filesystem",
|
||||||
|
Long: "Remove a directory recursively, either local or in an S3 bucket",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
destination := args[0]
|
||||||
|
return runCleanup(destination)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runExport(source, destination string) error {
|
||||||
|
// Check if source file exists
|
||||||
|
if _, err := os.Stat(source); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("source file '%s' does not exist", source)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||||
|
if attempt > 1 {
|
||||||
|
fmt.Printf("Retry attempt %d/%d after %v\n", attempt, maxRetries, retryDelay)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if strings.HasPrefix(destination, "s3://") {
|
||||||
|
err = uploadToS3(source, destination)
|
||||||
|
} else {
|
||||||
|
err = copyLocal(source, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf("Transfer completed successfully: %s -> %s\n", source, destination)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
fmt.Printf("Attempt %d failed: %v\n", attempt, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("transfer failed after %d attempts: %w", maxRetries, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCleanup(destination string) error {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||||
|
if attempt > 1 {
|
||||||
|
fmt.Printf("Retry attempt %d/%d after %v\n", attempt, maxRetries, retryDelay)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if strings.HasPrefix(destination, "s3://") {
|
||||||
|
err = deleteFromS3(destination)
|
||||||
|
} else {
|
||||||
|
err = deleteLocal(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf("Cleanup completed successfully: %s\n", destination)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
fmt.Printf("Attempt %d failed: %v\n", attempt, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cleanup failed after %d attempts: %w", maxRetries, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getS3Client(ctx context.Context) (*s3.Client, error) {
|
||||||
|
var cfg aws.Config
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Determine region
|
||||||
|
awsRegion := region
|
||||||
|
if awsRegion == "" {
|
||||||
|
awsRegion = os.Getenv("AWS_REGION")
|
||||||
|
}
|
||||||
|
if awsRegion == "" {
|
||||||
|
awsRegion = os.Getenv("AWS_DEFAULT_REGION")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build config options
|
||||||
|
optFns := []func(*config.LoadOptions) error{}
|
||||||
|
|
||||||
|
if awsRegion != "" {
|
||||||
|
optFns = append(optFns, config.WithRegion(awsRegion))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load base config
|
||||||
|
cfg, err = config.LoadDefaultConfig(ctx, optFns...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load AWS config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle authentication methods
|
||||||
|
if awsAccessKeyID != "" && awsSecretKey != "" {
|
||||||
|
// Use explicit credentials
|
||||||
|
fmt.Println("Using explicit AWS credentials")
|
||||||
|
cfg.Credentials = credentials.NewStaticCredentialsProvider(awsAccessKeyID, awsSecretKey, "")
|
||||||
|
} else if useRole && roleName != "" {
|
||||||
|
// Assume specific role
|
||||||
|
fmt.Printf("Attempting to assume role: %s\n", roleName)
|
||||||
|
stsClient := sts.NewFromConfig(cfg)
|
||||||
|
|
||||||
|
// Get account ID for role ARN
|
||||||
|
identity, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get caller identity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roleARN := fmt.Sprintf("arn:aws:iam::%s:role/%s", *identity.Account, roleName)
|
||||||
|
cfg.Credentials = stscreds.NewAssumeRoleProvider(stsClient, roleARN)
|
||||||
|
} else if useCurrentRole {
|
||||||
|
// Use current role (default credential chain handles this)
|
||||||
|
fmt.Println("Using current role from environment")
|
||||||
|
// The default config already uses the credential chain which includes
|
||||||
|
// web identity token if AWS_WEB_IDENTITY_TOKEN_FILE is set
|
||||||
|
} else {
|
||||||
|
fmt.Println("Using default credential provider chain")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create S3 client options
|
||||||
|
s3Opts := []func(*s3.Options){}
|
||||||
|
if endpointURL != "" {
|
||||||
|
s3Opts = append(s3Opts, func(o *s3.Options) {
|
||||||
|
o.BaseEndpoint = aws.String(endpointURL)
|
||||||
|
o.UsePathStyle = true // Required for most S3-compatible services
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return s3.NewFromConfig(cfg, s3Opts...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseS3Path(s3Path string) (bucket, key string) {
|
||||||
|
path := strings.TrimPrefix(s3Path, "s3://")
|
||||||
|
parts := strings.SplitN(path, "/", 2)
|
||||||
|
bucket = parts[0]
|
||||||
|
if len(parts) > 1 {
|
||||||
|
key = parts[1]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadToS3(source, destination string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
client, err := getS3Client(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create S3 client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket, key := parseS3Path(destination)
|
||||||
|
|
||||||
|
file, err := os.Open(source) // #nosec G304 -- source path is provided by operator via CLI args
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open source file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
fmt.Printf("Uploading %s to s3://%s/%s\n", source, bucket, key)
|
||||||
|
|
||||||
|
_, err = client.PutObject(ctx, &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
Body: file,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to upload to S3: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyLocal(source, destination string) error {
|
||||||
|
// Create destination directory if it doesn't exist
|
||||||
|
destDir := filepath.Dir(destination)
|
||||||
|
if err := os.MkdirAll(destDir, 0750); err != nil { // #nosec G301 -- restricted permissions for backup directory
|
||||||
|
return fmt.Errorf("failed to create destination directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open source file
|
||||||
|
srcFile, err := os.Open(source) // #nosec G304 -- source path is provided by operator via CLI args
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open source file: %w", err)
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
// Get source file info for permissions
|
||||||
|
srcInfo, err := srcFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stat source file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create destination file
|
||||||
|
dstFile, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE|os.O_TRUNC, srcInfo.Mode()) // #nosec G304 -- destination path is provided by operator via CLI args
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create destination file: %w", err)
|
||||||
|
}
|
||||||
|
defer dstFile.Close()
|
||||||
|
|
||||||
|
// Copy content
|
||||||
|
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy file content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Copied %s to %s\n", source, destination)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteFromS3(destination string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
client, err := getS3Client(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create S3 client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket, prefix := parseS3Path(destination)
|
||||||
|
|
||||||
|
fmt.Printf("Deleting objects from s3://%s/%s\n", bucket, prefix)
|
||||||
|
|
||||||
|
// List and delete objects
|
||||||
|
paginator := s3.NewListObjectsV2Paginator(client, &s3.ListObjectsV2Input{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Prefix: aws.String(prefix),
|
||||||
|
})
|
||||||
|
|
||||||
|
totalDeleted := 0
|
||||||
|
for paginator.HasMorePages() {
|
||||||
|
page, err := paginator.NextPage(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list objects: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(page.Contents) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build list of objects to delete
|
||||||
|
var objectsToDelete []string
|
||||||
|
for _, obj := range page.Contents {
|
||||||
|
objectsToDelete = append(objectsToDelete, *obj.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete objects in batches of 1000 (S3 limit)
|
||||||
|
for i := 0; i < len(objectsToDelete); i += 1000 {
|
||||||
|
end := i + 1000
|
||||||
|
if end > len(objectsToDelete) {
|
||||||
|
end = len(objectsToDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := objectsToDelete[i:end]
|
||||||
|
deleteObjects := make([]types.ObjectIdentifier, len(batch))
|
||||||
|
for j, key := range batch {
|
||||||
|
deleteObjects[j] = types.ObjectIdentifier{Key: aws.String(key)}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Delete: &types.Delete{
|
||||||
|
Objects: deleteObjects,
|
||||||
|
Quiet: aws.Bool(true),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete objects: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDeleted += len(batch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Deleted %d objects from s3://%s/%s\n", totalDeleted, bucket, prefix)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteLocal(destination string) error {
|
||||||
|
// Check if path exists
|
||||||
|
if _, err := os.Stat(destination); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("Directory %s does not exist, nothing to delete\n", destination)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove directory recursively
|
||||||
|
if err := os.RemoveAll(destination); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Deleted directory %s\n", destination)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
|||||||
kind: CustomResourceDefinition
|
kind: CustomResourceDefinition
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
controller-gen.kubebuilder.io/version: v0.16.1
|
controller-gen.kubebuilder.io/version: v0.17.1
|
||||||
name: clusterimageexports.raczylo.com
|
name: clusterimageexports.raczylo.com
|
||||||
spec:
|
spec:
|
||||||
group: raczylo.com
|
group: raczylo.com
|
||||||
@@ -108,6 +108,9 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
maxConcurrentJobs:
|
maxConcurrentJobs:
|
||||||
|
default: 5
|
||||||
|
maximum: 100
|
||||||
|
minimum: 1
|
||||||
type: integer
|
type: integer
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
@@ -115,6 +118,25 @@ spec:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
retention:
|
||||||
|
description: |-
|
||||||
|
Retention specifies how many completed exports to keep per base path.
|
||||||
|
Oldest exports beyond this limit will be deleted (including their backed up images).
|
||||||
|
WARNING: Deletion removes both the CRD and the actual backed up images from storage.
|
||||||
|
properties:
|
||||||
|
maxFailed:
|
||||||
|
default: 1
|
||||||
|
description: Maximum number of failed exports to keep
|
||||||
|
format: int32
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
maxSuccessful:
|
||||||
|
default: 3
|
||||||
|
description: Maximum number of successful exports to keep
|
||||||
|
format: int32
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
storage:
|
storage:
|
||||||
description: ClusterImageStorageSpec defines the desired state of
|
description: ClusterImageStorageSpec defines the desired state of
|
||||||
ClusterImageStorage
|
ClusterImageStorage
|
||||||
@@ -151,12 +173,20 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
target:
|
target:
|
||||||
enum:
|
enum:
|
||||||
- file
|
- FILE
|
||||||
- S3
|
- S3
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- target
|
- target
|
||||||
type: object
|
type: object
|
||||||
|
ttlDaysAfterFinished:
|
||||||
|
description: |-
|
||||||
|
TTLDaysAfterFinished specifies how many days to keep completed exports.
|
||||||
|
If set, the export (and its backed up images) will be deleted after this many days.
|
||||||
|
WARNING: Deletion removes both the CRD and the actual backed up images from storage.
|
||||||
|
format: int32
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
required:
|
required:
|
||||||
- basePath
|
- basePath
|
||||||
- maxConcurrentJobs
|
- maxConcurrentJobs
|
||||||
@@ -166,6 +196,11 @@ spec:
|
|||||||
status:
|
status:
|
||||||
description: ClusterImageExportStatus defines the observed state of ClusterImageExport
|
description: ClusterImageExportStatus defines the observed state of ClusterImageExport
|
||||||
properties:
|
properties:
|
||||||
|
completedAt:
|
||||||
|
description: CompletedAt is the timestamp when the export completed
|
||||||
|
(SUCCESS or FAILED)
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
completedImages:
|
completedImages:
|
||||||
description: Number of images that have completed export
|
description: Number of images that have completed export
|
||||||
type: integer
|
type: integer
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
|||||||
kind: CustomResourceDefinition
|
kind: CustomResourceDefinition
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
controller-gen.kubebuilder.io/version: v0.16.1
|
controller-gen.kubebuilder.io/version: v0.17.1
|
||||||
name: clusterimages.raczylo.com
|
name: clusterimages.raczylo.com
|
||||||
spec:
|
spec:
|
||||||
group: raczylo.com
|
group: raczylo.com
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
|
# This patch adds the args to allow exposing the metrics endpoint securely using HTTPS
|
||||||
- op: add
|
- op: add
|
||||||
path: /spec/template/spec/containers/0/args/0
|
path: /spec/template/spec/containers/0/args/0
|
||||||
value: --metrics-bind-address=:8443
|
value: --metrics-bind-address=:8443
|
||||||
|
- op: add
|
||||||
|
path: /spec/template/spec/containers/0/args/1
|
||||||
|
value: --metrics-secure
|
||||||
|
|||||||
+67
-68
@@ -4,71 +4,70 @@ kind: ClusterRole
|
|||||||
metadata:
|
metadata:
|
||||||
name: impex-mgr
|
name: impex-mgr
|
||||||
rules:
|
rules:
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- ""
|
- ""
|
||||||
resources:
|
resources:
|
||||||
- pods
|
- pods
|
||||||
verbs:
|
verbs:
|
||||||
- create
|
- create
|
||||||
- delete
|
- delete
|
||||||
- deletecollection
|
- get
|
||||||
- get
|
- list
|
||||||
- list
|
- patch
|
||||||
- patch
|
- update
|
||||||
- update
|
- watch
|
||||||
- watch
|
- apiGroups:
|
||||||
- apiGroups:
|
- apps
|
||||||
- apps
|
resources:
|
||||||
resources:
|
- daemonsets
|
||||||
- daemonsets
|
- deployments
|
||||||
- deployments
|
verbs:
|
||||||
verbs:
|
- get
|
||||||
- get
|
- list
|
||||||
- list
|
- watch
|
||||||
- watch
|
- apiGroups:
|
||||||
- apiGroups:
|
- batch
|
||||||
- batch
|
resources:
|
||||||
resources:
|
- cronjobs
|
||||||
- cronjobs
|
verbs:
|
||||||
verbs:
|
- get
|
||||||
- get
|
- list
|
||||||
- list
|
- watch
|
||||||
- watch
|
- apiGroups:
|
||||||
- apiGroups:
|
- batch
|
||||||
- batch
|
resources:
|
||||||
resources:
|
- jobs
|
||||||
- jobs
|
verbs:
|
||||||
verbs:
|
- create
|
||||||
- create
|
- delete
|
||||||
- delete
|
- get
|
||||||
- get
|
- list
|
||||||
- list
|
- patch
|
||||||
- patch
|
- update
|
||||||
- update
|
- watch
|
||||||
- watch
|
- apiGroups:
|
||||||
- apiGroups:
|
- raczylo.com
|
||||||
- raczylo.com
|
resources:
|
||||||
resources:
|
- '*'
|
||||||
- "*"
|
verbs:
|
||||||
verbs:
|
- create
|
||||||
- create
|
- delete
|
||||||
- delete
|
- get
|
||||||
- get
|
- list
|
||||||
- list
|
- patch
|
||||||
- patch
|
- update
|
||||||
- update
|
- watch
|
||||||
- watch
|
- apiGroups:
|
||||||
- apiGroups:
|
- raczylo.com
|
||||||
- raczylo.com
|
resources:
|
||||||
resources:
|
- '*/finalizers'
|
||||||
- "*/finalizers"
|
verbs:
|
||||||
verbs:
|
- update
|
||||||
- update
|
- apiGroups:
|
||||||
- apiGroups:
|
- raczylo.com
|
||||||
- raczylo.com
|
resources:
|
||||||
resources:
|
- '*/status'
|
||||||
- "*/status"
|
verbs:
|
||||||
verbs:
|
- get
|
||||||
- get
|
- patch
|
||||||
- patch
|
- update
|
||||||
- update
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
from botocore.exceptions import ClientError
|
|
||||||
from tenacity import retry, stop_after_attempt, wait_fixed
|
|
||||||
|
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
from s3_utils import get_s3_client, parse_s3_path, add_common_arguments, validate_args
|
|
||||||
|
|
||||||
@retry(stop=stop_after_attempt(5), wait=wait_fixed(5))
|
|
||||||
def remove_directory(destination, use_role=False, role_name=None, aws_access_key_id=None, aws_secret_access_key=None, endpoint_url=None, region=None):
|
|
||||||
"""
|
|
||||||
Remove a directory recursively, either local or in an S3 bucket
|
|
||||||
"""
|
|
||||||
if destination.startswith('s3://'):
|
|
||||||
# Removing from S3
|
|
||||||
s3_client = get_s3_client(use_role, role_name, aws_access_key_id, aws_secret_access_key, endpoint_url, region)
|
|
||||||
bucket, prefix = parse_s3_path(destination)
|
|
||||||
try:
|
|
||||||
paginator = s3_client.get_paginator('list_objects_v2')
|
|
||||||
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
|
||||||
if 'Contents' in page:
|
|
||||||
objects_to_delete = [{'Key': obj['Key']} for obj in page['Contents']]
|
|
||||||
s3_client.delete_objects(Bucket=bucket, Delete={'Objects': objects_to_delete})
|
|
||||||
print(f"Directory {destination} removed successfully from S3")
|
|
||||||
except ClientError as e:
|
|
||||||
print(f"Error removing directory from S3: {str(e)}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
# Removing local directory
|
|
||||||
try:
|
|
||||||
import shutil
|
|
||||||
if os.path.exists(destination):
|
|
||||||
shutil.rmtree(destination)
|
|
||||||
print(f"Directory {destination} removed successfully")
|
|
||||||
else:
|
|
||||||
print(f"Directory {destination} does not exist")
|
|
||||||
except IOError as e:
|
|
||||||
print(f"Error removing directory: {str(e)}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser(description="Remove a directory recursively, either local or in an S3 bucket.")
|
|
||||||
parser.add_argument("destination", help="The directory path (local) or S3 path (e.g., 's3://bucket/prefix') to remove")
|
|
||||||
add_common_arguments(parser)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
validate_args(args, parser)
|
|
||||||
|
|
||||||
success = remove_directory(
|
|
||||||
args.destination,
|
|
||||||
args.use_role,
|
|
||||||
args.role_name,
|
|
||||||
args.aws_access_key_id,
|
|
||||||
args.aws_secret_access_key,
|
|
||||||
args.endpoint_url,
|
|
||||||
args.region
|
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
print("Cleanup completed successfully.")
|
|
||||||
else:
|
|
||||||
print("Cleanup failed.")
|
|
||||||
exit(1)
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
from botocore.exceptions import ClientError, BotoCoreError
|
|
||||||
from tenacity import retry, stop_after_attempt, wait_fixed
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
from s3_utils import get_s3_client, parse_s3_path, add_common_arguments, validate_args
|
|
||||||
|
|
||||||
def log_error_details(e):
|
|
||||||
"""Log detailed error information from AWS exceptions"""
|
|
||||||
if hasattr(e, 'response'):
|
|
||||||
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
||||||
error_message = e.response.get('Error', {}).get('Message', str(e))
|
|
||||||
request_id = e.response.get('ResponseMetadata', {}).get('RequestId', 'Unknown')
|
|
||||||
logger.error(f"AWS Error Details:")
|
|
||||||
logger.error(f"- Error Code: {error_code}")
|
|
||||||
logger.error(f"- Error Message: {error_message}")
|
|
||||||
logger.error(f"- Request ID: {request_id}")
|
|
||||||
logger.error(f"- Full Response: {e.response}")
|
|
||||||
else:
|
|
||||||
logger.error(f"Non-AWS Error: {str(e)}")
|
|
||||||
|
|
||||||
@retry(stop=stop_after_attempt(5), wait=wait_fixed(5))
|
|
||||||
def transfer_file(source, destination, use_role=False, role_name=None, use_current_role=False, aws_access_key_id=None, aws_secret_access_key=None, endpoint_url=None, region=None):
|
|
||||||
"""
|
|
||||||
Transfer a file from a local source to either a local destination or an S3 bucket
|
|
||||||
"""
|
|
||||||
if not os.path.isfile(source):
|
|
||||||
logger.error(f"Error: Source file '{source}' does not exist or is not a file.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if destination.startswith('s3://'):
|
|
||||||
# Uploading to S3
|
|
||||||
try:
|
|
||||||
logger.info(f"Attempting to upload {source} to {destination}")
|
|
||||||
s3_client = get_s3_client(use_role, role_name, use_current_role, aws_access_key_id, aws_secret_access_key, endpoint_url, region)
|
|
||||||
bucket, s3_key = parse_s3_path(destination)
|
|
||||||
|
|
||||||
try:
|
|
||||||
s3_client.upload_file(source, bucket, s3_key)
|
|
||||||
logger.info(f"File {source} uploaded successfully to {destination}")
|
|
||||||
except ClientError as e:
|
|
||||||
log_error_details(e)
|
|
||||||
if "AccessDenied" in str(e):
|
|
||||||
logger.error("Access denied. Please check:")
|
|
||||||
logger.error("1. IAM role/user permissions")
|
|
||||||
logger.error("2. S3 bucket permissions")
|
|
||||||
logger.error("3. Web identity token configuration")
|
|
||||||
return False
|
|
||||||
except BotoCoreError as e:
|
|
||||||
logger.error(f"Boto3 error during upload: {str(e)}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Unexpected error during S3 client creation or upload: {str(e)}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
# Copying to local destination
|
|
||||||
try:
|
|
||||||
import shutil
|
|
||||||
logger.info(f"Attempting to copy {source} to local destination {destination}")
|
|
||||||
# Create destination directory if it doesn't exist
|
|
||||||
os.makedirs(os.path.dirname(destination), exist_ok=True)
|
|
||||||
shutil.copy2(source, destination)
|
|
||||||
logger.info(f"File {source} copied successfully to {destination}")
|
|
||||||
except IOError as e:
|
|
||||||
logger.error(f"Error copying file: {str(e)}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser(description="Transfer a file from a local source to either a local destination or an S3 bucket.")
|
|
||||||
parser.add_argument("source", help="The local source file path")
|
|
||||||
parser.add_argument("destination", help="The destination file path (local) or S3 path (e.g., 's3://bucket/key')")
|
|
||||||
add_common_arguments(parser)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
validate_args(args, parser)
|
|
||||||
|
|
||||||
success = transfer_file(
|
|
||||||
args.source,
|
|
||||||
args.destination,
|
|
||||||
args.use_role,
|
|
||||||
args.role_name,
|
|
||||||
args.use_current_role,
|
|
||||||
args.aws_access_key_id,
|
|
||||||
args.aws_secret_access_key,
|
|
||||||
args.endpoint_url,
|
|
||||||
args.region
|
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
logger.info("Transfer completed successfully.")
|
|
||||||
else:
|
|
||||||
logger.error("Transfer failed.")
|
|
||||||
exit(1)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
boto3
|
|
||||||
botocore
|
|
||||||
jmespath
|
|
||||||
tenacity
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
import boto3
|
|
||||||
from botocore.exceptions import ClientError
|
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
|
|
||||||
def get_s3_client(use_role=False, role_name=None, use_current_role=False, aws_access_key_id=None, aws_secret_access_key=None, endpoint_url=None, region=None):
|
|
||||||
"""
|
|
||||||
Create and return an S3 client based on the provided authentication method, endpoint, and region.
|
|
||||||
"""
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
client_kwargs = {}
|
|
||||||
|
|
||||||
# Log authentication method being attempted
|
|
||||||
logger.info("Attempting S3 client creation with:")
|
|
||||||
logger.info(f"- Region: {region if region else 'default'}")
|
|
||||||
logger.info(f"- Endpoint URL: {endpoint_url if endpoint_url else 'default'}")
|
|
||||||
|
|
||||||
if endpoint_url:
|
|
||||||
client_kwargs['endpoint_url'] = endpoint_url
|
|
||||||
if region:
|
|
||||||
client_kwargs['region_name'] = region
|
|
||||||
|
|
||||||
# Check for AWS Web Identity token
|
|
||||||
token_file = os.environ.get('AWS_WEB_IDENTITY_TOKEN_FILE')
|
|
||||||
role_arn = os.environ.get('AWS_ROLE_ARN')
|
|
||||||
if token_file or role_arn:
|
|
||||||
logger.info("AWS Web Identity configuration detected:")
|
|
||||||
logger.info(f"- Token file path: {token_file}")
|
|
||||||
logger.info(f"- Role ARN: {role_arn}")
|
|
||||||
logger.info(f"- Session name: {os.environ.get('AWS_ROLE_SESSION_NAME', 'default')}")
|
|
||||||
|
|
||||||
if aws_access_key_id and aws_secret_access_key:
|
|
||||||
logger.info("Using explicit AWS credentials")
|
|
||||||
# Use explicit credentials if provided
|
|
||||||
client_kwargs['aws_access_key_id'] = aws_access_key_id
|
|
||||||
client_kwargs['aws_secret_access_key'] = aws_secret_access_key
|
|
||||||
return boto3.client('s3', **client_kwargs)
|
|
||||||
elif use_role and role_name:
|
|
||||||
# Assume specific role if requested
|
|
||||||
logger.info(f"Attempting to assume role: {role_name}")
|
|
||||||
try:
|
|
||||||
sts_client = boto3.client('sts')
|
|
||||||
# Get current identity for logging
|
|
||||||
identity = sts_client.get_caller_identity()
|
|
||||||
logger.info(f"Current identity: {identity['Arn']}")
|
|
||||||
|
|
||||||
assumed_role_object = sts_client.assume_role(
|
|
||||||
RoleArn=f"arn:aws:iam::{boto3.client('sts').get_caller_identity()['Account']}:role/{role_name}",
|
|
||||||
RoleSessionName="AssumeRoleSession"
|
|
||||||
)
|
|
||||||
credentials = assumed_role_object['Credentials']
|
|
||||||
client_kwargs['aws_access_key_id'] = credentials['AccessKeyId']
|
|
||||||
client_kwargs['aws_secret_access_key'] = credentials['SecretAccessKey']
|
|
||||||
client_kwargs['aws_session_token'] = credentials['SessionToken']
|
|
||||||
return boto3.client('s3', **client_kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to assume role {role_name}: {str(e)}")
|
|
||||||
raise
|
|
||||||
elif use_current_role:
|
|
||||||
# Use the current role (e.g., from Kubernetes service account)
|
|
||||||
logger.info("Using current role from environment")
|
|
||||||
try:
|
|
||||||
# Log environment for debugging
|
|
||||||
for key, value in sorted(os.environ.items()):
|
|
||||||
if any(k in key.lower() for k in ['aws', 'role', 'auth', 'token', 'credential']):
|
|
||||||
logger.info(f"Environment: {key}={value}")
|
|
||||||
|
|
||||||
# Get the AWS region from environment or parameter
|
|
||||||
aws_region = os.environ.get('AWS_REGION') or os.environ.get('AWS_DEFAULT_REGION')
|
|
||||||
if not aws_region and not region:
|
|
||||||
raise ValueError("AWS region must be specified either through region parameter or AWS_REGION environment variable")
|
|
||||||
|
|
||||||
# Use region from parameter only if not set in environment
|
|
||||||
if not aws_region:
|
|
||||||
aws_region = region
|
|
||||||
# Set it in environment for other AWS clients
|
|
||||||
os.environ['AWS_REGION'] = region
|
|
||||||
|
|
||||||
logger.info(f"Using AWS region: {aws_region}")
|
|
||||||
|
|
||||||
# Create an STS client in the correct region
|
|
||||||
sts_kwargs = {'endpoint_url': f'https://sts.{aws_region}.amazonaws.com'}
|
|
||||||
if not os.environ.get('AWS_REGION') and not os.environ.get('AWS_DEFAULT_REGION'):
|
|
||||||
sts_kwargs['region_name'] = aws_region
|
|
||||||
sts = boto3.client('sts', **sts_kwargs)
|
|
||||||
|
|
||||||
# Read the web identity token
|
|
||||||
token_file = os.environ.get('AWS_WEB_IDENTITY_TOKEN_FILE')
|
|
||||||
role_arn = os.environ.get('AWS_ROLE_ARN')
|
|
||||||
|
|
||||||
if not token_file or not role_arn:
|
|
||||||
raise ValueError("AWS_WEB_IDENTITY_TOKEN_FILE and AWS_ROLE_ARN must be set")
|
|
||||||
|
|
||||||
with open(token_file, 'r') as f:
|
|
||||||
token = f.read().strip()
|
|
||||||
|
|
||||||
logger.info("Successfully read web identity token")
|
|
||||||
logger.info(f"Using role ARN: {role_arn}")
|
|
||||||
|
|
||||||
# Assume role with web identity using regional endpoint
|
|
||||||
try:
|
|
||||||
response = sts.assume_role_with_web_identity(
|
|
||||||
RoleArn=role_arn,
|
|
||||||
RoleSessionName=os.environ.get('AWS_ROLE_SESSION_NAME', 'WebIdentitySession'),
|
|
||||||
WebIdentityToken=token
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the temporary credentials
|
|
||||||
credentials = response['Credentials']
|
|
||||||
|
|
||||||
# Create the S3 client with the temporary credentials
|
|
||||||
s3_kwargs = {
|
|
||||||
'aws_access_key_id': credentials['AccessKeyId'],
|
|
||||||
'aws_secret_access_key': credentials['SecretAccessKey'],
|
|
||||||
'aws_session_token': credentials['SessionToken']
|
|
||||||
}
|
|
||||||
# Only set region_name if not already in environment
|
|
||||||
if not os.environ.get('AWS_REGION') and not os.environ.get('AWS_DEFAULT_REGION'):
|
|
||||||
s3_kwargs['region_name'] = aws_region
|
|
||||||
# Add any additional kwargs
|
|
||||||
s3_kwargs.update(client_kwargs)
|
|
||||||
client = boto3.client('s3', **s3_kwargs)
|
|
||||||
|
|
||||||
logger.info(f"Successfully assumed role with web identity: {response['AssumedRoleUser']['Arn']}")
|
|
||||||
|
|
||||||
# Test the credentials
|
|
||||||
try:
|
|
||||||
# Try to get caller identity first
|
|
||||||
sts_test = boto3.client(
|
|
||||||
'sts',
|
|
||||||
region_name=aws_region,
|
|
||||||
aws_access_key_id=credentials['AccessKeyId'],
|
|
||||||
aws_secret_access_key=credentials['SecretAccessKey'],
|
|
||||||
aws_session_token=credentials['SessionToken']
|
|
||||||
)
|
|
||||||
identity = sts_test.get_caller_identity()
|
|
||||||
logger.info(f"Successfully verified credentials as: {identity['Arn']}")
|
|
||||||
|
|
||||||
# Then try S3 access
|
|
||||||
bucket_name = os.environ.get('BUCKET_NAME', 'default-bucket')
|
|
||||||
try:
|
|
||||||
client.head_bucket(Bucket=bucket_name)
|
|
||||||
logger.info(f"Successfully verified S3 access to bucket: {bucket_name}")
|
|
||||||
except ClientError as e:
|
|
||||||
error_code = e.response['Error']['Code']
|
|
||||||
if error_code == '404':
|
|
||||||
logger.warning(f"Bucket {bucket_name} does not exist, but credentials work")
|
|
||||||
else:
|
|
||||||
logger.warning(f"S3 access check failed: {error_code} - {e.response['Error']['Message']}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Could not verify credentials: {str(e)}")
|
|
||||||
|
|
||||||
return client
|
|
||||||
|
|
||||||
except ClientError as e:
|
|
||||||
error_code = e.response['Error']['Code']
|
|
||||||
error_message = e.response['Error']['Message']
|
|
||||||
logger.error("Failed to assume role with web identity:")
|
|
||||||
logger.error(f"Error Code: {error_code}")
|
|
||||||
logger.error(f"Error Message: {error_message}")
|
|
||||||
logger.error("Trust policy might need to be updated to allow sts:AssumeRoleWithWebIdentity")
|
|
||||||
logger.error("Current role ARN: " + role_arn)
|
|
||||||
logger.error("Token file path: " + token_file)
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to use current role: {str(e)}")
|
|
||||||
logger.error("Current environment:")
|
|
||||||
for key, value in sorted(os.environ.items()):
|
|
||||||
if any(k in key.lower() for k in ['aws', 'role', 'auth', 'token', 'credential']):
|
|
||||||
logger.error(f" {key}: {value}")
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
# Use default credentials (environment, instance profile, or pod service account)
|
|
||||||
logger.info("Using default credential provider chain")
|
|
||||||
try:
|
|
||||||
client = boto3.client('s3', **client_kwargs)
|
|
||||||
# Try to get caller identity to verify credentials
|
|
||||||
sts = boto3.client('sts')
|
|
||||||
identity = sts.get_caller_identity()
|
|
||||||
logger.info(f"Successfully authenticated as: {identity['Arn']}")
|
|
||||||
return client
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to create S3 client: {str(e)}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def parse_s3_path(s3_path):
|
|
||||||
"""
|
|
||||||
Parse an S3 path into bucket and key
|
|
||||||
"""
|
|
||||||
parts = s3_path.replace('s3://', '').split('/', 1)
|
|
||||||
bucket = parts[0]
|
|
||||||
key = parts[1] if len(parts) > 1 else ''
|
|
||||||
return bucket, key
|
|
||||||
|
|
||||||
def add_common_arguments(parser):
|
|
||||||
"""
|
|
||||||
Add common command-line arguments to an ArgumentParser object
|
|
||||||
"""
|
|
||||||
auth_group = parser.add_mutually_exclusive_group()
|
|
||||||
auth_group.add_argument("--use_role", action="store_true", help="Use IAM role for authentication")
|
|
||||||
auth_group.add_argument("--use_current_role", action="store_true", help="Use current AWS role (e.g. from Kubernetes service account)")
|
|
||||||
parser.add_argument("--role_name", help="The name of the IAM role to assume (only when --use_role is set)")
|
|
||||||
parser.add_argument("--aws_access_key_id", help="AWS access key ID")
|
|
||||||
parser.add_argument("--aws_secret_access_key", help="AWS secret access key")
|
|
||||||
parser.add_argument("--endpoint_url", help="S3-compatible endpoint URL")
|
|
||||||
parser.add_argument("--region", help="AWS region (ignored if endpoint_url is specified)")
|
|
||||||
|
|
||||||
def validate_args(args, parser):
|
|
||||||
"""
|
|
||||||
Validate command-line arguments
|
|
||||||
"""
|
|
||||||
if args.destination.startswith('s3://'):
|
|
||||||
# Check for conflicting auth methods
|
|
||||||
if args.use_role and not args.role_name:
|
|
||||||
parser.error("--role_name is required when using --use_role")
|
|
||||||
|
|
||||||
if args.role_name and not args.use_role:
|
|
||||||
parser.error("--role_name can only be used with --use_role")
|
|
||||||
|
|
||||||
if args.use_current_role and (args.aws_access_key_id or args.aws_secret_access_key):
|
|
||||||
parser.error("When using current role (--use_current_role), access key and secret should not be specified")
|
|
||||||
|
|
||||||
# If using explicit credentials, require both key and secret
|
|
||||||
if (args.aws_access_key_id or args.aws_secret_access_key) and not (args.aws_access_key_id and args.aws_secret_access_key):
|
|
||||||
parser.error("Both --aws_access_key_id and --aws_secret_access_key must be provided when using access key authentication")
|
|
||||||
@@ -1,98 +1,129 @@
|
|||||||
module github.com/lukaszraczylo/kubernetes-images-sync-operator
|
module github.com/lukaszraczylo/kubernetes-images-sync-operator
|
||||||
|
|
||||||
go 1.22.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-logr/logr v1.4.2
|
github.com/aws/aws-sdk-go-v2 v1.41.0
|
||||||
github.com/onsi/ginkgo/v2 v2.19.0
|
github.com/aws/aws-sdk-go-v2/config v1.32.6
|
||||||
github.com/onsi/gomega v1.33.1
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
|
||||||
k8s.io/api v0.31.0
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0
|
||||||
k8s.io/apimachinery v0.31.0
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5
|
||||||
k8s.io/client-go v0.31.0
|
github.com/go-logr/logr v1.4.3
|
||||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
github.com/onsi/ginkgo/v2 v2.27.3
|
||||||
sigs.k8s.io/controller-runtime v0.19.0
|
github.com/onsi/gomega v1.38.2
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
k8s.io/api v0.35.0
|
||||||
|
k8s.io/apimachinery v0.35.0
|
||||||
|
k8s.io/client-go v0.35.0
|
||||||
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
|
||||||
|
sigs.k8s.io/controller-runtime v0.22.4
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
cel.dev/expr v0.25.1 // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||||
|
github.com/aws/smithy-go v1.24.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-logr/zapr v1.3.0 // indirect
|
github.com/go-logr/zapr v1.3.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||||
github.com/go-openapi/swag v0.22.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/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/google/cel-go v0.26.1 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/google/gnostic-models v0.7.1 // indirect
|
||||||
github.com/google/cel-go v0.20.1 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/google/gnostic-models v0.6.8 // indirect
|
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
|
||||||
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||||
github.com/imdario/mergo v0.3.6 // indirect
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.55.0 // indirect
|
github.com/prometheus/common v0.67.4 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/spf13/cobra v1.8.1 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 // 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/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.26.0 // indirect
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/oauth2 v0.21.0 // indirect
|
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/mod v0.31.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/term v0.27.0 // indirect
|
golang.org/x/oauth2 v0.34.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
golang.org/x/term v0.38.0 // indirect
|
||||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
golang.org/x/tools v0.40.0 // indirect
|
||||||
google.golang.org/grpc v1.65.0 // indirect
|
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // 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/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
k8s.io/apiextensions-apiserver v0.31.0 // indirect
|
k8s.io/apiextensions-apiserver v0.35.0 // indirect
|
||||||
k8s.io/apiserver v0.31.0 // indirect
|
k8s.io/apiserver v0.35.0 // indirect
|
||||||
k8s.io/component-base v0.31.0 // indirect
|
k8s.io/component-base v0.35.0 // indirect
|
||||||
k8s.io/klog/v2 v2.130.1 // indirect
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
|
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
|
||||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,251 +1,314 @@
|
|||||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||||
|
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||||
|
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
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/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 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
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 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||||
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
|
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||||
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
|
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
|
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
|
||||||
|
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||||
|
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
|
||||||
|
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
|
||||||
|
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
|
||||||
|
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
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 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
|
||||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
|
||||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
|
||||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
|
||||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
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 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
|
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||||
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
|
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
|
||||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM=
|
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f h1:HU1RgM6NALf/KW9HEY6zry3ADbDKcmpQ+hJedoNGQYQ=
|
||||||
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f/go.mod h1:67FPmZWbr+KDT/VlpWtw6sO9XSjpJmLuHpoLmWiTGgY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
|
||||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||||
|
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||||
|
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
|
||||||
|
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
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.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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
|
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
|
||||||
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
|
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||||
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
|
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||||
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/stretchr/testify v1.7.1/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/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
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 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
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/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
|
||||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
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 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
|
||||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
|
||||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
|
||||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
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 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/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-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
|
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
||||||
k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
|
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
||||||
k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk=
|
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
|
||||||
k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk=
|
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
|
||||||
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
|
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||||
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||||
k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY=
|
k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4=
|
||||||
k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk=
|
k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds=
|
||||||
k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
|
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
||||||
k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
|
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
||||||
k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs=
|
k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
|
||||||
k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo=
|
k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
|
||||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
|
||||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY=
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec=
|
||||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||||
sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
|
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||||
sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
|
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
|
||||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
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=
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
@@ -13,8 +14,9 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/ptr"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
@@ -28,6 +30,7 @@ type ClusterImageReconciler struct {
|
|||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
MaxParallelJobs int
|
MaxParallelJobs int
|
||||||
ActiveJobs int
|
ActiveJobs int
|
||||||
|
activeJobsMu sync.Mutex // protects ActiveJobs counter
|
||||||
KubeClient *kubernetes.Clientset
|
KubeClient *kubernetes.Clientset
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ func (r *ClusterImageReconciler) Reconcile(ctx context.Context, req ctrl.Request
|
|||||||
if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
|
if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
latest.Status.Progress = shared.STATUS_PENDING
|
latest.Status.Progress = shared.STATUS_PENDING
|
||||||
if err := r.Status().Update(ctx, latest); err != nil {
|
if err := r.Status().Update(ctx, latest); err != nil {
|
||||||
if errors.IsConflict(err) {
|
if errors.IsConflict(err) {
|
||||||
@@ -80,7 +83,10 @@ func (r *ClusterImageReconciler) Reconcile(ctx context.Context, req ctrl.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we've reached the maximum number of parallel jobs, requeue
|
// If we've reached the maximum number of parallel jobs, requeue
|
||||||
if r.ActiveJobs >= r.MaxParallelJobs && clusterImage.Status.Progress == shared.STATUS_PENDING {
|
r.activeJobsMu.Lock()
|
||||||
|
activeJobs := r.ActiveJobs
|
||||||
|
r.activeJobsMu.Unlock()
|
||||||
|
if activeJobs >= r.MaxParallelJobs && clusterImage.Status.Progress == shared.STATUS_PENDING {
|
||||||
return ctrl.Result{RequeueAfter: time.Second * 30}, nil
|
return ctrl.Result{RequeueAfter: time.Second * 30}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +123,7 @@ func (r *ClusterImageReconciler) handlePendingClusterImage(ctx context.Context,
|
|||||||
if err := r.Get(ctx, types.NamespacedName{Name: clusterImage.Name, Namespace: clusterImage.Namespace}, latest); err != nil {
|
if err := r.Get(ctx, types.NamespacedName{Name: clusterImage.Name, Namespace: clusterImage.Namespace}, latest); err != nil {
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
latest.Status.Progress = shared.STATUS_PRESENT
|
latest.Status.Progress = shared.STATUS_PRESENT
|
||||||
if err := r.Status().Update(ctx, latest); err != nil {
|
if err := r.Status().Update(ctx, latest); err != nil {
|
||||||
if errors.IsConflict(err) {
|
if errors.IsConflict(err) {
|
||||||
@@ -151,7 +157,9 @@ func (r *ClusterImageReconciler) handlePendingClusterImage(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Increment the active jobs count
|
// Increment the active jobs count
|
||||||
|
r.activeJobsMu.Lock()
|
||||||
r.ActiveJobs++
|
r.ActiveJobs++
|
||||||
|
r.activeJobsMu.Unlock()
|
||||||
|
|
||||||
return ctrl.Result{Requeue: true}, nil
|
return ctrl.Result{Requeue: true}, nil
|
||||||
}
|
}
|
||||||
@@ -217,7 +225,9 @@ func (r *ClusterImageReconciler) handleRunningClusterImage(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
latest.Status.Progress = shared.STATUS_SUCCESS
|
latest.Status.Progress = shared.STATUS_SUCCESS
|
||||||
|
r.activeJobsMu.Lock()
|
||||||
r.ActiveJobs--
|
r.ActiveJobs--
|
||||||
|
r.activeJobsMu.Unlock()
|
||||||
// Update the status before cleaning up the job
|
// Update the status before cleaning up the job
|
||||||
if err := r.Status().Update(ctx, latest); err != nil {
|
if err := r.Status().Update(ctx, latest); err != nil {
|
||||||
if errors.IsConflict(err) {
|
if errors.IsConflict(err) {
|
||||||
@@ -232,7 +242,9 @@ func (r *ClusterImageReconciler) handleRunningClusterImage(ctx context.Context,
|
|||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
} else if existingJob.Status.Failed > 0 {
|
} else if existingJob.Status.Failed > 0 {
|
||||||
|
r.activeJobsMu.Lock()
|
||||||
r.ActiveJobs--
|
r.ActiveJobs--
|
||||||
|
r.activeJobsMu.Unlock()
|
||||||
if clusterImage.Status.RetryCount < 3 {
|
if clusterImage.Status.RetryCount < 3 {
|
||||||
// Cleanup the failed job before retrying
|
// Cleanup the failed job before retrying
|
||||||
if err := r.cleanupJobAndPods(ctx, existingJob); err != nil {
|
if err := r.cleanupJobAndPods(ctx, existingJob); err != nil {
|
||||||
@@ -300,8 +312,22 @@ func (r *ClusterImageReconciler) handleRunningClusterImage(ctx context.Context,
|
|||||||
return r.updateClusterImageExportStatus(ctx, clusterImage)
|
return r.updateClusterImageExportStatus(ctx, clusterImage)
|
||||||
}
|
}
|
||||||
func (r *ClusterImageReconciler) cleanupJobAndPods(ctx context.Context, job *v1batch.Job) error {
|
func (r *ClusterImageReconciler) cleanupJobAndPods(ctx context.Context, job *v1batch.Job) error {
|
||||||
// Add a short delay to allow status updates to propagate
|
// Wait for job status to propagate before deletion
|
||||||
time.Sleep(2 * time.Second)
|
jobKey := types.NamespacedName{Name: job.Name, Namespace: job.Namespace}
|
||||||
|
err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (done bool, err error) {
|
||||||
|
currentJob := &v1batch.Job{}
|
||||||
|
if err := r.Get(ctx, jobKey, currentJob); err != nil {
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
return true, nil // Job already deleted
|
||||||
|
}
|
||||||
|
return false, nil // Retry on transient errors
|
||||||
|
}
|
||||||
|
// Job status has been updated, proceed with deletion
|
||||||
|
return currentJob.Status.Active == 0, nil
|
||||||
|
})
|
||||||
|
if err != nil && !errors.IsNotFound(err) && err != context.DeadlineExceeded {
|
||||||
|
return fmt.Errorf("failed to wait for job status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the job
|
// Delete the job
|
||||||
if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil && !errors.IsNotFound(err) {
|
if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil && !errors.IsNotFound(err) {
|
||||||
@@ -334,12 +360,12 @@ func (r *ClusterImageReconciler) createBackupJob(ctx context.Context, clusterIma
|
|||||||
if clusterImage.Spec.Storage == shared.STORAGE_S3 {
|
if clusterImage.Spec.Storage == shared.STORAGE_S3 {
|
||||||
s3Params := shared.SetupS3Params(clusterImageExport.Spec.Storage.S3)
|
s3Params := shared.SetupS3Params(clusterImageExport.Spec.Storage.S3)
|
||||||
additionalCommands := []string{
|
additionalCommands := []string{
|
||||||
"./export.py " + strings.Join(s3Params, " ") + " '/tmp/" + normalisedImageName + ".tar' " + "'s3://" + clusterImageExport.Spec.Storage.S3.Bucket + clusterImage.Spec.ExportPath + "/" + clusterImage.Spec.ExportName + "/" + normalisedImageName + ".tar'",
|
"./worker export " + strings.Join(s3Params, " ") + " '/tmp/" + normalisedImageName + ".tar' " + "'s3://" + clusterImageExport.Spec.Storage.S3.Bucket + clusterImage.Spec.ExportPath + "/" + clusterImage.Spec.ExportName + "/" + normalisedImageName + ".tar'",
|
||||||
}
|
}
|
||||||
defaultCommands = append(defaultCommands, additionalCommands...)
|
defaultCommands = append(defaultCommands, additionalCommands...)
|
||||||
} else if clusterImage.Spec.Storage == shared.STORAGE_FILE {
|
} else if clusterImage.Spec.Storage == shared.STORAGE_FILE {
|
||||||
additionalCommands := []string{
|
additionalCommands := []string{
|
||||||
"./export.py /tmp/" + normalisedImageName + ".tar" + " " + clusterImage.Spec.ExportPath + "/" + clusterImage.Spec.ExportName + "/" + normalisedImageName + ".tar",
|
"./worker export '/tmp/" + normalisedImageName + ".tar' '" + clusterImage.Spec.ExportPath + "/" + clusterImage.Spec.ExportName + "/" + normalisedImageName + ".tar'",
|
||||||
}
|
}
|
||||||
defaultCommands = append(defaultCommands, additionalCommands...)
|
defaultCommands = append(defaultCommands, additionalCommands...)
|
||||||
}
|
}
|
||||||
@@ -374,8 +400,8 @@ func (r *ClusterImageReconciler) createBackupJob(ctx context.Context, clusterIma
|
|||||||
Kind: clusterImage.Kind,
|
Kind: clusterImage.Kind,
|
||||||
Name: clusterImage.Name,
|
Name: clusterImage.Name,
|
||||||
UID: clusterImage.UID,
|
UID: clusterImage.UID,
|
||||||
BlockOwnerDeletion: pointer.Bool(true),
|
BlockOwnerDeletion: ptr.To(true),
|
||||||
Controller: pointer.Bool(true),
|
Controller: ptr.To(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,38 +33,73 @@ import (
|
|||||||
var _ = Describe("ClusterImage Controller", func() {
|
var _ = Describe("ClusterImage Controller", func() {
|
||||||
Context("When reconciling a resource", func() {
|
Context("When reconciling a resource", func() {
|
||||||
const resourceName = "test-resource"
|
const resourceName = "test-resource"
|
||||||
|
const exportName = "test-export"
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
typeNamespacedName := types.NamespacedName{
|
typeNamespacedName := types.NamespacedName{
|
||||||
Name: resourceName,
|
Name: resourceName,
|
||||||
Namespace: "default", // TODO(user):Modify as needed
|
Namespace: "default",
|
||||||
|
}
|
||||||
|
exportNamespacedName := types.NamespacedName{
|
||||||
|
Name: exportName,
|
||||||
|
Namespace: "default",
|
||||||
}
|
}
|
||||||
clusterimage := &raczylocomv1.ClusterImage{}
|
clusterimage := &raczylocomv1.ClusterImage{}
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
|
By("creating the ClusterImageExport that the ClusterImage references")
|
||||||
|
export := &raczylocomv1.ClusterImageExport{}
|
||||||
|
err := k8sClient.Get(ctx, exportNamespacedName, export)
|
||||||
|
if err != nil && errors.IsNotFound(err) {
|
||||||
|
exportResource := &raczylocomv1.ClusterImageExport{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: exportName,
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: raczylocomv1.ClusterImageExportSpec{
|
||||||
|
Name: exportName,
|
||||||
|
BasePath: "/backups/test",
|
||||||
|
MaxConcurrentJobs: 1,
|
||||||
|
Storage: raczylocomv1.ClusterImageStorageSpec{
|
||||||
|
StorageTarget: "FILE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(k8sClient.Create(ctx, exportResource)).To(Succeed())
|
||||||
|
}
|
||||||
|
|
||||||
By("creating the custom resource for the Kind ClusterImage")
|
By("creating the custom resource for the Kind ClusterImage")
|
||||||
err := k8sClient.Get(ctx, typeNamespacedName, clusterimage)
|
err = k8sClient.Get(ctx, typeNamespacedName, clusterimage)
|
||||||
if err != nil && errors.IsNotFound(err) {
|
if err != nil && errors.IsNotFound(err) {
|
||||||
resource := &raczylocomv1.ClusterImage{
|
resource := &raczylocomv1.ClusterImage{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: resourceName,
|
Name: resourceName,
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
},
|
},
|
||||||
// TODO(user): Specify other spec details if needed.
|
Spec: raczylocomv1.ClusterImageSpec{
|
||||||
|
ExportName: exportName,
|
||||||
|
Image: "nginx:latest",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
AfterEach(func() {
|
AfterEach(func() {
|
||||||
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
By("Cleanup the specific resource instance ClusterImage")
|
||||||
resource := &raczylocomv1.ClusterImage{}
|
resource := &raczylocomv1.ClusterImage{}
|
||||||
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
if err == nil {
|
||||||
|
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||||
|
}
|
||||||
|
|
||||||
By("Cleanup the specific resource instance ClusterImage")
|
By("Cleanup the ClusterImageExport")
|
||||||
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
export := &raczylocomv1.ClusterImageExport{}
|
||||||
|
err = k8sClient.Get(ctx, exportNamespacedName, export)
|
||||||
|
if err == nil {
|
||||||
|
Expect(k8sClient.Delete(ctx, export)).To(Succeed())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
It("should successfully reconcile the resource", func() {
|
It("should successfully reconcile the resource", func() {
|
||||||
By("Reconciling the created resource")
|
By("Reconciling the created resource")
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package raczylocom
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5" // #nosec G501 - MD5 used for non-cryptographic unique identifiers only
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
@@ -14,7 +15,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/client-go/util/retry"
|
"k8s.io/client-go/util/retry"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/ptr"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
@@ -27,7 +28,7 @@ import (
|
|||||||
// ClusterImageExportReconciler reconciles a ClusterImageExport object
|
// ClusterImageExportReconciler reconciles a ClusterImageExport object
|
||||||
type ClusterImageExportReconciler struct {
|
type ClusterImageExportReconciler struct {
|
||||||
client.Client
|
client.Client
|
||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
podAnnotations map[string]string
|
podAnnotations map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +62,19 @@ func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.R
|
|||||||
return r.handleDeletion(ctx, clusterImageExport)
|
return r.handleDeletion(ctx, clusterImageExport)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this export should be deleted by TTL
|
||||||
|
if r.shouldDeleteByTTL(clusterImageExport) {
|
||||||
|
l.Info("Deleting export due to TTL expiration",
|
||||||
|
"export", clusterImageExport.Name,
|
||||||
|
"ttlDays", *clusterImageExport.Spec.TTLDaysAfterFinished,
|
||||||
|
"completedAt", clusterImageExport.Status.CompletedAt)
|
||||||
|
if err := r.Delete(ctx, clusterImageExport); err != nil && !errors.IsNotFound(err) {
|
||||||
|
l.Error(err, "Failed to delete export by TTL")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Add finalizer and creation timestamp annotation if they don't exist
|
// Add finalizer and creation timestamp annotation if they don't exist
|
||||||
needsUpdate := false
|
needsUpdate := false
|
||||||
if !controllerutil.ContainsFinalizer(clusterImageExport, clusterImageExportFinalizer) {
|
if !controllerutil.ContainsFinalizer(clusterImageExport, clusterImageExportFinalizer) {
|
||||||
@@ -121,6 +135,7 @@ func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.R
|
|||||||
|
|
||||||
for _, image := range fullImagesList.Containers {
|
for _, image := range fullImagesList.Containers {
|
||||||
// Include creation timestamp in the hash to differentiate between exports with the same name
|
// Include creation timestamp in the hash to differentiate between exports with the same name
|
||||||
|
// #nosec G401 - MD5 used for non-cryptographic unique identifier generation, not security
|
||||||
nameHash := fmt.Sprintf("%x", md5.Sum([]byte(clusterImageExport.Name+image.Image+image.Tag+image.Sha+
|
nameHash := fmt.Sprintf("%x", md5.Sum([]byte(clusterImageExport.Name+image.Image+image.Tag+image.Sha+
|
||||||
clusterImageExport.Annotations["export.raczylo.com/creation-timestamp"])))[:14]
|
clusterImageExport.Annotations["export.raczylo.com/creation-timestamp"])))[:14]
|
||||||
|
|
||||||
@@ -156,7 +171,7 @@ func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.R
|
|||||||
Kind: clusterImageExport.Kind,
|
Kind: clusterImageExport.Kind,
|
||||||
Name: clusterImageExport.Name,
|
Name: clusterImageExport.Name,
|
||||||
UID: clusterImageExport.UID,
|
UID: clusterImageExport.UID,
|
||||||
Controller: pointer.Bool(true),
|
Controller: ptr.To(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -185,7 +200,7 @@ func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.R
|
|||||||
failedCount := 0
|
failedCount := 0
|
||||||
pendingCount := 0
|
pendingCount := 0
|
||||||
clusterImageList := &raczylocomv1.ClusterImageList{}
|
clusterImageList := &raczylocomv1.ClusterImageList{}
|
||||||
if err := r.List(ctx, clusterImageList, client.InNamespace(clusterImageExport.Namespace),
|
if err := r.List(ctx, clusterImageList, client.InNamespace(clusterImageExport.Namespace),
|
||||||
client.MatchingFields{"spec.exportName": clusterImageExport.Name}); err != nil {
|
client.MatchingFields{"spec.exportName": clusterImageExport.Name}); err != nil {
|
||||||
l.Error(err, "unable to list ClusterImages")
|
l.Error(err, "unable to list ClusterImages")
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
@@ -213,6 +228,11 @@ func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.R
|
|||||||
} else {
|
} else {
|
||||||
export.Status.Progress = shared.STATUS_SUCCESS
|
export.Status.Progress = shared.STATUS_SUCCESS
|
||||||
}
|
}
|
||||||
|
// Set CompletedAt timestamp when export completes
|
||||||
|
if export.Status.CompletedAt == nil {
|
||||||
|
now := metav1.Now()
|
||||||
|
export.Status.CompletedAt = &now
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -220,6 +240,15 @@ func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.R
|
|||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If export is complete, run retention cleanup
|
||||||
|
if clusterImageExport.Status.Progress == shared.STATUS_SUCCESS ||
|
||||||
|
clusterImageExport.Status.Progress == shared.STATUS_FAILED {
|
||||||
|
if err := r.cleanupByRetention(ctx, clusterImageExport); err != nil {
|
||||||
|
l.Error(err, "Failed to cleanup by retention policy")
|
||||||
|
// Don't return error - this is non-critical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If there are still pending images, requeue
|
// If there are still pending images, requeue
|
||||||
if pendingCount > 0 {
|
if pendingCount > 0 {
|
||||||
return ctrl.Result{Requeue: true}, nil
|
return ctrl.Result{Requeue: true}, nil
|
||||||
@@ -250,21 +279,6 @@ func (r *ClusterImageExportReconciler) updateStatusWithRetry(ctx context.Context
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClusterImageExportReconciler) checkAllClusterImagesCompleted(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) (bool, error) {
|
|
||||||
clusterImageList := &raczylocomv1.ClusterImageList{}
|
|
||||||
if err := r.List(ctx, clusterImageList, client.InNamespace(clusterImageExport.Namespace), client.MatchingFields{"spec.exportName": clusterImageExport.Name}); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ci := range clusterImageList.Items {
|
|
||||||
if ci.Status.Progress != shared.STATUS_SUCCESS && ci.Status.Progress != shared.STATUS_PRESENT {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupWithManager sets up the controller with the Manager.
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
|
||||||
func (r *ClusterImageExportReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
func (r *ClusterImageExportReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
@@ -398,18 +412,18 @@ func (r *ClusterImageExportReconciler) runCleanupJob(ctx context.Context, cluste
|
|||||||
if clusterImageExport.Spec.Storage.StorageTarget == shared.STORAGE_S3 {
|
if clusterImageExport.Spec.Storage.StorageTarget == shared.STORAGE_S3 {
|
||||||
s3Params := shared.SetupS3Params(clusterImageExport.Spec.Storage.S3)
|
s3Params := shared.SetupS3Params(clusterImageExport.Spec.Storage.S3)
|
||||||
additionalCommands := []string{
|
additionalCommands := []string{
|
||||||
"./cleanup.py " + strings.Join(s3Params, " ") + " 's3://" + clusterImageExport.Spec.Storage.S3.Bucket + clusterImageExport.Spec.BasePath + "/" + clusterImageExport.ObjectMeta.Name + "/'",
|
"./worker cleanup " + strings.Join(s3Params, " ") + " 's3://" + clusterImageExport.Spec.Storage.S3.Bucket + clusterImageExport.Spec.BasePath + "/" + clusterImageExport.ObjectMeta.Name + "/'",
|
||||||
}
|
}
|
||||||
defaultCommands = append(defaultCommands, additionalCommands...)
|
defaultCommands = append(defaultCommands, additionalCommands...)
|
||||||
} else if clusterImageExport.Spec.Storage.StorageTarget == shared.STORAGE_FILE {
|
} else if clusterImageExport.Spec.Storage.StorageTarget == shared.STORAGE_FILE {
|
||||||
additionalCommands := []string{
|
additionalCommands := []string{
|
||||||
"./cleanup.py" + "'" + clusterImageExport.Spec.BasePath + "/" + clusterImageExport.ObjectMeta.Name + "/'",
|
"./worker cleanup '" + clusterImageExport.Spec.BasePath + "/" + clusterImageExport.ObjectMeta.Name + "/'",
|
||||||
}
|
}
|
||||||
defaultCommands = append(defaultCommands, additionalCommands...)
|
defaultCommands = append(defaultCommands, additionalCommands...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the cleanup job with retry limits and TTL
|
// Set up the cleanup job with retry limits and TTL
|
||||||
backoffLimit := int32(2) // 3 total attempts (initial + 2 retries)
|
backoffLimit := int32(2) // 3 total attempts (initial + 2 retries)
|
||||||
ttlSecondsAfterFinished := int32(300) // Delete job 5 minutes after completion
|
ttlSecondsAfterFinished := int32(300) // Delete job 5 minutes after completion
|
||||||
|
|
||||||
// Merge annotations from different sources
|
// Merge annotations from different sources
|
||||||
@@ -443,16 +457,16 @@ func (r *ClusterImageExportReconciler) runCleanupJob(ctx context.Context, cluste
|
|||||||
}
|
}
|
||||||
|
|
||||||
jobParams := shared.JobParams{
|
jobParams := shared.JobParams{
|
||||||
Name: normalisedImageName,
|
Name: normalisedImageName,
|
||||||
Namespace: clusterImageExport.Namespace,
|
Namespace: clusterImageExport.Namespace,
|
||||||
Image: shared.BACKUP_JOB_IMAGE,
|
Image: shared.BACKUP_JOB_IMAGE,
|
||||||
Commands: defaultCommands,
|
Commands: defaultCommands,
|
||||||
Annotations: mergedAnnotations,
|
Annotations: mergedAnnotations,
|
||||||
ServiceAccount: "",
|
ServiceAccount: "",
|
||||||
ImagePullSecrets: clusterImageExport.Spec.ImagePullSecrets,
|
ImagePullSecrets: clusterImageExport.Spec.ImagePullSecrets,
|
||||||
BackoffLimit: &backoffLimit,
|
BackoffLimit: &backoffLimit,
|
||||||
TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
|
TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
|
||||||
EnvVars: envVars,
|
EnvVars: envVars,
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupJob := shared.CreateJob(jobParams, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
cleanupJob := shared.CreateJob(jobParams, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||||
@@ -466,3 +480,110 @@ func (r *ClusterImageExportReconciler) runCleanupJob(ctx context.Context, cluste
|
|||||||
l.Info("Created cleanup job with retry limit and TTL")
|
l.Info("Created cleanup job with retry limit and TTL")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shouldDeleteByTTL checks if the export should be deleted based on TTL (in days)
|
||||||
|
func (r *ClusterImageExportReconciler) shouldDeleteByTTL(clusterImageExport *raczylocomv1.ClusterImageExport) bool {
|
||||||
|
// Only apply TTL to completed exports
|
||||||
|
if clusterImageExport.Status.Progress != shared.STATUS_SUCCESS &&
|
||||||
|
clusterImageExport.Status.Progress != shared.STATUS_FAILED {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if TTL is configured
|
||||||
|
if clusterImageExport.Spec.TTLDaysAfterFinished == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if CompletedAt is set
|
||||||
|
if clusterImageExport.Status.CompletedAt == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert days to duration (24 hours per day)
|
||||||
|
ttlDuration := time.Duration(*clusterImageExport.Spec.TTLDaysAfterFinished) * 24 * time.Hour
|
||||||
|
expirationTime := clusterImageExport.Status.CompletedAt.Add(ttlDuration)
|
||||||
|
|
||||||
|
return time.Now().After(expirationTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupByRetention enforces the retention policy for completed exports
|
||||||
|
func (r *ClusterImageExportReconciler) cleanupByRetention(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) error {
|
||||||
|
l := log.FromContext(ctx)
|
||||||
|
|
||||||
|
// Check if retention policy is configured
|
||||||
|
if clusterImageExport.Spec.Retention == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all ClusterImageExports in the same namespace
|
||||||
|
exportList := &raczylocomv1.ClusterImageExportList{}
|
||||||
|
if err := r.List(ctx, exportList, client.InNamespace(clusterImageExport.Namespace)); err != nil {
|
||||||
|
return fmt.Errorf("failed to list ClusterImageExports: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate successful and failed exports, sorted by completion time
|
||||||
|
var successfulExports, failedExports []*raczylocomv1.ClusterImageExport
|
||||||
|
for i := range exportList.Items {
|
||||||
|
export := &exportList.Items[i]
|
||||||
|
// Skip exports that don't have the same base path (different backup sets)
|
||||||
|
if export.Spec.BasePath != clusterImageExport.Spec.BasePath {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Skip exports that are still running
|
||||||
|
if export.Status.Progress != shared.STATUS_SUCCESS &&
|
||||||
|
export.Status.Progress != shared.STATUS_FAILED {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if export.Status.Progress == shared.STATUS_SUCCESS {
|
||||||
|
successfulExports = append(successfulExports, export)
|
||||||
|
} else if export.Status.Progress == shared.STATUS_FAILED {
|
||||||
|
failedExports = append(failedExports, export)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by CompletedAt (newest first)
|
||||||
|
sortByCompletionTime := func(exports []*raczylocomv1.ClusterImageExport) {
|
||||||
|
for i := 0; i < len(exports); i++ {
|
||||||
|
for j := i + 1; j < len(exports); j++ {
|
||||||
|
iTime := exports[i].Status.CompletedAt
|
||||||
|
jTime := exports[j].Status.CompletedAt
|
||||||
|
if iTime == nil || (jTime != nil && jTime.After(iTime.Time)) {
|
||||||
|
exports[i], exports[j] = exports[j], exports[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByCompletionTime(successfulExports)
|
||||||
|
sortByCompletionTime(failedExports)
|
||||||
|
|
||||||
|
// Delete excess successful exports
|
||||||
|
if clusterImageExport.Spec.Retention.MaxSuccessful != nil {
|
||||||
|
maxSuccessful := int(*clusterImageExport.Spec.Retention.MaxSuccessful)
|
||||||
|
if len(successfulExports) > maxSuccessful {
|
||||||
|
for _, export := range successfulExports[maxSuccessful:] {
|
||||||
|
l.Info("Deleting export due to retention policy (maxSuccessful exceeded)",
|
||||||
|
"export", export.Name, "maxSuccessful", maxSuccessful)
|
||||||
|
if err := r.Delete(ctx, export); err != nil && !errors.IsNotFound(err) {
|
||||||
|
l.Error(err, "Failed to delete export for retention", "export", export.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete excess failed exports
|
||||||
|
if clusterImageExport.Spec.Retention.MaxFailed != nil {
|
||||||
|
maxFailed := int(*clusterImageExport.Spec.Retention.MaxFailed)
|
||||||
|
if len(failedExports) > maxFailed {
|
||||||
|
for _, export := range failedExports[maxFailed:] {
|
||||||
|
l.Info("Deleting export due to retention policy (maxFailed exceeded)",
|
||||||
|
"export", export.Name, "maxFailed", maxFailed)
|
||||||
|
if err := r.Delete(ctx, export); err != nil && !errors.IsNotFound(err) {
|
||||||
|
l.Error(err, "Failed to delete export for retention", "export", export.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,20 +51,26 @@ var _ = Describe("ClusterImageExport Controller", func() {
|
|||||||
Name: resourceName,
|
Name: resourceName,
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
},
|
},
|
||||||
// TODO(user): Specify other spec details if needed.
|
Spec: raczylocomv1.ClusterImageExportSpec{
|
||||||
|
Name: resourceName,
|
||||||
|
BasePath: "/backups/test",
|
||||||
|
MaxConcurrentJobs: 1,
|
||||||
|
Storage: raczylocomv1.ClusterImageStorageSpec{
|
||||||
|
StorageTarget: "FILE",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
AfterEach(func() {
|
AfterEach(func() {
|
||||||
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
By("Cleanup the specific resource instance ClusterImageExport")
|
||||||
resource := &raczylocomv1.ClusterImageExport{}
|
resource := &raczylocomv1.ClusterImageExport{}
|
||||||
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
if err == nil {
|
||||||
|
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||||
By("Cleanup the specific resource instance ClusterImageExport")
|
}
|
||||||
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
|
||||||
})
|
})
|
||||||
It("should successfully reconcile the resource", func() {
|
It("should successfully reconcile the resource", func() {
|
||||||
By("Reconciling the created resource")
|
By("Reconciling the created resource")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ package raczylocom
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -28,6 +29,8 @@ import (
|
|||||||
|
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
@@ -58,17 +61,18 @@ var _ = BeforeSuite(func() {
|
|||||||
ctx, cancel = context.WithCancel(context.TODO())
|
ctx, cancel = context.WithCancel(context.TODO())
|
||||||
|
|
||||||
By("bootstrapping test environment")
|
By("bootstrapping test environment")
|
||||||
|
|
||||||
|
// Use KUBEBUILDER_ASSETS if set (CI), otherwise fall back to local path
|
||||||
|
binaryAssetsDir := os.Getenv("KUBEBUILDER_ASSETS")
|
||||||
|
if binaryAssetsDir == "" {
|
||||||
|
binaryAssetsDir = filepath.Join("..", "..", "..", "bin", "k8s",
|
||||||
|
fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH))
|
||||||
|
}
|
||||||
|
|
||||||
testEnv = &envtest.Environment{
|
testEnv = &envtest.Environment{
|
||||||
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
|
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
|
||||||
ErrorIfCRDPathMissing: true,
|
ErrorIfCRDPathMissing: true,
|
||||||
|
BinaryAssetsDirectory: binaryAssetsDir,
|
||||||
// The BinaryAssetsDirectory is only required if you want to run the tests directly
|
|
||||||
// without call the makefile target test. If not informed it will look for the
|
|
||||||
// default path defined in controller-runtime which is /usr/local/kubebuilder/.
|
|
||||||
// Note that you must have the required binaries setup under the bin directory to perform
|
|
||||||
// the tests directly. When we run make test it will be setup and used automatically.
|
|
||||||
BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s",
|
|
||||||
fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@@ -82,8 +86,38 @@ var _ = BeforeSuite(func() {
|
|||||||
|
|
||||||
// +kubebuilder:scaffold:scheme
|
// +kubebuilder:scaffold:scheme
|
||||||
|
|
||||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
// Create a manager to get a cache-backed client that supports field selectors
|
||||||
|
// Explicitly configure cache to watch ClusterImage and ClusterImageExport resources
|
||||||
|
// This is required for field selectors to work in tests (without registering controllers)
|
||||||
|
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
|
||||||
|
Scheme: scheme.Scheme,
|
||||||
|
Cache: cache.Options{
|
||||||
|
ByObject: map[client.Object]cache.ByObject{
|
||||||
|
&raczylocomv1.ClusterImage{}: {},
|
||||||
|
&raczylocomv1.ClusterImageExport{}: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Add field index for spec.exportName on ClusterImage
|
||||||
|
err = mgr.GetFieldIndexer().IndexField(ctx, &raczylocomv1.ClusterImage{}, "spec.exportName", func(obj client.Object) []string {
|
||||||
|
clusterImage := obj.(*raczylocomv1.ClusterImage)
|
||||||
|
return []string{clusterImage.Spec.ExportName}
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Start the manager cache in background
|
||||||
|
go func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
err := mgr.Start(ctx)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for cache to sync
|
||||||
|
Expect(mgr.GetCache().WaitForCacheSync(ctx)).To(BeTrue())
|
||||||
|
|
||||||
|
k8sClient = mgr.GetClient()
|
||||||
Expect(k8sClient).NotTo(BeNil())
|
Expect(k8sClient).NotTo(BeNil())
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
var BACKUP_JOB_IMAGE string
|
var BACKUP_JOB_IMAGE string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
BACKUP_JOB_IMAGE = os.Getenv("WORKER_IMAGE")
|
BACKUP_JOB_IMAGE = os.Getenv("WORKER_IMAGE")
|
||||||
if BACKUP_JOB_IMAGE == "" {
|
if BACKUP_JOB_IMAGE == "" {
|
||||||
BACKUP_JOB_IMAGE = "ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:latest" // fallback
|
BACKUP_JOB_IMAGE = "ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:0.5.54" // fallback to known stable version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -0,0 +1,643 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestScenario represents a test scenario classification
|
||||||
|
type TestScenario string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScenarioGood TestScenario = "good"
|
||||||
|
ScenarioNotGood TestScenario = "not_good"
|
||||||
|
ScenarioReallyBad TestScenario = "really_bad"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefinitionsTestSuite tests the definitions and utility functions
|
||||||
|
type DefinitionsTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefinitionsTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(DefinitionsTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNormalizeImageName tests the NormalizeImageName function with matrix strategy
|
||||||
|
func (s *DefinitionsTestSuite) TestNormalizeImageName() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
scenario TestScenario
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
// Good scenarios - standard image names
|
||||||
|
{
|
||||||
|
name: "simple image name",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: "nginx",
|
||||||
|
expected: "nginx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image with tag",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: "nginx:latest",
|
||||||
|
expected: "nginx-latest",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image with version tag",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: "nginx:1.21.0",
|
||||||
|
expected: "nginx-1.21.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full registry path",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: "quay.io/cilium/cilium:v1.18.4",
|
||||||
|
expected: "quay.io-cilium-cilium-v1.18.4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ghcr registry",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: "ghcr.io/owner/repo:v1.0.0",
|
||||||
|
expected: "ghcr.io-owner-repo-v1.0.0",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Not good scenarios - unusual but valid formats
|
||||||
|
{
|
||||||
|
name: "image with SHA digest",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
input: "nginx@sha256:abc123def456",
|
||||||
|
expected: "nginx-sha256-abc123def456",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image with tag and SHA",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
input: "quay.io/cilium/cilium:v1.18.4@sha256:49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f",
|
||||||
|
expected: "quay.io-cilium-cilium-v1.18.4-sha256-49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple colons in path",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
input: "registry:5000/image:tag",
|
||||||
|
expected: "registry-5000-image-tag",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Really bad scenarios - edge cases and potential problems
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only special characters",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ":///@",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple consecutive special chars",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: "image:::tag",
|
||||||
|
expected: "image-tag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leading special characters",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: "//image:tag",
|
||||||
|
expected: "image-tag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trailing special characters",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: "image:tag//",
|
||||||
|
expected: "image-tag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spaces in name",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: "image name:tag",
|
||||||
|
expected: "image-name-tag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode characters",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: "image:tag-日本語",
|
||||||
|
expected: "image-tag-日本語",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "very long image name",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: "registry.example.com/very/long/path/to/image:tag@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||||
|
expected: "registry.example.com-very-long-path-to-image-tag-sha256-abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query string characters",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: "image?foo=bar&baz=qux",
|
||||||
|
expected: "image-foo-bar-baz-qux",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "brackets and special chars",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: "image[tag]{version}",
|
||||||
|
expected: "image-tag-version",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.Run(tc.name, func() {
|
||||||
|
result := NormalizeImageName(tc.input)
|
||||||
|
assert.Equal(s.T(), tc.expected, result, "Scenario: %s", tc.scenario)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRemoveDuplicates tests duplicate removal functionality
|
||||||
|
func (s *DefinitionsTestSuite) TestRemoveDuplicates() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
scenario TestScenario
|
||||||
|
input ContainersList
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
// Good scenarios
|
||||||
|
{
|
||||||
|
name: "no duplicates",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||||
|
{Image: "redis", Tag: "6", FullName: "redis:6"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with duplicates",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||||
|
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||||
|
{Image: "redis", Tag: "6", FullName: "redis:6"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Not good scenarios
|
||||||
|
{
|
||||||
|
name: "same image different tags",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||||
|
{Image: "nginx", Tag: "1.21", FullName: "nginx:1.21"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: 2, // Should keep both
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same image with and without SHA",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||||
|
{Image: "nginx", Tag: "latest", Sha: "sha256:abc", FullName: "nginx:latest@sha256:abc"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: 2, // Different because of SHA
|
||||||
|
},
|
||||||
|
|
||||||
|
// Really bad scenarios
|
||||||
|
{
|
||||||
|
name: "empty list",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{Containers: []Container{}},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all duplicates",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||||
|
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||||
|
{Image: "nginx", Tag: "latest", FullName: "nginx:latest"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil containers",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{Containers: nil},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.Run(tc.name, func() {
|
||||||
|
result := RemoveDuplicates(tc.input)
|
||||||
|
assert.Len(s.T(), result.Containers, tc.expected, "Scenario: %s", tc.scenario)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRemoveExcludedImages tests image exclusion functionality
|
||||||
|
func (s *DefinitionsTestSuite) TestRemoveExcludedImages() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
scenario TestScenario
|
||||||
|
input ContainersList
|
||||||
|
excludes []string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
// Good scenarios
|
||||||
|
{
|
||||||
|
name: "no exclusions",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest"},
|
||||||
|
{Image: "redis", Tag: "6"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
excludes: []string{},
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exclude one image",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest"},
|
||||||
|
{Image: "redis", Tag: "6"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
excludes: []string{"nginx"},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exclude by registry",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "gcr.io/google/nginx", Tag: "latest"},
|
||||||
|
{Image: "docker.io/library/redis", Tag: "6"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
excludes: []string{"gcr.io"},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Not good scenarios
|
||||||
|
{
|
||||||
|
name: "case insensitive exclusion",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "NGINX", Tag: "latest"},
|
||||||
|
{Image: "Redis", Tag: "6"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
excludes: []string{"nginx"},
|
||||||
|
expected: 1, // Should exclude NGINX
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partial match exclusion",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "my-nginx-custom", Tag: "latest"},
|
||||||
|
{Image: "redis", Tag: "6"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
excludes: []string{"nginx"},
|
||||||
|
expected: 1, // Should exclude my-nginx-custom
|
||||||
|
},
|
||||||
|
|
||||||
|
// Really bad scenarios
|
||||||
|
{
|
||||||
|
name: "exclude all images",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest"},
|
||||||
|
{Image: "redis", Tag: "6"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
excludes: []string{"nginx", "redis"},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty exclude list on empty containers",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{Containers: []Container{}},
|
||||||
|
excludes: []string{},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exclude with empty string",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
excludes: []string{""},
|
||||||
|
expected: 0, // Empty string matches all
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.Run(tc.name, func() {
|
||||||
|
result := RemoveExcludedImages(tc.input, tc.excludes)
|
||||||
|
assert.Len(s.T(), result.Containers, tc.expected, "Scenario: %s", tc.scenario)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIncludeOnlyImages tests image inclusion filtering
|
||||||
|
func (s *DefinitionsTestSuite) TestIncludeOnlyImages() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
scenario TestScenario
|
||||||
|
input ContainersList
|
||||||
|
includes []string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
// Good scenarios
|
||||||
|
{
|
||||||
|
name: "include specific image",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest"},
|
||||||
|
{Image: "redis", Tag: "6"},
|
||||||
|
{Image: "postgres", Tag: "14"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includes: []string{"nginx"},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "include multiple images",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest"},
|
||||||
|
{Image: "redis", Tag: "6"},
|
||||||
|
{Image: "postgres", Tag: "14"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includes: []string{"nginx", "redis"},
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Not good scenarios
|
||||||
|
{
|
||||||
|
name: "include by partial match",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "my-nginx-app", Tag: "latest"},
|
||||||
|
{Image: "nginx-proxy", Tag: "v1"},
|
||||||
|
{Image: "redis", Tag: "6"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includes: []string{"nginx"},
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Really bad scenarios
|
||||||
|
{
|
||||||
|
name: "include non-existent image",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includes: []string{"nonexistent"},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty includes list",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", Tag: "latest"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includes: []string{},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.Run(tc.name, func() {
|
||||||
|
result := IncludeOnlyImages(tc.input, tc.includes)
|
||||||
|
assert.Len(s.T(), result.Containers, tc.expected, "Scenario: %s", tc.scenario)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFilterOnlyFromNamespaces tests namespace filtering
|
||||||
|
func (s *DefinitionsTestSuite) TestFilterOnlyFromNamespaces() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
scenario TestScenario
|
||||||
|
input ContainersList
|
||||||
|
namespaces []string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
// Good scenarios
|
||||||
|
{
|
||||||
|
name: "filter single namespace",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", ImageNamespace: "default"},
|
||||||
|
{Image: "redis", ImageNamespace: "kube-system"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
namespaces: []string{"default"},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Not good scenarios
|
||||||
|
{
|
||||||
|
name: "filter multiple namespaces",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", ImageNamespace: "default"},
|
||||||
|
{Image: "redis", ImageNamespace: "kube-system"},
|
||||||
|
{Image: "postgres", ImageNamespace: "database"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
namespaces: []string{"default", "database"},
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Really bad scenarios
|
||||||
|
{
|
||||||
|
name: "filter non-existent namespace",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", ImageNamespace: "default"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
namespaces: []string{"nonexistent"},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty namespace filter",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", ImageNamespace: "default"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
namespaces: []string{},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.Run(tc.name, func() {
|
||||||
|
result := FilterOnlyFromNamespaces(tc.input, tc.namespaces)
|
||||||
|
assert.Len(s.T(), result.Containers, tc.expected, "Scenario: %s", tc.scenario)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFilterOutWholeNamespaces tests namespace exclusion
|
||||||
|
func (s *DefinitionsTestSuite) TestFilterOutWholeNamespaces() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
scenario TestScenario
|
||||||
|
input ContainersList
|
||||||
|
namespaces []string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
// Good scenarios
|
||||||
|
{
|
||||||
|
name: "exclude kube-system",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", ImageNamespace: "default"},
|
||||||
|
{Image: "coredns", ImageNamespace: "kube-system"},
|
||||||
|
{Image: "redis", ImageNamespace: "apps"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
namespaces: []string{"kube-system"},
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Not good scenarios
|
||||||
|
{
|
||||||
|
name: "exclude multiple system namespaces",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", ImageNamespace: "default"},
|
||||||
|
{Image: "coredns", ImageNamespace: "kube-system"},
|
||||||
|
{Image: "cilium", ImageNamespace: "kube-system"},
|
||||||
|
{Image: "local-path", ImageNamespace: "local-path-storage"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
namespaces: []string{"kube-system", "local-path-storage"},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Really bad scenarios
|
||||||
|
{
|
||||||
|
name: "exclude all namespaces",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", ImageNamespace: "default"},
|
||||||
|
{Image: "redis", ImageNamespace: "apps"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
namespaces: []string{"default", "apps"},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty exclusion list",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
input: ContainersList{
|
||||||
|
Containers: []Container{
|
||||||
|
{Image: "nginx", ImageNamespace: "default"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
namespaces: []string{},
|
||||||
|
expected: 1, // No exclusions = keep all
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.Run(tc.name, func() {
|
||||||
|
result := FilterOutWholeNamespaces(tc.input, tc.namespaces)
|
||||||
|
assert.Len(s.T(), result.Containers, tc.expected, "Scenario: %s", tc.scenario)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestContainerStruct tests Container struct behavior
|
||||||
|
func (s *DefinitionsTestSuite) TestContainerStruct() {
|
||||||
|
s.Run("full container with all fields", func() {
|
||||||
|
c := Container{
|
||||||
|
Image: "quay.io/cilium/cilium",
|
||||||
|
Tag: "v1.18.4",
|
||||||
|
Sha: "sha256:49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f",
|
||||||
|
FullName: "quay.io/cilium/cilium:v1.18.4@sha256:49d87af187eeeb9e9e3ec2bc6bd372261a0b5cb2d845659463ba7cc10fe9e45f",
|
||||||
|
ImageNamespace: "kube-system",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(s.T(), "quay.io/cilium/cilium", c.Image)
|
||||||
|
assert.Equal(s.T(), "v1.18.4", c.Tag)
|
||||||
|
assert.Contains(s.T(), c.Sha, "sha256:")
|
||||||
|
assert.Contains(s.T(), c.FullName, "@")
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("container equality for deduplication", func() {
|
||||||
|
c1 := Container{Image: "nginx", Tag: "latest", FullName: "nginx:latest"}
|
||||||
|
c2 := Container{Image: "nginx", Tag: "latest", FullName: "nginx:latest"}
|
||||||
|
c3 := Container{Image: "nginx", Tag: "1.21", FullName: "nginx:1.21"}
|
||||||
|
|
||||||
|
assert.Equal(s.T(), c1, c2)
|
||||||
|
assert.NotEqual(s.T(), c1, c3)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConstants tests that constants are defined correctly
|
||||||
|
func (s *DefinitionsTestSuite) TestConstants() {
|
||||||
|
// Status constants
|
||||||
|
assert.Equal(s.T(), "PENDING", STATUS_PENDING)
|
||||||
|
assert.Equal(s.T(), "STARTING", STATUS_STARTING)
|
||||||
|
assert.Equal(s.T(), "RETRYING", STATUS_RETRYING)
|
||||||
|
assert.Equal(s.T(), "RUNNING", STATUS_RUNNING)
|
||||||
|
assert.Equal(s.T(), "FAILED", STATUS_FAILED)
|
||||||
|
assert.Equal(s.T(), "COMPLETED", STATUS_SUCCESS)
|
||||||
|
assert.Equal(s.T(), "PRESENT", STATUS_PRESENT)
|
||||||
|
|
||||||
|
// Storage constants
|
||||||
|
assert.Equal(s.T(), "S3", STORAGE_S3)
|
||||||
|
assert.Equal(s.T(), "FILE", STORAGE_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBackupJobImage tests the BACKUP_JOB_IMAGE initialization
|
||||||
|
func (s *DefinitionsTestSuite) TestBackupJobImage() {
|
||||||
|
require.NotEmpty(s.T(), BACKUP_JOB_IMAGE)
|
||||||
|
assert.Contains(s.T(), BACKUP_JOB_IMAGE, "kubernetes-images-sync-worker")
|
||||||
|
}
|
||||||
@@ -9,13 +9,13 @@ import (
|
|||||||
batchv1 "k8s.io/api/batch/v1"
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JobParams struct {
|
type JobParams struct {
|
||||||
Name string
|
Name string
|
||||||
Namespace string
|
Namespace string
|
||||||
Annotations map[string]string
|
Annotations map[string]string
|
||||||
Image string
|
Image string
|
||||||
Commands []string
|
Commands []string
|
||||||
EnvVars []corev1.EnvVar
|
EnvVars []corev1.EnvVar
|
||||||
@@ -88,7 +88,7 @@ func CreateJob[T any](params JobParams, setupFunc func(T) []string) *batchv1.Job
|
|||||||
VolumeMounts: volumeMounts,
|
VolumeMounts: volumeMounts,
|
||||||
Env: params.EnvVars,
|
Env: params.EnvVars,
|
||||||
SecurityContext: &corev1.SecurityContext{
|
SecurityContext: &corev1.SecurityContext{
|
||||||
Privileged: pointer.Bool(true),
|
Privileged: ptr.To(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,547 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
raczylocomv1 "github.com/lukaszraczylo/kubernetes-images-sync-operator/api/raczylo.com/v1"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JobsTestSuite tests job creation and related functionality
|
||||||
|
type JobsTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobsTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(JobsTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateJob tests the CreateJob function with various scenarios
|
||||||
|
func (s *JobsTestSuite) TestCreateJob() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
scenario TestScenario
|
||||||
|
params JobParams
|
||||||
|
expectJobName string
|
||||||
|
expectNamespace string
|
||||||
|
expectImage string
|
||||||
|
expectAnnotations map[string]string
|
||||||
|
expectSecrets int
|
||||||
|
expectEnvVars int
|
||||||
|
}{
|
||||||
|
// Good scenarios
|
||||||
|
{
|
||||||
|
name: "basic job creation",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
params: JobParams{
|
||||||
|
Name: "test-job",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo hello"},
|
||||||
|
},
|
||||||
|
expectJobName: "test-job",
|
||||||
|
expectNamespace: "default",
|
||||||
|
expectImage: "worker:latest",
|
||||||
|
expectSecrets: 0,
|
||||||
|
expectEnvVars: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "job with annotations",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
params: JobParams{
|
||||||
|
Name: "annotated-job",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo hello"},
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectJobName: "annotated-job",
|
||||||
|
expectNamespace: "default",
|
||||||
|
expectImage: "worker:latest",
|
||||||
|
expectAnnotations: map[string]string{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2",
|
||||||
|
},
|
||||||
|
expectSecrets: 0,
|
||||||
|
expectEnvVars: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "job with image pull secrets",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
params: JobParams{
|
||||||
|
Name: "secret-job",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "private-registry/image:latest",
|
||||||
|
Commands: []string{"echo hello"},
|
||||||
|
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||||
|
{Name: "my-registry-secret"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectJobName: "secret-job",
|
||||||
|
expectNamespace: "default",
|
||||||
|
expectImage: "private-registry/image:latest",
|
||||||
|
expectSecrets: 1,
|
||||||
|
expectEnvVars: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "job with multiple secrets",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
params: JobParams{
|
||||||
|
Name: "multi-secret-job",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "private-registry/image:latest",
|
||||||
|
Commands: []string{"echo hello"},
|
||||||
|
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||||
|
{Name: "secret1"},
|
||||||
|
{Name: "secret2"},
|
||||||
|
{Name: "secret3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectJobName: "multi-secret-job",
|
||||||
|
expectNamespace: "default",
|
||||||
|
expectImage: "private-registry/image:latest",
|
||||||
|
expectSecrets: 3,
|
||||||
|
expectEnvVars: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "job with environment variables",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
params: JobParams{
|
||||||
|
Name: "env-job",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo $MY_VAR"},
|
||||||
|
EnvVars: []corev1.EnvVar{
|
||||||
|
{Name: "MY_VAR", Value: "my-value"},
|
||||||
|
{Name: "AWS_REGION", Value: "us-east-1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectJobName: "env-job",
|
||||||
|
expectNamespace: "default",
|
||||||
|
expectImage: "worker:latest",
|
||||||
|
expectSecrets: 0,
|
||||||
|
expectEnvVars: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Not good scenarios
|
||||||
|
{
|
||||||
|
name: "job with backoff limit",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
params: JobParams{
|
||||||
|
Name: "retry-job",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"might-fail"},
|
||||||
|
BackoffLimit: int32Ptr(3),
|
||||||
|
},
|
||||||
|
expectJobName: "retry-job",
|
||||||
|
expectNamespace: "default",
|
||||||
|
expectImage: "worker:latest",
|
||||||
|
expectSecrets: 0,
|
||||||
|
expectEnvVars: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "job with TTL after finished",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
params: JobParams{
|
||||||
|
Name: "ttl-job",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo done"},
|
||||||
|
TTLSecondsAfterFinished: int32Ptr(300),
|
||||||
|
},
|
||||||
|
expectJobName: "ttl-job",
|
||||||
|
expectNamespace: "default",
|
||||||
|
expectImage: "worker:latest",
|
||||||
|
expectSecrets: 0,
|
||||||
|
expectEnvVars: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Really bad scenarios - edge cases
|
||||||
|
{
|
||||||
|
name: "job with empty commands",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
params: JobParams{
|
||||||
|
Name: "empty-cmd-job",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{},
|
||||||
|
},
|
||||||
|
expectJobName: "empty-cmd-job",
|
||||||
|
expectNamespace: "default",
|
||||||
|
expectImage: "worker:latest",
|
||||||
|
expectSecrets: 0,
|
||||||
|
expectEnvVars: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "job with very long name",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
params: JobParams{
|
||||||
|
Name: "this-is-a-very-long-job-name-that-might-cause-issues-in-kubernetes-because-names-have-limits",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo hello"},
|
||||||
|
},
|
||||||
|
expectJobName: "this-is-a-very-long-job-name-that-might-cause-issues-in-kubernetes-because-names-have-limits",
|
||||||
|
expectNamespace: "default",
|
||||||
|
expectImage: "worker:latest",
|
||||||
|
expectSecrets: 0,
|
||||||
|
expectEnvVars: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.Run(tc.name, func() {
|
||||||
|
job := CreateJob(tc.params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||||
|
|
||||||
|
require.NotNil(s.T(), job, "Job should not be nil")
|
||||||
|
|
||||||
|
// Verify job metadata
|
||||||
|
assert.Equal(s.T(), tc.expectJobName, job.ObjectMeta.Name)
|
||||||
|
assert.Equal(s.T(), tc.expectNamespace, job.ObjectMeta.Namespace)
|
||||||
|
|
||||||
|
// Verify labels
|
||||||
|
assert.Equal(s.T(), "image-export", job.ObjectMeta.Labels["app"])
|
||||||
|
assert.Equal(s.T(), "image-export", job.Spec.Template.ObjectMeta.Labels["app"])
|
||||||
|
|
||||||
|
// Verify annotations if expected
|
||||||
|
if tc.expectAnnotations != nil {
|
||||||
|
for k, v := range tc.expectAnnotations {
|
||||||
|
assert.Equal(s.T(), v, job.ObjectMeta.Annotations[k])
|
||||||
|
assert.Equal(s.T(), v, job.Spec.Template.ObjectMeta.Annotations[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify pod template
|
||||||
|
podSpec := job.Spec.Template.Spec
|
||||||
|
require.Len(s.T(), podSpec.Containers, 1, "Should have exactly one container")
|
||||||
|
|
||||||
|
container := podSpec.Containers[0]
|
||||||
|
assert.Equal(s.T(), "exporter", container.Name)
|
||||||
|
assert.Equal(s.T(), tc.expectImage, container.Image)
|
||||||
|
assert.True(s.T(), container.TTY)
|
||||||
|
|
||||||
|
// Verify restart policy
|
||||||
|
assert.Equal(s.T(), corev1.RestartPolicyOnFailure, podSpec.RestartPolicy)
|
||||||
|
|
||||||
|
// Verify secrets
|
||||||
|
assert.Len(s.T(), podSpec.ImagePullSecrets, tc.expectSecrets)
|
||||||
|
assert.Len(s.T(), podSpec.Volumes, tc.expectSecrets)
|
||||||
|
assert.Len(s.T(), container.VolumeMounts, tc.expectSecrets)
|
||||||
|
|
||||||
|
// Verify environment variables
|
||||||
|
assert.Len(s.T(), container.Env, tc.expectEnvVars)
|
||||||
|
|
||||||
|
// Verify security context (privileged for podman)
|
||||||
|
require.NotNil(s.T(), container.SecurityContext)
|
||||||
|
require.NotNil(s.T(), container.SecurityContext.Privileged)
|
||||||
|
assert.True(s.T(), *container.SecurityContext.Privileged)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateJobWithOwnerReferences tests owner reference handling
|
||||||
|
func (s *JobsTestSuite) TestCreateJobWithOwnerReferences() {
|
||||||
|
ownerRefs := []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
APIVersion: "raczylo.com/v1",
|
||||||
|
Kind: "ClusterImage",
|
||||||
|
Name: "test-image",
|
||||||
|
UID: "test-uid-12345",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
params := JobParams{
|
||||||
|
Name: "owned-job",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo hello"},
|
||||||
|
OwnerReferences: ownerRefs,
|
||||||
|
}
|
||||||
|
|
||||||
|
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||||
|
|
||||||
|
require.Len(s.T(), job.ObjectMeta.OwnerReferences, 1)
|
||||||
|
assert.Equal(s.T(), "ClusterImage", job.ObjectMeta.OwnerReferences[0].Kind)
|
||||||
|
assert.Equal(s.T(), "test-image", job.ObjectMeta.OwnerReferences[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateJobCommands tests command concatenation
|
||||||
|
func (s *JobsTestSuite) TestCreateJobCommands() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
commands []string
|
||||||
|
expectedArgsLen int
|
||||||
|
expectedJoined string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single command",
|
||||||
|
commands: []string{"echo hello"},
|
||||||
|
expectedArgsLen: 3, // /bin/bash, -c, "command"
|
||||||
|
expectedJoined: "echo hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple commands",
|
||||||
|
commands: []string{"echo hello", "echo world"},
|
||||||
|
expectedArgsLen: 3,
|
||||||
|
expectedJoined: "echo hello && echo world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex podman commands",
|
||||||
|
commands: []string{
|
||||||
|
"podman pull nginx:latest",
|
||||||
|
"podman save --quiet -o /tmp/nginx.tar nginx:latest",
|
||||||
|
"./worker export /tmp/nginx.tar s3://bucket/path",
|
||||||
|
},
|
||||||
|
expectedArgsLen: 3,
|
||||||
|
expectedJoined: "podman pull nginx:latest && podman save --quiet -o /tmp/nginx.tar nginx:latest && ./worker export /tmp/nginx.tar s3://bucket/path",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.Run(tc.name, func() {
|
||||||
|
params := JobParams{
|
||||||
|
Name: "cmd-test",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: tc.commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||||
|
|
||||||
|
container := job.Spec.Template.Spec.Containers[0]
|
||||||
|
assert.Len(s.T(), container.Args, tc.expectedArgsLen)
|
||||||
|
assert.Equal(s.T(), "/bin/bash", container.Args[0])
|
||||||
|
assert.Equal(s.T(), "-c", container.Args[1])
|
||||||
|
assert.Equal(s.T(), tc.expectedJoined, container.Args[2])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateJobBackoffAndTTL tests backoff limit and TTL settings
|
||||||
|
func (s *JobsTestSuite) TestCreateJobBackoffAndTTL() {
|
||||||
|
backoff := int32(5)
|
||||||
|
ttl := int32(600)
|
||||||
|
|
||||||
|
params := JobParams{
|
||||||
|
Name: "backoff-ttl-job",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo hello"},
|
||||||
|
BackoffLimit: &backoff,
|
||||||
|
TTLSecondsAfterFinished: &ttl,
|
||||||
|
}
|
||||||
|
|
||||||
|
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||||
|
|
||||||
|
require.NotNil(s.T(), job.Spec.BackoffLimit)
|
||||||
|
assert.Equal(s.T(), int32(5), *job.Spec.BackoffLimit)
|
||||||
|
|
||||||
|
require.NotNil(s.T(), job.Spec.TTLSecondsAfterFinished)
|
||||||
|
assert.Equal(s.T(), int32(600), *job.Spec.TTLSecondsAfterFinished)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetupS3Params tests S3 parameter generation
|
||||||
|
func (s *JobsTestSuite) TestSetupS3Params() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
scenario TestScenario
|
||||||
|
config raczylocomv1.ClusterImageStorageS3
|
||||||
|
expectContains []string
|
||||||
|
expectNotIn []string
|
||||||
|
}{
|
||||||
|
// Good scenarios
|
||||||
|
{
|
||||||
|
name: "basic credentials",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
config: raczylocomv1.ClusterImageStorageS3{
|
||||||
|
Bucket: "my-bucket",
|
||||||
|
Region: "us-east-1",
|
||||||
|
AccessKey: "AKIAIOSFODNN7EXAMPLE",
|
||||||
|
SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||||
|
},
|
||||||
|
expectContains: []string{
|
||||||
|
"--aws_access_key_id='AKIAIOSFODNN7EXAMPLE'",
|
||||||
|
"--aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'",
|
||||||
|
},
|
||||||
|
expectNotIn: []string{"--use_role", "--use_current_role"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "use current role (IRSA)",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
config: raczylocomv1.ClusterImageStorageS3{
|
||||||
|
Bucket: "my-bucket",
|
||||||
|
Region: "us-east-1",
|
||||||
|
UseRole: true,
|
||||||
|
},
|
||||||
|
expectContains: []string{"--use_current_role"},
|
||||||
|
expectNotIn: []string{"--aws_access_key_id", "--aws_secret_access_key"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "use specific role ARN",
|
||||||
|
scenario: ScenarioGood,
|
||||||
|
config: raczylocomv1.ClusterImageStorageS3{
|
||||||
|
Bucket: "my-bucket",
|
||||||
|
Region: "us-east-1",
|
||||||
|
UseRole: true,
|
||||||
|
RoleARN: "arn:aws:iam::123456789:role/MyRole",
|
||||||
|
},
|
||||||
|
expectContains: []string{
|
||||||
|
"--use_role",
|
||||||
|
"--role_name='arn:aws:iam::123456789:role/MyRole'",
|
||||||
|
},
|
||||||
|
expectNotIn: []string{"--aws_access_key_id", "--use_current_role"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Not good scenarios
|
||||||
|
{
|
||||||
|
name: "with custom endpoint (MinIO)",
|
||||||
|
scenario: ScenarioNotGood,
|
||||||
|
config: raczylocomv1.ClusterImageStorageS3{
|
||||||
|
Bucket: "my-bucket",
|
||||||
|
Region: "us-east-1",
|
||||||
|
AccessKey: "minioadmin",
|
||||||
|
SecretKey: "minioadmin",
|
||||||
|
Endpoint: "http://minio.local:9000",
|
||||||
|
},
|
||||||
|
expectContains: []string{
|
||||||
|
"--endpoint_url='http://minio.local:9000'",
|
||||||
|
"--aws_access_key_id='minioadmin'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Really bad scenarios
|
||||||
|
{
|
||||||
|
name: "empty credentials with UseRole false",
|
||||||
|
scenario: ScenarioReallyBad,
|
||||||
|
config: raczylocomv1.ClusterImageStorageS3{
|
||||||
|
Bucket: "my-bucket",
|
||||||
|
Region: "us-east-1",
|
||||||
|
},
|
||||||
|
expectContains: []string{
|
||||||
|
"--aws_access_key_id=''",
|
||||||
|
"--aws_secret_access_key=''",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.Run(tc.name, func() {
|
||||||
|
params := SetupS3Params(tc.config)
|
||||||
|
|
||||||
|
// Check expected parameters are present
|
||||||
|
for _, expected := range tc.expectContains {
|
||||||
|
found := false
|
||||||
|
for _, param := range params {
|
||||||
|
if param == expected {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(s.T(), found, "Expected parameter not found: %s", expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check unexpected parameters are not present
|
||||||
|
for _, notExpected := range tc.expectNotIn {
|
||||||
|
for _, param := range params {
|
||||||
|
assert.NotContains(s.T(), param, notExpected, "Unexpected parameter found: %s", notExpected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJobParamsDefaults tests JobParams default handling
|
||||||
|
func (s *JobsTestSuite) TestJobParamsDefaults() {
|
||||||
|
s.Run("nil backoff limit", func() {
|
||||||
|
params := JobParams{
|
||||||
|
Name: "test",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo"},
|
||||||
|
}
|
||||||
|
|
||||||
|
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||||
|
assert.Nil(s.T(), job.Spec.BackoffLimit)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("nil TTL", func() {
|
||||||
|
params := JobParams{
|
||||||
|
Name: "test",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo"},
|
||||||
|
}
|
||||||
|
|
||||||
|
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||||
|
assert.Nil(s.T(), job.Spec.TTLSecondsAfterFinished)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("empty service account uses env var", func() {
|
||||||
|
// This test verifies that when ServiceAccount is empty,
|
||||||
|
// the job will use POD_SERVICE_ACCOUNT env var
|
||||||
|
params := JobParams{
|
||||||
|
Name: "test",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo"},
|
||||||
|
ServiceAccount: "", // Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||||
|
// Service account will be set from environment variable POD_SERVICE_ACCOUNT
|
||||||
|
// In tests, this will be empty, so we just verify the job is created
|
||||||
|
require.NotNil(s.T(), job)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSecretVolumeMounting tests that secrets are properly mounted
|
||||||
|
func (s *JobsTestSuite) TestSecretVolumeMounting() {
|
||||||
|
secrets := []corev1.LocalObjectReference{
|
||||||
|
{Name: "docker-registry"},
|
||||||
|
{Name: "gcr-json-key"},
|
||||||
|
{Name: "ecr-credentials"},
|
||||||
|
}
|
||||||
|
|
||||||
|
params := JobParams{
|
||||||
|
Name: "secret-mount-test",
|
||||||
|
Namespace: "default",
|
||||||
|
Image: "worker:latest",
|
||||||
|
Commands: []string{"echo"},
|
||||||
|
ImagePullSecrets: secrets,
|
||||||
|
}
|
||||||
|
|
||||||
|
job := CreateJob(params, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||||
|
podSpec := job.Spec.Template.Spec
|
||||||
|
container := podSpec.Containers[0]
|
||||||
|
|
||||||
|
// Verify volumes are created
|
||||||
|
require.Len(s.T(), podSpec.Volumes, 3)
|
||||||
|
for i, vol := range podSpec.Volumes {
|
||||||
|
assert.Equal(s.T(), secrets[i].Name, vol.VolumeSource.Secret.SecretName)
|
||||||
|
assert.Contains(s.T(), vol.Name, "secret-")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify volume mounts
|
||||||
|
require.Len(s.T(), container.VolumeMounts, 3)
|
||||||
|
for i, mount := range container.VolumeMounts {
|
||||||
|
assert.Contains(s.T(), mount.MountPath, ".docker-secret-")
|
||||||
|
assert.True(s.T(), mount.ReadOnly)
|
||||||
|
assert.Contains(s.T(), mount.Name, "secret-")
|
||||||
|
// Verify index is correct
|
||||||
|
expectedIndex := i
|
||||||
|
assert.Equal(s.T(), "/home/runner/.docker-secret-"+string(rune('0'+expectedIndex)), mount.MountPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function
|
||||||
|
func int32Ptr(i int32) *int32 {
|
||||||
|
return &i
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package e2e
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
|
||||||
. "github.com/onsi/gomega"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Run e2e tests using the Ginkgo runner.
|
|
||||||
func TestE2E(t *testing.T) {
|
|
||||||
RegisterFailHandler(Fail)
|
|
||||||
_, _ = fmt.Fprintf(GinkgoWriter, "Starting kubernetes-images-sync-operator suite\n")
|
|
||||||
RunSpecs(t, "e2e suite")
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package e2e
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
|
||||||
. "github.com/onsi/gomega"
|
|
||||||
|
|
||||||
"github.com/lukaszraczylo/kubernetes-images-sync-operator/test/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const namespace = "kubernetes-images-sync-operator-system"
|
|
||||||
|
|
||||||
var _ = Describe("controller", Ordered, func() {
|
|
||||||
BeforeAll(func() {
|
|
||||||
By("installing prometheus operator")
|
|
||||||
Expect(utils.InstallPrometheusOperator()).To(Succeed())
|
|
||||||
|
|
||||||
By("installing the cert-manager")
|
|
||||||
Expect(utils.InstallCertManager()).To(Succeed())
|
|
||||||
|
|
||||||
By("creating manager namespace")
|
|
||||||
cmd := exec.Command("kubectl", "create", "ns", namespace)
|
|
||||||
_, _ = utils.Run(cmd)
|
|
||||||
})
|
|
||||||
|
|
||||||
AfterAll(func() {
|
|
||||||
By("uninstalling the Prometheus manager bundle")
|
|
||||||
utils.UninstallPrometheusOperator()
|
|
||||||
|
|
||||||
By("uninstalling the cert-manager bundle")
|
|
||||||
utils.UninstallCertManager()
|
|
||||||
|
|
||||||
By("removing manager namespace")
|
|
||||||
cmd := exec.Command("kubectl", "delete", "ns", namespace)
|
|
||||||
_, _ = utils.Run(cmd)
|
|
||||||
})
|
|
||||||
|
|
||||||
Context("Operator", func() {
|
|
||||||
It("should run successfully", func() {
|
|
||||||
var controllerPodName string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// projectimage stores the name of the image used in the example
|
|
||||||
var projectimage = "example.com/kubernetes-images-sync-operator:v0.0.1"
|
|
||||||
|
|
||||||
By("building the manager(Operator) image")
|
|
||||||
cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage))
|
|
||||||
_, err = utils.Run(cmd)
|
|
||||||
ExpectWithOffset(1, err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
By("loading the the manager(Operator) image on Kind")
|
|
||||||
err = utils.LoadImageToKindClusterWithName(projectimage)
|
|
||||||
ExpectWithOffset(1, err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
By("installing CRDs")
|
|
||||||
cmd = exec.Command("make", "install")
|
|
||||||
_, err = utils.Run(cmd)
|
|
||||||
ExpectWithOffset(1, err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
By("deploying the sa")
|
|
||||||
cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage))
|
|
||||||
_, err = utils.Run(cmd)
|
|
||||||
ExpectWithOffset(1, err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
By("validating that the sa pod is running as expected")
|
|
||||||
verifyControllerUp := func() error {
|
|
||||||
// Get pod name
|
|
||||||
|
|
||||||
cmd = exec.Command("kubectl", "get",
|
|
||||||
"pods", "-l", "control-plane=sa",
|
|
||||||
"-o", "go-template={{ range .items }}"+
|
|
||||||
"{{ if not .metadata.deletionTimestamp }}"+
|
|
||||||
"{{ .metadata.name }}"+
|
|
||||||
"{{ \"\\n\" }}{{ end }}{{ end }}",
|
|
||||||
"-n", namespace,
|
|
||||||
)
|
|
||||||
|
|
||||||
podOutput, err := utils.Run(cmd)
|
|
||||||
ExpectWithOffset(2, err).NotTo(HaveOccurred())
|
|
||||||
podNames := utils.GetNonEmptyLines(string(podOutput))
|
|
||||||
if len(podNames) != 1 {
|
|
||||||
return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames))
|
|
||||||
}
|
|
||||||
controllerPodName = podNames[0]
|
|
||||||
ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("sa"))
|
|
||||||
|
|
||||||
// Validate pod status
|
|
||||||
cmd = exec.Command("kubectl", "get",
|
|
||||||
"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
|
|
||||||
"-n", namespace,
|
|
||||||
)
|
|
||||||
status, err := utils.Run(cmd)
|
|
||||||
ExpectWithOffset(2, err).NotTo(HaveOccurred())
|
|
||||||
if string(status) != "Running" {
|
|
||||||
return fmt.Errorf("controller pod in %s status", status)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed())
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2" //nolint:golint,revive
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
prometheusOperatorVersion = "v0.72.0"
|
|
||||||
prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" +
|
|
||||||
"releases/download/%s/bundle.yaml"
|
|
||||||
|
|
||||||
certmanagerVersion = "v1.14.4"
|
|
||||||
certmanagerURLTmpl = "https://github.com/jetstack/cert-manager/releases/download/%s/cert-manager.yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
func warnError(err error) {
|
|
||||||
_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics.
|
|
||||||
func InstallPrometheusOperator() error {
|
|
||||||
url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
|
|
||||||
cmd := exec.Command("kubectl", "create", "-f", url)
|
|
||||||
_, err := Run(cmd)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run executes the provided command within this context
|
|
||||||
func Run(cmd *exec.Cmd) ([]byte, error) {
|
|
||||||
dir, _ := GetProjectDir()
|
|
||||||
cmd.Dir = dir
|
|
||||||
|
|
||||||
if err := os.Chdir(cmd.Dir); err != nil {
|
|
||||||
_, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Env = append(os.Environ(), "GO111MODULE=on")
|
|
||||||
command := strings.Join(cmd.Args, " ")
|
|
||||||
_, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UninstallPrometheusOperator uninstalls the prometheus
|
|
||||||
func UninstallPrometheusOperator() {
|
|
||||||
url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
|
|
||||||
cmd := exec.Command("kubectl", "delete", "-f", url)
|
|
||||||
if _, err := Run(cmd); err != nil {
|
|
||||||
warnError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UninstallCertManager uninstalls the cert manager
|
|
||||||
func UninstallCertManager() {
|
|
||||||
url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
|
|
||||||
cmd := exec.Command("kubectl", "delete", "-f", url)
|
|
||||||
if _, err := Run(cmd); err != nil {
|
|
||||||
warnError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallCertManager installs the cert manager bundle.
|
|
||||||
func InstallCertManager() error {
|
|
||||||
url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
|
|
||||||
cmd := exec.Command("kubectl", "apply", "-f", url)
|
|
||||||
if _, err := Run(cmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Wait for cert-manager-webhook to be ready, which can take time if cert-manager
|
|
||||||
// was re-installed after uninstalling on a cluster.
|
|
||||||
cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
|
|
||||||
"--for", "condition=Available",
|
|
||||||
"--namespace", "cert-manager",
|
|
||||||
"--timeout", "5m",
|
|
||||||
)
|
|
||||||
|
|
||||||
_, err := Run(cmd)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadImageToKindClusterWithName loads a local docker image to the kind cluster
|
|
||||||
func LoadImageToKindClusterWithName(name string) error {
|
|
||||||
cluster := "kind"
|
|
||||||
if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
|
|
||||||
cluster = v
|
|
||||||
}
|
|
||||||
kindOptions := []string{"load", "docker-image", name, "--name", cluster}
|
|
||||||
cmd := exec.Command("kind", kindOptions...)
|
|
||||||
_, err := Run(cmd)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNonEmptyLines converts given command output string into individual objects
|
|
||||||
// according to line breakers, and ignores the empty elements in it.
|
|
||||||
func GetNonEmptyLines(output string) []string {
|
|
||||||
var res []string
|
|
||||||
elements := strings.Split(output, "\n")
|
|
||||||
for _, element := range elements {
|
|
||||||
if element != "" {
|
|
||||||
res = append(res, element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProjectDir will return the directory where the project is
|
|
||||||
func GetProjectDir() (string, error) {
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return wd, err
|
|
||||||
}
|
|
||||||
wd = strings.Replace(wd, "/test/e2e", "", -1)
|
|
||||||
return wd, nil
|
|
||||||
}
|
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Setting up envtest binaries..."
|
||||||
|
|
||||||
|
# Install setup-envtest
|
||||||
|
go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
|
||||||
|
|
||||||
|
# Add GOPATH/bin to PATH
|
||||||
|
export PATH="${PATH}:$(go env GOPATH)/bin"
|
||||||
|
|
||||||
|
# Download and setup envtest binaries
|
||||||
|
ENVTEST_ASSETS=$(setup-envtest use 1.31.0 --bin-dir /tmp/envtest -p path)
|
||||||
|
echo "KUBEBUILDER_ASSETS=${ENVTEST_ASSETS}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "Envtest binaries installed at: ${ENVTEST_ASSETS}"
|
||||||
Reference in New Issue
Block a user