refactor(devbox): Devbox and related gateway services (#7098)

remove(devbox): Devbox and related gateway services
This commit is contained in:
zijiren
2026-07-24 17:56:28 +08:00
committed by GitHub
parent f550632d91
commit 95131f1208
175 changed files with 98 additions and 34056 deletions
-317
View File
@@ -1,317 +0,0 @@
name: Build Service-RS Image
on:
workflow_call:
inputs:
module:
description: "Module name (e.g., httpgate)"
required: true
type: string
push_image:
description: "Push image"
required: false
type: boolean
default: false
push_image_tag:
description: "Push image tag"
default: "latest"
required: false
type: string
workflow_dispatch:
inputs:
module:
description: "Rust service module (e.g., httpgate)"
required: true
type: string
push_image:
description: "Push image"
required: false
type: boolean
default: false
push_image_tag:
description: "Push image tag"
default: "latest"
required: false
type: string
env:
DEFAULT_OWNER: "labring"
ALIYUN_REGISTRY: ${{ secrets.ALIYUN_REGISTRY }}
ALIYUN_REPO_PREFIX: ${{ secrets.ALIYUN_REPO_PREFIX && secrets.ALIYUN_REPO_PREFIX || secrets.ALIYUN_USERNAME && format('{0}/{1}', secrets.ALIYUN_REGISTRY, secrets.ALIYUN_USERNAME) || '' }}
jobs:
image-build:
strategy:
matrix:
include:
- arch: amd64
- arch: arm64
runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.runs-on || 'ubuntu-24.04' }}
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set image repo
id: set_repo
env:
MODULE: ${{ inputs.module }}
REPOSITORY_OWNER: ${{ github.repository_owner }}
run: |
echo "GHCR_REPO=ghcr.io/${REPOSITORY_OWNER}/sealos-${MODULE}-service" >> $GITHUB_ENV
if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then
echo "ALIYUN_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-${MODULE}-service" >> $GITHUB_ENV
fi
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.GHCR_REPO }}
${{ env.ALIYUN_REPO }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Github Container Hub
if: ${{ inputs.push_image }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }}
- name: Login to Aliyun Registry
if: ${{ inputs.push_image && env.ALIYUN_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.ALIYUN_REGISTRY }}
username: ${{ secrets.ALIYUN_USERNAME }}
password: ${{ secrets.ALIYUN_PASSWORD }}
- name: Build
id: build
uses: docker/build-push-action@v6
with:
context: ./service-rs
file: ./service-rs/${{ inputs.module }}/Dockerfile
platforms: linux/${{ matrix.arch }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=${{ env.GHCR_REPO }}${{ env.ALIYUN_REPO && format(',{0}', env.ALIYUN_REPO) || '' }}",name-canonical=true,push-by-digest=${{ inputs.push_image }},push=${{ inputs.push_image }}
- name: Export digest
env:
TEMP_DIR: ${{ runner.temp }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
mkdir -p "${TEMP_DIR}/digests"
touch "${TEMP_DIR}/digests/${DIGEST#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ inputs.module }}-${{ matrix.arch }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
image-release:
name: Push Docker Images
needs: image-build
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
if: ${{ inputs.push_image }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Login to Github Container Hub
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }}
- name: Login to Aliyun Registry
if: ${{ env.ALIYUN_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.ALIYUN_REGISTRY }}
username: ${{ secrets.ALIYUN_USERNAME }}
password: ${{ secrets.ALIYUN_PASSWORD }}
- name: Set image repo
id: set_repo
env:
MODULE: ${{ inputs.module }}
REPOSITORY_OWNER: ${{ github.repository_owner }}
run: |
echo "GHCR_REPO=ghcr.io/${REPOSITORY_OWNER}/sealos-${MODULE}-service" >> $GITHUB_ENV
if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then
echo "ALIYUN_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-${MODULE}-service" >> $GITHUB_ENV
fi
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-${{ inputs.module }}-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.GHCR_REPO }}
${{ env.ALIYUN_REPO }}
tags: |
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
type=raw,value=${{ inputs.push_image_tag }},enable=${{ inputs.push_image_tag != '' && inputs.push_image_tag != 'latest' }}
type=ref,event=branch
type=ref,event=tag
type=sha
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
env:
DOCKER_METADATA_SHORT_SHA_LENGTH: 9
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
env:
GHCR_REPO: ${{ env.GHCR_REPO }}
IMAGE_SOURCE: https://github.com/${{ github.repository }}
run: |
for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do
docker buildx imagetools create \
--annotation "index:org.opencontainers.image.source=${IMAGE_SOURCE}" \
-t $TAG \
$(printf "${GHCR_REPO}@sha256:%s " *)
sleep 5
done
- name: Inspect image
env:
GHCR_REPO: ${{ env.GHCR_REPO }}
IMAGE_VERSION: ${{ steps.meta.outputs.version }}
run: |
docker buildx imagetools inspect "${GHCR_REPO}:${IMAGE_VERSION}"
cluster-image-build:
needs:
- image-release
runs-on: ubuntu-24.04
if: ${{ (github.event_name == 'push') || (github.event_name == 'create') || (inputs.push_image == true) }}
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set image repo
env:
MODULE: ${{ inputs.module }}
REPOSITORY_OWNER: ${{ github.repository_owner }}
run: |
echo "MODULE_NAME=${MODULE}" >> $GITHUB_ENV
# Docker image repo (always use GHCR for values.yaml to avoid Aliyun bandwidth costs)
echo "GHCR_DOCKER_REPO=ghcr.io/${REPOSITORY_OWNER}/sealos-${MODULE}-service" >> $GITHUB_ENV
# Cluster image repos
echo "GHCR_CLUSTER_REPO=ghcr.io/${REPOSITORY_OWNER}/sealos-cloud-${MODULE}-service" >> $GITHUB_ENV
if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then
echo "ALIYUN_CLUSTER_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-cloud-${MODULE}-service" >> $GITHUB_ENV
fi
- name: Docker meta for cluster image
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.GHCR_CLUSTER_REPO }}
${{ env.ALIYUN_CLUSTER_REPO }}
tags: |
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
type=raw,value=${{ inputs.push_image_tag }},enable=${{ inputs.push_image_tag != '' && inputs.push_image_tag != 'latest' }}
type=ref,event=branch
type=ref,event=tag
type=sha
env:
DOCKER_METADATA_SHORT_SHA_LENGTH: 9
- name: Install sealos
run: |
sudo bash ./.github/scripts/install.sh
- name: Build ${{ env.MODULE_NAME }} cluster image
working-directory: service-rs/${{ inputs.module }}/deploy
env:
MODULE_NAME: ${{ env.MODULE_NAME }}
GHCR_DOCKER_REPO: ${{ env.GHCR_DOCKER_REPO }}
IMAGE_SOURCE: https://github.com/${{ github.repository }}
run: |
# Build cluster images for each tag (amd64)
for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do
# Update image in values.yaml - always use GHCR to avoid Aliyun bandwidth costs
if [[ -f charts/${MODULE_NAME}/values.yaml ]]; then
IMAGE_TAG="${TAG##*:}"
echo "Updating image in charts/${MODULE_NAME}/values.yaml to ${GHCR_DOCKER_REPO}:${IMAGE_TAG}"
sed -i "s|repository:.*|repository: ${GHCR_DOCKER_REPO}|" charts/${MODULE_NAME}/values.yaml
sed -i "s|tag:.*|tag: \"${IMAGE_TAG}\"|" charts/${MODULE_NAME}/values.yaml
fi
sudo rm -rf registry
echo "Building ${TAG}-amd64"
sudo sealos build -t "${TAG}-amd64" --platform linux/amd64 --label "org.opencontainers.image.source=${IMAGE_SOURCE}" -f Kubefile
done
# Build cluster images for each tag (arm64)
for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do
if [[ -f charts/${MODULE_NAME}/values.yaml ]]; then
IMAGE_TAG="${TAG##*:}"
sed -i "s|repository:.*|repository: ${GHCR_DOCKER_REPO}|" charts/${MODULE_NAME}/values.yaml
sed -i "s|tag:.*|tag: \"${IMAGE_TAG}\"|" charts/${MODULE_NAME}/values.yaml
fi
sudo rm -rf registry
echo "Building ${TAG}-arm64"
sudo sealos build -t "${TAG}-arm64" --platform linux/arm64 --label "org.opencontainers.image.source=${IMAGE_SOURCE}" -f Kubefile
done
- name: Sealos login to ghcr.io
env:
REPOSITORY_OWNER: ${{ github.repository_owner }}
GH_PAT: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }}
run: |
sudo sealos login -u "$REPOSITORY_OWNER" -p "$GH_PAT" --debug ghcr.io
- name: Sealos login to Aliyun Registry
if: ${{ env.ALIYUN_REGISTRY }}
env:
ALIYUN_USERNAME: ${{ secrets.ALIYUN_USERNAME }}
ALIYUN_PASSWORD: ${{ secrets.ALIYUN_PASSWORD }}
run: |
sudo sealos login -u "$ALIYUN_USERNAME" -p "$ALIYUN_PASSWORD" --debug ${{ env.ALIYUN_REGISTRY }}
- name: Manifest Cluster Images
run: |
sudo sealos images
for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do
echo "Creating manifest for ${TAG}"
bash scripts/manifest-cluster-images.sh "$TAG"
done
-63
View File
@@ -1,63 +0,0 @@
name: Build Service-RS Images
on:
workflow_call:
inputs:
push_image:
description: "Push image"
required: false
type: boolean
default: false
push_image_tag:
description: "Push image tag"
default: "latest"
required: false
type: string
workflow_dispatch:
inputs:
push_image:
description: "Push image"
required: false
type: boolean
default: false
push_image_tag:
description: "Push image tag"
default: "latest"
required: false
type: string
push:
branches: ["*"]
paths:
- "service-rs/**"
- ".github/workflows/service-rs.yml"
- ".github/workflows/service-rs-build.yml"
- "!**/*.md"
pull_request:
branches: ["*"]
paths:
- "service-rs/**"
- ".github/workflows/service-rs.yml"
- ".github/workflows/service-rs-build.yml"
- "!**/*.md"
# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts
concurrency:
group: service-rs-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
image-build:
uses: ./.github/workflows/service-rs-build.yml
permissions:
contents: read
packages: write
secrets: inherit
strategy:
fail-fast: false
matrix:
module:
- httpgate
with:
module: ${{ matrix.module }}
push_image: ${{ (github.event_name == 'push') || (github.event_name == 'create') || (inputs.push_image == true) }}
push_image_tag: ${{ inputs.push_image_tag }}
-3
View File
@@ -1,3 +0,0 @@
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
# Ignore build and test binaries.
#bin/
-30
View File
@@ -1,30 +0,0 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/*
Dockerfile.cross
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Go workspace file
go.work
# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*
# editor and IDE paraphernalia
.idea
.vscode
*.swp
*.swo
*~
# ignore deploy.yaml
deploy/manifests/deploy.yaml
-22
View File
@@ -1,22 +0,0 @@
# Copyright © 2023 sealos.
#
# 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.
FROM gcr.io/distroless/static:nonroot
ARG TARGETARCH
WORKDIR /
USER 65532:65532
COPY bin/controller-devbox-$TARGETARCH /manager
ENTRYPOINT ["/manager"]
-146
View File
@@ -1,146 +0,0 @@
# Image URL to use all building/pushing image targets
IMG ?= ghcr.io/labring/sealos-devbox-controller:latest
TARGETARCH ?= amd64
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.28.0
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
# Setting SHELL to bash allows bash commands to be executed by recipes.
# This is a requirement for 'setup-envtest.sh' in the test target.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec
.PHONY: all
all: build
##@ General
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for devbox formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php
.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Development
.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
.PHONY: fmt
fmt: ## Run go fmt against code.
go fmt ./...
.PHONY: vet
vet: ## Run go vet against code.
go vet ./...
.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
##@ Build
.PHONY: build
build: ## Build manager binary.
CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "-s -w" -trimpath -o bin/manager cmd/main.go
.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
go run ./cmd/main.go
.PHONY: debug
debug:
go run ./cmd/main.go --debug
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
mv bin/manager bin/controller-devbox-${TARGETARCH}
chmod +x bin/controller-devbox-${TARGETARCH}
docker build -t ${IMG} . --build-arg TARGETARCH=${TARGETARCH}
.PHONY: docker-push
docker-push: ## Push docker image with the manager.
docker push ${IMG}
##@ Deployment
ifndef ignore-not-found
ignore-not-found = false
endif
.PHONY: install
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
$(KUSTOMIZE) build config/crd | kubectl apply -f -
.PHONY: uninstall
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
$(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
.PHONY: pre-deploy
pre-deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default > deploy/manifests/deploy.yaml
.PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | kubectl apply -f -
.PHONY: undeploy
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
$(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
##@ Build Dependencies
## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p $(LOCALBIN)
## Tool Binaries
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
## Tool Versions
KUSTOMIZE_VERSION ?= v5.3.0
CONTROLLER_TOOLS_VERSION ?= v0.18.0
KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN)
curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN)
.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN)
GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
.PHONY: envtest
envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
$(ENVTEST): $(LOCALBIN)
GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
-38
View File
@@ -1,38 +0,0 @@
# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
domain: sealos.io
layout:
- go.kubebuilder.io/v4
projectName: devbox
repo: github.com/labring/sealos/controllers/devbox
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: sealos.io
group: devbox
kind: Devbox
path: github.com/labring/sealos/controllers/devbox/api/v1alpha2
version: v1alpha2
- api:
crdVersion: v1
namespaced: true
controller: true
domain: sealos.io
group: devbox
kind: DevBoxReleases
path: github.com/labring/sealos/controllers/devbox/api/v1alpha2
version: v1alpha2
- api:
crdVersion: v1
namespaced: true
controller: true
domain: sealos.io
group: devbox
kind: DevBoxRelease
path: github.com/labring/sealos/controllers/devbox/api/v1alpha2
version: v1alpha2
version: "3"
-115
View File
@@ -1,115 +0,0 @@
# devbox
// TODO(user): Add simple overview of use/purpose
## Description
// TODO(user): An in-depth paragraph about your project and overview of use
## Getting Started
### Prerequisites
- go version v1.22.0+
- docker version 17.03+.
- kubectl version v1.11.3+.
- Access to a Kubernetes v1.11.3+ cluster.
### To Deploy on the cluster
**Build and push your image to the location specified by `IMG`:**
```sh
make docker-build docker-push IMG=<some-registry>/devbox:tag
```
**NOTE:** This image ought to be published in the personal registry you specified.
And it is required to have access to pull the image from the working environment.
Make sure you have the proper permission to the registry if the above commands dont work.
**Install the CRDs into the cluster:**
```sh
make install
```
**Deploy the Manager to the cluster with the image specified by `IMG`:**
```sh
make deploy IMG=<some-registry>/devbox:tag
```
> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin
privileges or be logged in as admin.
**Create instances of your solution**
You can apply the samples (examples) from the config/sample:
```sh
kubectl apply -k config/samples/
```
>**NOTE**: Ensure that the samples has default values to test it out.
### To Uninstall
**Delete the instances (CRs) from the cluster:**
```sh
kubectl delete -k config/samples/
```
**Delete the APIs(CRDs) from the cluster:**
```sh
make uninstall
```
**UnDeploy the controller from the cluster:**
```sh
make undeploy
```
## Project Distribution
Following are the steps to build the installer and distribute this project to users.
1. Build the installer for the image built and published in the registry:
```sh
make build-installer IMG=<some-registry>/devbox:tag
```
NOTE: The makefile target mentioned above generates an 'install.yaml'
file in the dist directory. This file contains all the resources built
with Kustomize, which are necessary to install this project without
its dependencies.
2. Using the installer
Users can just run kubectl apply -f <URL for YAML BUNDLE> to install the project, i.e.:
```sh
kubectl apply -f https://raw.githubusercontent.com/<org>/devbox/<tag or branch>/dist/install.yaml
```
## Contributing
// TODO(user): Add detailed information on how you would like others to contribute to this project
**NOTE:** Run `make help` for more information on all potential `make` targets
More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
## License
Copyright 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.
# devbox-controller
@@ -1,57 +0,0 @@
package v1alpha2
import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Devbox condition types.
//
// These are intentionally minimal for now: they provide observability and a stable
// surface for future state-machine hardening (e.g. preventing commit from being
// skipped due to concurrent status updates).
const (
// DevboxConditionStateTransitionPending indicates spec.state != status.state and
// a transition is pending processing by the state-change handler.
DevboxConditionStateTransitionPending = "StateTransitionPending"
// DevboxConditionCommitInProgress indicates a commit workflow is in progress for
// a transition (typically Running/Paused -> Shutdown/Stopped).
DevboxConditionCommitInProgress = "CommitInProgress"
// Resource sync conditions (controller reconcile steps).
DevboxConditionSecretSynced = "SecretSynced"
DevboxConditionStartupConfigMapSynced = "StartupConfigMapSynced"
DevboxConditionNetworkSynced = "NetworkSynced"
DevboxConditionPodSynced = "PodSynced"
DevboxConditionPhaseSynced = "PhaseSynced"
)
// Devbox condition reasons.
const (
DevboxReasonSpecStateChanged = "SpecStateChanged"
DevboxReasonCommitStarted = "CommitStarted"
DevboxReasonCommitSucceeded = "CommitSucceeded"
DevboxReasonCommitFailed = "CommitFailed"
DevboxReasonStateTransitionSynced = "StateTransitionSynced"
DevboxReasonCommitNotInProgress = "CommitNotInProgress"
DevboxReasonSyncSucceeded = "SyncSucceeded"
DevboxReasonSyncFailed = "SyncFailed"
DevboxReasonNotConfigured = "NotConfigured"
)
// SetCondition sets (or updates) a status condition on the Devbox.
func (d *Devbox) SetCondition(cond metav1.Condition) {
meta.SetStatusCondition(&d.Status.Conditions, cond)
}
// GetCondition returns the condition pointer if present.
func (d *Devbox) GetCondition(conditionType string) *metav1.Condition {
for i := range d.Status.Conditions {
if d.Status.Conditions[i].Type == conditionType {
return &d.Status.Conditions[i]
}
}
return nil
}
@@ -1,317 +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 v1alpha2
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// FinalizerName is the finalizer for Devbox
FinalizerName = "devbox.sealos.io/finalizer"
// Annotate the devbox pod with the devbox init
AnnotationInit = "devbox.sealos.io/init"
// Annotate the devbox pod with the storage limit
AnnotationStorageLimit = "devbox.sealos.io/storage-limit"
// Annotate the devbox pod with the devbox part of
AnnotationContentID = "devbox.sealos.io/content-id"
// Annotate the devbox node with container filesystem threshold
AnnotationContainerFSAvailableThreshold = "devbox.sealos.io/container-fs-available-threshold"
// Annotate the devbox node with cpu request and limit ratio
AnnotationCPURequestRatio = "devbox.sealos.io/cpu-request-ratio"
AnnotationCPULimitRatio = "devbox.sealos.io/cpu-limit-ratio"
// Annotate the devbox node with memory request and limit ratio
AnnotationMemoryRequestRatio = "devbox.sealos.io/memory-request-ratio"
AnnotationMemoryLimitRatio = "devbox.sealos.io/memory-limit-ratio"
// Annotate the devbox pod with the runtime
AnnotationRuntime = "io.containerd.cri.runtime-handler"
// Annotate the devbox pod with the blockio resources
AnnotationBlockIOResources = "blockio.resources.beta.kubernetes.io/pod"
// Label the devbox pod with the devbox part of
LabelDevBoxPartOf = "devbox"
// Index for pod node name
PodNodeNameIndex = "spec.nodeName"
// Pod runtime handler for devbox pod
PodRuntimeHandler = "devbox-runc"
)
type DevboxState string
const (
// DevboxStateRunning means the Devbox is running
DevboxStateRunning DevboxState = "Running"
// DevboxStatePending means the Devbox is pending
DevboxStatePending DevboxState = "Pending"
// DevboxStatePaused means the Devbox is paused, pod will be released but content lv and nodeport service will be retained
DevboxStatePaused DevboxState = "Paused"
// DevboxStateStopped means the Devbox is stopped, pod and content lv will be released but nodeport service will be retained
DevboxStateStopped DevboxState = "Stopped"
// DevboxStateShutdown means the devbox is shutdown, pod, content lv and nodeport service will be released
DevboxStateShutdown DevboxState = "Shutdown"
)
type NetworkType string
const (
NetworkTypeNodePort NetworkType = "NodePort"
NetworkTypeTailnet NetworkType = "Tailnet"
NetworkTypeSSHGate NetworkType = "SSHGate"
)
type RuntimeRef struct {
// +kubebuilder:validation:Required
Name string `json:"name"`
// +kubebuilder:validation:Optional
Namespace string `json:"namespace,omitempty"`
}
type NetworkSpec struct {
// +kubebuilder:validation:Required
// +kubebuilder:validation:Enum=NodePort;Tailnet;SSHGate
Type NetworkType `json:"type"`
// +kubebuilder:validation:Optional
ExtraPorts []corev1.ContainerPort `json:"extraPorts,omitempty"`
}
type Config struct {
// +kubebuilder:validation:Optional
// +kubebuilder:default=devbox
User string `json:"user"`
// +kubebuilder:validation:Optional
Labels map[string]string `json:"labels,omitempty"`
// +kubebuilder:validation:Optional
Annotations map[string]string `json:"annotations,omitempty"`
// +kubebuilder:validation:Optional
Command []string `json:"command,omitempty"`
// kubebuilder:validation:Optional
Args []string `json:"args,omitempty"`
// +kubebuilder:validation:Optional
// +kubebuilder:default=/home/devbox/project
WorkingDir string `json:"workingDir,omitempty"`
// +kubebuilder:validation:Optional
Env []corev1.EnvVar `json:"env,omitempty"`
// +kubebuilder:validation:Optional
// +kubebuilder:default={/bin/bash,-c}
ReleaseCommand []string `json:"releaseCommand,omitempty"`
// +kubebuilder:validation:Optional
// +kubebuilder:default={/home/devbox/project/entrypoint.sh}
ReleaseArgs []string `json:"releaseArgs,omitempty"`
// TODO: in v1alpha2 api we need fix the port and app port into one field and create a new type for it.
// +kubebuilder:validation:Optional
// +kubebuilder:default={{name:"devbox-ssh-port",containerPort:22,protocol:TCP}}
Ports []corev1.ContainerPort `json:"ports,omitempty"`
// +kubebuilder:validation:Optional
// +kubebuilder:default={{name:"devbox-app-port",port:8080,protocol:TCP}}
AppPorts []corev1.ServicePort `json:"appPorts,omitempty"`
// +kubebuilder:validation:Optional
VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`
// +kubebuilder:validation:Optional
Volumes []corev1.Volume `json:"volumes,omitempty"`
}
// DevboxSpec defines the desired state of Devbox
type DevboxSpec struct {
// +kubebuilder:validation:Required
// +kubebuilder:validation:Enum=Running;Paused;Stopped;Shutdown
// +kubebuilder:default=Running
State DevboxState `json:"state"`
// +kubebuilder:validation:Required
Resource corev1.ResourceList `json:"resource"`
// +kubebuilder:validation:Required
Image string `json:"image"`
// +kubebuilder:validation:Optional
TemplateID string `json:"templateID"`
// +kubebuilder:validation:Required
Config Config `json:"config"`
// +kubebuilder:validation:Optional
// devbox storage limit, `storageLimit` will be used to generate the devbox pod label.
StorageLimit string `json:"storageLimit,omitempty"`
// +kubebuilder:validation:Required
NetworkSpec NetworkSpec `json:"network,omitempty"`
// +kubebuilder:validation:Optional
RuntimeClassName string `json:"runtimeClassName,omitempty"`
// +kubebuilder:validation:Optional
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// +kubebuilder:validation:Optional
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
// +kubebuilder:validation:Optional
Affinity *corev1.Affinity `json:"affinity,omitempty"`
}
type NetworkStatus struct {
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Enum=NodePort;Tailnet;SSHGate
Type NetworkType `json:"type,omitempty"`
// +kubebuilder:validation:Optional
NodePort int32 `json:"nodePort,omitempty"`
// +kubebuilder:validation:Optional
UniqueID string `json:"uniqueID,omitempty"`
// todo TailNet
// +kubebuilder:validation:Optional
TailNet string `json:"tailnet,omitempty"`
}
type CommitStatus string
const (
CommitStatusSuccess CommitStatus = "Success"
CommitStatusFailed CommitStatus = "Failed"
CommitStatusPending CommitStatus = "Pending"
CommitStatusCommitting CommitStatus = "Committing"
)
type DevboxPhase string
const (
// DevboxPhaseRunning means Devbox is run and run success
DevboxPhaseRunning DevboxPhase = "Running"
// DevboxPhasePending means Devbox is run but not run success
DevboxPhasePending DevboxPhase = "Pending"
// DevboxPhasePaused means Devbox is paused and paused success
DevboxPhasePaused DevboxPhase = "Paused"
// DevboxPhasePausing means Devbox is pausing
DevboxPhasePausing DevboxPhase = "Pausing"
// DevboxPhaseStopped means Devbox is stop and stopped success
DevboxPhaseStopped DevboxPhase = "Stopped"
// DevboxPhaseStopping means Devbox is stopping
DevboxPhaseStopping DevboxPhase = "Stopping"
// DevboxPhaseShutdown means Devbox is shutdown and service is deleted
DevboxPhaseShutdown DevboxPhase = "Shutdown"
// DevboxPhaseShutting means Devbox is shutting
DevboxPhaseShutting DevboxPhase = "Shutting"
// DevboxPhaseError means Devbox is error
DevboxPhaseError DevboxPhase = "Error"
// DevboxPhaseUnknown means Devbox is unknown
DevboxPhaseUnknown DevboxPhase = "Unknown"
)
type CommitRecord struct {
// BaseImage is the image of the that devbox is running on
// +kubebuilder:validation:Optional
BaseImage string `json:"baseImage"`
// CommitImage is the image of the that devbox is committed to
// +kubebuilder:validation:Optional
CommitImage string `json:"commitImage"`
// Node is the node name
// +kubebuilder:validation:Optional
Node string `json:"node"`
// GenerateTime is the time when the commit is generated
// +kubebuilder:validation:Optional
GenerateTime metav1.Time `json:"generateTime"`
// ScheduleTime is the time when the commit is scheduled
// +kubebuilder:validation:Optional
ScheduleTime metav1.Time `json:"scheduleTime"`
// UpdateTime is the time when the commit is updated
// +kubebuilder:validation:Optional
UpdateTime metav1.Time `json:"updateTime"`
// CommitTime is the time when the commit is created
// +kubebuilder:validation:Optional
CommitTime metav1.Time `json:"commitTime"`
// CommitStatus is the status of the commit
// +kubebuilder:validation:Enum=Success;Failed;Pending;Committing
// +kubebuilder:default=Pending
CommitStatus CommitStatus `json:"commitStatus"`
}
// CommitRecordMap is a map of commit records, key is the commit id
type CommitRecordMap map[string]*CommitRecord
// DevboxStatus defines the observed state of Devbox
type DevboxStatus struct {
// ObservedGeneration is the most recent generation observed by the controller.
// It is updated when the controller has reconciled the spec for that generation.
// +kubebuilder:validation:Optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Conditions represent the latest available observations of the Devbox's state.
// +kubebuilder:validation:Optional
// +kubebuilder:default={}
Conditions []metav1.Condition `json:"conditions,omitempty"`
// +kubebuilder:validation:Optional
ContentID string `json:"contentID"`
// +kubebuilder:validation:Optional
Node string `json:"node"`
// +kubebuilder:validation:Optional
// +kubebuilder:default=Running
State DevboxState `json:"state"`
// CommitRecords is the records of the devbox commits
CommitRecords CommitRecordMap `json:"commitRecords"`
// +kubebuilder:validation:Optional
Phase DevboxPhase `json:"phase"`
// +kubebuilder:validation:Optional
Network NetworkStatus `json:"network,omitempty"`
// +kubebuilder:validation:Optional
LastContainerStatus corev1.ContainerStatus `json:"lastContainerStatus"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".spec.state"
// +kubebuilder:printcolumn:name="NetworkType",type="string",JSONPath=".status.network.type"
// +kubebuilder:printcolumn:name="UniqueID",type="string",JSONPath=".status.network.uniqueID"
// +kubebuilder:printcolumn:name="NodePort",type="integer",JSONPath=".status.network.nodePort"
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
// +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.node"
// Devbox is the Schema for the devboxes API
type Devbox struct {
// +kubebuilder:validation:XValidation:rule="self.spec.state == oldSelf.spec.state || (self.status.contentID in self.status.commitRecords && self.status.commitRecords[self.status.contentID].commitStatus != 'Committing')"
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec DevboxSpec `json:"spec,omitempty"`
Status DevboxStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// DevboxList contains a list of Devbox
type DevboxList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Devbox `json:"items"`
}
func init() {
SchemeBuilder.Register(&Devbox{}, &DevboxList{})
}
@@ -1,86 +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 v1alpha2
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// DevBoxReleaseSpec defines the desired state of Devboxrelease.
type DevBoxReleaseSpec struct {
// +kubebuilder:validation:Required
DevboxName string `json:"devboxName"`
// +kubebuilder:validation:Required
Version string `json:"version"`
// +kubebuilder:validation:Optional
Notes string `json:"notes,omitempty"`
// +kubebuilder:validation:Optional
// +kubebuilder:default=false
StartDevboxAfterRelease bool `json:"startDevboxAfterRelease,omitempty"`
}
type DevBoxReleasePhase string
const (
// DevBoxReleasePhaseSuccess means the Devbox has been released
DevBoxReleasePhaseSuccess DevBoxReleasePhase = "Success"
// DevBoxReleasePhasePending means the Devbox has not been released
DevBoxReleasePhasePending DevBoxReleasePhase = "Pending"
// DevBoxReleasePhaseFailed means the Devbox has not been released
DevBoxReleasePhaseFailed DevBoxReleasePhase = "Failed"
)
type DevBoxReleaseStatus struct {
// +kubebuilder:validation:Optional
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Success;Pending;Failed
Phase DevBoxReleasePhase `json:"phase,omitempty"`
OriginalDevboxState DevboxState `json:"originalDevboxState,omitempty"`
SourceImage string `json:"sourceImage,omitempty"`
TargetImage string `json:"targetImage,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".spec.version"
// +kubebuilder:printcolumn:name="SourceImage",type="string",JSONPath=".status.sourceImage"
// +kubebuilder:printcolumn:name="TargetImage",type="string",JSONPath=".status.targetImage"
// +kubebuilder:printcolumn:name="StartDevboxAfterRelease",type="boolean",JSONPath=".spec.startDevboxAfterRelease"
// DevBoxRelease is the Schema for the devboxreleases API.
type DevBoxRelease struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec DevBoxReleaseSpec `json:"spec,omitempty"`
Status DevBoxReleaseStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// DevBoxReleaseList contains a list of DevBoxRelease.
type DevBoxReleaseList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []DevBoxRelease `json:"items"`
}
func init() {
SchemeBuilder.Register(&DevBoxRelease{}, &DevBoxReleaseList{})
}
@@ -1,36 +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 v1alpha2 contains API Schema definitions for the devbox v1alpha2 API group
// +kubebuilder:object:generate=true
// +groupName=devbox.sealos.io
package v1alpha2
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "devbox.sealos.io", Version: "v1alpha2"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
@@ -1,431 +0,0 @@
//go:build !ignore_autogenerated
/*
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.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha2
import (
"k8s.io/api/core/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CommitRecord) DeepCopyInto(out *CommitRecord) {
*out = *in
in.GenerateTime.DeepCopyInto(&out.GenerateTime)
in.ScheduleTime.DeepCopyInto(&out.ScheduleTime)
in.UpdateTime.DeepCopyInto(&out.UpdateTime)
in.CommitTime.DeepCopyInto(&out.CommitTime)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommitRecord.
func (in *CommitRecord) DeepCopy() *CommitRecord {
if in == nil {
return nil
}
out := new(CommitRecord)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in CommitRecordMap) DeepCopyInto(out *CommitRecordMap) {
{
in := &in
*out = make(CommitRecordMap, len(*in))
for key, val := range *in {
var outVal *CommitRecord
if val == nil {
(*out)[key] = nil
} else {
inVal := (*in)[key]
in, out := &inVal, &outVal
*out = new(CommitRecord)
(*in).DeepCopyInto(*out)
}
(*out)[key] = outVal
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommitRecordMap.
func (in CommitRecordMap) DeepCopy() CommitRecordMap {
if in == nil {
return nil
}
out := new(CommitRecordMap)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Config) DeepCopyInto(out *Config) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Command != nil {
in, out := &in.Command, &out.Command
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Args != nil {
in, out := &in.Args, &out.Args
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Env != nil {
in, out := &in.Env, &out.Env
*out = make([]v1.EnvVar, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ReleaseCommand != nil {
in, out := &in.ReleaseCommand, &out.ReleaseCommand
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ReleaseArgs != nil {
in, out := &in.ReleaseArgs, &out.ReleaseArgs
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Ports != nil {
in, out := &in.Ports, &out.Ports
*out = make([]v1.ContainerPort, len(*in))
copy(*out, *in)
}
if in.AppPorts != nil {
in, out := &in.AppPorts, &out.AppPorts
*out = make([]v1.ServicePort, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.VolumeMounts != nil {
in, out := &in.VolumeMounts, &out.VolumeMounts
*out = make([]v1.VolumeMount, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Volumes != nil {
in, out := &in.Volumes, &out.Volumes
*out = make([]v1.Volume, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config.
func (in *Config) DeepCopy() *Config {
if in == nil {
return nil
}
out := new(Config)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DevBoxRelease) DeepCopyInto(out *DevBoxRelease) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevBoxRelease.
func (in *DevBoxRelease) DeepCopy() *DevBoxRelease {
if in == nil {
return nil
}
out := new(DevBoxRelease)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DevBoxRelease) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DevBoxReleaseList) DeepCopyInto(out *DevBoxReleaseList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]DevBoxRelease, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevBoxReleaseList.
func (in *DevBoxReleaseList) DeepCopy() *DevBoxReleaseList {
if in == nil {
return nil
}
out := new(DevBoxReleaseList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DevBoxReleaseList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DevBoxReleaseSpec) DeepCopyInto(out *DevBoxReleaseSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevBoxReleaseSpec.
func (in *DevBoxReleaseSpec) DeepCopy() *DevBoxReleaseSpec {
if in == nil {
return nil
}
out := new(DevBoxReleaseSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DevBoxReleaseStatus) DeepCopyInto(out *DevBoxReleaseStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevBoxReleaseStatus.
func (in *DevBoxReleaseStatus) DeepCopy() *DevBoxReleaseStatus {
if in == nil {
return nil
}
out := new(DevBoxReleaseStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Devbox) DeepCopyInto(out *Devbox) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Devbox.
func (in *Devbox) DeepCopy() *Devbox {
if in == nil {
return nil
}
out := new(Devbox)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Devbox) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DevboxList) DeepCopyInto(out *DevboxList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Devbox, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevboxList.
func (in *DevboxList) DeepCopy() *DevboxList {
if in == nil {
return nil
}
out := new(DevboxList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DevboxList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DevboxSpec) DeepCopyInto(out *DevboxSpec) {
*out = *in
if in.Resource != nil {
in, out := &in.Resource, &out.Resource
*out = make(v1.ResourceList, len(*in))
for key, val := range *in {
(*out)[key] = val.DeepCopy()
}
}
in.Config.DeepCopyInto(&out.Config)
in.NetworkSpec.DeepCopyInto(&out.NetworkSpec)
if in.NodeSelector != nil {
in, out := &in.NodeSelector, &out.NodeSelector
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]v1.Toleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Affinity != nil {
in, out := &in.Affinity, &out.Affinity
*out = new(v1.Affinity)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevboxSpec.
func (in *DevboxSpec) DeepCopy() *DevboxSpec {
if in == nil {
return nil
}
out := new(DevboxSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DevboxStatus) DeepCopyInto(out *DevboxStatus) {
*out = *in
if in.CommitRecords != nil {
in, out := &in.CommitRecords, &out.CommitRecords
*out = make(CommitRecordMap, len(*in))
for key, val := range *in {
var outVal *CommitRecord
if val == nil {
(*out)[key] = nil
} else {
inVal := (*in)[key]
in, out := &inVal, &outVal
*out = new(CommitRecord)
(*in).DeepCopyInto(*out)
}
(*out)[key] = outVal
}
}
out.Network = in.Network
in.LastContainerStatus.DeepCopyInto(&out.LastContainerStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevboxStatus.
func (in *DevboxStatus) DeepCopy() *DevboxStatus {
if in == nil {
return nil
}
out := new(DevboxStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkSpec) DeepCopyInto(out *NetworkSpec) {
*out = *in
if in.ExtraPorts != nil {
in, out := &in.ExtraPorts, &out.ExtraPorts
*out = make([]v1.ContainerPort, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkSpec.
func (in *NetworkSpec) DeepCopy() *NetworkSpec {
if in == nil {
return nil
}
out := new(NetworkSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkStatus) DeepCopyInto(out *NetworkStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkStatus.
func (in *NetworkStatus) DeepCopy() *NetworkStatus {
if in == nil {
return nil
}
out := new(NetworkStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RuntimeRef) DeepCopyInto(out *RuntimeRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeRef.
func (in *RuntimeRef) DeepCopy() *RuntimeRef {
if in == nil {
return nil
}
out := new(RuntimeRef)
in.DeepCopyInto(out)
return out
}
-475
View File
@@ -1,475 +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 main
import (
"context"
"crypto/tls"
"flag"
"os"
"time"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
"github.com/labring/sealos/controllers/devbox/internal/commit"
"github.com/labring/sealos/controllers/devbox/internal/controller"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/matcher"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/nodes"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/registry"
utilresource "github.com/labring/sealos/controllers/devbox/internal/controller/utils/resource"
"github.com/labring/sealos/controllers/devbox/internal/stat"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlconfig "sigs.k8s.io/controller-runtime/pkg/config"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(devboxv1alpha2.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)
// debug flag
var debugMode bool
// registry flag
var registryAddr string
var registryUser string
var registryPassword string
// resource flag
var requestCPURate float64
var requestMemoryRate float64
var requestEphemeralStorage string
var limitEphemeralStorage string
var maximumLimitEphemeralStorage string
// pod matcher flag
var enablePodResourceMatcher bool
var enablePodEnvMatcher bool
var enablePodPortMatcher bool
var enablePodEphemeralStorageMatcher bool
var enablePodStorageLimitMatcher bool
// config qps and burst
var configQPS int
var configBurst int
// config restart predicate duration
var restartPredicateDuration time.Duration
// devbox node label
var devboxNodeLabel string
var acceptanceThreshold int
// merge base image layers flag
var mergeBaseImageTopLayer bool
// default base image flag for setLvRemovable's temp container
var defaultBaseImage string
// when this option is enabled, the controller will set up the block io resource configuration of a devbox pod
var enableBlockIOResouce bool
flag.StringVar(
&defaultBaseImage,
"default-base-image",
"alpine:3.19",
"The default base image for setLvRemovable's temp container",
)
flag.StringVar(
&metricsAddr,
"metrics-bind-address",
"0",
"The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.",
)
flag.StringVar(
&probeAddr,
"health-probe-bind-address",
":8081",
"The address the probe endpoint binds to.",
)
flag.BoolVar(
&secureMetrics,
"metrics-secure",
true,
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.",
)
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
// debug flag
flag.BoolVar(&debugMode, "debug", false, "If set, debug mode will be enabled")
// registry flag
flag.StringVar(&registryAddr, "registry-addr", "sealos.hub:5000", "The address of the registry")
flag.StringVar(&registryUser, "registry-user", "admin", "The user of the registry")
flag.StringVar(
&registryPassword,
"registry-password",
"passw0rd",
"The password of the registry",
)
// resource flag
flag.Float64Var(
&requestCPURate,
"request-cpu-rate",
10,
"The request rate of cpu limit in devbox.",
)
flag.Float64Var(
&requestMemoryRate,
"request-memory-rate",
10,
"The request rate of memory limit in devbox.",
)
flag.StringVar(
&requestEphemeralStorage,
"request-ephemeral-storage",
"500Mi",
"The default request value of ephemeral storage in devbox.",
)
flag.StringVar(
&limitEphemeralStorage,
"limit-ephemeral-storage",
"10Gi",
"The default limit value of ephemeral storage in devbox.",
)
flag.StringVar(
&maximumLimitEphemeralStorage,
"maximum-limit-ephemeral-storage",
"50Gi",
"The maximum limit value of ephemeral storage in devbox.",
)
// pod matcher flag, pod resource matcher, env matcher, port matcher will be enabled by default, ephemeral storage matcher will be disabled by default
flag.BoolVar(
&enablePodResourceMatcher,
"enable-pod-resource-matcher",
true,
"If set, pod resource matcher will be enabled",
)
flag.BoolVar(
&enablePodEnvMatcher,
"enable-pod-env-matcher",
true,
"If set, pod env matcher will be enabled",
)
flag.BoolVar(
&enablePodPortMatcher,
"enable-pod-port-matcher",
true,
"If set, pod port matcher will be enabled",
)
flag.BoolVar(
&enablePodEphemeralStorageMatcher,
"enable-pod-ephemeral-storage-matcher",
false,
"If set, pod ephemeral storage matcher will be enabled",
)
flag.BoolVar(
&enablePodStorageLimitMatcher,
"enable-pod-storage-limit-matcher",
false,
"If set, pod storage limit matcher will be enabled",
)
// config qps and burst
flag.IntVar(&configQPS, "config-qps", 50, "The qps of the config")
flag.IntVar(&configBurst, "config-burst", 100, "The burst of the config")
// config restart predicate duration
flag.DurationVar(
&restartPredicateDuration,
"restart-predicate-duration",
10000*time.Hour,
"Sets the restart predicate time duration for devbox controller restart. By default, the duration is set to 2 hours.",
)
// devbox node label
flag.StringVar(
&devboxNodeLabel,
"devbox-node-label",
"devbox.sealos.io/node",
"The label of the devbox node",
)
// scheduling flags
flag.IntVar(
&acceptanceThreshold,
"acceptance-threshold",
16,
"The minimum acceptance score for scheduling devbox to node. Default is 16, which means the node must have enough resources to run the devbox.",
)
// merge base image layers flag
flag.BoolVar(
&mergeBaseImageTopLayer,
"merge-base-image-top-layer",
false,
"If set true, devbox will merge base image top layers during create and remove top layer during commit.",
)
flag.BoolVar(
&enableBlockIOResouce,
"enable-block-io-resource",
false,
"If this option is set to true, the controller will set up the block io resource configuration of a devbox pod",
)
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
// Rapid Reset CVEs. For more information see:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("disabling http/2")
c.NextProtos = []string{"http/1.1"}
}
if !enableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}
webhookServer := webhook.NewServer(webhook.Options{
TLSOpts: tlsOpts,
})
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
// More info:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
// TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are
// not provided, self-signed certificates will be generated by default. This option is not recommended for
// production environments as self-signed certificates do not offer the same level of trust and security
// as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing
// unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName
// to provide certificates, ensuring the server communicates using trusted and secure certificates.
TLSOpts: tlsOpts,
}
if secureMetrics {
// FilterProvider is used to protect the metrics endpoint with authn/authz.
// These configurations ensure that only authorized users and service accounts
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/filters#WithAuthenticationAndAuthorization
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
}
cacheObjLabelSelector := labels.SelectorFromSet(map[string]string{
"app.kubernetes.io/managed-by": "sealos",
"app.kubernetes.io/part-of": "devbox",
})
config := ctrl.GetConfigOrDie()
// set qps and burst to config qps and burst for kube-config
config.QPS = float32(configQPS)
config.Burst = configBurst
mgr, err := ctrl.NewManager(config, ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: false,
// LeaderElectionID: "b6694722.sealos.io",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
NewCache: func(config *rest.Config, opts cache.Options) (cache.Cache, error) {
opts.ByObject = map[client.Object]cache.ByObject{
&corev1.Service{}: {Label: cacheObjLabelSelector},
&corev1.Pod{}: {Label: cacheObjLabelSelector},
&corev1.Secret{}: {Label: cacheObjLabelSelector},
}
// set sync period to 1 hour for devbox controller to reconcile all devboxes.
opts.SyncPeriod = ptr.To(time.Hour)
return cache.New(config, opts)
},
Controller: ctrlconfig.Controller{
UsePriorityQueue: ptr.To(true),
},
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
podMatchers := []matcher.PodMatcher{}
if enablePodResourceMatcher {
podMatchers = append(podMatchers, matcher.ResourceMatcher{})
}
if enablePodEnvMatcher {
podMatchers = append(podMatchers, matcher.EnvVarMatcher{})
}
if enablePodPortMatcher {
podMatchers = append(podMatchers, matcher.PortMatcher{})
}
if enablePodEphemeralStorageMatcher {
podMatchers = append(podMatchers, matcher.EphemeralStorageMatcher{})
}
if enablePodStorageLimitMatcher {
podMatchers = append(podMatchers, matcher.StorageLimitMatcher{})
}
stateChangeBroadcaster := record.NewBroadcaster()
startupCMName := os.Getenv("DEVBOX_STARTUP_CM_NAME")
startupCMNamespace := os.Getenv("DEVBOX_STARTUP_CM_NAMESPACE")
if (startupCMName != "" && startupCMNamespace == "") ||
(startupCMName == "" && startupCMNamespace != "") {
setupLog.Error(
nil,
"both DEVBOX_STARTUP_CM_NAME and DEVBOX_STARTUP_CM_NAMESPACE must be set together",
)
os.Exit(1)
}
if err = (&controller.DevboxReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("devbox-controller"),
StateChangeRecorder: stateChangeBroadcaster.NewRecorder(
mgr.GetScheme(),
corev1.EventSource{Component: "devbox-controller", Host: nodes.GetNodeName()}),
CommitImageRegistry: registryAddr,
RequestRate: utilresource.RequestRate{
CPU: requestCPURate,
Memory: requestMemoryRate,
},
EphemeralStorage: utilresource.EphemeralStorage{
DefaultRequest: resource.MustParse(requestEphemeralStorage),
DefaultLimit: resource.MustParse(limitEphemeralStorage),
MaximumLimit: resource.MustParse(maximumLimitEphemeralStorage),
},
PodMatchers: podMatchers,
DebugMode: debugMode,
EnableBlockIOResource: enableBlockIOResouce,
StartupConfigMapName: startupCMName,
StartupConfigMapNamespace: startupCMNamespace,
RestartPredicateDuration: restartPredicateDuration,
NodeName: nodes.GetNodeName(),
AcceptanceThreshold: acceptanceThreshold,
NodeStatsProvider: &stat.NodeStatsProviderImpl{},
MergeBaseImageTopLayer: mergeBaseImageTopLayer,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Devbox")
os.Exit(1)
}
committer, err := commit.NewCommitter(
registryAddr,
registryUser,
registryPassword,
mergeBaseImageTopLayer,
)
if err != nil {
setupLog.Error(err, "unable to create committer")
os.Exit(1)
}
// if err := committer.InitializeGC(context.Background()); err != nil {
// setupLog.Error(err, "unable to initialize GC")
// os.Exit(1)
// }
stateChangeHandler := controller.EventHandler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("state-change-handler"),
Committer: committer,
CommitImageRegistry: registryAddr,
NodeName: nodes.GetNodeName(),
Logger: ctrl.Log.WithName("state-change-handler"),
DefaultBaseImage: defaultBaseImage,
}
setupLog.Info("StateChangeHandler initialized", "nodeName", nodes.GetNodeName())
watcher := stateChangeBroadcaster.StartEventWatcher(func(event *corev1.Event) {
setupLog.Info("Event received by watcher",
"event", event.Name,
"eventSourceHost", event.Source.Host,
"eventType", event.Type,
"eventReason", event.Reason)
if err := stateChangeHandler.Handle(context.TODO(), event); err != nil {
setupLog.Error(err, "failed to handle event", "event", event.Name)
}
})
defer watcher.Stop()
if err = (&controller.DevboxreleaseReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Registry: registry.Registry{
Host: registryAddr,
BasicAuth: registry.BasicAuth{
Username: registryUser,
Password: registryPassword,
},
},
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Devboxrelease")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
File diff suppressed because it is too large Load Diff
@@ -1,91 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.18.0
name: devboxreleases.devbox.sealos.io
spec:
group: devbox.sealos.io
names:
kind: DevBoxRelease
listKind: DevBoxReleaseList
plural: devboxreleases
singular: devboxrelease
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.phase
name: Phase
type: string
- jsonPath: .spec.version
name: Version
type: string
- jsonPath: .status.sourceImage
name: SourceImage
type: string
- jsonPath: .status.targetImage
name: TargetImage
type: string
- jsonPath: .spec.startDevboxAfterRelease
name: StartDevboxAfterRelease
type: boolean
name: v1alpha2
schema:
openAPIV3Schema:
description: DevBoxRelease is the Schema for the devboxreleases 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: DevBoxReleaseSpec defines the desired state of Devboxrelease.
properties:
devboxName:
type: string
notes:
type: string
startDevboxAfterRelease:
default: false
type: boolean
version:
type: string
required:
- devboxName
- version
type: object
status:
properties:
originalDevboxState:
type: string
phase:
default: Pending
enum:
- Success
- Pending
- Failed
type: string
sourceImage:
type: string
targetImage:
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
@@ -1,37 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
# This kustomization.yaml is not intended to be run by itself,
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/devbox.sealos.io_devboxes.yaml
- bases/devbox.sealos.io_devboxreleases.yaml
# +kubebuilder:scaffold:crdkustomizeresource
patches:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
# +kubebuilder:scaffold:crdkustomizewebhookpatch
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
# patches here are for enabling the CA injection for each CRD
#- path: patches/cainjection_in_devboxes.yaml
# +kubebuilder:scaffold:crdkustomizecainjectionpatch
# [WEBHOOK] To enable webhook, uncomment the following section
# the following config is for teaching kustomize how to do kustomization for CRDs.
#configurations:
#- kustomizeconfig.yaml
@@ -1,33 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhook/clientConfig/service/name
namespace:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhook/clientConfig/service/namespace
create: false
varReference:
- path: metadata/annotations
@@ -1,160 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
# Adds namespace to all resources.
namespace: devbox-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: devbox-
# Labels to add to all resources and selectors.
#labels:
#- includeSelectors: true
# pairs:
# someName: someValue
resources:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
#- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
# [METRICS] Expose the controller manager metrics service.
- metrics_service.yaml
# Uncomment the patches line if you enable Metrics, and/or are using webhooks and cert-manager
patches:
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
# More info: https://book.kubebuilder.io/reference/metrics
- path: manager_metrics_patch.yaml
target:
kind: Deployment
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- path: manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
#- path: webhookcainjection_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
# Uncomment the following replacements to add the cert-manager CA injection annotations
#replacements:
# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
# fieldPath: .metadata.namespace # namespace of the certificate CR
# targets:
# - select:
# kind: ValidatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 0
# create: true
# - select:
# kind: MutatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 0
# create: true
# - select:
# kind: CustomResourceDefinition
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 0
# create: true
# - source:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
# fieldPath: .metadata.name
# targets:
# - select:
# kind: ValidatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 1
# create: true
# - select:
# kind: MutatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 1
# create: true
# - select:
# kind: CustomResourceDefinition
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 1
# create: true
# - source: # Add cert-manager annotation to the webhook Service
# kind: Service
# version: v1
# name: webhook-service
# fieldPath: .metadata.name # namespace of the service
# targets:
# - select:
# kind: Certificate
# group: cert-manager.io
# version: v1
# fieldPaths:
# - .spec.dnsNames.0
# - .spec.dnsNames.1
# options:
# delimiter: '.'
# index: 0
# create: true
# - source:
# kind: Service
# version: v1
# name: webhook-service
# fieldPath: .metadata.namespace # namespace of the service
# targets:
# - select:
# kind: Certificate
# group: cert-manager.io
# version: v1
# fieldPaths:
# - .spec.dnsNames.0
# - .spec.dnsNames.1
# options:
# delimiter: '.'
# index: 1
# create: true
@@ -1,18 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
- op: add
path: /spec/template/spec/containers/0/args/0
value: --metrics-bind-address=:8443
@@ -1,31 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
apiVersion: v1
kind: Service
metadata:
labels:
control-plane: controller-manager
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: controller-manager-metrics-service
namespace: system
spec:
ports:
- name: https
port: 8443
protocol: TCP
targetPort: 8443
selector:
control-plane: controller-manager
@@ -1,22 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
resources:
- manager.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: ghcr.io/labring/sealos-devbox-controller
newTag: latest
@@ -1,112 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
apiVersion: v1
kind: Namespace
metadata:
labels:
control-plane: controller-manager
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
labels:
control-plane: controller-manager
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
spec:
selector:
matchLabels:
control-plane: controller-manager
replicas: 2
template:
metadata:
annotations:
kubectl.kubernetes.io/default-container: manager
labels:
control-plane: controller-manager
spec:
# TODO(user): Uncomment the following code to configure the nodeAffinity expression
# according to the platforms which are supported by your solution.
# It is considered best practice to support multiple architectures. You can
# build your manager image using the makefile target docker-buildx.
# affinity:
# nodeAffinity:
# requiredDuringSchedulingIgnoredDuringExecution:
# nodeSelectorTerms:
# - matchExpressions:
# - key: kubernetes.io/arch
# operator: In
# values:
# - amd64
# - arm64
# - ppc64le
# - s390x
# - key: kubernetes.io/os
# operator: In
# values:
# - linux
securityContext:
runAsNonRoot: true
# TODO(user): For common cases that do not require escalating privileges
# it is recommended to ensure that all your Pods/Containers are restrictive.
# More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
# Please uncomment the following code if your project does NOT have to work on old Kubernetes
# versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
# seccompProfile:
# type: RuntimeDefault
containers:
- command:
- /manager
args:
- --leader-elect
- --health-probe-bind-address=:8081
- --registry-addr={{ .registryAddr }}
- --registry-user={{ .registryUser }}
- --registry-password={{ .registryPassword }}
image: controller:latest
name: manager
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
# TODO(user): Configure the resources accordingly based on the project requirements.
# More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
resources:
limits:
cpu: 1500m
memory: 2000Mi
requests:
cpu: 100m
memory: 640Mi
serviceAccountName: controller-manager
terminationGracePeriodSeconds: 10
@@ -1,16 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
resources:
- monitor.yaml
@@ -1,44 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
control-plane: controller-manager
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: controller-manager-metrics-monitor
namespace: system
spec:
endpoints:
- path: /metrics
port: https # Ensure this is the name of the port that exposes HTTPS metrics
scheme: https
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
tlsConfig:
# TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
# certificate verification. This poses a significant security risk by making the system vulnerable to
# man-in-the-middle attacks, where an attacker could intercept and manipulate the communication between
# Prometheus and the monitored services. This could lead to unauthorized access to sensitive metrics data,
# compromising the integrity and confidentiality of the information.
# Please use the following options for secure configurations:
# caFile: /etc/metrics-certs/ca.crt
# certFile: /etc/metrics-certs/tls.crt
# keyFile: /etc/metrics-certs/tls.key
insecureSkipVerify: true
selector:
matchLabels:
control-plane: controller-manager
@@ -1,41 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
# permissions for end users to edit devboxes.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: devbox-editor-role
rules:
- apiGroups:
- devbox.sealos.io
resources:
- devboxes
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- devbox.sealos.io
resources:
- devboxes/status
verbs:
- get
@@ -1,37 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
# permissions for end users to view devboxes.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: devbox-viewer-role
rules:
- apiGroups:
- devbox.sealos.io
resources:
- devboxes
verbs:
- get
- list
- watch
- apiGroups:
- devbox.sealos.io
resources:
- devboxes/status
verbs:
- get
@@ -1,27 +0,0 @@
# This rule is not used by the project devbox itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over devbox.sealos.io.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: devboxrelease-admin-role
rules:
- apiGroups:
- devbox.sealos.io
resources:
- devboxreleases
verbs:
- '*'
- apiGroups:
- devbox.sealos.io
resources:
- devboxreleases/status
verbs:
- get
@@ -1,33 +0,0 @@
# This rule is not used by the project devbox itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the devbox.sealos.io.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: devboxrelease-editor-role
rules:
- apiGroups:
- devbox.sealos.io
resources:
- devboxreleases
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- devbox.sealos.io
resources:
- devboxreleases/status
verbs:
- get
@@ -1,29 +0,0 @@
# This rule is not used by the project devbox itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to devbox.sealos.io resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: devboxrelease-viewer-role
rules:
- apiGroups:
- devbox.sealos.io
resources:
- devboxreleases
verbs:
- get
- list
- watch
- apiGroups:
- devbox.sealos.io
resources:
- devboxreleases/status
verbs:
- get
@@ -1,47 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
resources:
# All RBAC will be applied under this service account in
# the deployment namespace. You may comment out this resource
# if your manager will use a service account that exists at
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
# subjects if changing service account names.
- service_account.yaml
- role.yaml
- role_binding.yaml
- leader_election_role.yaml
- leader_election_role_binding.yaml
# The following RBAC configurations are used to protect
# the metrics endpoint with authn/authz. These configurations
# ensure that only authorized users and service accounts
# can access the metrics endpoint. Comment the following
# permissions if you want to disable this protection.
# More info: https://book.kubebuilder.io/reference/metrics.html
- metrics_auth_role.yaml
- metrics_auth_role_binding.yaml
- metrics_reader_role.yaml
# For each CRD, "Editor" and "Viewer" roles are scaffolded by
# default, aiding admins in cluster management. Those roles are
# not used by the Project itself. You can comment the following lines
# if you do not want those helpers be installed with your Project.
- devbox_editor_role.yaml
- devbox_viewer_role.yaml
# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
# default, aiding admins in cluster management. Those roles are
# not used by the {{ .ProjectName }} itself. You can comment the following lines
# if you do not want those helpers be installed with your Project.
- devboxrelease_admin_role.yaml
- devboxrelease_editor_role.yaml
- devboxrelease_viewer_role.yaml
@@ -1,54 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
# permissions to do leader election.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: leader-election-role
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
@@ -1,29 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: leader-election-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: leader-election-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system
@@ -1,31 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-auth-role
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
@@ -1,26 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: metrics-auth-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: metrics-auth-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system
@@ -1,23 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-reader
rules:
- nonResourceURLs:
- "/metrics"
verbs:
- get
-69
View File
@@ -1,69 +0,0 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups:
- ""
resources:
- configmaps
- events
- pods
- secrets
- services
verbs:
- '*'
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- nodes/status
verbs:
- get
- apiGroups:
- ""
resources:
- pods/status
verbs:
- get
- patch
- update
- apiGroups:
- devbox.sealos.io
resources:
- devboxes
- devboxreleases
- runtimeclasses
- runtimes
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- devbox.sealos.io
resources:
- devboxes/finalizers
- devboxreleases/finalizers
verbs:
- update
- apiGroups:
- devbox.sealos.io
resources:
- devboxes/status
- devboxreleases/status
verbs:
- get
- patch
- update
@@ -1,29 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: manager-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: manager-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system
@@ -1,22 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: controller-manager
namespace: system
@@ -1,23 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
apiVersion: devbox.sealos.io/v1alpha2
kind: OperationRequest
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: operationrequest-sample
spec:
# TODO(user): Add fields here
@@ -1,11 +0,0 @@
apiVersion: devbox.sealos.io/v1alpha2
kind: Devboxrelease
metadata:
labels:
app.kubernetes.io/name: devbox
app.kubernetes.io/managed-by: kustomize
name: devboxrelease-sample
spec:
devboxName: devbox-sample-1
version: 1.0.0
notes: "This is a sample devbox release"
@@ -1,20 +0,0 @@
# Copyright © 2024 sealos.
#
# 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.
## Append samples of your project ##
resources:
- devbox_v1alpha2_devbox.yaml
- devbox_v1alpha2_devboxreleases.yaml
- devbox_v1alpha2_devboxrelease.yaml
# +kubebuilder:scaffold:manifestskustomizesamples
-13
View File
@@ -1,13 +0,0 @@
FROM scratch
USER 65532:65532
COPY registry registry
COPY manifests manifests
ENV registryAddr="sealos.hub:5000"
ENV registryUser=admin
ENV registryPassword=passw0rd
ENV authAddr="sealos.hub:5000"
CMD ["kubectl apply -f manifests"]
File diff suppressed because it is too large Load Diff
@@ -1,37 +0,0 @@
# Copyright © 2023 sealos.
#
# 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.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: devbox-default-user
namespace: devbox-system
rules:
- apiGroups: [ "devbox.sealos.io" ]
resources: [ "runtimes", "runtimeclasses"]
verbs: [ "get", "watch", "list" ]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: devbox-default-user-rolebinding
namespace: devbox-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: devbox-default-user
subjects:
- kind: Group
name: system:serviceaccounts
apiGroup: rbac.authorization.k8s.io
-211
View File
@@ -1,211 +0,0 @@
module github.com/labring/sealos/controllers/devbox
go 1.24.0
require (
github.com/containerd/containerd/v2 v2.1.4
github.com/containerd/errdefs v1.0.0
github.com/containerd/nerdctl/v2 v2.1.3
github.com/go-logr/logr v1.4.3
github.com/google/go-containerregistry v0.20.6
github.com/google/uuid v1.6.0
github.com/onsi/ginkgo/v2 v2.25.1
github.com/onsi/gomega v1.38.1
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.42.0
google.golang.org/grpc v1.73.0
k8s.io/api v0.33.3
k8s.io/apimachinery v0.33.3
k8s.io/client-go v0.33.3
k8s.io/cri-api v0.33.3
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/controller-runtime v0.20.4
)
replace (
github.com/containerd/nerdctl => github.com/labring/nerdctl/v2 v2.1.6-labring.1
github.com/containerd/nerdctl/mod/tigron => github.com/labring/nerdctl/mod/tigron v0.0.0-20251118102352-14d1009e039d
github.com/containerd/nerdctl/v2 => github.com/labring/nerdctl/v2 v2.1.6-labring.1
)
require (
cel.dev/expr v0.23.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.13.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cilium/ebpf v0.16.0 // indirect
github.com/containerd/accelerated-container-image v1.3.0 // indirect
github.com/containerd/cgroups/v3 v3.0.5 // indirect
github.com/containerd/console v1.0.5 // indirect
github.com/containerd/containerd/api v1.9.0 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/fifo v1.1.0 // indirect
github.com/containerd/go-cni v1.1.13 // indirect
github.com/containerd/go-runc v1.1.0 // indirect
github.com/containerd/imgcrypt/v2 v2.0.1 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/nydus-snapshotter v0.15.4 // indirect
github.com/containerd/platforms v1.0.0-rc.1 // indirect
github.com/containerd/plugin v1.0.0 // indirect
github.com/containerd/stargz-snapshotter v0.17.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.17.0 // indirect
github.com/containerd/stargz-snapshotter/ipfs v0.17.0 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/containernetworking/cni v1.3.0 // indirect
github.com/containernetworking/plugins v1.8.0 // indirect
github.com/containers/ocicrypt v1.2.1 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/docker/cli v28.4.0+incompatible // indirect
github.com/docker/docker v28.4.0+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fahedouch/go-logrotate v0.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fluent/fluent-logger-golang v1.10.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.22.0 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/go-cid v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/kpechenenko/rword v0.0.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/sys/mount v0.3.4 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/sys/symlink v0.3.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr v0.16.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runtime-spec v1.2.1 // indirect
github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect
github.com/opencontainers/selinux v1.12.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rootless-containers/bypass4netns v0.4.2 // indirect
github.com/rootless-containers/rootlesskit/v2 v2.3.5 // indirect
github.com/sasha-s/go-deadlock v0.3.5 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/smallstep/pkcs7 v0.1.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/tjarratt/babble v0.0.0-20210505082055-cbca2a4833c1 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yuchanns/srslog v1.1.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.36.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.32.1 // indirect
k8s.io/apiserver v0.32.3 // indirect
k8s.io/component-base v0.32.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
lukechampine.com/blake3 v1.3.0 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
tags.cncf.io/container-device-interface v1.0.1 // indirect
tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect
)
-652
View File
@@ -1,652 +0,0 @@
cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss=
cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA=
github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/compose-spec/compose-go/v2 v2.9.0 h1:UHSv/QHlo6QJtrT4igF1rdORgIUhDo1gWuyJUoiNNIM=
github.com/compose-spec/compose-go/v2 v2.9.0/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE=
github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU=
github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0=
github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI=
github.com/containerd/containerd/v2 v2.1.4 h1:/hXWjiSFd6ftrBOBGfAZ6T30LJcx1dBjdKEeI8xucKQ=
github.com/containerd/containerd/v2 v2.1.4/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/go-cni v1.1.13 h1:eFSGOKlhoYNxpJ51KRIMHZNlg5UgocXEIEBGkY7Hnis=
github.com/containerd/go-cni v1.1.13/go.mod h1:nTieub0XDRmvCZ9VI/SBG6PyqT95N4FIhxsauF1vSBI=
github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gGleA=
github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U=
github.com/containerd/imgcrypt/v2 v2.0.1 h1:gQcmeCKA97fAl0wlpq0itSY/PagFBsn4/mlKUy6kOio=
github.com/containerd/imgcrypt/v2 v2.0.1/go.mod h1:/qIJL8nxzdzMA2n5iYyyuIY36KfoVQWmgTWdfVtyebM=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/nydus-snapshotter v0.15.4 h1:l59kGRVMtwMLDLh322HsWhEsBCkRKMkGWYV5vBeLYCE=
github.com/containerd/nydus-snapshotter v0.15.4/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms=
github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E=
github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=
github.com/containerd/stargz-snapshotter v0.17.0 h1:djNS4KU8ztFhLdEDZ1bsfzOiYuVHT6TgSU5qwRk+cNc=
github.com/containerd/stargz-snapshotter v0.17.0/go.mod h1:ySEul1ck7jCE4jqsuFCo8FFLrHU20UWQeI9g7mdsanI=
github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE=
github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM=
github.com/containerd/stargz-snapshotter/ipfs v0.17.0 h1:Q7UO2U0nKXtQFYVeX8WUmMKXtJR9ZAPgISt/sMEo8Ng=
github.com/containerd/stargz-snapshotter/ipfs v0.17.0/go.mod h1:zRJECfc6IPSr50ljYX36kVmrSd1Wdi3aXLzZhFuhfR4=
github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo=
github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4=
github.com/containernetworking/plugins v1.8.0 h1:WjGbV/0UQyo8A4qBsAh6GaDAtu1hevxVxsEuqtBqUFk=
github.com/containernetworking/plugins v1.8.0/go.mod h1:JG3BxoJifxxHBhG3hFyxyhid7JgRVBu/wtooGEvWf1c=
github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM=
github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ=
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
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/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 h1:aYo8nnk3ojoQkP5iErif5Xxv0Mo0Ga/FR5+ffl/7+Nk=
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/fahedouch/go-logrotate v0.3.0 h1:XP+dHIDgWZ1ckz43mG6gl5ASer3PZDVr755SVMyzaUQ=
github.com/fahedouch/go-logrotate v0.3.0/go.mod h1:X49m0bvPLkk71MHNCQ1yEfVEw8W/u+qvHa/hOnhCYf4=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluent/fluent-logger-golang v1.10.1 h1:wu54iN1O2afll5oQrtTjhgZRwWcfOeFFzwRsEkABfFQ=
github.com/fluent/fluent-logger-golang v1.10.1/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb21xfNYOzn0roAtp7mfGAE=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
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-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g=
github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kpechenenko/rword v0.0.4 h1:kwPwwx+gkGemHigIk1conGDijHWJs+lErl9g3KJQVMg=
github.com/kpechenenko/rword v0.0.4/go.mod h1:xOh1FRoUbuuaUR25Bljbza63hhpqu8lvqAJ3NVdlm6M=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labring/nerdctl/mod/tigron v0.0.0-20251118102352-14d1009e039d h1:6A0+TJfRXARP5LpjgE4ZCIgnW3aQV2hOao5a43BVEzY=
github.com/labring/nerdctl/mod/tigron v0.0.0-20251118102352-14d1009e039d/go.mod h1:gmUZh2wUVxr/msGogKUi6v9eJbP5ASO4fVYEPzHH4iI=
github.com/labring/nerdctl/v2 v2.1.6-labring.1 h1:k1MqC4uv7ZOmvUv3/IL6ShEEUUpS+kdGe8zd4JXRHlM=
github.com/labring/nerdctl/v2 v2.1.6-labring.1/go.mod h1:XBIa8yzX2BddwiJtN47w4w4Xmki/Y6w8HHd5DfkjzlA=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b/go.mod h1:pzzDgJWZ34fGzaAZGFW22KVZDfyrYW+QABMrWnJBnSs=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww=
github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=
github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU=
github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc=
github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY=
github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk=
github.com/onsi/gomega v1.38.1 h1:FaLA8GlcpXDwsb7m0h2A9ew2aTk3vnZMlzFgg5tz/pk=
github.com/onsi/gomega v1.38.1/go.mod h1:LfcV8wZLvwcYRwPiJysphKAEsmcFnLMK/9c+PjvlX8g=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 h1:DmNGcqH3WDbV5k8OJ+esPWbqUOX5rMLR2PMvziDMJi0=
github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI=
github.com/opencontainers/selinux v1.9.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw=
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rootless-containers/bypass4netns v0.4.2 h1:JUZcpX7VLRfDkLxBPC6fyNalJGv9MjnjECOilZIvKRc=
github.com/rootless-containers/bypass4netns v0.4.2/go.mod h1:iOY28IeFVqFHnK0qkBCQ3eKzKQgSW5DtlXFQJyJMAQk=
github.com/rootless-containers/rootlesskit/v2 v2.3.5 h1:WGY05oHE7xQpSkCGfYP9lMY5z19tCxA8PhWlvP1cKx8=
github.com/rootless-containers/rootlesskit/v2 v2.3.5/go.mod h1:83EIYLeMX8UeNgLHkR1PefoSV76aKEC+OyI3vzrEfvw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU=
github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU=
github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw=
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tjarratt/babble v0.0.0-20210505082055-cbca2a4833c1 h1:j8whCiEmvLCXI3scVn+YnklCU8mwJ9ZJ4/DGAKqQbRE=
github.com/tjarratt/babble v0.0.0-20210505082055-cbca2a4833c1/go.mod h1:O5hBrCGqzfb+8WyY8ico2AyQau7XQwAfEQeEQ5/5V9E=
github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
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/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuchanns/srslog v1.1.0 h1:CEm97Xxxd8XpJThE0gc/XsqUGgPufh5u5MUjC27/KOk=
github.com/yuchanns/srslog v1.1.0/go.mod h1:HsLjdv3XV02C3kgBW2bTyW6i88OQE+VYJZIxrPKPPak=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
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/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8=
k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE=
k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw=
k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto=
k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA=
k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8=
k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc=
k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA=
k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg=
k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k=
k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI=
k8s.io/cri-api v0.33.3 h1:aQvK3UxsaVMul4z71lOiblMHdhw9ROaw3Cgg15xDrD4=
k8s.io/cri-api v0.33.3/go.mod h1:OLQvT45OpIA+tv91ZrpuFIGY+Y2Ho23poS7n115Aocs=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc=
tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0=
tags.cncf.io/container-device-interface/specs-go v1.0.0 h1:8gLw29hH1ZQP9K1YtAzpvkHCjjyIxHZYzBAvlQ+0vD8=
tags.cncf.io/container-device-interface/specs-go v1.0.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ=
@@ -1,15 +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.
*/
@@ -1,658 +0,0 @@
package commit
import (
"context"
"errors"
"fmt"
"io"
"log"
"strings"
"time"
containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/remotes"
"github.com/containerd/containerd/v2/core/remotes/docker"
"github.com/containerd/containerd/v2/core/remotes/docker/config"
"github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/nerdctl/v2/pkg/api/types"
"github.com/containerd/nerdctl/v2/pkg/cmd/container"
"github.com/containerd/nerdctl/v2/pkg/cmd/image"
"github.com/containerd/nerdctl/v2/pkg/cmd/login"
"github.com/containerd/nerdctl/v2/pkg/containerutil"
ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults"
"github.com/labring/sealos/controllers/devbox/api/v1alpha2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type Committer interface {
CreateContainer(ctx context.Context, devboxName, contentID, baseImage string) (string, error)
Commit(
ctx context.Context,
devboxName, contentID, baseImage, commitImage string,
) (string, error)
Push(ctx context.Context, imageName string) error
RemoveImages(ctx context.Context, imageNames []string, force, async bool) error
RemoveContainers(ctx context.Context, containerNames []string) error
InitializeGC(ctx context.Context) error
SetLvRemovable(ctx context.Context, containerID, contentID string) error
}
type CommitterImpl struct {
containerdClient *containerd.Client // containerd client
conn *grpc.ClientConn // gRPC connection
globalOptions *types.GlobalCommandOptions // global options
registryAddr string
registryUsername string
registryPassword string
// Merge base image layers control
mergeBaseImageTopLayer bool
}
// NewCommitter new a CommitterImpl with registry configuration
func NewCommitter(
registryAddr, registryUsername, registryPassword string,
merge bool,
) (Committer, error) {
var conn *grpc.ClientConn
var err error
// login to registry
err = login.Login(context.Background(), types.LoginCommandOptions{
GOptions: *NewGlobalOptionConfig(),
ServerAddress: registryAddr,
Username: registryUsername,
Password: registryPassword,
}, io.Discard)
if err != nil {
return nil, err
}
// retry to connect
for i := 0; i <= DefaultMaxRetries; i++ {
if i > 0 {
log.Printf("Retrying connection to containerd (attempt %d/%d)...", i, DefaultMaxRetries)
time.Sleep(DefaultRetryDelay)
}
// create gRPC connection
conn, err = grpc.NewClient(
DefaultContainerdAddress,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err == nil {
log.Printf("Successfully connected to containerd at %s", DefaultContainerdAddress)
break
}
log.Printf(
"Failed to connect to containerd (attempt %d/%d): %v",
i+1,
DefaultMaxRetries+1,
err,
)
if i == DefaultMaxRetries {
return nil, fmt.Errorf(
"failed to connect to containerd after %d attempts: %w",
DefaultMaxRetries+1,
err,
)
}
}
// create Containerd client
containerdClient, err := containerd.NewWithConn(
conn,
containerd.WithDefaultNamespace(DefaultNamespace),
)
if err != nil {
conn.Close()
return nil, fmt.Errorf("failed to create containerd client: %w", err)
}
return &CommitterImpl{
containerdClient: containerdClient,
conn: conn,
globalOptions: NewGlobalOptionConfig(),
registryAddr: registryAddr,
registryUsername: registryUsername,
registryPassword: registryPassword,
mergeBaseImageTopLayer: merge,
}, nil
}
// CreateContainer create container with labels
func (c *CommitterImpl) CreateContainer(
ctx context.Context,
devboxName, contentID, baseImage string,
) (string, error) {
log.Printf(
"========>>>> create container, devboxName: %s, contentID: %s, baseImage: %s",
devboxName,
contentID,
baseImage,
)
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
// check connection status, if connection is bad, try to reconnect
if err := c.CheckConnection(ctx); err != nil {
log.Printf("Connection check failed: %v, attempting to reconnect...", err)
if reconnectErr := c.Reconnect(ctx); reconnectErr != nil {
return "", fmt.Errorf("failed to reconnect: %w", reconnectErr)
}
}
// create container with labels
originalAnnotations := map[string]string{
v1alpha2.AnnotationContentID: contentID,
v1alpha2.AnnotationStorageLimit: AnnotationUseLimitValue,
AnnotationKeyNamespace: DefaultNamespace,
AnnotationKeyImageName: baseImage,
}
// Add merge base image layers annotation if enabled
if c.mergeBaseImageTopLayer {
originalAnnotations[v1alpha2.AnnotationInit] = AnnotationImageFromValue
}
// convert labels to "containerd.io/snapshot/devbox-" format
convertedLabels := convertLabels(originalAnnotations)
convertedAnnotations := convertMapToSlice(originalAnnotations)
// create container options
createOpt := types.ContainerCreateOptions{
GOptions: *c.globalOptions,
Runtime: DefaultRuntime, // user devbox runtime
Name: fmt.Sprintf("devbox-%s-container-%d", devboxName, time.Now().UnixMicro()),
Pull: "missing",
InRun: false, // not start container
Rm: false,
LogDriver: "json-file",
StopSignal: "SIGTERM",
Restart: "unless-stopped",
Interactive: false, // not interactive, avoid conflict with Detach
Cgroupns: "host", // add cgroupns mode
Detach: true, // run in background
Rootfs: false,
Label: convertedAnnotations,
SnapshotLabels: convertedLabels,
ImagePullOpt: types.ImagePullOptions{
GOptions: *c.globalOptions,
},
}
// create network manager
networkManager, err := containerutil.NewNetworkingOptionsManager(createOpt.GOptions,
types.NetworkOptions{
NetworkSlice: []string{DefaultNetworkMode},
}, c.containerdClient)
if err != nil {
log.Println("failed to create network manager:", err)
return "", fmt.Errorf("failed to create network manager: %w", err)
}
// create container
container, cleanup, err := container.Create(
ctx,
c.containerdClient,
[]string{originalAnnotations[AnnotationKeyImageName]},
networkManager,
createOpt,
)
if err != nil {
log.Println("failed to create container:", err)
return "", fmt.Errorf("failed to create container: %w", err)
}
if cleanup != nil {
defer cleanup()
}
log.Printf("container created successfully: %s\n", container.ID())
return container.ID(), nil
}
// DeleteContainer delete container
func (c *CommitterImpl) DeleteContainer(ctx context.Context, containerName string) error {
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
container, err := c.containerdClient.LoadContainer(ctx, containerName)
if err != nil {
return fmt.Errorf("failed to load container: %w", err)
}
// try to get and stop task
task, err := container.Task(ctx, nil)
if err == nil {
log.Printf("Stopping task for container: %s", containerName)
// force kill task
err = task.Kill(ctx, 9) // SIGKILL
if err != nil {
log.Printf("Warning: failed to send SIGKILL: %v", err)
} else {
log.Printf("Sent SIGKILL to task")
}
// delete task
log.Printf("Deleting task...")
_, err = task.Delete(ctx, containerd.WithProcessKill)
if err != nil {
log.Printf("Warning: failed to delete task: %v", err)
} else {
log.Printf("Task deleted for container: %s", containerName)
}
}
// delete container (include snapshot)
err = container.Delete(ctx, containerd.WithSnapshotCleanup)
if err != nil {
return fmt.Errorf("failed to delete container: %w", err)
}
log.Printf("Container deleted: %s successfully", containerName)
return nil
}
func (c *CommitterImpl) SetLvRemovable(ctx context.Context, containerID, contentID string) error {
log.Printf(
"========>>>> set lv removable for container, containerID: %s, contentID: %s",
containerID,
contentID,
)
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
// check connection status, if connection is bad, try to reconnect
if err := c.CheckConnection(ctx); err != nil {
log.Printf("Connection check failed: %v, attempting to reconnect...", err)
if reconnectErr := c.Reconnect(ctx); reconnectErr != nil {
return fmt.Errorf("failed to reconnect: %w", reconnectErr)
}
}
_, err := c.containerdClient.SnapshotService(DefaultDevboxSnapshotter).
Update(ctx, snapshots.Info{
Name: containerID,
Labels: map[string]string{RemoveContentIDkey: contentID},
}, "labels."+RemoveContentIDkey)
if err != nil {
return err
}
return nil
}
// RemoveContainer remove container
func (c *CommitterImpl) RemoveContainers(ctx context.Context, containerNames []string) error {
if len(containerNames) == 0 {
return errors.New("[RemoveContainers]containerNames is empty")
}
log.Printf("========>>>> remove container, containerNames: %v", containerNames)
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
// check connection status, if connection is bad, try to reconnect
if err := c.CheckConnection(ctx); err != nil {
log.Printf("Connection check failed: %v, attempting to reconnect...", err)
if reconnectErr := c.Reconnect(ctx); reconnectErr != nil {
return fmt.Errorf("failed to reconnect: %w", reconnectErr)
}
}
global := NewGlobalOptionConfig()
opt := types.ContainerRemoveOptions{
Stdout: io.Discard,
Force: DefaultRemoveContainerForce,
Volumes: false,
GOptions: *global,
}
err := container.Remove(ctx, c.containerdClient, containerNames, opt)
if err != nil {
return fmt.Errorf("failed to remove container: %w", err)
}
return nil
}
// Commit commit container to image
func (c *CommitterImpl) Commit(
ctx context.Context,
devboxName, contentID, baseImage, commitImage string,
) (string, error) {
log.Printf(
"========>>>> commit devbox, devboxName: %s, contentID: %s, baseImage: %s, commitImage: %s",
devboxName,
contentID,
baseImage,
commitImage,
)
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
containerID, err := c.CreateContainer(ctx, devboxName, contentID, baseImage)
if err != nil {
return containerID, fmt.Errorf("failed to create container: %w", err)
}
// // mark for gc
// defer c.MarkForGC(containerID, commitImage)
// create commit options
global := NewGlobalOptionConfig()
opt := types.ContainerCommitOptions{
Stdout: io.Discard,
GOptions: *global,
Pause: PauseContainerDuringCommit,
// Remove base image top layer:
DevboxOptions: types.DevboxOptions{
RemoveBaseImageTopLayer: c.mergeBaseImageTopLayer,
},
}
// commit container
err = container.Commit(ctx, c.containerdClient, commitImage, containerID, opt)
if err != nil {
return containerID, fmt.Errorf("failed to commit container: %w", err)
}
return containerID, nil
}
// GetContainerAnnotations get container annotations
func (c *CommitterImpl) GetContainerAnnotations(
ctx context.Context,
containerName string,
) (map[string]string, error) {
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
container, err := c.containerdClient.LoadContainer(ctx, containerName)
if err != nil {
return nil, fmt.Errorf("failed to load container: %w", err)
}
// get container labels (annotations)
labels, err := container.Labels(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get container labels: %w", err)
}
return labels, nil
}
// Push pushes an image to a remote repository
func (c *CommitterImpl) Push(ctx context.Context, imageName string) error {
log.Printf("========>>>> push image, imageName: %s", imageName)
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
// check connection status, if connection is bad, try to reconnect
if err := c.CheckConnection(ctx); err != nil {
log.Printf("Connection check failed: %v, attempting to reconnect...", err)
if reconnectErr := c.Reconnect(ctx); reconnectErr != nil {
return fmt.Errorf("failed to reconnect: %w", reconnectErr)
}
}
// set resolver
resolver, err := GetResolver(ctx, c.registryUsername, c.registryPassword)
if err != nil {
log.Printf("failed to set resolver, Image: %s, err: %v\n", imageName, err)
return err
}
imageRef, err := c.containerdClient.GetImage(ctx, imageName)
if err != nil {
log.Printf("failed to get image: %s, err: %v\n", imageName, err)
return err
}
// push image
err = c.containerdClient.Push(ctx, imageName, imageRef.Target(),
containerd.WithResolver(resolver),
)
if err != nil {
log.Printf("failed to push image: %s, err: %v\n", imageName, err)
return err
}
log.Printf("Pushed image success Image: %s\n", imageName)
return nil
}
// RemoveImage remove image
func (c *CommitterImpl) RemoveImages(
ctx context.Context,
imageNames []string,
force, async bool,
) error {
if len(imageNames) == 0 {
return errors.New("[RemoveImages]imageNames is empty")
}
log.Printf("========>>>> remove image, imageNames: %v", imageNames)
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
// check connection status, if connection is bad, try to reconnect
if err := c.CheckConnection(ctx); err != nil {
log.Printf("Connection check failed: %v, attempting to reconnect...", err)
if reconnectErr := c.Reconnect(ctx); reconnectErr != nil {
return fmt.Errorf("failed to reconnect: %w", reconnectErr)
}
}
global := NewGlobalOptionConfig()
opt := types.ImageRemoveOptions{
Stdout: io.Discard,
GOptions: *global,
Force: force,
Async: async,
}
return image.Remove(ctx, c.containerdClient, imageNames, opt)
}
// InitializeGC initialize force GC
func (c *CommitterImpl) InitializeGC(ctx context.Context) error {
gcCtx, cancel := context.WithCancel(ctx)
defer cancel()
if err := c.forceGC(gcCtx); err != nil {
log.Printf("Failed to initialize force GC, err: %v", err)
return fmt.Errorf("failed to initialize force GC: %w", err)
}
log.Println("Force GC initialized successfully")
return nil
}
// forceGC force gc container and image
func (c *CommitterImpl) forceGC(ctx context.Context) error {
log.Printf("Starting force GC in namespace: %s", DefaultNamespace)
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
containers, err := c.containerdClient.Containers(ctx)
if err != nil {
log.Printf("Failed to get containers, err: %v", err)
return err
}
// gc container
containerNames := make([]string, 0)
for _, container := range containers {
containerNames = append(containerNames, container.ID())
}
if len(containerNames) > 0 {
if err := c.RemoveContainers(ctx, containerNames); err != nil {
log.Printf("Failed to remove containers, err: %v", err)
return err
}
}
// gc image
images, err := c.containerdClient.ListImages(ctx)
if err != nil {
log.Printf("Failed to get images, err: %v", err)
return err
}
imageNames := make([]string, 0)
for _, image := range images {
imageNames = append(imageNames, image.Name())
}
if len(imageNames) > 0 {
if err := c.RemoveImages(
ctx,
imageNames,
DefaultRemoveImageForce,
DefaultRemoveImageAsync,
); err != nil {
log.Printf("Failed to remove images, err: %v", err)
return err
}
}
return nil
}
// GetResolver get resolver
func GetResolver(ctx context.Context, username, secret string) (remotes.Resolver, error) {
resolverOptions := docker.ResolverOptions{
Tracker: docker.NewInMemoryTracker(),
}
hostOptions := config.HostOptions{}
if username == "" && secret == "" {
hostOptions.Credentials = nil
} else {
// TODO: fix this, use flags or configs to set mulit registry credentials
hostOptions.Credentials = func(host string) (string, string, error) {
return username, secret, nil
}
}
hostOptions.DefaultScheme = "http"
hostOptions.DefaultTLS = nil
resolverOptions.Hosts = config.ConfigureHosts(ctx, hostOptions)
return docker.NewResolver(resolverOptions), nil
}
// convertLabels convert labels to "containerd.io/snapshot/devbox-" format
func convertLabels(labels map[string]string) map[string]string {
convertedLabels := make(map[string]string)
for key, value := range labels {
if strings.HasPrefix(key, ContainerLabelPrefix) {
// convert "devbox.sealos.io/" to "containerd.io/snapshot/devbox-"
newKey := SnapshotLabelPrefix + key[len(ContainerLabelPrefix):]
convertedLabels[newKey] = value
}
}
return convertedLabels
}
// convertMapToSlice convert map to slice
func convertMapToSlice(labels map[string]string) []string {
slice := make([]string, 0, len(labels))
for key, value := range labels {
slice = append(slice, fmt.Sprintf("%s=%s", key, value))
}
return slice
}
// NewGlobalOptionConfig new global option config
func NewGlobalOptionConfig() *types.GlobalCommandOptions {
return &types.GlobalCommandOptions{
Namespace: DefaultNamespace,
Address: DefaultContainerdAddress,
DataRoot: DefaultNerdctlDataRoot,
Debug: false,
DebugFull: false,
Snapshotter: DefaultDevboxSnapshotter,
CNIPath: ncdefaults.CNIPath(),
CNINetConfPath: ncdefaults.CNINetConfPath(),
CgroupManager: ncdefaults.CgroupManager(),
InsecureRegistry: InsecureRegistry,
HostsDir: []string{DefaultNerdctlHostsDir},
Experimental: true,
HostGatewayIP: ncdefaults.HostGatewayIP(),
KubeHideDupe: false,
CDISpecDirs: ncdefaults.CDISpecDirs(),
UsernsRemap: "",
DNS: []string{},
DNSOpts: []string{},
DNSSearch: []string{},
}
}
// CheckConnection check if the connection is still alive
func (c *CommitterImpl) CheckConnection(ctx context.Context) error {
if c.conn == nil {
return errors.New("connection is nil")
}
// check connection state
state := c.conn.GetState()
if state.String() == "TRANSIENT_FAILURE" || state.String() == "SHUTDOWN" {
return fmt.Errorf("connection is in bad state: %s", state.String())
}
// try to ping containerd
_, err := c.containerdClient.Version(ctx)
if err != nil {
return fmt.Errorf("failed to ping containerd: %w", err)
}
return nil
}
// Reconnect attempt to reconnect to containerd
func (c *CommitterImpl) Reconnect(ctx context.Context) error {
log.Printf("Attempting to reconnect to containerd...")
// close old connection
if c.conn != nil {
c.conn.Close()
}
var conn *grpc.ClientConn
var err error
for i := 0; i <= DefaultMaxRetries; i++ {
if i > 0 {
log.Printf("Retrying connection to containerd (attempt %d/%d)...", i, DefaultMaxRetries)
time.Sleep(DefaultRetryDelay)
}
// create gRPC connection
conn, err = grpc.NewClient(
DefaultContainerdAddress,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err == nil {
log.Printf("Successfully connected to containerd at %s", DefaultContainerdAddress)
break
}
log.Printf(
"Failed to connect to containerd (attempt %d/%d): %v",
i+1,
DefaultMaxRetries+1,
err,
)
if i == DefaultMaxRetries {
return fmt.Errorf(
"failed to connect to containerd after %d attempts: %w",
DefaultMaxRetries+1,
err,
)
}
}
// recreate containerd client
containerdClient, err := containerd.NewWithConn(
conn,
containerd.WithDefaultNamespace(DefaultNamespace),
)
if err != nil {
conn.Close()
return fmt.Errorf("failed to recreate containerd client: %w", err)
}
// update instance
c.containerdClient = containerdClient
c.conn = conn
log.Printf("Successfully reconnected to containerd")
return nil
}
// Close close the connection
func (c *CommitterImpl) Close() error {
if c.conn != nil {
return c.conn.Close()
}
return nil
}
@@ -1,824 +0,0 @@
package commit
import (
"context"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/errdefs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
baseImageBusyBox = "docker.io/library/busybox:latest"
baseImageNginx = "docker.io/library/nginx:latest"
baseImageAlpine = "docker.io/library/alpine:latest"
)
// init Committer
func TestNewCommitter(t *testing.T) {
committer, err := NewCommitter("", "", "", true)
if err != nil {
t.Fatalf("NewCommitter failed: %v", err)
}
assert.NotNil(t, committer)
}
// test commit flow
func TestCommitFlow(t *testing.T) {
ctx := context.Background()
// 1. create committer
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
// 2. prepare test data
devboxName := fmt.Sprintf("test-devbox-%d", time.Now().Unix())
contentID := "test-content-id-123"
commitImage := fmt.Sprintf("test-devbox-commit-%d", time.Now().Unix())
// 3. create container and commit container
_, err = committer.Commit(ctx, devboxName, contentID, baseImageBusyBox, commitImage)
assert.NoError(t, err)
}
// test create container
func TestCreateContainer(t *testing.T) {
ctx := context.Background()
committer, err := NewCommitter("", "", "", true)
require.NoError(t, err)
// create container
devboxName := fmt.Sprintf("test-devbox-%d", time.Now().Unix())
contentID := fmt.Sprintf("test-content-id-%d", time.Now().Unix())
containerID, err := committer.
CreateContainer(ctx, devboxName, contentID, baseImageNginx)
require.NoError(t, err)
// verify container labels
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
var annotations map[string]string
if annotations, err = committerImpl.GetContainerAnnotations(ctx, containerID); err != nil {
t.Fatalf("GetContainerAnnotations failed: %v", err)
}
t.Logf("annotations: %+v", annotations)
}
// test delete container
func TestDeleteContainer(t *testing.T) {
ctx := context.Background()
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
// create a container
devboxName := fmt.Sprintf("test-devbox-%d", time.Now().Unix())
var containerID string
if containerID, err = committer.CreateContainer(
ctx,
devboxName,
"test-content-id-789",
baseImageAlpine,
); err != nil {
t.Fatalf("CreateContainer failed: %v", err)
}
// show all containers in current namespace
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
var containers []containerd.Container
if containers, err = committerImpl.containerdClient.Containers(ctx); err != nil {
t.Fatalf("Containers failed: %v", err)
}
fmt.Printf("=== All Containers in current namespace ===\n")
for _, container := range containers {
fmt.Printf("Container ID: %s\n", container.ID())
}
fmt.Printf("=== Total %d containers ===\n", len(containers))
// delete container
if err = committerImpl.DeleteContainer(ctx, containerID); err != nil {
t.Fatalf("DeleteContainer failed: %v", err)
}
if containers, err = committerImpl.containerdClient.Containers(ctx); err != nil {
t.Fatalf("Containers failed: %v", err)
}
fmt.Printf("=== All Containers in current namespace ===\n")
for _, container := range containers {
fmt.Printf("Container ID: %s\n", container.ID())
}
fmt.Printf("=== Total %d containers ===\n", len(containers))
// verify container is deleted (try to get labels should return error)
if _, err = committerImpl.GetContainerAnnotations(ctx, containerID); err == nil {
t.Fatalf("expected error when getting annotations for deleted container")
}
}
// test remove container
func TestRemoveContainer(t *testing.T) {
ctx := context.Background()
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
// create a container
devboxName := fmt.Sprintf("test-devbox-%d", time.Now().Unix())
var containerID string
if containerID, err = committer.CreateContainer(
ctx,
devboxName,
"test-content-id-789",
baseImageAlpine,
); err != nil {
t.Fatalf("CreateContainer failed: %v", err)
}
// show all containers in current namespace
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
var containers []containerd.Container
if containers, err = committerImpl.containerdClient.Containers(ctx); err != nil {
t.Fatalf("Containers failed: %v", err)
}
fmt.Printf("=== All Containers in current namespace ===\n")
for _, container := range containers {
fmt.Printf("Container ID: %s\n", container.ID())
}
fmt.Printf("=== Total %d containers ===\n", len(containers))
// delete container
if err = committer.RemoveContainers(ctx, []string{containerID}); err != nil {
t.Fatalf("RemoveContainers failed: %v", err)
}
if containers, err = committerImpl.containerdClient.Containers(ctx); err != nil {
t.Fatalf("Containers failed: %v", err)
}
fmt.Printf("=== All Containers in current namespace ===\n")
for _, container := range containers {
fmt.Printf("Container ID: %s\n", container.ID())
}
fmt.Printf("=== Total %d containers ===\n", len(containers))
// verify container is deleted (try to get labels should return error)
if _, err = committerImpl.GetContainerAnnotations(ctx, containerID); err == nil {
t.Fatalf("expected error when getting annotations for removed container")
}
}
// test error cases
func TestErrorCases(t *testing.T) {
ctx := context.Background()
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
// test use not exist image to create container
if _, err = committer.CreateContainer(
ctx,
"test-devbox",
"test-content-id",
"not-exist-image:latest",
); err == nil {
t.Fatalf("expected error when creating container with non-exist image")
}
// test use not exist container to delete
if err = committerImpl.DeleteContainer(ctx, "not-exist-container"); err == nil {
t.Fatalf("expected error when deleting non-exist container")
}
// test get not exist container label
if _, err = committerImpl.GetContainerAnnotations(ctx, "not-exist-container"); err == nil {
t.Fatalf("expected error when getting annotations of non-exist container")
}
}
// test concurrent operations
func TestConcurrentOperations(t *testing.T) {
ctx := context.Background()
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
// concurrent to create container
containerCount := 3
wg := sync.WaitGroup{}
wg.Add(containerCount)
var mu sync.Mutex
containers := make([]string, containerCount)
for i := range containerCount {
go func(index int) {
defer wg.Done()
devboxName := fmt.Sprintf("test-devbox-concurrent-%d-%d", time.Now().Unix(), index)
var containerID string
var err error
if containerID, err = committer.CreateContainer(ctx, devboxName,
fmt.Sprintf("test-content-id-%d", index),
baseImageBusyBox); err != nil {
t.Errorf("Failed to create container: %v", err)
return
}
mu.Lock()
containers = append(containers, containerID)
mu.Unlock()
}(i)
}
wg.Wait()
// delete containers
for _, containerID := range containers {
err := committer.RemoveContainers(ctx, []string{containerID})
if err != nil {
t.Logf("Warning: failed to delete container %s: %v", containerID, err)
}
}
// get current containers list
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
var currentContainers []containerd.Container
if currentContainers, err = committerImpl.containerdClient.Containers(ctx); err != nil {
t.Fatalf("Containers failed: %v", err)
}
fmt.Printf("=== All Containers in current namespace ===\n")
for _, container := range currentContainers {
fmt.Printf("Container ID: %s\n", container.ID())
}
fmt.Printf("=== Total %d containers ===\n", len(currentContainers))
}
// test runtime selection
func TestRuntimeSelection(t *testing.T) {
ctx := context.Background()
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
// create container with specific runtime
devboxName := fmt.Sprintf("test-runtime-%d", time.Now().Unix())
contentID := "test-runtime-content-id"
var containerID string
if containerID, err = committer.CreateContainer(
ctx,
devboxName,
contentID,
baseImageBusyBox,
); err != nil {
t.Fatalf("CreateContainer failed: %v", err)
}
assert.NotEmpty(t, containerID)
// get container info to verify runtime
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
var container containerd.Container
if container, err = committerImpl.containerdClient.LoadContainer(ctx, containerID); err != nil {
t.Fatalf("LoadContainer failed: %v", err)
}
var info containers.Container
if info, err = container.Info(ctx); err != nil {
t.Fatalf("container.Info failed: %v", err)
}
fmt.Printf("=== Container Runtime Information ===\n")
fmt.Printf("Container ID: %s\n", containerID)
fmt.Printf("Runtime Name: %s\n", info.Runtime.Name)
fmt.Printf("Runtime Options: %+v\n", info.Runtime.Options)
fmt.Printf("Expected Runtime: %s\n", DefaultRuntime)
fmt.Printf("Runtime Match: %v\n", info.Runtime.Name == DefaultRuntime)
// cleanup
if err = committerImpl.DeleteContainer(ctx, containerID); err != nil {
t.Fatalf("DeleteContainer failed: %v", err)
}
}
// test connection management
func TestConnectionManagement(t *testing.T) {
ctx := context.Background()
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
defer func() {
if cerr := committerImpl.Close(); cerr != nil {
t.Fatalf("Close failed: %v", cerr)
}
}()
// test connection check
if err = committerImpl.CheckConnection(ctx); err != nil {
t.Fatalf("CheckConnection failed: %v", err)
}
// create container
devboxName := fmt.Sprintf("test-devbox-%d", time.Now().Unix())
contentID := "903b3c87-1458-4dd8-b0f4-9da7184cf8ca"
testImage := "ghcr.io/labring-actions/devbox/go-1.23.0:13aacd8"
var containerID string
if containerID, err = committer.CreateContainer(
ctx,
devboxName,
contentID,
testImage,
); err != nil {
t.Fatalf("CreateContainer failed: %v", err)
}
assert.NotEmpty(t, containerID)
// delete container
if err = committer.RemoveContainers(ctx, []string{containerID}); err != nil {
t.Fatalf("RemoveContainers failed: %v", err)
}
// test reconnect
if err = committerImpl.Reconnect(ctx); err != nil {
t.Fatalf("Reconnect failed: %v", err)
}
// create container again
if containerID, err = committer.CreateContainer(
ctx,
devboxName,
contentID,
testImage,
); err != nil {
t.Fatalf("CreateContainer failed: %v", err)
}
assert.NotEmpty(t, containerID)
// test connection check again
if err = committerImpl.CheckConnection(ctx); err != nil {
t.Fatalf("CheckConnection failed: %v", err)
}
if err = committer.RemoveContainers(ctx, []string{containerID}); err != nil {
t.Fatalf("RemoveContainers failed: %v", err)
}
// test connection check again
if err = committerImpl.CheckConnection(ctx); err != nil {
t.Fatalf("CheckConnection failed: %v", err)
}
fmt.Printf("Connection management test passed\n")
}
// test push to Docker Hub
func TestPushToDockerHub(t *testing.T) {
ctx := context.Background()
// use test registry
registryAddr := "docker.io"
registryUser := "cunzili"
registryPassword := "123456789"
committer, err := NewCommitter(registryAddr, registryUser, registryPassword, true)
if err != nil {
t.Errorf("Skip Docker Hub push test: failed to create committer: %v", err)
}
// create a test image name
testImageName := fmt.Sprintf("docker.io/cunzili/cunzili:test-%d", time.Now().Unix())
// create a container and commit it to image
devboxName := fmt.Sprintf("test-dockerhub-%d", time.Now().Unix())
contentID := fmt.Sprintf("test-dockerhub-content-id-%d", time.Now().Unix())
containerID, err := committer.Commit(
ctx,
devboxName,
contentID,
baseImageBusyBox,
testImageName,
)
if err != nil {
t.Errorf("Skip Docker Hub push test: failed to create test image: %v", err)
}
// containers, err := committer.(*CommitterImpl).containerdClient.Containers(ctx)
// assert.NoError(t, err)
// fmt.Printf("=== All Containers in current namespace ===\n")
// for _, container := range containers {
// fmt.Printf("Container ID: %s\n", container.ID())
// }
// fmt.Printf("=== Total Containers: %d\n", len(containers))
// push to Docker Hub
err = committer.Push(ctx, testImageName)
if err != nil {
t.Errorf("Failed to push image to Docker Hub: %v", err)
} else {
fmt.Printf("Successfully pushed image to Docker Hub: %s\n", testImageName)
fmt.Printf("You can view the image at: https://hub.docker.com/r/cunzili/cunzili/tags\n")
}
// remove image
err = committer.RemoveImages(ctx, []string{testImageName}, false, false)
assert.NoError(t, err)
// verify image is deleted
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
if _, err = committerImpl.containerdClient.GetImage(ctx, testImageName); err == nil {
t.Fatalf("expected error when getting deleted image")
}
fmt.Println("can not find image:", testImageName)
// remove container
if err = committerImpl.RemoveContainers(ctx, []string{containerID}); err != nil {
t.Fatalf("RemoveContainers failed: %v", err)
}
// verify container is deleted
if _, err = committerImpl.containerdClient.LoadContainer(ctx, containerID); err == nil {
t.Fatalf("expected error when loading deleted container")
}
fmt.Println("can not find container:", containerID)
}
// test push without authentication
func TestPushWithoutAuth(t *testing.T) {
ctx := context.Background()
// no authentication
registryAddr := "docker.io"
registryUser := ""
registryPassword := ""
committer, err := NewCommitter(registryAddr, registryUser, registryPassword, true)
if err != nil {
t.Skipf("Skip no-auth push test: failed to create committer: %v", err)
}
// use a existing image for test
testImageName := "docker.io/cunzili/cunzili:test-no-auth-1754277739"
// create a container and commit it to image
devboxName := fmt.Sprintf("test-no-auth-%d", time.Now().Unix())
contentID := "test-no-auth-content-id"
_, err = committer.Commit(ctx, devboxName, contentID, baseImageBusyBox, testImageName)
if err != nil {
t.Skipf("Skip no-auth push test: failed to create test image: %v", err)
}
// test push to Docker Hub (no authentication)
err = committer.Push(ctx, testImageName)
if err != nil {
fmt.Printf("Expected error when pushing without auth: %v\n", err)
// should fail, because it needs authentication
t.Logf("Push failed as expected: %v", err)
} else {
t.Errorf("Push succeeded unexpectedly without authentication")
}
}
// test remove image
func TestRemoveImage(t *testing.T) {
ctx := context.Background()
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
// create a test devbox name and content id
devboxName := fmt.Sprintf("test-remove-devbox-%d", time.Now().Unix())
contentID := fmt.Sprintf("test-remove-content-id-%d", time.Now().Unix())
imageName := fmt.Sprintf("test-remove-image-%d", time.Now().Unix())
// create a container and commit it to image
_, err = committer.Commit(ctx, devboxName, contentID, baseImageBusyBox, imageName)
assert.NoError(t, err)
// // push image
// err = committer.Push(ctx, imageName)
// assert.NoError(t, err)
// remove image
if err = committer.RemoveImages(ctx, []string{imageName}, false, false); err != nil {
t.Fatalf("RemoveImages failed: %v", err)
}
// verify image is deleted
if _, err = committerImpl.containerdClient.GetImage(ctx, imageName); err == nil {
t.Fatalf("expected error when getting deleted image")
}
}
// TestAtomicLabels test containerd's atomic label update
func TestAtomicLabels(t *testing.T) {
ctx := context.Background()
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
// 1. create a test container
devboxName := fmt.Sprintf("test-atomic-%d", time.Now().Unix())
contentID := fmt.Sprintf("test-atomic-content-%d", time.Now().Unix())
var containerID string
if containerID, err = committer.CreateContainer(
ctx,
devboxName,
contentID,
baseImageBusyBox,
); err != nil {
t.Fatalf("CreateContainer failed: %v", err)
}
// ensure cleanup container after test
defer func() {
if cleanupErr := committer.RemoveContainers(ctx, []string{containerID}); cleanupErr != nil {
fmt.Printf("Failed to cleanup container: %v", cleanupErr)
}
}()
// 2. get container object
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
var container containerd.Container
if container, err = committerImpl.containerdClient.LoadContainer(ctx, containerID); err != nil {
t.Fatalf("LoadContainer failed: %v", err)
}
// 3. concurrent update label count
concurrentUpdates := 10
successCount := int32(0)
var wg sync.WaitGroup
wg.Add(concurrentUpdates)
// 4. concurrent update same label
for i := range concurrentUpdates {
go func(index int) {
defer wg.Done()
// get current label
labels, err := container.Labels(ctx)
if err != nil {
fmt.Printf("Failed to get labels: %v", err)
return
}
// copy label map
newLabels := make(map[string]string)
for k, v := range labels {
newLabels[k] = v
}
// add or update test label
val1 := fmt.Sprintf("value-%d", index)
val2 := time.Now().Format(time.RFC3339Nano)
newLabels["test-atomic-label"] = val1
newLabels["timestamp"] = val2
// try to update label
_, err = container.SetLabels(ctx, newLabels)
if err != nil {
if errdefs.IsAlreadyExists(err) {
fmt.Printf("Concurrent update detected for index %d\n", index)
} else {
fmt.Printf("Failed to set labels for index %d: %v\n", index, err)
}
return
}
// update success count
atomic.AddInt32(&successCount, 1)
fmt.Printf(
"Successfully updated labels for index %d, val1: %s, val2: %s\n",
index,
val1,
val2,
)
}(i)
}
// wait for all goroutine
wg.Wait()
// verify result
finalLabels, err := container.Labels(ctx)
assert.NoError(t, err)
fmt.Printf("Final label value: %s\n", finalLabels["test-atomic-label"])
fmt.Printf("Final timestamp: %s\n", finalLabels["timestamp"])
fmt.Printf("Successful updates: %d/%d\n", successCount, concurrentUpdates)
}
// test get image
func TestGetImage(t *testing.T) {
ctx := context.Background()
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
var images []containerd.Image
if images, err = committerImpl.containerdClient.ListImages(ctx); err != nil {
t.Fatalf("ListImages failed: %v", err)
}
for _, image := range images {
fmt.Printf("Image ID: %s\n", image.Target().Digest.String())
}
}
// TestRemoveImagePerformance test RemoveImage performance
func TestRemoveImagePerformance(t *testing.T) {
ctx := context.Background()
committer, err := NewCommitter("", "", "", true)
assert.NoError(t, err)
committerImpl, ok := committer.(*CommitterImpl)
if !ok {
t.Fatalf("failed to assert committer to CommitterImpl")
}
ctx = namespaces.WithNamespace(ctx, DefaultNamespace)
fmt.Printf("\n====================================\n")
fmt.Printf("image remove test\n")
fmt.Printf("Namespace: %s\n", DefaultNamespace)
fmt.Printf("====================================\n\n")
// list all images
var images []containerd.Image
if images, err = committerImpl.containerdClient.ListImages(ctx); err != nil {
t.Fatalf("ListImages failed: %v", err)
}
fmt.Printf("current image count: %d\n", len(images))
if len(images) > 0 {
fmt.Println("\nimage list:")
for i, img := range images {
fmt.Printf(" [%d] %s\n", i+1, img.Name())
size, err := img.Size(ctx)
if err == nil {
fmt.Printf(" Size: %.2f MB\n", float64(size)/1024/1024)
}
}
} else {
fmt.Println("no image found, test finished")
return
}
fmt.Printf("\n====================================\n")
fmt.Printf("start to remove all images\n")
fmt.Printf("====================================\n\n")
// record each image remove result
type RemoveResult struct {
ImageName string
Size float64
Duration time.Duration
Success bool
Error error
}
results := make([]RemoveResult, 0, len(images))
totalStart := time.Now()
// remove all images
for i, image := range images {
imageName := image.Name()
size, err := image.Size(ctx)
sizeMB := float64(size) / 1024 / 1024
if err != nil {
fmt.Printf(
"[%d/%d] remove image: %s (size unknown, failed to get size: %v)\n",
i+1,
len(images),
imageName,
err,
)
} else {
fmt.Printf("[%d/%d] remove image: %s (%.2f MB)\n", i+1, len(images), imageName, sizeMB)
}
start := time.Now()
err = committer.RemoveImages(ctx, []string{imageName}, true, false)
duration := time.Since(start)
result := RemoveResult{
ImageName: imageName,
Size: sizeMB,
Duration: duration,
Success: err == nil,
Error: err,
}
results = append(results, result)
if err != nil {
fmt.Printf(" status: failed - %v\n", err)
} else {
fmt.Printf(" status: success\n")
}
fmt.Printf(" duration: %v (%.3f seconds)\n\n", duration, duration.Seconds())
}
totalDuration := time.Since(totalStart)
// output statistics result
fmt.Printf("====================================\n")
fmt.Printf("remove statistics\n")
fmt.Printf("====================================\n\n")
successCount := 0
failCount := 0
for _, result := range results {
if result.Success {
successCount++
} else {
failCount++
}
}
fmt.Printf("total image count: %d\n", len(images))
fmt.Printf("success remove: %d\n", successCount)
fmt.Printf("failed remove: %d\n", failCount)
fmt.Printf("total duration: %v (%.3f seconds)\n\n", totalDuration, totalDuration.Seconds())
// detailed statistics
fmt.Println("each image remove duration:")
fmt.Println("-----------------------------------")
for i, result := range results {
status := "✓"
if !result.Success {
status = "✗"
}
fmt.Printf(" [%d] %s %s\n", i+1, status, result.ImageName)
fmt.Printf(" Size: %.2f MB\n", result.Size)
fmt.Printf(" Time: %v (%.3f seconds)\n", result.Duration, result.Duration.Seconds())
if !result.Success {
fmt.Printf(" Error: %v\n", result.Error)
}
}
// verify remove result
fmt.Printf("\n====================================\n")
fmt.Printf("verify remove result\n")
fmt.Printf("====================================\n")
var finalImages []containerd.Image
if finalImages, err = committerImpl.containerdClient.ListImages(ctx); err != nil {
t.Fatalf("ListImages failed: %v", err)
}
fmt.Printf("remaining image count after remove: %d\n", len(finalImages))
if len(finalImages) > 0 {
fmt.Println("\nremaining images:")
for i, img := range finalImages {
fmt.Printf(" [%d] %s\n", i+1, img.Name())
}
}
fmt.Printf("\ntest finished!\n")
}
@@ -1,32 +0,0 @@
package commit
import "time"
const (
DefaultNamespace = "k8s.io"
DefaultContainerdAddress = "unix:///var/run/containerd/containerd.sock"
DefaultRuntime = "io.containerd.runc.v2"
DefaultNerdctlDataRoot = "/var/lib/containerd"
DefaultNerdctlHostsDir = "/etc/containerd/certs.d"
DefaultDevboxSnapshotter = "devbox"
DefaultNetworkMode = "none"
DefaultRemoveImageAsync = true
DefaultRemoveImageForce = false
DefaultRemoveContainerForce = false
InsecureRegistry = true
PauseContainerDuringCommit = false
AnnotationKeyNamespace = "namespace"
AnnotationKeyImageName = "image.name"
AnnotationImageFromValue = "true"
AnnotationUseLimitValue = "1Gi"
DevboxOptionsRemoveBaseImageTopLayer = true
SnapshotLabelPrefix = "containerd.io/snapshot/devbox-"
ContainerLabelPrefix = "devbox.sealos.io/"
RemoveContentIDkey = "containerd.io/snapshot/devbox-remove-content-id"
DefaultMaxRetries = 3
DefaultRetryDelay = 5 * time.Second
DefaultGcInterval = 20 * time.Minute
)
@@ -1,910 +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 controller
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/events"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/matcher"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/resource"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/rwords"
"github.com/labring/sealos/controllers/devbox/internal/stat"
"github.com/labring/sealos/controllers/devbox/label"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
// DevboxReconciler reconciles a Devbox object
type DevboxReconciler struct {
CommitImageRegistry string
DevboxNodeLabel string
NodeName string
RequestRate resource.RequestRate
EphemeralStorage resource.EphemeralStorage
PodMatchers []matcher.PodMatcher
DebugMode bool
MergeBaseImageTopLayer bool
EnableBlockIOResource bool
StartupConfigMapName string
StartupConfigMapNamespace string
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
StateChangeRecorder record.EventRecorder
RestartPredicateDuration time.Duration
AcceptanceThreshold int
stat.NodeStatsProvider
}
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=devboxes,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=devboxes/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=devboxes/finalizers,verbs=update
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=runtimes,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=runtimeclasses,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=pods,verbs=*
// +kubebuilder:rbac:groups="",resources=pods/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=nodes/status,verbs=get
// +kubebuilder:rbac:groups="",resources=services,verbs=*
// +kubebuilder:rbac:groups="",resources=secrets,verbs=*
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=*
// +kubebuilder:rbac:groups="",resources=events,verbs=*
func (r *DevboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx).WithValues("devbox", req.NamespacedName)
// 1) Fetch the object. If it's gone, nothing to do.
devbox := &devboxv1alpha2.Devbox{}
if err := r.Get(ctx, req.NamespacedName, devbox); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
recLabels := label.RecommendedLabels(&label.Recommended{
Name: devbox.Name,
ManagedBy: label.DefaultManagedBy,
PartOf: devboxv1alpha2.LabelDevBoxPartOf,
})
logger.Info("start reconciling devbox")
if r.StartupConfigMapName != "" {
logger.Info(
"startup config map set",
"startupConfigMapName", r.StartupConfigMapName,
"startupConfigMapNamespace", r.StartupConfigMapNamespace,
)
}
// 2) Deletion flow: make best-effort to delete sub-resources, then remove finalizer.
if !devbox.DeletionTimestamp.IsZero() {
return r.reconcileDevboxDeletion(ctx, devbox, recLabels)
}
// 3) Ensure finalizer exists (idempotent).
if err := r.ensureDevboxFinalizer(ctx, req.NamespacedName); err != nil {
return ctrl.Result{}, err
}
// 4) Per-node controller ownership: if another node already owns this devbox, we should not reconcile it.
if devbox.Status.Node != "" && devbox.Status.Node != r.NodeName {
logger.Info(
"devbox already scheduled to another node, skip reconcile",
"node",
devbox.Status.Node,
)
return ctrl.Result{}, nil
}
// 5) Initialize status (idempotent). If we updated status, requeue to continue with the persisted status.
updated, err := r.initDevboxStatus(ctx, devbox)
if err != nil {
return ctrl.Result{}, err
}
if updated {
return ctrl.Result{Requeue: true}, nil
}
// 6) Validate required status fields for the rest of the flow.
commitRecord, requeue, err := r.getCurrentCommitRecord(devbox)
if err != nil {
return ctrl.Result{}, err
}
if requeue {
logger.Info("commit record is not found, requeue to wait for commit record to be created")
return ctrl.Result{Requeue: true}, nil
}
// 7) Scheduling/claiming ownership (only when running).
if devbox.Spec.State == devboxv1alpha2.DevboxStateRunning {
res, err := r.ensureDevboxScheduledToThisNodeIfPossible(
ctx,
req.NamespacedName,
devbox,
commitRecord,
)
if err != nil {
return ctrl.Result{}, err
}
if res.Requeue || res.RequeueAfter > 0 {
return res, nil
}
}
// 8) Reconcile desired resources (pods/services/secrets/etc).
if err := r.runSyncPipeline(ctx, devbox, recLabels); err != nil {
return ctrl.Result{}, err
}
// 9) State transition observability (emit event once per generation).
if err := r.maybeEmitStateChangeEvent(ctx, devbox); err != nil {
return ctrl.Result{}, err
}
// 10) Keep conditions/ObservedGeneration in sync.
if err := r.syncDevboxConditions(ctx, devbox); err != nil {
return ctrl.Result{}, err
}
logger.Info("devbox reconcile success")
return ctrl.Result{}, nil
}
func (r *DevboxReconciler) initDevboxStatus(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
) (updated bool, err error) {
// only fill missing fields; avoid overriding existing status
changed := false
// init devbox status network type
if devbox.Status.Network.Type == "" {
devbox.Status.Network.Type = devbox.Spec.NetworkSpec.Type
changed = true
}
// init devbox status content id
if devbox.Status.ContentID == "" {
devbox.Status.ContentID = uuid.New().String()
changed = true
}
currentContentID := devbox.Status.ContentID
// init devbox status commit record map
if devbox.Status.CommitRecords == nil {
devbox.Status.CommitRecords = make(map[string]*devboxv1alpha2.CommitRecord)
changed = true
}
// init devbox status commit record for current content id
if devbox.Status.CommitRecords[currentContentID] == nil {
devbox.Status.CommitRecords[currentContentID] = &devboxv1alpha2.CommitRecord{
Node: "",
BaseImage: devbox.Spec.Image,
CommitImage: r.generateImageName(devbox),
CommitStatus: devboxv1alpha2.CommitStatusPending,
GenerateTime: metav1.Now(),
}
changed = true
}
// init devbox status state
if devbox.Status.State == "" {
devbox.Status.State = devbox.Spec.State
changed = true
}
// init devbox status network unique id
if devbox.Status.Network.UniqueID == "" {
devbox.Status.Network.UniqueID = rwords.GenerateRandomWords()
changed = true
}
// update devbox status, and do not return error to avoid infinite loop because multiple controller will reconcile this devbox
if changed {
if err := r.Status().Update(ctx, devbox); err != nil {
return false, err
}
return true, nil
}
return false, nil
}
// reconcileDevboxDeletion deletes owned resources then removes the devbox finalizer.
func (r *DevboxReconciler) reconcileDevboxDeletion(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
recLabels map[string]string,
) (ctrl.Result, error) {
logger := log.FromContext(ctx).WithValues("devbox", client.ObjectKeyFromObject(devbox))
logger.Info("devbox deleted, remove all resources")
if err := r.handleSubResourceDelete(ctx, devbox, recLabels); err != nil {
return ctrl.Result{}, err
}
// delete storage:
if err := r.handleStorageDelete(ctx, devbox); err != nil {
return ctrl.Result{}, err
}
logger.Info("devbox deleted, remove finalizer")
if controllerutil.RemoveFinalizer(devbox, devboxv1alpha2.FinalizerName) {
if err := r.Update(ctx, devbox); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// ensureDevboxFinalizer ensures the devbox has the controller finalizer (idempotent).
func (r *DevboxReconciler) ensureDevboxFinalizer(ctx context.Context, key client.ObjectKey) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
latest := &devboxv1alpha2.Devbox{}
if err := r.Get(ctx, key, latest); err != nil {
return client.IgnoreNotFound(err)
}
if !controllerutil.ContainsFinalizer(latest, devboxv1alpha2.FinalizerName) {
controllerutil.AddFinalizer(latest, devboxv1alpha2.FinalizerName)
return r.Update(ctx, latest)
}
return nil
})
}
// getCurrentCommitRecord returns the current commit record.
// If it is missing, caller should requeue (not an error).
func (r *DevboxReconciler) getCurrentCommitRecord(
devbox *devboxv1alpha2.Devbox,
) (record *devboxv1alpha2.CommitRecord, requeue bool, err error) {
if devbox.Status.ContentID == "" {
return nil, true, nil
}
if devbox.Status.CommitRecords == nil {
return nil, true, nil
}
rec := devbox.Status.CommitRecords[devbox.Status.ContentID]
if rec == nil {
return nil, true, nil
}
return rec, false, nil
}
// ensureDevboxScheduledToThisNodeIfPossible tries to claim ownership for a running devbox.
// It returns a ctrl.Result to requeue/slow-requeue when needed.
func (r *DevboxReconciler) ensureDevboxScheduledToThisNodeIfPossible(
ctx context.Context,
key client.ObjectKey,
devbox *devboxv1alpha2.Devbox,
commitRecord *devboxv1alpha2.CommitRecord,
) (ctrl.Result, error) {
logger := log.FromContext(ctx).WithValues("devbox", key)
// If the current content is owned by another node, skip.
if commitRecord.Node != "" && commitRecord.Node != r.NodeName {
logger.Info("devbox already scheduled to node", "node", commitRecord.Node)
return ctrl.Result{}, nil
}
// Already ours: continue.
if commitRecord.Node == r.NodeName {
return ctrl.Result{}, nil
}
// Try to claim ownership when unscheduled.
score := r.getAcceptanceScore(ctx, devbox)
if score < r.AcceptanceThreshold {
logger.Info("devbox not scheduled to node, try scheduling to us later",
"nodeName", r.NodeName,
"score", score,
"acceptanceThreshold", r.AcceptanceThreshold)
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}
logger.Info("devbox not scheduled to node, try scheduling to us now",
"nodeName", r.NodeName,
"score", score,
"acceptanceThreshold", r.AcceptanceThreshold)
claimedByUs := false
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latest := &devboxv1alpha2.Devbox{}
if err := r.Get(ctx, key, latest); err != nil {
return err
}
if latest.Status.CommitRecords == nil ||
latest.Status.CommitRecords[latest.Status.ContentID] == nil {
return fmt.Errorf("commit record missing for contentID %s", latest.Status.ContentID)
}
latestRecord := latest.Status.CommitRecords[latest.Status.ContentID]
// Someone else (or us) already claimed it.
if latestRecord.Node != "" {
return nil
}
latestRecord.Node = r.NodeName
latest.Status.Node = r.NodeName
if err := r.Status().Update(ctx, latest); err != nil {
return err
}
claimedByUs = true
return nil
})
if err != nil {
return ctrl.Result{}, err
}
// If we claimed it, requeue to continue with persisted status and emit event once.
if claimedByUs {
r.Recorder.Eventf(
devbox,
corev1.EventTypeNormal,
"Devbox scheduled to node",
"Devbox scheduled to node",
)
return ctrl.Result{Requeue: true}, nil
}
// Someone else claimed it; stop reconciling on this node.
return ctrl.Result{}, nil
}
// maybeEmitStateChangeEvent records state change event once per generation when this node is allowed to sync.
func (r *DevboxReconciler) maybeEmitStateChangeEvent(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
) error {
logger := log.FromContext(ctx).WithValues("devbox", client.ObjectKeyFromObject(devbox))
if devbox.Status.CommitRecords == nil ||
devbox.Status.CommitRecords[devbox.Status.ContentID] == nil {
return nil
}
// Only the node that owns the current content should sync state.
// Exception: for stop/shutdown transitions, allow syncing when not yet scheduled.
contentID := devbox.Status.ContentID
currentRecord := devbox.Status.CommitRecords[contentID]
ownedByThisNode := currentRecord.Node == r.NodeName
stopOrShutdown := devbox.Spec.State == devboxv1alpha2.DevboxStateStopped ||
devbox.Spec.State == devboxv1alpha2.DevboxStateShutdown
unscheduled := currentRecord.Node == ""
allowedToSyncState := ownedByThisNode || (stopOrShutdown && unscheduled)
needsStateTransition := devbox.Spec.State != devbox.Status.State
if !allowedToSyncState || !needsStateTransition {
return nil
}
shouldEmit, err := r.markStateTransitionPendingAndReturnShouldEmit(ctx, devbox)
if err != nil {
return err
}
if !shouldEmit {
return nil
}
logger.Info(
"recording state change event for devbox",
"devbox",
devbox.Name,
"from",
devbox.Status.State,
"to",
devbox.Spec.State,
)
r.StateChangeRecorder.Eventf(
devbox,
corev1.EventTypeNormal,
events.ReasonDevboxStateChanged,
"Devbox state changed from %s to %s",
devbox.Status.State,
devbox.Spec.State,
)
r.Recorder.Eventf(
devbox,
corev1.EventTypeNormal,
events.ReasonDevboxStateChanged,
"Devbox state changed from %s to %s",
devbox.Status.State,
devbox.Spec.State,
)
return nil
}
func (r *DevboxReconciler) markStateTransitionPendingAndReturnShouldEmit(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
) (bool, error) {
var shouldEmit bool
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latest := &devboxv1alpha2.Devbox{}
if err := r.Get(ctx, client.ObjectKeyFromObject(devbox), latest); err != nil {
return err
}
// If the state is already synced, there's nothing to emit.
if latest.Spec.State == latest.Status.State {
shouldEmit = false
return nil
}
latest.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionStateTransitionPending,
Status: metav1.ConditionTrue,
ObservedGeneration: latest.Generation,
Reason: devboxv1alpha2.DevboxReasonSpecStateChanged,
Message: "spec.state differs from status.state; state transition pending",
LastTransitionTime: metav1.Now(),
})
shouldEmit = true
return r.Status().Update(ctx, latest)
})
return shouldEmit, err
}
func (r *DevboxReconciler) syncDevboxConditions(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
latest := &devboxv1alpha2.Devbox{}
if err := r.Get(ctx, client.ObjectKeyFromObject(devbox), latest); err != nil {
return err
}
// Only advance ObservedGeneration when the state transition is fully synced.
// (Other spec changes are currently reconciled in the same flow; using state
// as the hard gate avoids reporting a generation as "observed" while a
// transition is still pending.)
if latest.Spec.State == latest.Status.State {
latest.Status.ObservedGeneration = latest.Generation
}
// State transition pending condition
if latest.Spec.State != latest.Status.State {
latest.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionStateTransitionPending,
Status: metav1.ConditionTrue,
ObservedGeneration: latest.Generation,
Reason: devboxv1alpha2.DevboxReasonSpecStateChanged,
Message: "spec.state differs from status.state; state transition pending",
LastTransitionTime: metav1.Now(),
})
} else {
latest.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionStateTransitionPending,
Status: metav1.ConditionFalse,
ObservedGeneration: latest.Generation,
Reason: devboxv1alpha2.DevboxReasonStateTransitionSynced,
Message: "spec.state matches status.state",
LastTransitionTime: metav1.Now(),
})
}
// Commit in progress condition (authoritative record is the current ContentID).
// Guard against missing commit record to avoid panics.
var committing bool
if latest.Status.CommitRecords != nil && latest.Status.ContentID != "" {
if rec := latest.Status.CommitRecords[latest.Status.ContentID]; rec != nil {
committing = rec.CommitStatus == devboxv1alpha2.CommitStatusCommitting
}
}
if committing {
latest.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionCommitInProgress,
Status: metav1.ConditionTrue,
ObservedGeneration: latest.Generation,
Reason: devboxv1alpha2.DevboxReasonCommitStarted,
Message: "commit workflow in progress",
LastTransitionTime: metav1.Now(),
})
} else {
latest.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionCommitInProgress,
Status: metav1.ConditionFalse,
ObservedGeneration: latest.Generation,
Reason: devboxv1alpha2.DevboxReasonCommitNotInProgress,
Message: "no commit workflow in progress",
LastTransitionTime: metav1.Now(),
})
}
return r.Status().Update(ctx, latest)
})
}
func (r *DevboxReconciler) handleStorageDelete(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
) error {
logger := log.FromContext(ctx)
// Early return if storage is already cleaned up
if r.isStorageAlreadyCleanedUp(devbox) {
logger.Info("devbox storage already cleaned up, skipping cleanup",
"devbox", devbox.Name,
"state", devbox.Status.State)
return nil
}
// Validate and get commit record
commitRecord, err := r.validateAndGetCommitRecord(devbox)
if err != nil {
logger.Error(err, "failed to validate commit record", "devbox", devbox.Name)
return err
}
// Check if this node should handle the cleanup
if !r.shouldHandleStorageCleanup(commitRecord) {
logger.Info("skipping storage cleanup - not responsible node",
"devbox", devbox.Name,
"commitRecordNode", commitRecord.Node,
"currentNode", r.NodeName)
return nil
}
// Request storage cleanup
return r.requestStorageCleanup(ctx, devbox, commitRecord)
}
// isStorageAlreadyCleanedUp checks if storage cleanup is already done
// shutdown or stopped devbox is already cleaned up
func (r *DevboxReconciler) isStorageAlreadyCleanedUp(devbox *devboxv1alpha2.Devbox) bool {
return devbox.Status.State == devboxv1alpha2.DevboxStateShutdown ||
devbox.Status.State == devboxv1alpha2.DevboxStateStopped
}
// validateAndGetCommitRecord validates devbox status and returns the current commit record
func (r *DevboxReconciler) validateAndGetCommitRecord(
devbox *devboxv1alpha2.Devbox,
) (*devboxv1alpha2.CommitRecord, error) {
contentID := devbox.Status.ContentID
if contentID == "" {
return nil, fmt.Errorf("contentID is empty for devbox %s", devbox.Name)
}
if devbox.Status.CommitRecords == nil {
return nil, fmt.Errorf("commit records is nil for devbox %s", devbox.Name)
}
commitRecord, exists := devbox.Status.CommitRecords[contentID]
if !exists || commitRecord == nil {
return nil, fmt.Errorf(
"commit record not found for contentID %s in devbox %s",
contentID,
devbox.Name,
)
}
if commitRecord.BaseImage == "" {
return nil, fmt.Errorf("baseImage is empty in commit record for devbox %s", devbox.Name)
}
return commitRecord, nil
}
// shouldHandleStorageCleanup determines if the current node should handle storage cleanup
func (r *DevboxReconciler) shouldHandleStorageCleanup(
commitRecord *devboxv1alpha2.CommitRecord,
) bool {
return commitRecord.Node == r.NodeName
}
// requestStorageCleanup sends a storage cleanup request via event recorder
func (r *DevboxReconciler) requestStorageCleanup(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
commitRecord *devboxv1alpha2.CommitRecord,
) error {
logger := log.FromContext(ctx)
logger.Info("requesting devbox storage cleanup",
"devbox", devbox.Name,
"contentID", devbox.Status.ContentID,
"baseImage", commitRecord.BaseImage)
r.StateChangeRecorder.AnnotatedEventf(
devbox,
events.BuildStorageCleanupAnnotations(
devbox.Name,
devbox.Status.ContentID,
commitRecord.BaseImage,
),
corev1.EventTypeNormal,
events.ReasonStorageCleanupRequested,
"devbox storage cleanup requested",
)
return nil
}
func (r *DevboxReconciler) generateImageName(devbox *devboxv1alpha2.Devbox) string {
now := time.Now()
return fmt.Sprintf(
"%s/%s/%s:%s-%s",
r.CommitImageRegistry,
devbox.Namespace,
devbox.Name,
rand.String(5),
now.Format("2006-01-02-150405"),
)
}
func (r *DevboxReconciler) handleSubResourceDelete(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
recLabels map[string]string,
) error {
logger := log.FromContext(ctx)
// Delete Pod
podList := &corev1.PodList{}
if err := r.List(
ctx,
podList,
client.InNamespace(devbox.Namespace),
client.MatchingLabels(recLabels),
); err != nil {
return err
}
for i := range podList.Items {
pod := &podList.Items[i]
originalPodUID := pod.UID
// Remove finalizer with retry and UID check
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestPod := &corev1.Pod{}
if err := r.Get(ctx, client.ObjectKeyFromObject(pod), latestPod); err != nil {
if apierrors.IsNotFound(err) {
// Pod already deleted
logger.Info("pod already deleted, skip finalizer removal", "pod", pod.Name)
return nil
}
return err
}
// Check if UID matches
if latestPod.UID != originalPodUID {
logger.Info("pod UID changed, skip finalizer removal",
"pod", pod.Name,
"originalUID", originalPodUID,
"currentUID", latestPod.UID)
return nil
}
if controllerutil.RemoveFinalizer(latestPod, devboxv1alpha2.FinalizerName) {
return r.Update(ctx, latestPod)
}
return nil
})
if err != nil {
logger.Error(err, "failed to remove finalizer from pod", "pod", pod.Name)
return err
}
}
if err := r.deleteResourcesByLabels(
ctx,
&corev1.Pod{},
devbox.Namespace,
recLabels,
); err != nil {
return err
}
// Delete Service
if err := r.deleteResourcesByLabels(
ctx,
&corev1.Service{},
devbox.Namespace,
recLabels,
); err != nil {
return err
}
// Delete Configmap
if err := r.deleteResourcesByLabels(
ctx,
&corev1.ConfigMap{},
devbox.Namespace,
recLabels,
); err != nil {
return err
}
// Delete Secret
return r.deleteResourcesByLabels(ctx, &corev1.Secret{}, devbox.Namespace, recLabels)
}
func (r *DevboxReconciler) deleteResourcesByLabels(
ctx context.Context,
obj client.Object,
namespace string,
labels map[string]string,
) error {
err := r.DeleteAllOf(ctx, obj,
client.InNamespace(namespace),
client.MatchingLabels(labels),
)
return client.IgnoreNotFound(err)
}
func (r *DevboxReconciler) setConditionWithRetry(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
cond metav1.Condition,
) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
latest := &devboxv1alpha2.Devbox{}
if err := r.Get(ctx, client.ObjectKeyFromObject(devbox), latest); err != nil {
return err
}
cond.ObservedGeneration = latest.Generation
cond.LastTransitionTime = metav1.Now()
latest.SetCondition(cond)
return r.Status().Update(ctx, latest)
})
}
func (r *DevboxReconciler) setSyncCondition(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
conditionType string,
ok bool,
message string,
) {
logger := log.FromContext(ctx)
status := metav1.ConditionFalse
reason := devboxv1alpha2.DevboxReasonSyncFailed
if ok {
status = metav1.ConditionTrue
reason = devboxv1alpha2.DevboxReasonSyncSucceeded
}
if err := r.setConditionWithRetry(ctx, devbox, metav1.Condition{
Type: conditionType,
Status: status,
Reason: reason,
Message: message,
}); err != nil {
logger.Info(
"failed to update condition (best-effort)",
"conditionType",
conditionType,
"error",
err,
)
}
}
// ContentIDChangedPredicate triggers reconcile when devbox status.contentID changes
type ContentIDChangedPredicate struct {
predicate.Funcs
}
func (p ContentIDChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil || e.ObjectNew == nil {
return false
}
oldDevbox, oldOk := e.ObjectOld.(*devboxv1alpha2.Devbox)
newDevbox, newOk := e.ObjectNew.(*devboxv1alpha2.Devbox)
if oldOk && newOk {
return oldDevbox.Status.ContentID != newDevbox.Status.ContentID
}
return false
}
// LastContainerStatusChangedPredicate triggers reconcile when devbox status.lastContainerStatus changes
type LastContainerStatusChangedPredicate struct {
predicate.Funcs
}
func (p LastContainerStatusChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil || e.ObjectNew == nil {
return false
}
oldDevbox, oldOk := e.ObjectOld.(*devboxv1alpha2.Devbox)
newDevbox, newOk := e.ObjectNew.(*devboxv1alpha2.Devbox)
if oldOk && newOk {
return oldDevbox.Status.LastContainerStatus.ContainerID != newDevbox.Status.LastContainerStatus.ContainerID
}
return false
}
// NetworkTypeChangedPredicate triggers reconcile when devbox status.network.type changes
type NetworkTypeChangedPredicate struct {
predicate.Funcs
}
func (p NetworkTypeChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil || e.ObjectNew == nil {
return false
}
oldDevbox, oldOk := e.ObjectOld.(*devboxv1alpha2.Devbox)
newDevbox, newOk := e.ObjectNew.(*devboxv1alpha2.Devbox)
if oldOk && newOk {
return oldDevbox.Status.Network.Type != newDevbox.Status.Network.Type
}
return false
}
// PhaseChangedPredicate triggers reconcile when devbox status.phase changes or status.phase is `Error`
type PhaseChangedPredicate struct {
predicate.Funcs
}
func (p PhaseChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil || e.ObjectNew == nil {
return false
}
oldDevbox, oldOk := e.ObjectOld.(*devboxv1alpha2.Devbox)
newDevbox, newOk := e.ObjectNew.(*devboxv1alpha2.Devbox)
if oldOk && newOk {
return oldDevbox.Status.Phase != newDevbox.Status.Phase ||
newDevbox.Status.Phase == devboxv1alpha2.DevboxPhaseError
}
return false
}
// SetupWithManager sets up the controller with the Manager.
func (r *DevboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
if err := mgr.GetFieldIndexer().
IndexField(context.Background(), &corev1.Pod{}, devboxv1alpha2.PodNodeNameIndex, func(rawObj client.Object) []string {
pod, _ := rawObj.(*corev1.Pod)
if pod.Spec.NodeName == "" {
return nil
}
return []string{pod.Spec.NodeName}
}); err != nil {
return fmt.Errorf("failed to index field %s: %w", devboxv1alpha2.PodNodeNameIndex, err)
}
return ctrl.NewControllerManagedBy(mgr).
WithOptions(controller.Options{MaxConcurrentReconciles: 10}).
For(&devboxv1alpha2.Devbox{}, builder.WithPredicates(predicate.Or(
predicate.GenerationChangedPredicate{}, // enqueue request if devbox spec is updated
NetworkTypeChangedPredicate{}, // enqueue request if devbox status.network.type is updated
ContentIDChangedPredicate{}, // enqueue request if devbox status.contentID is updated
LastContainerStatusChangedPredicate{}, // enqueue request if devbox status.lastContainerStatus is updated
PhaseChangedPredicate{}, // enqueue request if devbox status.phase is updated or status.phase is `Error`
))).
Owns(&corev1.Pod{}, builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})).
// enqueue request if pod spec/status is updated
Owns(&corev1.Service{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
// enqueue request if service spec is updated
Owns(&corev1.Secret{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Complete(r)
}
@@ -1,82 +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 controller
import (
"context"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var _ = Describe("Devbox Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "test-resource"
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed
}
devbox := &devboxv1alpha2.Devbox{}
BeforeEach(func() {
By("creating the custom resource for the Kind Devbox")
err := k8sClient.Get(ctx, typeNamespacedName, devbox)
if err != nil && errors.IsNotFound(err) {
resource := &devboxv1alpha2.Devbox{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: "default",
},
// TODO(user): Specify other spec details if needed.
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
})
AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
resource := &devboxv1alpha2.Devbox{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance Devbox")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &DevboxReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
}
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
})
})
})
@@ -1,337 +0,0 @@
package controller
import (
"context"
"fmt"
"math"
"strconv"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
"github.com/labring/sealos/controllers/devbox/internal/controller/helper"
"github.com/labring/sealos/controllers/devbox/internal/stat"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
func (r *DevboxReconciler) getAcceptanceConsideration(
ctx context.Context,
) (helper.AcceptanceConsideration, error) {
logger := log.FromContext(ctx)
node := &corev1.Node{}
if err := r.Get(context.Background(), client.ObjectKey{Name: r.NodeName}, node); err != nil {
return helper.AcceptanceConsideration{}, err
}
ann := node.Annotations
ac := helper.AcceptanceConsideration{}
if v, err := strconv.ParseFloat(
ann[devboxv1alpha2.AnnotationContainerFSAvailableThreshold],
64,
); err != nil {
logger.Info(
"failed to parse containerfs available threshold. use default value instead",
"value",
ann[devboxv1alpha2.AnnotationContainerFSAvailableThreshold],
)
ac.ContainerFSAvailableThreshold = helper.DefaultContainerFSAvailableThreshold
} else {
ac.ContainerFSAvailableThreshold = v
}
if v, err := strconv.ParseFloat(ann[devboxv1alpha2.AnnotationCPURequestRatio], 64); err != nil {
logger.Info(
"failed to parse CPU request ratio. use default value instead",
"value",
ann[devboxv1alpha2.AnnotationCPURequestRatio],
)
ac.CPURequestRatio = helper.DefaultCPURequestRatio
} else {
ac.CPURequestRatio = v
}
if v, err := strconv.ParseFloat(ann[devboxv1alpha2.AnnotationCPULimitRatio], 64); err != nil {
logger.Info(
"failed to parse CPU limit ratio. use default value instead",
"value",
ann[devboxv1alpha2.AnnotationCPULimitRatio],
)
ac.CPULimitRatio = helper.DefaultCPULimitRatio
} else {
ac.CPULimitRatio = v
}
if v, err := strconv.ParseFloat(
ann[devboxv1alpha2.AnnotationMemoryRequestRatio],
64,
); err != nil {
logger.Info(
"failed to parse memory request ratio. use default value instead",
"value",
ann[devboxv1alpha2.AnnotationMemoryRequestRatio],
)
ac.MemoryRequestRatio = helper.DefaultMemoryRequestRatio
} else {
ac.MemoryRequestRatio = v
}
if v, err := strconv.ParseFloat(
ann[devboxv1alpha2.AnnotationMemoryLimitRatio],
64,
); err != nil {
logger.Info(
"failed to parse memory limit ratio. use default value instead",
"value",
ann[devboxv1alpha2.AnnotationMemoryLimitRatio],
)
ac.MemoryLimitRatio = helper.DefaultMemoryLimitRatio
} else {
ac.MemoryLimitRatio = v
}
return ac, nil
}
func (r *DevboxReconciler) getAcceptanceScore(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
) int {
logger := log.FromContext(ctx)
var (
ac helper.AcceptanceConsideration
containerFsStats stat.FsStats
err error
availableBytes uint64
availablePercentage float64
capacityBytes uint64
cpuRequestRatio float64
cpuLimitRatio float64
memoryRequestRatio float64
memoryLimitRatio float64
storageLimitBytes int64
score int
)
ac, err = r.getAcceptanceConsideration(ctx)
if err != nil {
logger.Error(err, "failed to get acceptance consideration")
goto unsuitable // If we can't get the acceptance consideration, we assume the node is not suitable
}
containerFsStats, err = r.ContainerFsStats(ctx)
switch {
case err != nil:
logger.Error(err, "failed to get container filesystem stats")
goto unsuitable // If we can't get the container filesystem stats, we assume the node is not suitable
case containerFsStats.AvailableBytes == nil:
logger.Info("available bytes is nil, assume the node is not suitable")
goto unsuitable // If we can't get the available bytes, we assume the node is not suitable
case containerFsStats.CapacityBytes == nil:
logger.Info("capacity bytes is nil, assume the node is not suitable")
goto unsuitable // If we can't get the capacity bytes, we assume the node is not suitable
}
availableBytes = *containerFsStats.AvailableBytes
capacityBytes = *containerFsStats.CapacityBytes
if storageLimitBytes, err = helper.GetStorageLimitInBytes(devbox); err != nil {
logger.Error(err, "failed to get storage limit")
goto unsuitable // If we can't get the storage limit, we assume the node is not suitable
} else if storageLimitBytes > 0 && availableBytes < uint64(storageLimitBytes) {
logger.Info(
"available bytes less than storage limit",
"availableBytes",
availableBytes,
"storageLimitBytes",
storageLimitBytes,
)
goto unsuitable // If available bytes are less than the storage limit, we assume the node is not suitable
}
availablePercentage = float64(availableBytes) / float64(capacityBytes) * 100
if availablePercentage > ac.ContainerFSAvailableThreshold {
logger.Info("container filesystem available percentage is greater than threshold",
"availablePercentage", availablePercentage,
"threshold", ac.ContainerFSAvailableThreshold)
score += getScoreUnit(1)
}
cpuRequestRatio, err = r.getTotalCPURequestRatio(ctx)
if err != nil {
logger.Error(err, "failed to get total CPU request")
goto unsuitable // If we can't get the CPU request, we assume the node is not suitable
} else if cpuRequestRatio < ac.CPURequestRatio {
logger.Info(
"cpu request ratio is less than cpu overcommitment request ratio",
"RequestRatio",
cpuRequestRatio,
"ratio",
ac.CPURequestRatio,
)
score += getScoreUnit(0)
}
cpuLimitRatio, err = r.getTotalCPULimitRatio(ctx)
if err != nil {
logger.Error(err, "failed to get total CPU limit")
goto unsuitable // If we can't get the CPU limit, we assume the node is not suitable
} else if cpuLimitRatio < ac.CPULimitRatio {
logger.Info(
"cpu limit ratio is less than cpu overcommitment limit ratio",
"LimitRatio",
cpuLimitRatio,
"ratio",
ac.CPULimitRatio,
)
score += getScoreUnit(0)
}
memoryRequestRatio, err = r.getTotalMemoryRequestRatio(ctx)
if err != nil {
logger.Error(err, "failed to get total memory request")
goto unsuitable // If we can't get the memory request, we assume the node is not suitable
} else if memoryRequestRatio < ac.MemoryRequestRatio {
logger.Info(
"memory request ratio is less than memory overcommitment request ratio",
"RequestRatio",
memoryRequestRatio,
"ratio",
ac.MemoryRequestRatio,
)
score += getScoreUnit(0)
}
memoryLimitRatio, err = r.getTotalMemoryLimitRatio(ctx)
if err != nil {
logger.Error(err, "failed to get total memory limit")
goto unsuitable // If we can't get the memory limit, we assume the node is not suitable
} else if memoryLimitRatio < ac.MemoryLimitRatio {
logger.Info(
"memory limit ratio is less than memory overcommitment limit ratio",
"LimitRatio",
memoryLimitRatio,
"ratio",
ac.MemoryLimitRatio,
)
score += getScoreUnit(0)
}
return score
unsuitable:
return math.MinInt
}
// This function may lead to overflow if p is too large, but since p is always in the range of 0-6, it should be safe.
// Use with caution.
func getScoreUnit(p uint) int {
return 16 << (p * 4)
}
// getTotalCPURequestRatio returns the total CPU requests (in millicores) ratio for all pods in the namespace.
func (r *DevboxReconciler) getTotalCPURequestRatio(ctx context.Context) (float64, error) {
podList := &corev1.PodList{}
listOpts := []client.ListOption{
client.MatchingFields{devboxv1alpha2.PodNodeNameIndex: r.NodeName},
}
if err := r.List(ctx, podList, listOpts...); err != nil {
return 0, err
}
var totalCPURequest int64
for _, pod := range podList.Items {
for _, container := range pod.Spec.Containers {
if cpuReq, ok := container.Resources.Requests[corev1.ResourceCPU]; ok {
// TODO: check if this could lead to overflow
totalCPURequest += cpuReq.Value()
}
}
}
node := &corev1.Node{}
if err := r.Get(ctx, client.ObjectKey{Name: r.NodeName}, node); err != nil {
return 0, err
}
allocatableCPU := node.Status.Allocatable[corev1.ResourceCPU]
allocatableMilli := allocatableCPU.Value()
if allocatableMilli == 0 {
return 0, fmt.Errorf("node %s allocatable CPU is zero", r.NodeName)
}
ratio := float64(totalCPURequest) / float64(allocatableMilli)
return ratio, nil
}
// getTotalCPULimitRatio returns the total CPU limits (in millicores) ratio for all pods in the namespace.
func (r *DevboxReconciler) getTotalCPULimitRatio(ctx context.Context) (float64, error) {
podList := &corev1.PodList{}
listOpts := []client.ListOption{
client.MatchingFields{devboxv1alpha2.PodNodeNameIndex: r.NodeName},
}
if err := r.List(ctx, podList, listOpts...); err != nil {
return 0, err
}
var totalCPULimit int64
for _, pod := range podList.Items {
for _, container := range pod.Spec.Containers {
if cpuLimit, ok := container.Resources.Limits[corev1.ResourceCPU]; ok {
// TODO: check if this could lead to overflow
totalCPULimit += cpuLimit.Value()
}
}
}
node := &corev1.Node{}
if err := r.Get(ctx, client.ObjectKey{Name: r.NodeName}, node); err != nil {
return 0, err
}
allocatableCPU := node.Status.Allocatable[corev1.ResourceCPU]
allocatableMilli := allocatableCPU.Value()
if allocatableMilli == 0 {
return 0, fmt.Errorf("node %s allocatable CPU is zero", r.NodeName)
}
ratio := float64(totalCPULimit) / float64(allocatableMilli)
return ratio, nil
}
// getTotalMemoryRequestRatio returns the total memory requests ratio for all pods in the namespace.
func (r *DevboxReconciler) getTotalMemoryRequestRatio(ctx context.Context) (float64, error) {
podList := &corev1.PodList{}
listOpts := []client.ListOption{
client.MatchingFields{devboxv1alpha2.PodNodeNameIndex: r.NodeName},
}
if err := r.List(ctx, podList, listOpts...); err != nil {
return 0, err
}
var totalMemoryRequest int64
for _, pod := range podList.Items {
for _, container := range pod.Spec.Containers {
if memReq, ok := container.Resources.Requests[corev1.ResourceMemory]; ok {
// TODO: check if this could lead to overflow
totalMemoryRequest += memReq.Value()
}
}
}
node := &corev1.Node{}
if err := r.Get(ctx, client.ObjectKey{Name: r.NodeName}, node); err != nil {
return 0, err
}
allocatableMemory := node.Status.Allocatable[corev1.ResourceMemory]
allocatableBytes := allocatableMemory.Value()
if allocatableBytes == 0 {
return 0, fmt.Errorf("node %s allocatable memory is zero", r.NodeName)
}
ratio := float64(totalMemoryRequest) / float64(allocatableBytes)
return ratio, nil
}
// getTotalMemoryLimitRatio returns the total memory limits ratio for all pods in the namespace.
func (r *DevboxReconciler) getTotalMemoryLimitRatio(ctx context.Context) (float64, error) {
podList := &corev1.PodList{}
listOpts := []client.ListOption{
client.MatchingFields{devboxv1alpha2.PodNodeNameIndex: r.NodeName},
}
if err := r.List(ctx, podList, listOpts...); err != nil {
return 0, err
}
var totalMemoryLimit int64
for _, pod := range podList.Items {
for _, container := range pod.Spec.Containers {
if memLimit, ok := container.Resources.Limits[corev1.ResourceMemory]; ok {
// TODO: check if this could lead to overflow
totalMemoryLimit += memLimit.Value()
}
}
}
node := &corev1.Node{}
if err := r.Get(ctx, client.ObjectKey{Name: r.NodeName}, node); err != nil {
return 0, err
}
allocatableMemory := node.Status.Allocatable[corev1.ResourceMemory]
allocatableBytes := allocatableMemory.Value()
if allocatableBytes == 0 {
return 0, fmt.Errorf("node %s allocatable memory is zero", r.NodeName)
}
ratio := float64(totalMemoryLimit) / float64(allocatableBytes)
return ratio, nil
}
File diff suppressed because it is too large Load Diff
@@ -1,363 +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 controller
import (
"context"
"errors"
"fmt"
"time"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/registry"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// DevboxreleaseReconciler reconciles a Devboxrelease object
type DevboxreleaseReconciler struct {
client.Client
registry.Registry
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=devboxreleases,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=devboxreleases/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=devbox.sealos.io,resources=devboxreleases/finalizers,verbs=update
func (r *DevboxreleaseReconciler) Reconcile(
ctx context.Context,
req ctrl.Request,
) (ctrl.Result, error) {
logger := log.FromContext(ctx)
devboxRelease := &devboxv1alpha2.DevBoxRelease{}
if err := r.Get(ctx, req.NamespacedName, devboxRelease); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if devboxRelease.DeletionTimestamp.IsZero() {
// Add finalizer with retry
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestRelease := &devboxv1alpha2.DevBoxRelease{}
if err := r.Get(ctx, req.NamespacedName, latestRelease); err != nil {
return err
}
if controllerutil.AddFinalizer(latestRelease, devboxv1alpha2.FinalizerName) {
return r.Update(ctx, latestRelease)
}
return nil
})
if err != nil {
return ctrl.Result{}, err
}
} else {
// Remove finalizer with retry
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestRelease := &devboxv1alpha2.DevBoxRelease{}
if err := r.Get(ctx, req.NamespacedName, latestRelease); err != nil {
return client.IgnoreNotFound(err)
}
if controllerutil.RemoveFinalizer(latestRelease, devboxv1alpha2.FinalizerName) {
return r.Update(ctx, latestRelease)
}
return nil
})
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
logger.Info(
"Reconciling DevBoxRelease",
"devbox",
devboxRelease.Spec.DevboxName,
"version",
devboxRelease.Spec.Version,
"phase",
devboxRelease.Status.Phase,
)
devbox := &devboxv1alpha2.Devbox{}
if err := r.Get(
ctx,
client.ObjectKey{Namespace: devboxRelease.Namespace, Name: devboxRelease.Spec.DevboxName},
devbox,
); err != nil {
logger.Error(err, "Failed to get devbox", "devbox", devboxRelease.Spec.DevboxName)
return ctrl.Result{}, err
}
// if devboxRelease.Status.Phase is success, skip release
if devboxRelease.Status.Phase == devboxv1alpha2.DevBoxReleasePhaseSuccess {
logger.Info(
"DevBoxRelease is already released, skipping release",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
return ctrl.Result{}, nil
}
// if devbox is running, skip release
if devbox.Status.State == devboxv1alpha2.DevboxStateRunning ||
devbox.Status.State == devboxv1alpha2.DevboxStatePaused {
logger.Info(
"Devbox is running or paused, skipping release",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
return ctrl.Result{RequeueAfter: time.Second * 10}, nil
}
if devboxRelease.Status.Phase == "" {
// Initialize release phase with retry
sourceImage := devbox.Status.CommitRecords[devbox.Status.ContentID].BaseImage
targetImage := fmt.Sprintf(
"%s/%s/%s:%s",
r.Host,
devboxRelease.Namespace,
devboxRelease.Spec.DevboxName,
devboxRelease.Spec.Version,
)
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestRelease := &devboxv1alpha2.DevBoxRelease{}
if err := r.Get(
ctx,
client.ObjectKeyFromObject(devboxRelease),
latestRelease,
); err != nil {
return err
}
latestRelease.Status.Phase = devboxv1alpha2.DevBoxReleasePhasePending
latestRelease.Status.OriginalDevboxState = devbox.Spec.State
latestRelease.Status.SourceImage = sourceImage
latestRelease.Status.TargetImage = targetImage
return r.Status().Update(ctx, latestRelease)
})
if err != nil {
logger.Error(
err,
"Failed to update status",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: true}, nil
}
if devboxRelease.Status.Phase == devboxv1alpha2.DevBoxReleasePhasePending {
logger.Info(
"Creating release tag",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
err := r.Release(ctx, devboxRelease)
if err != nil && errors.Is(err, registry.ErrManifestNotFound) {
logger.Info(
"Manifest not found, retrying",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
return ctrl.Result{RequeueAfter: time.Second * 10}, nil
} else if err != nil {
logger.Error(
err,
"Failed to create release tag",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
// Update status to failed with retry
_ = retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestRelease := &devboxv1alpha2.DevBoxRelease{}
if err := r.Get(
ctx,
client.ObjectKeyFromObject(devboxRelease),
latestRelease,
); err != nil {
return err
}
latestRelease.Status.Phase = devboxv1alpha2.DevBoxReleasePhaseFailed
return r.Status().Update(ctx, latestRelease)
})
return ctrl.Result{}, err
}
logger.Info(
"Release tag created",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
// Update status to success with retry
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestRelease := &devboxv1alpha2.DevBoxRelease{}
if err := r.Get(
ctx,
client.ObjectKeyFromObject(devboxRelease),
latestRelease,
); err != nil {
return err
}
latestRelease.Status.Phase = devboxv1alpha2.DevBoxReleasePhaseSuccess
return r.Status().Update(ctx, latestRelease)
})
if err != nil {
logger.Error(
err,
"Failed to update status",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
return ctrl.Result{}, err
}
if devboxRelease.Spec.StartDevboxAfterRelease {
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
devbox := &devboxv1alpha2.Devbox{}
if err := r.Get(
ctx,
client.ObjectKey{
Namespace: devboxRelease.Namespace,
Name: devboxRelease.Spec.DevboxName,
},
devbox,
); err != nil {
return err
}
logger.Info(
"Starting devbox after release",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
devbox.Spec.State = devboxv1alpha2.DevboxStateRunning
if err = r.Update(ctx, devbox); err != nil {
logger.Error(
err,
"Failed to update devbox",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
return err
}
return nil
})
if err != nil {
logger.Error(
err,
"Failed to update devbox",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
return ctrl.Result{}, err
}
}
}
logger.Info(
"Reconciliation complete",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
return ctrl.Result{}, nil
}
func (r *DevboxreleaseReconciler) Release(
ctx context.Context,
devboxRelease *devboxv1alpha2.DevBoxRelease,
) error {
logger := log.FromContext(ctx)
if err := r.ReTag(
devboxRelease.Status.SourceImage,
devboxRelease.Status.TargetImage,
); err != nil {
logger.Error(
err,
"Failed to re-tag image",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
return err
}
logger.Info(
"Image re-tagged",
"devbox",
devboxRelease.Spec.DevboxName,
"devboxRelease",
devboxRelease.Name,
"version",
devboxRelease.Spec.Version,
)
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *DevboxreleaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&devboxv1alpha2.DevBoxRelease{}).
Named("devboxrelease").
Complete(r)
}
@@ -1,82 +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 controller
import (
"context"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var _ = Describe("Devboxrelease Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "test-resource"
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed
}
devboxrelease := &devboxv1alpha2.DevBoxRelease{}
BeforeEach(func() {
By("creating the custom resource for the Kind Devboxrelease")
err := k8sClient.Get(ctx, typeNamespacedName, devboxrelease)
if err != nil && errors.IsNotFound(err) {
resource := &devboxv1alpha2.DevBoxRelease{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: "default",
},
// TODO(user): Specify other spec details if needed.
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
})
AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
resource := &devboxv1alpha2.DevBoxRelease{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance Devboxrelease")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &DevboxreleaseReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
}
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
})
})
})
@@ -1,706 +0,0 @@
package controller
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/go-logr/logr"
"github.com/google/uuid"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
"github.com/labring/sealos/controllers/devbox/internal/commit"
"github.com/labring/sealos/controllers/devbox/internal/controller/utils/events"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var (
commitMap = sync.Map{}
deleteMap = sync.Map{}
)
type EventHandler struct {
Committer commit.Committer
CommitImageRegistry string
NodeName string
DefaultBaseImage string
Logger logr.Logger
Client client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
// todo: handle state change event
func (h *EventHandler) Handle(ctx context.Context, event *corev1.Event) error {
h.Logger.Info("StateChangeHandler.Handle called",
"event", event.Name,
"eventSourceHost", event.Source.Host,
"handlerNodeName", h.NodeName,
"eventType", event.Type,
"eventReason", event.Reason,
"eventMessage", event.Message)
if event.Source.Host != h.NodeName {
h.Logger.Info("event source host is not the node name, skip", "event", event)
return nil
}
switch event.Reason {
// handle storage cleanup
case events.ReasonStorageCleanupRequested:
return h.handleStorageCleanup(ctx, event)
// handle state change
case events.ReasonDevboxStateChanged:
return h.handleDevboxStateChange(ctx, event)
default:
return errors.New("invalid event")
}
}
// handleDevboxStateChange handle new structured state change event
func (h *EventHandler) handleDevboxStateChange(ctx context.Context, event *corev1.Event) error {
h.Logger.Info(
"Devbox state change event detected",
"event",
event.Name,
"message",
event.Message,
)
devbox := &devboxv1alpha2.Devbox{}
if err := h.Client.Get(
ctx,
types.NamespacedName{Namespace: event.Namespace, Name: event.InvolvedObject.Name},
devbox,
); err != nil {
h.Logger.Error(err, "failed to get devbox", "devbox", event.InvolvedObject.Name)
return err
}
// Check if state transition is valid and handle accordingly
currentState := devbox.Status.State
targetState := devbox.Spec.State
// Handle invalid state transition
if currentState == devboxv1alpha2.DevboxStateShutdown &&
targetState == devboxv1alpha2.DevboxStateStopped {
h.Recorder.Eventf(
devbox,
corev1.EventTypeWarning,
"Shutdown state is not allowed to be changed to stopped state",
"Shutdown state is not allowed to be changed to stopped state",
)
h.Logger.Error(
errors.New("shutdown state is not allowed to be changed to stopped state"),
"shutdown state is not allowed to be changed to stopped state",
"devbox",
devbox.Name,
)
return errors.New("shutdown state is not allowed to be changed to stopped state")
}
// Handle state transitions that require commit, only running and paused devbox can be shutdown or stopped
needsCommit := (targetState == devboxv1alpha2.DevboxStateShutdown || targetState == devboxv1alpha2.DevboxStateStopped) &&
(currentState == devboxv1alpha2.DevboxStateRunning || currentState == devboxv1alpha2.DevboxStatePaused)
if needsCommit {
// Keep the lock held across the whole retry loop to prevent concurrent commits during backoff windows.
commitKey := devbox.Status.ContentID
if commitKey == "" {
err := errors.New("empty contentID, cannot start commit")
h.Logger.Error(err, "invalid devbox for commit", "devbox", devbox.Name)
return err
}
// Check if commit is already in progress to prevent duplicate requests
if _, loaded := commitMap.LoadOrStore(commitKey, true); loaded {
h.Logger.Info(
"commit already in progress, skipping duplicate request",
"devbox",
devbox.Name,
"contentID",
commitKey,
)
return nil
}
defer commitMap.Delete(commitKey)
start := time.Now()
h.Logger.Info(
"start commit devbox",
"devbox",
devbox.Name,
"contentID",
devbox.Status.ContentID,
"time",
start,
)
// retry commit devbox with retry logic
// backoff: fixed 10s, up to 30 steps (~5min)
err := retry.OnError(wait.Backoff{
Duration: 10 * time.Second,
Factor: 1.0,
Jitter: 0.1,
Steps: 30,
}, func(err error) bool {
// Don't retry if the context is cancelled/timed out, or if devbox is not found
// Controller will handle storage cleanup when devbox is not found
return !errors.Is(err, context.Canceled) &&
!errors.Is(err, context.DeadlineExceeded) &&
!apierrors.IsNotFound(err)
}, func() error {
err := h.commitDevbox(ctx, devbox, targetState)
if err != nil {
h.Logger.Error(err, "failed to commit devbox in retry", "devbox", devbox.Name)
return err
}
return nil
})
if err != nil {
h.Logger.Error(err, "failed to commit devbox after retries", "devbox", devbox.Name)
return err
}
h.Logger.Info(
"commit devbox success",
"devbox",
devbox.Name,
"contentID",
devbox.Status.ContentID,
"time",
time.Since(start),
)
} else if currentState != targetState {
// Handle simple state transitions without commit with retry
h.Logger.Info(
"update devbox status",
"devbox",
devbox.Name,
"from",
currentState,
"to",
targetState,
)
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestDevbox := &devboxv1alpha2.Devbox{}
if err := h.Client.Get(
ctx,
types.NamespacedName{Namespace: devbox.Namespace, Name: devbox.Name},
latestDevbox,
); err != nil {
return err
}
latestDevbox.Status.State = targetState
// Transition synced; clear pending and advance observedGeneration.
if latestDevbox.Spec.State == latestDevbox.Status.State {
latestDevbox.Status.ObservedGeneration = latestDevbox.Generation
latestDevbox.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionStateTransitionPending,
Status: metav1.ConditionFalse,
ObservedGeneration: latestDevbox.Generation,
Reason: devboxv1alpha2.DevboxReasonStateTransitionSynced,
Message: "spec.state matches status.state",
LastTransitionTime: metav1.Now(),
})
}
latestDevbox.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionCommitInProgress,
Status: metav1.ConditionFalse,
ObservedGeneration: latestDevbox.Generation,
Reason: devboxv1alpha2.DevboxReasonCommitNotInProgress,
Message: "no commit workflow in progress",
LastTransitionTime: metav1.Now(),
})
return h.Client.Status().Update(ctx, latestDevbox)
})
if err != nil {
h.Logger.Error(err, "failed to update devbox status", "devbox", devbox.Name)
return err
}
}
return nil
}
func (h *EventHandler) handleStorageCleanup(ctx context.Context, event *corev1.Event) error {
h.Logger.Info("Storage cleanup event detected", "event", event.Name, "message", event.Message)
if _, loaded := deleteMap.LoadOrStore(event.InvolvedObject.Name, true); loaded {
h.Logger.Info(
"delete devbox already in progress, skipping duplicate request",
"devbox",
event.InvolvedObject.Name,
)
return nil
}
defer func() {
deleteMap.Delete(event.InvolvedObject.Name)
}()
if err := h.removeStorage(ctx, event); err != nil {
h.Logger.Error(err, "failed to clean up storage during delete devbox", "devbox", event.Name)
h.Recorder.Eventf(&corev1.ObjectReference{
Kind: event.InvolvedObject.Kind,
Name: event.InvolvedObject.Name,
Namespace: event.InvolvedObject.Namespace,
}, corev1.EventTypeWarning, "Storage cleanup failed",
"Failed to cleanup Storage: %v", err)
} else {
h.Logger.Info("Successfully cleaned up storage during deletion", "devbox", event.Name)
h.Recorder.Eventf(&corev1.ObjectReference{
Kind: event.InvolvedObject.Kind,
Name: event.InvolvedObject.Name,
Namespace: event.InvolvedObject.Namespace,
}, corev1.EventTypeNormal, "Storage cleanup succeeded",
"Successfully cleaned up storage for devbox %s", event.Name)
}
return nil
}
func (h *EventHandler) commitDevbox(
ctx context.Context,
devbox *devboxv1alpha2.Devbox,
targetState devboxv1alpha2.DevboxState,
) error {
if err := h.Client.Get(
ctx,
types.NamespacedName{Namespace: devbox.Namespace, Name: devbox.Name},
devbox,
); err != nil {
if apierrors.IsNotFound(err) {
h.Logger.Info("devbox not found at start of commit", "devbox", devbox.Name)
return err
}
h.Logger.Error(err, "failed to get devbox", "devbox", devbox.Name)
return err
}
// do commit, update devbox commit record, update devbox status state to shutdown, add a new commit record for the new content id
// step 0: set commit status to committing to prevent duplicate requests with retry
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestDevbox := &devboxv1alpha2.Devbox{}
if err := h.Client.Get(
ctx,
types.NamespacedName{Namespace: devbox.Namespace, Name: devbox.Name},
latestDevbox,
); err != nil {
// If devbox is not found, return the error to stop retrying
if apierrors.IsNotFound(err) {
return err
}
return err
}
latestDevbox.Status.CommitRecords[latestDevbox.Status.ContentID].CommitStatus = devboxv1alpha2.CommitStatusCommitting
latestDevbox.Status.CommitRecords[latestDevbox.Status.ContentID].UpdateTime = metav1.Now()
latestDevbox.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionCommitInProgress,
Status: metav1.ConditionTrue,
ObservedGeneration: latestDevbox.Generation,
Reason: devboxv1alpha2.DevboxReasonCommitStarted,
Message: "commit workflow in progress",
LastTransitionTime: metav1.Now(),
})
return h.Client.Status().Update(ctx, latestDevbox)
}); err != nil {
if apierrors.IsNotFound(err) {
h.Logger.Info("devbox not found when setting commit status", "devbox", devbox.Name)
return err
}
h.Logger.Error(err, "failed to update commit status to committing", "devbox", devbox.Name)
return err
}
h.Logger.Info(
"set commit status to committing",
"devbox",
devbox.Name,
"contentID",
devbox.Status.ContentID,
)
if err := h.Client.Get(
ctx,
types.NamespacedName{Namespace: devbox.Namespace, Name: devbox.Name},
devbox,
); err != nil {
if apierrors.IsNotFound(err) {
h.Logger.Info("devbox not found before commit", "devbox", devbox.Name)
return err
}
h.Logger.Error(err, "failed to get devbox", "devbox", devbox.Name)
return err
}
// step 1: do commit, push image, remove container whether commit success or not
baseImage := devbox.Status.CommitRecords[devbox.Status.ContentID].BaseImage
commitImage := devbox.Status.CommitRecords[devbox.Status.ContentID].CommitImage
oldContentID := devbox.Status.ContentID
h.Logger.Info(
"commit devbox",
"devbox",
devbox.Name,
"baseImage",
baseImage,
"commitImage",
commitImage,
)
var containerID string
var commitErr error
removeImageNames := make([]string, 0, 2)
defer func() {
// remove container whether commit success or not
if err := h.Committer.RemoveContainers(ctx, []string{containerID}); err != nil {
h.Logger.Error(err, "failed to remove container", "containerID", containerID)
}
// remove after push image whether push success
if len(removeImageNames) > 0 {
if err := h.Committer.RemoveImages(
ctx,
removeImageNames,
commit.DefaultRemoveImageForce,
commit.DefaultRemoveImageAsync,
); err != nil {
h.Logger.Error(err, "failed to remove image", "removeImageNames", removeImageNames)
}
}
}()
if containerID, commitErr = h.Committer.Commit(
ctx,
devbox.Name,
devbox.Status.ContentID,
baseImage,
commitImage,
); commitErr != nil {
h.Logger.Error(commitErr, "failed to commit devbox", "devbox", devbox.Name)
// Update commit status to failed on commit error with retry
updateErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestDevbox := &devboxv1alpha2.Devbox{}
if err := h.Client.Get(
ctx,
types.NamespacedName{Namespace: devbox.Namespace, Name: devbox.Name},
latestDevbox,
); err != nil {
// If devbox is not found, return the error
// RetryOnConflict will return this error immediately without retrying
if apierrors.IsNotFound(err) {
return err
}
return err
}
latestDevbox.Status.CommitRecords[latestDevbox.Status.ContentID].CommitStatus = devboxv1alpha2.CommitStatusFailed
latestDevbox.Status.CommitRecords[latestDevbox.Status.ContentID].UpdateTime = metav1.Now()
latestDevbox.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionCommitInProgress,
Status: metav1.ConditionFalse,
ObservedGeneration: latestDevbox.Generation,
Reason: devboxv1alpha2.DevboxReasonCommitFailed,
Message: "commit workflow failed",
LastTransitionTime: metav1.Now(),
})
return h.Client.Status().Update(ctx, latestDevbox)
})
if updateErr != nil {
if apierrors.IsNotFound(updateErr) {
h.Logger.Info(
"devbox not found when updating commit status to failed",
"devbox",
devbox.Name,
)
return updateErr
}
h.Logger.Error(
updateErr,
"failed to update commit status to failed",
"devbox",
devbox.Name,
)
}
return commitErr
}
if err := h.Client.Get(
ctx,
types.NamespacedName{Namespace: devbox.Namespace, Name: devbox.Name},
devbox,
); err != nil {
if apierrors.IsNotFound(err) {
h.Logger.Info("devbox not found before push", "devbox", devbox.Name)
return err
}
h.Logger.Error(err, "failed to get devbox", "devbox", devbox.Name)
return err
}
if err := h.Committer.Push(ctx, commitImage); err != nil {
h.Logger.Error(err, "failed to push commit image", "commitImage", commitImage)
// Update commit status to failed on push error with retry
updateErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestDevbox := &devboxv1alpha2.Devbox{}
if err := h.Client.Get(
ctx,
types.NamespacedName{Namespace: devbox.Namespace, Name: devbox.Name},
latestDevbox,
); err != nil {
// If devbox is not found, return the error
if apierrors.IsNotFound(err) {
return err
}
return err
}
latestDevbox.Status.CommitRecords[latestDevbox.Status.ContentID].CommitStatus = devboxv1alpha2.CommitStatusFailed
latestDevbox.Status.CommitRecords[latestDevbox.Status.ContentID].UpdateTime = metav1.Now()
latestDevbox.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionCommitInProgress,
Status: metav1.ConditionFalse,
ObservedGeneration: latestDevbox.Generation,
Reason: devboxv1alpha2.DevboxReasonCommitFailed,
Message: "commit workflow failed (push error)",
LastTransitionTime: metav1.Now(),
})
return h.Client.Status().Update(ctx, latestDevbox)
})
if updateErr != nil {
if apierrors.IsNotFound(updateErr) {
h.Logger.Info(
"devbox not found when updating commit status to failed after push error",
"devbox",
devbox.Name,
)
return updateErr
}
h.Logger.Error(
updateErr,
"failed to update commit status to failed",
"devbox",
devbox.Name,
)
}
return err
}
removeImageNames = append(removeImageNames, commitImage, baseImage)
// step 2: update devbox commit record
// step 3: update devbox status state to shutdown
// step 4: add a new commit record for the new content id
// make sure that always have a new commit record for shutdown state
newContentID := uuid.New().String()
newCommitImage := h.generateImageName(devbox)
h.Logger.Info("update devbox status to shutdown", "devbox", devbox.Name)
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latestDevbox := &devboxv1alpha2.Devbox{}
if err := h.Client.Get(
ctx,
types.NamespacedName{Namespace: devbox.Namespace, Name: devbox.Name},
latestDevbox,
); err != nil {
// If devbox is not found, return the error to stop retrying
if apierrors.IsNotFound(err) {
return err
}
return err
}
latestDevbox.Status.CommitRecords[latestDevbox.Status.ContentID].CommitStatus = devboxv1alpha2.CommitStatusSuccess
latestDevbox.Status.CommitRecords[latestDevbox.Status.ContentID].CommitTime = metav1.Now()
latestDevbox.Status.State = targetState
latestDevbox.Status.ContentID = newContentID
latestDevbox.Status.CommitRecords[newContentID] = &devboxv1alpha2.CommitRecord{
CommitStatus: devboxv1alpha2.CommitStatusPending,
Node: "",
BaseImage: commitImage,
CommitImage: newCommitImage,
GenerateTime: metav1.Now(),
}
latestDevbox.Status.Node = ""
// Commit succeeded; clear in-progress, and clear pending transition if synced.
latestDevbox.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionCommitInProgress,
Status: metav1.ConditionFalse,
ObservedGeneration: latestDevbox.Generation,
Reason: devboxv1alpha2.DevboxReasonCommitSucceeded,
Message: "commit workflow succeeded",
LastTransitionTime: metav1.Now(),
})
if latestDevbox.Spec.State == latestDevbox.Status.State {
latestDevbox.Status.ObservedGeneration = latestDevbox.Generation
latestDevbox.SetCondition(metav1.Condition{
Type: devboxv1alpha2.DevboxConditionStateTransitionPending,
Status: metav1.ConditionFalse,
ObservedGeneration: latestDevbox.Generation,
Reason: devboxv1alpha2.DevboxReasonStateTransitionSynced,
Message: "spec.state matches status.state",
LastTransitionTime: metav1.Now(),
})
}
return h.Client.Status().Update(ctx, latestDevbox)
}); err != nil {
if apierrors.IsNotFound(err) {
h.Logger.Info("devbox not found when updating status", "devbox", devbox.Name)
return err
}
h.Logger.Error(err, "failed to update devbox status", "devbox", devbox.Name)
return err
}
// step 5: set LV removable
if err := h.Committer.SetLvRemovable(ctx, containerID, oldContentID); err != nil {
h.Logger.Error(
err,
"failed to set LV removable",
"containerID",
containerID,
"contentID",
oldContentID,
)
}
return nil
}
func (h *EventHandler) generateImageName(devbox *devboxv1alpha2.Devbox) string {
now := time.Now()
return fmt.Sprintf(
"%s/%s/%s:%s-%s",
h.CommitImageRegistry,
devbox.Namespace,
devbox.Name,
rand.String(5),
now.Format("2006-01-02-150405"),
)
}
func (h *EventHandler) removeStorage(ctx context.Context, event *corev1.Event) error {
h.Logger.Info(
"Starting devbox deletion Storage cleanup",
"devbox",
event.Name,
"message",
event.Message,
)
devboxName, contentID, baseImage := h.parseStorageCleanupAnno(event.Annotations)
// Use k8s.io/client-go/util/retry for robust retry logic
err := retry.OnError(
wait.Backoff{
Duration: 10 * time.Second,
Factor: 1.0,
Jitter: 0.1,
Steps: 30,
},
func(err error) bool { return true },
func() error {
return h.cleanupStorage(ctx, devboxName, contentID, baseImage)
},
)
if err != nil {
h.Logger.Error(err, "Failed to cleanup storage after all retries", "devbox", devboxName)
return fmt.Errorf(
"failed to cleanup storage for devbox %s after retries: %w",
devboxName,
err,
)
}
h.Logger.Info("Successfully completed storage cleanup", "devbox", devboxName)
return nil
}
func (h *EventHandler) cleanupStorage(
ctx context.Context,
devboxName, contentID, baseImage string,
) error {
h.Logger.Info(
"Starting Storage cleanup",
"devbox",
devboxName,
"contentID",
contentID,
"baseImage",
baseImage,
"defaultBaseImage",
h.DefaultBaseImage,
)
// create temp container
containerID, err := h.Committer.CreateContainer(
ctx,
fmt.Sprintf("temp-%s-%d", devboxName, time.Now().UnixMicro()),
contentID,
h.DefaultBaseImage,
)
if err != nil {
h.Logger.Error(
err,
"failed to create temp container",
"devbox",
devboxName,
"contentID",
contentID,
"defaultBaseImage",
h.DefaultBaseImage,
)
return err
}
// make sure remove container
defer func() {
if cleanupErr := h.Committer.RemoveContainers(
ctx,
[]string{containerID},
); cleanupErr != nil {
h.Logger.Error(
cleanupErr,
"failed to remove temporary container",
"devbox",
devboxName,
"containerID",
containerID,
)
} else {
h.Logger.Info(
"Successfully removed temporary container",
"devbox",
devboxName,
"containerID",
containerID,
)
}
}()
// remove storage
if err := h.Committer.SetLvRemovable(ctx, containerID, contentID); err != nil {
h.Logger.Error(
err,
"failed to set Storage removable",
"devbox",
devboxName,
"containerID",
containerID,
"contentID",
contentID,
)
return fmt.Errorf("failed to set Storage removable: %w", err)
}
h.Logger.Info(
"Successfully completed Storage cleanup",
"devbox",
devboxName,
"containerID",
containerID,
"contentID",
contentID,
)
return nil
}
// parseStorageCleanupAnno parses the annotations from the event and returns the devboxName, contentID, and baseImage
func (h *EventHandler) parseStorageCleanupAnno(
annotations events.Annotations,
) (devboxName, contentID, baseImage string) {
devboxName = annotations[events.KeyAnnotationDevboxName]
contentID = annotations[events.KeyAnnotationContentID]
baseImage = annotations[events.KeyAnnotationBaseImage]
return devboxName, contentID, baseImage
}
@@ -1,351 +0,0 @@
// Copyright © 2024 sealos.
//
// 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 helper
import (
"crypto/ed25519"
"crypto/rand"
"encoding/pem"
"fmt"
"strings"
"github.com/google/uuid"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
utilsresource "github.com/labring/sealos/controllers/devbox/internal/controller/utils/resource"
"github.com/labring/sealos/controllers/devbox/label"
"golang.org/x/crypto/ssh"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/utils/ptr"
)
const (
DevBoxPartOf = "devbox"
)
type DevboxPodOptions func(pod *corev1.Pod)
func WithPodImage(image string) DevboxPodOptions {
return func(pod *corev1.Pod) {
pod.Spec.Containers[0].Image = image
}
}
func WithPodContentID(contentID string) DevboxPodOptions {
return func(pod *corev1.Pod) {
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
pod.Annotations[devboxv1alpha2.AnnotationContentID] = contentID
}
}
func WithPodRuntimeHandler(runtime string) DevboxPodOptions {
return func(pod *corev1.Pod) {
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
pod.Annotations[devboxv1alpha2.AnnotationRuntime] = runtime
}
}
func WithPodInit(init string) DevboxPodOptions {
return func(pod *corev1.Pod) {
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
pod.Annotations[devboxv1alpha2.AnnotationInit] = init
}
}
func WithPodAnnotations(annotations map[string]string) DevboxPodOptions {
return func(pod *corev1.Pod) {
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
for k, v := range annotations {
pod.Annotations[k] = v
}
}
}
func WithPodLabels(labels map[string]string) DevboxPodOptions {
return func(pod *corev1.Pod) {
if pod.Labels == nil {
pod.Labels = make(map[string]string)
}
for k, v := range labels {
pod.Labels[k] = v
}
}
}
func WithPodNodeName(nodeName string) DevboxPodOptions {
return func(pod *corev1.Pod) {
pod.Spec.NodeName = nodeName
}
}
func NewContentID() string {
return uuid.New().String()
}
func GeneratePodLabels(devbox *devboxv1alpha2.Devbox) map[string]string {
labels := make(map[string]string)
if devbox.Spec.Config.Labels != nil {
for k, v := range devbox.Spec.Config.Labels {
labels[k] = v
}
}
recLabels := label.RecommendedLabels(&label.Recommended{
Name: devbox.Name,
ManagedBy: label.DefaultManagedBy,
PartOf: DevBoxPartOf,
})
for k, v := range recLabels {
labels[k] = v
}
return labels
}
func GeneratePodAnnotations(
devbox *devboxv1alpha2.Devbox,
enableBlockIOResource bool,
) map[string]string {
annotations := make(map[string]string)
if devbox.Spec.Config.Annotations != nil {
for k, v := range devbox.Spec.Config.Annotations {
annotations[k] = v
}
}
annotations[devboxv1alpha2.AnnotationStorageLimit] = devbox.Spec.StorageLimit
// If BlockIOClass is enabled, add the annotation for BlockIOResources.
// Currently we use a hardcoded value but may make it user configurable later.
if enableBlockIOResource {
annotations[devboxv1alpha2.AnnotationBlockIOResources] = "Devbox"
}
return annotations
}
func GenerateSSHKeyPair() ([]byte, []byte, error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
pemKey, err := ssh.MarshalPrivateKey(privKey, "")
if err != nil {
return nil, nil, err
}
privateKey := pem.EncodeToMemory(pemKey)
publicKey, err := ssh.NewPublicKey(pubKey)
if err != nil {
return nil, nil, err
}
sshPublicKey := ssh.MarshalAuthorizedKey(publicKey)
return sshPublicKey, privateKey, nil
}
// GenerateEnvProfile generates the env profile for the Devbox pod
// use devbox.Spec.Config.Env, generate an profile.d script
func GenerateEnvProfile(devbox *devboxv1alpha2.Devbox, devboxJWTSecret []byte) []byte {
envProfile := []byte("# Generated by Sealos Devbox\n")
for _, env := range devbox.Spec.Config.Env {
envProfile = append(
envProfile,
[]byte(fmt.Sprintf("export %s=\"%s\"\n", env.Name, env.Value))...)
}
envProfile = append(
envProfile,
[]byte(fmt.Sprintf("export DEVBOX_JWT_SECRET=\"%s\"\n", devboxJWTSecret))...)
return envProfile
}
func GenerateSSHVolumeMounts() []corev1.VolumeMount {
return []corev1.VolumeMount{
{
Name: "devbox-ssh-keys",
MountPath: "/usr/start/.ssh/authorized_keys",
SubPath: "authorized_keys",
ReadOnly: true,
},
{
Name: "devbox-ssh-keys",
MountPath: "/usr/start/.ssh/id.pub",
SubPath: "id.pub",
ReadOnly: true,
},
}
}
func GenerateEnvProfileVolumeMount() []corev1.VolumeMount {
return []corev1.VolumeMount{
{
Name: "devbox-env-profile",
MountPath: "/etc/profile.d/env-profile.sh",
SubPath: "env-profile.sh",
ReadOnly: true,
},
}
}
// GenerateSSHVolume generates a volume for SSH keys
func GenerateSSHVolume(devbox *devboxv1alpha2.Devbox) corev1.Volume {
return corev1.Volume{
Name: "devbox-ssh-keys",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: devbox.Name,
Items: []corev1.KeyToPath{
{
Key: "SEALOS_DEVBOX_PUBLIC_KEY",
Path: "id.pub",
},
{
Key: "SEALOS_DEVBOX_AUTHORIZED_KEYS",
Path: "authorized_keys",
},
},
DefaultMode: ptr.To(int32(420)),
},
},
}
}
func GenerateEnvProfileVolume(devbox *devboxv1alpha2.Devbox) corev1.Volume {
return corev1.Volume{
Name: "devbox-env-profile",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: devbox.Name,
Items: []corev1.KeyToPath{
{
Key: "SEALOS_DEVBOX_ENV_PROFILE",
Path: "env-profile.sh",
},
},
DefaultMode: ptr.To(int32(420)),
},
},
}
}
// GenerateResourceRequirements generates the resource requirements for the Devbox pod
func GenerateResourceRequirements(
devbox *devboxv1alpha2.Devbox,
requestRate utilsresource.RequestRate,
ephemeralStorage utilsresource.EphemeralStorage,
) corev1.ResourceRequirements {
return corev1.ResourceRequirements{
Limits: calculateResourceLimit(devbox.Spec.Resource, ephemeralStorage),
Requests: calculateResourceRequest(devbox.Spec.Resource, requestRate, ephemeralStorage),
}
}
func calculateResourceLimit(
original corev1.ResourceList,
ephemeralStorage utilsresource.EphemeralStorage,
) corev1.ResourceList {
limit := original.DeepCopy()
// If ephemeral storage limit is not set, set it to default limit
if l, ok := limit[corev1.ResourceEphemeralStorage]; !ok {
limit[corev1.ResourceEphemeralStorage] = ephemeralStorage.DefaultLimit
} else if l.AsApproximateFloat64() > ephemeralStorage.MaximumLimit.AsApproximateFloat64() {
// Check if the resource limit for ephemeral storage is set and compare it, if it is exceeded the maximum limit, set it to maximum limit
limit[corev1.ResourceEphemeralStorage] = ephemeralStorage.MaximumLimit
}
return limit
}
func calculateResourceRequest(
original corev1.ResourceList,
requestRate utilsresource.RequestRate,
ephemeralStorage utilsresource.EphemeralStorage,
) corev1.ResourceList {
// deep copy limit to request, only cpu and memory are calculated
request := original.DeepCopy()
// Calculate CPU request
if cpu, ok := original[corev1.ResourceCPU]; ok {
cpuValue := cpu.AsApproximateFloat64()
cpuRequest := cpuValue / requestRate.CPU
request[corev1.ResourceCPU] = *resource.NewMilliQuantity(int64(cpuRequest*1000), resource.DecimalSI)
}
// Calculate memory request
if memory, ok := original[corev1.ResourceMemory]; ok {
memoryValue := memory.AsApproximateFloat64()
memoryRequest := memoryValue / requestRate.Memory
request[corev1.ResourceMemory] = *resource.NewQuantity(int64(memoryRequest), resource.BinarySI)
}
// Set ephemeral storage request to default request
request[corev1.ResourceEphemeralStorage] = ephemeralStorage.DefaultRequest
return request
}
// GetWorkingDir get the working directory for the Devbox pod
func GetWorkingDir(devbox *devboxv1alpha2.Devbox) string {
return devbox.Spec.Config.WorkingDir
}
// GetCommand get the command for the Devbox pod
func GetCommand(devbox *devboxv1alpha2.Devbox) []string {
return devbox.Spec.Config.Command
}
// GetArgs get the arguments for the Devbox pod
func GetArgs(devbox *devboxv1alpha2.Devbox) []string {
return devbox.Spec.Config.Args
}
func IsExceededQuotaError(err error) bool {
return strings.Contains(err.Error(), "exceeded quota")
}
func GetStorageLimitInBytes(devbox *devboxv1alpha2.Devbox) (int64, error) {
if devbox.Spec.StorageLimit != "" {
storageLimit, err := resource.ParseQuantity(devbox.Spec.StorageLimit)
if err != nil {
return 0, err
}
return storageLimit.Value(), nil
}
return 0, nil
}
// GenerateStartupVolume generates a volume for the startup script configmap
func GenerateStartupVolume(devbox *devboxv1alpha2.Devbox) corev1.Volume {
return corev1.Volume{
Name: "devbox-startup",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: devbox.Name,
},
DefaultMode: ptr.To(int32(0o755)),
},
},
}
}
// GenerateStartupVolumeMounts generates volume mounts for the startup script
func GenerateStartupVolumeMounts() []corev1.VolumeMount {
return []corev1.VolumeMount{
{
Name: "devbox-startup",
MountPath: "/usr/start/startup.sh",
SubPath: "startup.sh",
ReadOnly: true,
},
}
}
@@ -1,51 +0,0 @@
package helper
import (
"fmt"
"testing"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
corev1 "k8s.io/api/core/v1"
)
func TestGenerateEnvProfile(t *testing.T) {
tests := []struct {
name string
envs []corev1.EnvVar
want string
}{
{
name: "no env variables",
envs: nil,
want: "# Generated by Sealos Devbox\n",
},
{
name: "multiple env variables",
envs: []corev1.EnvVar{
{Name: "FOO", Value: "bar"},
{Name: "HELLO", Value: "world"},
},
want: "# Generated by Sealos Devbox\n" +
"export FOO=\"bar\"\n" +
"export HELLO=\"world\"\n" +
"export SEALOS_DEVBOX_JWT_SECRET=\"test-jwt-secret\"\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devbox := &devboxv1alpha2.Devbox{
Spec: devboxv1alpha2.DevboxSpec{
Config: devboxv1alpha2.Config{
Env: tt.envs,
},
},
}
got := string(GenerateEnvProfile(devbox, []byte("test-jwt-secret")))
fmt.Print(got)
if got != tt.want {
t.Fatalf("GenerateEnvProfile() = %q, want %q", got, tt.want)
}
})
}
}
@@ -1,131 +0,0 @@
// Copyright © 2024 sealos.
//
// 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 helper
import (
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
corev1 "k8s.io/api/core/v1"
)
// State to Phase Mapping Table:
//
// DevboxState (Spec.State) -> DevboxPhase (Status.Phase)
//
// Running State:
// - Has running pod -> Running
// - Has pending pod / No pod -> Pending
// - Failed pod -> Error
// Paused State:
// - Has running pod -> Pausing
// - Has pending pod / No pod -> Paused
// Stopped/Shutdown States:
// - CommitRecord.node != "" -> Transitioning phase (Stopping/Shutting)
// - CommitRecord.node == "" -> Final phase (Stopped/Shutdown)
// GetLatestCommitRecord returns the latest commit record for the given contentID
func GetLatestCommitRecord(
commitRecords devboxv1alpha2.CommitRecordMap,
contentID string,
) *devboxv1alpha2.CommitRecord {
if len(commitRecords) == 0 || contentID == "" {
return nil
}
return commitRecords[contentID]
}
// PodStatus represents the aggregated status of pods
type PodStatus struct {
hasRunning bool
hasPending bool
hasFailed bool
hasTerminating bool
podCount int
}
// AnalyzePodStatus analyzes the pod list and returns aggregated pod status
func AnalyzePodStatus(podList *corev1.PodList) PodStatus {
status := PodStatus{
podCount: len(podList.Items),
}
for i := range podList.Items {
p := &podList.Items[i]
if !p.DeletionTimestamp.IsZero() {
status.hasTerminating = true
}
switch p.Status.Phase {
case corev1.PodRunning:
status.hasRunning = true
case corev1.PodPending:
status.hasPending = true
case corev1.PodFailed:
status.hasFailed = true
}
}
return status
}
// DerivePhase derives phase based on desired state, pod status, and commit record.
// It implements the State to Phase Mapping Table defined above.
func DerivePhase(
desiredState devboxv1alpha2.DevboxState,
podStatus PodStatus,
commitRecord *devboxv1alpha2.CommitRecord,
) devboxv1alpha2.DevboxPhase {
// Failed pod -> Error (applies to all states)
if podStatus.hasFailed {
return devboxv1alpha2.DevboxPhaseError
}
switch desiredState {
case devboxv1alpha2.DevboxStateRunning:
// Running State:
// - Has running pod -> Running
// - Has pending pod / No pod -> Pending
if podStatus.hasRunning {
return devboxv1alpha2.DevboxPhaseRunning
}
return devboxv1alpha2.DevboxPhasePending
case devboxv1alpha2.DevboxStatePaused:
// Paused State:
// - Has running pod -> Pausing
// - Has pending pod / No pod -> Paused
if podStatus.hasRunning {
return devboxv1alpha2.DevboxPhasePausing
}
return devboxv1alpha2.DevboxPhasePaused
case devboxv1alpha2.DevboxStateStopped:
// Stopped State:
// - CommitRecord.node != "" -> Stopping
// - CommitRecord.node == "" -> Stopped
if commitRecord != nil && commitRecord.Node != "" {
return devboxv1alpha2.DevboxPhaseStopping
}
return devboxv1alpha2.DevboxPhaseStopped
case devboxv1alpha2.DevboxStateShutdown:
// Shutdown State:
// - CommitRecord.node != "" -> Shutting
// - CommitRecord.node == "" -> Shutdown
if commitRecord != nil && commitRecord.Node != "" {
return devboxv1alpha2.DevboxPhaseShutting
}
return devboxv1alpha2.DevboxPhaseShutdown
default:
return devboxv1alpha2.DevboxPhaseUnknown
}
}
@@ -1,27 +0,0 @@
package helper
const (
DefaultContainerFSAvailableThreshold = 10.0
DefaultCPURequestRatio = 1.0
DefaultCPULimitRatio = 2.0
DefaultMemoryRequestRatio = 1.0
DefaultMemoryLimitRatio = 2.0
)
type AcceptanceConsideration struct {
// The percentage of available bytes required to consider the node suitable for scheduling devbox.
// Default is 10.0, which means the node must have at least 10% of available bytes in the container filesystem.
ContainerFSAvailableThreshold float64
// The ratio of expected overcommitment (total cpu request / available cpu) of CPU request.
// Default is 1.0, which means the CPU request cannot be overcommited by more than 100%.
CPURequestRatio float64
// The ratio of expected overcommitment (total cpu limit / available cpu) of CPU limit.
// Default is 2.0, which means the CPU limit cannot be overcommited by more than 200%.
CPULimitRatio float64
// The ratio of expected overcommitment (total memory request / available memory) of Memory request.
// Default is 1.0, which means the Memory request cannot be overcommited by more than 100%.
MemoryRequestRatio float64
// The ratio of expected overcommitment (total memory limit / available memory) of Memory limit.
// Default is 2.0, which means the Memory limit cannot be overcommited by more than 200%.
MemoryLimitRatio float64
}
@@ -1,88 +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 controller
import (
"fmt"
"path/filepath"
"runtime"
"testing"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var (
cfg *rest.Config
k8sClient client.Client
testEnv *envtest.Environment
)
func TestControllers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
// 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.30.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
}
var err error
// cfg is defined in this file globally.
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
err = devboxv1alpha2.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
})
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})
@@ -1,22 +0,0 @@
// Copyright © 2024 sealos.
//
// 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 errors
import "errors"
var ErrPodNotLastCommitHistory = errors.New("pod is not the last commit history pod")
// ErrDevboxNotFound is returned when devbox is not found during commit
var ErrDevboxNotFound = errors.New("devbox not found")
@@ -1,22 +0,0 @@
package events
const (
ReasonStorageCleanupRequested = "storage-cleanup-requested"
ReasonDevboxStateChanged = "devbox-state-changed"
KeyAnnotationReason = "reason"
KeyAnnotationDevboxName = "devbox-name"
KeyAnnotationContentID = "content-id"
KeyAnnotationBaseImage = "base-image"
)
type Annotations map[string]string
func BuildStorageCleanupAnnotations(devboxName, contentID, baseImage string) Annotations {
return Annotations{
KeyAnnotationReason: ReasonStorageCleanupRequested,
KeyAnnotationDevboxName: devboxName,
KeyAnnotationContentID: contentID,
KeyAnnotationBaseImage: baseImage,
}
}
@@ -1,174 +0,0 @@
// Copyright © 2024 sealos.
//
// 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 matcher
import (
"log/slog"
devboxv1alpha2 "github.com/labring/sealos/controllers/devbox/api/v1alpha2"
corev1 "k8s.io/api/core/v1"
)
type PodMatcher interface {
Match(expectPod, pod *corev1.Pod) bool
}
type ResourceMatcher struct{}
func (r ResourceMatcher) Match(expectPod, pod *corev1.Pod) bool {
if len(pod.Spec.Containers) == 0 {
slog.Info("Pod has no containers")
return false
}
container := pod.Spec.Containers[0]
expectContainer := expectPod.Spec.Containers[0]
if container.Resources.Requests.Cpu().Cmp(*expectContainer.Resources.Requests.Cpu()) != 0 {
slog.Info("CPU requests are not equal")
return false
}
if container.Resources.Limits.Cpu().Cmp(*expectContainer.Resources.Limits.Cpu()) != 0 {
slog.Info("CPU limits are not equal")
return false
}
if container.Resources.Requests.Memory().
Cmp(*expectContainer.Resources.Requests.Memory()) !=
0 {
slog.Info("Memory requests are not equal")
return false
}
if container.Resources.Limits.Memory().Cmp(*expectContainer.Resources.Limits.Memory()) != 0 {
slog.Info("Memory limits are not equal")
return false
}
return true
}
type EphemeralStorageMatcher struct{}
func (e EphemeralStorageMatcher) Match(expectPod, pod *corev1.Pod) bool {
if len(pod.Spec.Containers) == 0 {
slog.Info("Pod has no containers")
return false
}
container := pod.Spec.Containers[0]
expectContainer := expectPod.Spec.Containers[0]
if container.Resources.Limits.StorageEphemeral().
Cmp(*expectContainer.Resources.Limits.StorageEphemeral()) !=
0 {
slog.Info("Ephemeral-Storage limits are not equal")
return false
}
if container.Resources.Requests.StorageEphemeral().
Cmp(*expectContainer.Resources.Requests.StorageEphemeral()) !=
0 {
slog.Info("Ephemeral-Storage requests are not equal")
return false
}
return true
}
type EnvVarMatcher struct{}
func (e EnvVarMatcher) Match(expectPod, pod *corev1.Pod) bool {
if len(pod.Spec.Containers) == 0 {
slog.Info("Pod has no containers")
return false
}
container := pod.Spec.Containers[0]
expectContainer := expectPod.Spec.Containers[0]
if len(container.Env) != len(expectContainer.Env) {
slog.Info("Environment variable count mismatch")
return false
}
for _, env := range container.Env {
found := false
for _, expectEnv := range expectContainer.Env {
if env.Name == "SEALOS_COMMIT_IMAGE_NAME" {
found = true
break
}
if env.Name == expectEnv.Name && env.Value == expectEnv.Value {
found = true
break
}
}
if !found {
slog.Info(
"Environment variables are not equal",
"env not found",
env.Name,
"env value",
env.Value,
)
return false
}
}
return true
}
type PortMatcher struct{}
func (p PortMatcher) Match(expectPod, pod *corev1.Pod) bool {
if len(pod.Spec.Containers) == 0 {
slog.Info("Pod has no containers")
return false
}
container := pod.Spec.Containers[0]
expectContainer := expectPod.Spec.Containers[0]
if len(container.Ports) != len(expectContainer.Ports) {
slog.Info("Port count mismatch")
return false
}
for _, expectPort := range expectContainer.Ports {
found := false
for _, podPort := range container.Ports {
if expectPort.ContainerPort == podPort.ContainerPort &&
expectPort.Protocol == podPort.Protocol {
found = true
break
}
}
if !found {
slog.Info("Ports are not equal")
return false
}
}
return true
}
type StorageLimitMatcher struct{}
func (s StorageLimitMatcher) Match(expectPod, pod *corev1.Pod) bool {
return expectPod.Annotations[devboxv1alpha2.AnnotationStorageLimit] == pod.Annotations[devboxv1alpha2.AnnotationStorageLimit]
}
// PredicateCommitStatus returns the commit status of the pod
// if the pod container id is empty, it means the pod is pending or has't started, we can assume the image has not been committed
// otherwise, it means the pod has been started, we can assume the image has been committed
func PodMatchExpectations(expectPod, pod *corev1.Pod, matchers ...PodMatcher) bool {
for _, matcher := range matchers {
if !matcher.Match(expectPod, pod) {
return false
}
}
return true
}
@@ -1,174 +0,0 @@
// Copyright © 2024 sealos.
//
// 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 matcher
import (
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)
func TestPodMatchExpectations(t *testing.T) {
expectPod := &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
Env: []corev1.EnvVar{
{Name: "ENV_VAR_1", Value: "value1"},
{Name: "ENV_VAR_2", Value: "value2"},
},
Ports: []corev1.ContainerPort{
{ContainerPort: 8080, Protocol: corev1.ProtocolTCP},
{ContainerPort: 9090, Protocol: corev1.ProtocolTCP},
},
},
},
},
}
tests := []struct {
name string
pod *corev1.Pod
expected bool
}{
{
name: "consistent pod",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
Env: []corev1.EnvVar{
{Name: "ENV_VAR_1", Value: "value1"},
{Name: "ENV_VAR_2", Value: "value2"},
},
Ports: []corev1.ContainerPort{
{ContainerPort: 8080, Protocol: corev1.ProtocolTCP},
{ContainerPort: 9090, Protocol: corev1.ProtocolTCP},
},
},
},
},
},
expected: true,
},
{
name: "inconsistent CPU",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1000m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
Env: []corev1.EnvVar{
{Name: "ENV_VAR_1", Value: "value1"},
{Name: "ENV_VAR_2", Value: "value2"},
},
Ports: []corev1.ContainerPort{
{ContainerPort: 8080, Protocol: corev1.ProtocolTCP},
{ContainerPort: 9090, Protocol: corev1.ProtocolTCP},
},
},
},
},
},
expected: false,
},
{
name: "inconsistent environment variable",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
Env: []corev1.EnvVar{
{Name: "ENV_VAR_1", Value: "value1"},
{Name: "ENV_VAR_3", Value: "value3"},
},
Ports: []corev1.ContainerPort{
{ContainerPort: 8080, Protocol: corev1.ProtocolTCP},
{ContainerPort: 9090, Protocol: corev1.ProtocolTCP},
},
},
},
},
},
expected: false,
},
{
name: "inconsistent port",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("500m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
Env: []corev1.EnvVar{
{Name: "ENV_VAR_1", Value: "value1"},
{Name: "ENV_VAR_2", Value: "value2"},
},
Ports: []corev1.ContainerPort{
{ContainerPort: 8080, Protocol: corev1.ProtocolTCP},
{ContainerPort: 9091, Protocol: corev1.ProtocolTCP},
},
},
},
},
},
expected: false,
},
}
matchers := []PodMatcher{
ResourceMatcher{},
EnvVarMatcher{},
PortMatcher{},
EphemeralStorageMatcher{},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := PodMatchExpectations(expectPod, tt.pod, matchers...)
if result != tt.expected {
t.Errorf("CheckPodConsistency() = %v, expected %v", result, tt.expected)
}
})
}
}
@@ -1,14 +0,0 @@
package nodes
import (
"os"
)
func GetNodeName() string {
nodeName := os.Getenv("NODE_NAME")
if nodeName == "" {
// panic if node name is not set
panic("NODE_NAME is not set")
}
return nodeName
}
@@ -1,128 +0,0 @@
// Copyright © 2024 sealos.
//
// 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 registry
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"github.com/google/go-containerregistry/pkg/name"
)
// todo: refactor this struct, add opts for tls or something else
type Opts struct{}
type BasicAuth struct {
Username string
Password string
}
type Registry struct {
Host string
BasicAuth BasicAuth
}
var ErrManifestNotFound = errors.New("manifest not found")
// ReTag creates a new tag for an existing image by copying its manifest.
func (c *Registry) ReTag(source, target string) error {
manifest, err := c.pullManifest(source)
if err != nil {
return fmt.Errorf("failed to pull manifest for %s: %w", source, err)
}
if err := c.pushManifest(target, manifest); err != nil {
return fmt.Errorf("failed to push manifest for %s: %w", target, err)
}
return nil
}
// todo: refactor this function, add opts for tls
func (c *Registry) pullManifest(image string) ([]byte, error) {
ref, err := name.ParseReference(image)
if err != nil {
return nil, fmt.Errorf("failed to parse image: %w", err)
}
url := fmt.Sprintf(
"http://%s/v2/%s/manifests/%s",
ref.Context().RegistryStr(),
ref.Context().RepositoryStr(),
ref.Identifier(),
)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create GET request: %w", err)
}
req.SetBasicAuth(c.BasicAuth.Username, c.BasicAuth.Password)
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusNotFound:
return nil, ErrManifestNotFound
case http.StatusOK:
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return body, nil
default:
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, resp.Status)
}
}
// todo: refactor this function, add opts for tls
func (c *Registry) pushManifest(image string, manifest []byte) error {
ref, err := name.ParseReference(image)
if err != nil {
return fmt.Errorf("failed to parse image: %w", err)
}
url := fmt.Sprintf(
"http://%s/v2/%s/manifests/%s",
ref.Context().RegistryStr(),
ref.Context().RepositoryStr(),
ref.Identifier(),
)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(manifest))
if err != nil {
return fmt.Errorf("failed to create PUT request: %w", err)
}
req.SetBasicAuth(c.BasicAuth.Username, c.BasicAuth.Password)
req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, resp.Status)
}
return nil
}
@@ -1,59 +0,0 @@
// Copyright © 2024 sealos.
//
// 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 registry
import "testing"
func TestClient_TagImage(t1 *testing.T) {
type fields struct {
Username string
Password string
}
type args struct {
source string
target string
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "Test1",
fields: fields{
Username: "admin",
Password: "passw0rd",
},
args: args{
source: "sealos.hub:5000/default/devbox-sample:2024-08-21-072021",
target: "sealos.hub:5000/default/devbox-sample:test",
},
},
}
for _, tt := range tests {
t1.Run(tt.name, func(t1 *testing.T) {
t := &Registry{
BasicAuth: BasicAuth{
Username: tt.fields.Username,
Password: tt.fields.Password,
},
}
if err := t.ReTag(tt.args.source, tt.args.target); (err != nil) != tt.wantErr {
t1.Errorf("TagImage() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
@@ -1,30 +0,0 @@
// Copyright © 2024 sealos.
//
// 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 resource
import (
"k8s.io/apimachinery/pkg/api/resource"
)
type RequestRate struct {
CPU float64
Memory float64
}
type EphemeralStorage struct {
DefaultRequest resource.Quantity
DefaultLimit resource.Quantity
MaximumLimit resource.Quantity
}
@@ -1,109 +0,0 @@
package rwords
import (
"math/rand"
"sync"
"time"
)
var rng = struct {
sync.Mutex
rand *rand.Rand
}{
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
}
// Int returns a non-negative pseudo-random int.
func Int() int {
rng.Lock()
defer rng.Unlock()
return rng.rand.Int()
}
// Intn generates an integer in range [0,max).
// By design this should panic if input is invalid, <= 0.
func Intn(maxVal int) int {
rng.Lock()
defer rng.Unlock()
return rng.rand.Intn(maxVal)
}
// IntnRange generates an integer in range [min,max).
// By design this should panic if input is invalid, <= 0.
func IntnRange(minVal, maxVal int) int {
rng.Lock()
defer rng.Unlock()
return rng.rand.Intn(maxVal-minVal) + minVal
}
// Int63nRange generates an int64 integer in range [min,max).
// By design this should panic if input is invalid, <= 0.
func Int63nRange(minVal, maxVal int64) int64 {
rng.Lock()
defer rng.Unlock()
return rng.rand.Int63n(maxVal-minVal) + minVal
}
// Seed seeds the rng with the provided seed.
func Seed(seed int64) {
rng.Lock()
defer rng.Unlock()
rng.rand = rand.New(rand.NewSource(seed))
}
// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n)
// from the default Source.
func Perm(n int) []int {
rng.Lock()
defer rng.Unlock()
return rng.rand.Perm(n)
}
const (
// make a `good random string`
alphanums = "abcdefhiklmnorstuvwxz"
// No. of bits required to index into alphanums string.
alphanumsIdxBits = 5
// Mask used to extract last alphanumsIdxBits of an int.
alphanumsIdxMask = 1<<alphanumsIdxBits - 1
// No. of random letters we can extract from a single int63.
maxAlphanumsPerInt = 63 / alphanumsIdxBits
)
// String generates a random consonant-only string which is n
// characters long. This will panic if n is less than zero.
// How the random string is created:
// - we generate random int63's
// - from each int63, we are extracting multiple random letters by bit-shifting and masking
// - if some index is out of range of alphanums we neglect it (unlikely to happen multiple times in a row)
func String(n int) string {
b := make([]byte, n)
rng.Lock()
defer rng.Unlock()
randomInt63 := rng.rand.Int63()
remaining := maxAlphanumsPerInt
for i := 0; i < n; {
if remaining == 0 {
randomInt63, remaining = rng.rand.Int63(), maxAlphanumsPerInt
}
if idx := int(randomInt63 & alphanumsIdxMask); idx < len(alphanums) {
b[i] = alphanums[idx]
i++
}
randomInt63 >>= alphanumsIdxBits
remaining--
}
return string(b)
}
// SafeEncodeString encodes s using the same characters as rand.String. This reduces the chances of bad words and
// ensures that strings generated from hash functions appear consistent throughout the API.
func SafeEncodeString(s string) string {
r := make([]byte, len(s))
for i, b := range []rune(s) {
r[i] = alphanums[(int(b) % len(alphanums))]
}
return string(r)
}
@@ -1,19 +0,0 @@
package rwords
import (
"fmt"
)
const listLength = len(wordsList)
// data size is 2048*2048*21^4=815,712,436,224
// compare to 8 random characters, data size is 36^8 = 208,827,064,576
// outputs looks like: "abandon-ability-abcd"
func GenerateRandomWords() string {
return fmt.Sprintf(
"%s-%s-%s",
wordsList[Intn(listLength)],
wordsList[Intn(listLength)],
String(4),
)
}
@@ -1,210 +0,0 @@
package rwords
// length of wordsList is 2048
var wordsList = [...]string{
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse",
"access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act",
"action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit",
"adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent",
"agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert",
"alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter",
"always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger",
"angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique",
"anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic",
"area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest",
"arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset",
"assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction",
"audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake",
"aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge",
"bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain",
"barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become",
"beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit",
"best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology",
"bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless",
"blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body",
"boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss",
"bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread",
"breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze",
"broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb",
"bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy",
"butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call",
"calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas",
"canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry",
"cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category",
"cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century",
"cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase",
"chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child",
"chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle",
"citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk",
"clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close",
"cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut",
"code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort",
"comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control",
"convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost",
"cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle",
"craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek",
"crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial",
"cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup",
"cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad",
"damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal",
"debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense",
"define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny",
"depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk",
"despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond",
"diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur",
"direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance",
"divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain",
"donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama",
"drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop",
"drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf",
"dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo",
"ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow",
"elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody",
"embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless",
"endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough",
"enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip",
"era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate",
"eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange",
"excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit",
"exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye",
"eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame",
"family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father",
"fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female",
"fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file",
"film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first",
"fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor",
"flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly",
"foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest",
"forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile",
"frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen",
"fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy",
"gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp",
"gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture",
"ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance",
"glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue",
"goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown",
"grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid",
"grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt",
"guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy",
"harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health",
"heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden",
"high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole",
"holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital",
"host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred",
"hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea",
"identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune",
"impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate",
"indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury",
"inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install",
"intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue",
"item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel",
"job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior",
"junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney",
"kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife",
"knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language",
"laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit",
"layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal",
"legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level",
"liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit",
"link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster",
"local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love",
"loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad",
"magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage",
"mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market",
"marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum",
"maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt",
"member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message",
"metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor",
"minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile",
"model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral",
"more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie",
"much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual",
"myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature",
"near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net",
"network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee",
"noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now",
"nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe",
"obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often",
"oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online",
"only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order",
"ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output",
"outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact",
"paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper",
"parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol",
"pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen",
"penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo",
"phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot",
"pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate",
"play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar",
"pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post",
"potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare",
"present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private",
"prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property",
"prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin",
"punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle",
"pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit",
"raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp",
"ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor",
"ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle",
"reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release",
"relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen",
"repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response",
"result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib",
"ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot",
"ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket",
"romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal",
"rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness",
"safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand",
"satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter",
"scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script",
"scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed",
"seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service",
"session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell",
"sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop",
"short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side",
"siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since",
"sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill",
"skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight",
"slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth",
"snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda",
"soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry",
"sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn",
"speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin",
"spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring",
"spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp",
"stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick",
"still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street",
"strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway",
"success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny",
"sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey",
"suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim",
"swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag",
"tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi",
"teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text",
"thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought",
"three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber",
"time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler",
"toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool",
"tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist",
"toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer",
"trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick",
"trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust",
"truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle",
"twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella",
"unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform",
"unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade",
"uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful",
"useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van",
"vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue",
"verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory",
"video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual",
"vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage",
"wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash",
"wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather",
"web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat",
"wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will",
"win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise",
"wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world",
"worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year",
"yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo", "zjr",
}
-54
View File
@@ -1,54 +0,0 @@
package stat
import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// FsStats contains data about filesystem usage.
// This part of code is taken from k8s.io/kubelet/pkg/apis/stats/v1alpha1
// Maybe we should import it directly in the future.
type FsStats struct {
// The time at which these stats were updated.
Time metav1.Time `json:"time"`
// AvailableBytes represents the storage space available (bytes) for the filesystem.
// +optional
AvailableBytes *uint64 `json:"availableBytes,omitempty"`
// CapacityBytes represents the total capacity (bytes) of the filesystems underlying storage.
// +optional
CapacityBytes *uint64 `json:"capacityBytes,omitempty"`
// UsedBytes represents the bytes used for a specific task on the filesystem.
// This may differ from the total bytes used on the filesystem and may not equal CapacityBytes - AvailableBytes.
// e.g. For ContainerStats.Rootfs this is the bytes used by the container rootfs on the filesystem.
// +optional
UsedBytes *uint64 `json:"usedBytes,omitempty"`
// InodesFree represents the free inodes in the filesystem.
// +optional
InodesFree *uint64 `json:"inodesFree,omitempty"`
// Inodes represents the total inodes in the filesystem.
// +optional
Inodes *uint64 `json:"inodes,omitempty"`
// InodesUsed represents the inodes used by the filesystem
// This may not equal Inodes - InodesFree because this filesystem may share inodes with other "filesystems"
// e.g. For ContainerStats.Rootfs, this is the inodes used only by that container, and does not count inodes used by other containers.
InodesUsed *uint64 `json:"inodesUsed,omitempty"`
}
type NodeStatsProvider interface {
ContainerFsStats(ctx context.Context) (FsStats, error)
}
type NodeStatsProviderImpl struct {
// Client *containerd.Client
}
func (n *NodeStatsProviderImpl) ContainerFsStats(ctx context.Context) (FsStats, error) {
// This is a placeholder for the actual implementation.
// In a real implementation, this would return the filesystem stats of the container.
availableBytes := uint64(100000000000) // Example value
capacityBytes := uint64(200000000000) // Example value
return FsStats{
AvailableBytes: &availableBytes, // Example value
CapacityBytes: &capacityBytes, // Example value
}, nil
}
-68
View File
@@ -1,68 +0,0 @@
// Copyright © 2024 sealos.
//
// 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 label
type Recommended struct {
Name string
Instance string
Version string
Component string
PartOf string
ManagedBy string
}
const (
AppName AppKey = "app.kubernetes.io/name"
AppInstance AppKey = "app.kubernetes.io/instance"
AppVersion AppKey = "app.kubernetes.io/version"
AppComponent AppKey = "app.kubernetes.io/component"
AppPartOf AppKey = "app.kubernetes.io/part-of"
AppManagedBy AppKey = "app.kubernetes.io/managed-by"
)
type AppKey = string
const (
DefaultManagedBy = "sealos"
)
func (r *Recommended) Labels() map[string]string {
ret := map[string]string{}
if r.Name != "" {
ret[AppName] = r.Name
}
if r.Instance != "" {
ret[AppInstance] = r.Instance
}
if r.Version != "" {
ret[AppVersion] = r.Version
}
if r.Component != "" {
ret[AppComponent] = r.Component
}
if r.PartOf != "" {
ret[AppPartOf] = r.PartOf
}
if r.ManagedBy != "" {
ret[AppManagedBy] = r.ManagedBy
}
return ret
}
func RecommendedLabels(r *Recommended) map[string]string {
return r.Labels()
}
@@ -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 devbox suite\n")
RunSpecs(t, "e2e suite")
}
-123
View File
@@ -1,123 +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/labring/sealos/controllers/devbox/test/utils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
const namespace = "devbox-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
projectimage := "example.com/devbox:v0.0.1"
By("building the manager(Operator) image")
cmd := exec.Command("make", "docker-build", "IMG="+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 controller-manager")
cmd = exec.Command("make", "deploy", "IMG="+projectimage)
_, err = utils.Run(cmd)
ExpectWithOffset(1, err).NotTo(HaveOccurred())
By("validating that the controller-manager pod is running as expected")
verifyControllerUp := func() error {
// Get pod name
cmd = exec.Command("kubectl", "get",
"pods", "-l", "control-plane=controller-manager",
"-o", "go-template={{ range .items }}"+
"{{ if not .metadata.deletionTimestamp }}"+
"{{ .metadata.name }}"+
"{{ \"\\n\" }}{{ end }}{{ end }}",
"-n", namespace,
)
podOutput, err := utils.Run(cmd)
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("controller-manager"))
// 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())
})
})
})
-140
View File
@@ -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"
v2 "github.com/onsi/ginkgo/v2"
)
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(v2.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(v2.GinkgoWriter, "chdir dir: %s\n", err)
}
cmd.Env = append(os.Environ(), "GO111MODULE=on")
command := strings.Join(cmd.Args, " ")
_, _ = fmt.Fprintf(v2.GinkgoWriter, "running: %s\n", command)
output, err := cmd.CombinedOutput()
if err != nil {
return output, fmt.Errorf("%s failed with error: %w %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.ReplaceAll(wd, "/test/e2e", "")
return wd, nil
}
-1
View File
@@ -3,7 +3,6 @@ go 1.25.0
use (
./account
./app
./devbox
./job/heartbeat
./job/init
./license
+51
View File
@@ -1,6 +1,8 @@
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM=
cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
@@ -443,6 +445,7 @@ cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGB
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
@@ -1786,6 +1789,7 @@ git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik=
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 h1:dIScnXFlF784X79oi7MzVT6GWqr/W1uUt0pB5CsDs9M=
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2/go.mod h1:gCLVsLfv1egrcZu+GoJATN5ts75F2s62ih/457eWzOw=
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
@@ -1975,6 +1979,8 @@ github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwc
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed h1:ue9pVfIcP+QMEjfgo/Ez4ZjNZfonGgR6NgjMaJMu1Cg=
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI=
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM=
@@ -1999,6 +2005,7 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/authzed/controller-idioms v0.7.0 h1:HhNMUBb8hJzYqY3mhen3B2AC5nsIem3fBe0tC/AAOHo=
@@ -2169,6 +2176,7 @@ github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq
github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM=
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw=
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
@@ -2397,6 +2405,7 @@ github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76 h1:Lgdd/Qp96Qj8jqLpq2cI1I1X7BJnu06efS+XkhRoLUQ=
github.com/cyberphone/json-canonicalization v0.0.0-20230514072755-504adb8a8af1/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
@@ -2521,6 +2530,7 @@ github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1
github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g=
github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI=
github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
@@ -2537,6 +2547,7 @@ github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBF
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
@@ -2722,6 +2733,7 @@ github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -2740,6 +2752,8 @@ github.com/google/cel-go v0.12.6 h1:kjeKudqV0OygrAqA9fX6J55S8gj+Jre2tckIm5RoG4M=
github.com/google/cel-go v0.12.6/go.mod h1:Jk7ljRzLBhkmiAwBoUxB1sZSCVBAzkqPF25olK/iRDw=
github.com/google/cel-go v0.16.0/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY=
github.com/google/cel-go v0.16.1/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY=
github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g=
github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM=
github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
@@ -2748,6 +2762,8 @@ github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZat
github.com/google/fswalker v0.2.1-0.20200214223026-f0e929ba4126/go.mod h1:ZSEBqY0IHKqWPeAbTyvccv9bb9vCnaQfHe31cm911Ng=
github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
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/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q=
github.com/google/go-containerregistry v0.20.1 h1:eTgx9QNYugV4DN5mz4U8hiAGTi1ybXn0TPi4Smd8du0=
@@ -2874,6 +2890,7 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc=
@@ -2907,6 +2924,7 @@ github.com/hashicorp/go-kms-wrapping/v2 v2.0.8/go.mod h1:qTCjxGig/kjuj3hk1z8pOUr
github.com/hashicorp/go-memdb v1.3.2 h1:RBKHOsnSszpU6vxq80LzC2BaQjuuvoyaQbkLTf7V7g8=
github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.4.8 h1:CHGwpxYDOttQOY7HOWgETU9dyVjOXzniXDqJcYJE1zM=
github.com/hashicorp/go-plugin v1.4.8/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s=
@@ -2990,6 +3008,7 @@ github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK
github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
@@ -3178,6 +3197,7 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
@@ -3237,6 +3257,7 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b h1:Ga1nclDSe8gOw37MVLMhfu2QKWtD6gvtQ298zsKVh8g=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
@@ -3244,10 +3265,12 @@ github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os=
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok=
github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
@@ -3273,6 +3296,7 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P
github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/mrunalp/fileutils v0.5.1 h1:F+S7ZlNKnrwHfSwdlgNSkKo67ReVf8o9fel6C3dkm/Q=
github.com/mrunalp/fileutils v0.5.1/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
@@ -3543,6 +3567,7 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY=
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
@@ -3635,6 +3660,7 @@ github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
@@ -3657,6 +3683,8 @@ github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8W
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec h1:q6XVwXmKvCRHRqesF3cSv6lNqqHi0QWOvgDlSohg8UA=
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo=
github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8=
github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -3667,6 +3695,7 @@ github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRci
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
@@ -3763,9 +3792,13 @@ github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc=
github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
@@ -3907,6 +3940,8 @@ go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvS
go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI=
go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0=
go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=
go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo=
@@ -3986,6 +4021,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0/go.mod h1:HrbCVv40OOLT
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1/go.mod h1:xOvWoTOrQjxjW61xtOmD/WKGRYb/P4NzRo3bs65U6Rk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY=
@@ -3995,6 +4031,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.2/go.mod h
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0/go.mod h1:5w41DY6S9gZrbjuq6Y+753e96WfPha5IcsOSZTtullM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.14.0/go.mod h1:+N7zNjIJv4K+DeX67XXET0P+eIciESgaFDBqh+ZJFS4=
@@ -4043,6 +4080,7 @@ go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwP
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g=
go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE=
@@ -4081,6 +4119,7 @@ go.opentelemetry.io/proto/otlp v0.16.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI
go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY=
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
@@ -4385,6 +4424,7 @@ golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -4763,6 +4803,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas=
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY=
google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4=
@@ -4821,8 +4863,11 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
@@ -4881,6 +4926,8 @@ google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDom
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
@@ -4936,6 +4983,7 @@ gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
helm.sh/helm/v3 v3.12.3 h1:5y1+Sbty12t48T/t/CGNYUIME5BJ0WKfmW/sobYqkFg=
helm.sh/helm/v3 v3.12.3/go.mod h1:KPKQiX9IP5HX7o5YnnhViMnNuKiL/lJBVQ47GHe1R0k=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -4952,6 +5000,7 @@ k8s.io/apiserver v0.26.2/go.mod h1:GHcozwXgXsPuOJ28EnQ/jXEM9QeG6HT22YxSNmpYNh8=
k8s.io/apiserver v0.26.3/go.mod h1:CJe/VoQNcXdhm67EvaVjYXxR3QyfwpceKPuPaeLibTA=
k8s.io/apiserver v0.28.1/go.mod h1:d8aizlSRB6yRgJ6PKfDkdwCy2DXt/d1FDR6iJN9kY1w=
k8s.io/apiserver v0.28.3/go.mod h1:YIpM+9wngNAv8Ctt0rHG4vQuX/I5rvkEMtZtsxW2rNM=
k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak=
k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw=
k8s.io/cli-runtime v0.28.2 h1:64meB2fDj10/ThIMEJLO29a1oujSm0GQmKzh1RtA/uk=
k8s.io/cli-runtime v0.28.2/go.mod h1:bTpGOvpdsPtDKoyfG4EG041WIyFZLV9qq4rPlkyYfDA=
@@ -5143,6 +5192,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.35 h1:+xBL5uTc+BkPB
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.35/go.mod h1:WxjusMwXlKzfAs4p9km6XJRndVt2FROgMVCE4cdohFo=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.36/go.mod h1:WxjusMwXlKzfAs4p9km6XJRndVt2FROgMVCE4cdohFo=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2/go.mod h1:+qG7ISXqCDVVcyO8hLn12AKVYYUjM7ftlqsqmrhMZE0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-tools v0.6.2 h1:+Y8L0UsAugDipGRw8lrkPoAi6XqlQVZuf1DQHME3PgU=
sigs.k8s.io/gateway-api v0.4.0 h1:07IJkTt21NetZTHtPKJk2I4XIgDN4BAlTIq1wK7V11o=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
-3
View File
@@ -27,7 +27,6 @@ declare -A CONTROLLER_MODULES=(
["job-heartbeat"]="job/heartbeat"
["resources"]="resources"
["node"]="node"
["devbox"]="devbox"
["objectstorage"]="objectstorage"
)
@@ -38,10 +37,8 @@ declare -A SERVICE_MODULES=(
["minio"]="minio"
["launchpad"]="launchpad"
["exceptionmonitor"]="exceptionmonitor"
["devbox"]="devbox"
["vlogs"]="vlogs"
["hubble"]="hubble"
["sshgate"]="sshgate"
)
# Function to get all modules for a type
-8
View File
@@ -1,8 +0,0 @@
target/
.claude/
**/*.rs.bk
dhat-heap.json
.vscode
.idea
.cover
bleeper.user.toml
-1
View File
@@ -1 +0,0 @@
edition = "2021"
-3043
View File
File diff suppressed because it is too large Load Diff
-11
View File
@@ -1,11 +0,0 @@
[workspace]
resolver = "2"
members = ["httpgate"]
[workspace.dependencies]
[profile.bench]
debug = true
-3
View File
@@ -1,3 +0,0 @@
target/
Dockerfile
.github/
-8
View File
@@ -1,8 +0,0 @@
target/
.claude/
**/*.rs.bk
dhat-heap.json
.vscode
.idea
.cover
bleeper.user.toml
-3032
View File
File diff suppressed because it is too large Load Diff
-42
View File
@@ -1,42 +0,0 @@
[package]
name = "httpgate"
version = "0.1.0"
edition = "2021"
[dependencies]
# Pingora HTTP proxy
pingora-core = "0.6"
pingora-proxy = "0.6"
pingora-http = "0.6"
# Kubernetes
kube = { version = "2.0", features = ["runtime", "derive"] }
k8s-openapi = { version = "0.26", features = ["v1_32"] }
# Serialization (for CRD)
schemars = "1.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Async runtime
tokio = { version = "1", features = ["rt-multi-thread", "time", "sync"] }
async-trait = "0.1"
futures = "0.3"
# Utilities
bytes = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
regex = "1"
dashmap = "6"
[profile.release]
opt-level = 3
debug = 0
lto = "fat"
codegen-units = 1
incremental = false
panic = "unwind"
strip = "symbols"
debug-assertions = false
overflow-checks = false
-33
View File
@@ -1,33 +0,0 @@
FROM rust:alpine AS builder
RUN apk add \
musl-dev \
make \
cmake \
g++
WORKDIR /app
# Copy workspace root files
COPY Cargo.toml Cargo.lock ./
# Copy the target crate
COPY httpgate ./httpgate
# Build the specific package
RUN cargo build --release --package httpgate
FROM alpine
# Run as non-root user
RUN adduser -D -u 1000 httpgate
USER httpgate
WORKDIR /app
# Copy the binary from builder
COPY --from=builder /app/target/release/httpgate /app/httpgate
EXPOSE 8080
ENTRYPOINT ["/app/httpgate"]
-6
View File
@@ -1,6 +0,0 @@
FROM scratch
COPY registry registry
COPY install.sh install.sh
COPY charts charts
CMD ["bash install.sh"]
@@ -1,23 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
@@ -1,15 +0,0 @@
apiVersion: v2
name: httpgate
description: A Kubernetes-native HTTP gateway that routes HTTP requests to Devbox pods based on Host header
type: application
version: 0.1.0
appVersion: "latest"
keywords:
- http
- gateway
- devbox
- sealos
- proxy
- pingora
maintainers:
- name: httpgate
@@ -1,62 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "httpgate.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 "httpgate.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 "httpgate.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "httpgate.labels" -}}
helm.sh/chart: {{ include "httpgate.chart" . }}
{{ include "httpgate.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "httpgate.selectorLabels" -}}
app.kubernetes.io/name: {{ include "httpgate.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "httpgate.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "httpgate.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
@@ -1,17 +0,0 @@
{{- if .Values.rbac.create -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "httpgate.fullname" . }}
labels:
{{- include "httpgate.labels" . | nindent 4 }}
rules:
# Watch Pods to get Pod IPs
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
# Watch Devbox CRDs to get uniqueID mappings
- apiGroups: ["devbox.sealos.io"]
resources: ["devboxes"]
verbs: ["get", "list", "watch"]
{{- end }}
@@ -1,16 +0,0 @@
{{- if .Values.rbac.create -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "httpgate.fullname" . }}
labels:
{{- include "httpgate.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "httpgate.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "httpgate.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}
@@ -1,16 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "httpgate.fullname" . }}
labels:
{{- include "httpgate.labels" . | nindent 4 }}
data:
{{- if not (hasKey .Values.env "LISTEN_ADDR") }}
LISTEN_ADDR: {{ printf "0.0.0.0:%d" (int .Values.httpPort) | quote }}
{{- end }}
{{- if not (hasKey .Values.env "AGENT_PORT") }}
AGENT_PORT: {{ .Values.agentPort | quote }}
{{- end }}
{{- range $key, $value := .Values.env }}
{{ $key }}: {{ $value | quote }}
{{- end }}
@@ -1,73 +0,0 @@
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: {{ include "httpgate.fullname" . }}
labels:
{{- include "httpgate.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "httpgate.selectorLabels" . | nindent 6 }}
updateStrategy:
type: {{ .Values.updateStrategy.type }}
{{- if eq .Values.updateStrategy.type "RollingUpdate" }}
rollingUpdate:
maxUnavailable: {{ .Values.updateStrategy.rollingUpdate.maxUnavailable }}
{{- end }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "httpgate.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "httpgate.serviceAccountName" . }}
{{- with .Values.priorityClassName }}
priorityClassName: {{ . }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.httpPort }}
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "httpgate.fullname" . }}
livenessProbe:
tcpSocket:
port: {{ .Values.httpPort }}
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
tcpSocket:
port: {{ .Values.httpPort }}
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -1,113 +0,0 @@
{{- if .Values.ingress.enabled -}}
---
# HTTP Ingress (devbox- prefix, non-gRPC requests)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "httpgate.fullname" . }}
labels:
{{- include "httpgate.labels" . | nindent 4 }}
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
higress.io/prefix-match-header-host: devbox-
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.ingressClassName }}
ingressClassName: {{ .Values.ingress.ingressClassName }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
{{- $host := .host }}
{{- if not (hasPrefix "*." $host) }}
{{- $host = printf "*.%s" $host }}
{{- end }}
- host: {{ $host | quote }}
http:
paths:
{{- $paths := .paths | default (list (dict "path" "/" "pathType" "Prefix")) }}
{{- range $paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "httpgate.fullname" $ }}
port:
number: {{ $.Values.httpPort }}
{{- end }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
{{- $tlsHost := . }}
{{- if not (hasPrefix "*." $tlsHost) }}
{{- $tlsHost = printf "*.%s" $tlsHost }}
{{- end }}
- {{ $tlsHost | quote }}
{{- end }}
{{- if .secretName }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
{{- end }}
---
# gRPC Ingress (devbox- prefix, gRPC requests detected by content-type)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "httpgate.fullname" . }}-grpc
labels:
{{- include "httpgate.labels" . | nindent 4 }}
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
higress.io/prefix-match-header-host: devbox-
higress.io/prefix-match-header-content-type: application/grpc
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.ingressClassName }}
ingressClassName: {{ .Values.ingress.ingressClassName }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
{{- $host := .host }}
{{- if not (hasPrefix "*." $host) }}
{{- $host = printf "*.%s" $host }}
{{- end }}
- host: {{ $host | quote }}
http:
paths:
{{- $paths := .paths | default (list (dict "path" "/" "pathType" "Prefix")) }}
{{- range $paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "httpgate.fullname" $ }}-grpc
port:
number: {{ $.Values.httpPort }}
{{- end }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
{{- $tlsHost := . }}
{{- if not (hasPrefix "*." $tlsHost) }}
{{- $tlsHost = printf "*.%s" $tlsHost }}
{{- end }}
- {{ $tlsHost | quote }}
{{- end }}
{{- if .secretName }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

Some files were not shown because too many files have changed in this diff Show More