diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 8440c764..d41054d8 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -29,8 +29,8 @@ jobs: target: apiserver - name: solar-renderer target: renderer - - name: solar-discovery-worker - target: discovery-worker + - name: solar-discovery + target: discovery permissions: contents: read packages: write diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4477b7c1..03e8ce01 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -52,25 +52,25 @@ jobs: # Linux amd64 GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o bin/solar-apiserver-linux-amd64 ./cmd/solar-apiserver GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o bin/solar-controller-manager-linux-amd64 ./cmd/solar-controller-manager - GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o bin/solar-discovery-worker-linux-amd64 ./cmd/solar-discovery-worker + GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o bin/solar-discovery-linux-amd64 ./cmd/solar-discovery GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o bin/solar-renderer-linux-amd64 ./cmd/solar-renderer # Linux arm64 GOOS=linux GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o bin/solar-apiserver-linux-arm64 ./cmd/solar-apiserver GOOS=linux GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o bin/solar-controller-manager-linux-arm64 ./cmd/solar-controller-manager - GOOS=linux GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o bin/solar-discovery-worker-linux-arm64 ./cmd/solar-discovery-worker + GOOS=linux GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o bin/solar-discovery-linux-arm64 ./cmd/solar-discovery GOOS=linux GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o bin/solar-renderer-linux-arm64 ./cmd/solar-renderer # Darwin amd64 GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o bin/solar-apiserver-darwin-amd64 ./cmd/solar-apiserver GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o bin/solar-controller-manager-darwin-amd64 ./cmd/solar-controller-manager - GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o bin/solar-discovery-worker-darwin-amd64 ./cmd/solar-discovery-worker + GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o bin/solar-discovery-darwin-amd64 ./cmd/solar-discovery GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o bin/solar-renderer-darwin-amd64 ./cmd/solar-renderer # Darwin arm64 GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o bin/solar-apiserver-darwin-arm64 ./cmd/solar-apiserver GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o bin/solar-controller-manager-darwin-arm64 ./cmd/solar-controller-manager - GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o bin/solar-discovery-worker-darwin-arm64 ./cmd/solar-discovery-worker + GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o bin/solar-discovery-darwin-arm64 ./cmd/solar-discovery GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o bin/solar-renderer-darwin-arm64 ./cmd/solar-renderer - name: Create checksums @@ -93,7 +93,7 @@ jobs: needs: release strategy: matrix: - component: [solar-controller-manager, solar-apiserver, solar-renderer, solar-discovery-worker] + component: [solar-controller-manager, solar-apiserver, solar-renderer, solar-discovery] steps: - name: Checkout code uses: actions/checkout@v6 diff --git a/.gitignore b/.gitignore index c29cd39c..112c12ce 100644 --- a/.gitignore +++ b/.gitignore @@ -47,5 +47,22 @@ deep-docs/ *.coverprofile +# Node / Frontend +web/node_modules/ +web/dist/ +web/playwright-report/ +web/test-results/ +web/e2e/.auth/ +web/.devenv* + +# Embedded SPA build output (copied from web/dist by make ui-build) +pkg/ui/static/* +!pkg/ui/static/.gitkeep + +# Test fixtures (generated) test/fixtures/ocm-demo-ctf test/fixtures/ca.crt +test/fixtures/dex-ca.crt +test/fixtures/dex-ca.key +test/fixtures/dex-tls.crt +test/fixtures/dex-tls.key diff --git a/Dockerfile b/Dockerfile index abc4142c..8c0b58a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,7 @@ COPY api/ api/ COPY client-go/ client-go/ COPY cmd/ cmd/ COPY pkg/ pkg/ +COPY web/ web/ ARG TARGETOS ARG TARGETARCH @@ -34,16 +35,29 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg \ CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -ldflags="-s -w" ${GO_BUILD_FLAGS} -o bin/solar-controller-manager ./cmd/solar-controller-manager -FROM builder AS webhook-builder +FROM builder AS discovery-builder RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg \ - CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -ldflags="-s -w" ${GO_BUILD_FLAGS} -o bin/solar-discovery-worker ./cmd/solar-discovery-worker + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -ldflags="-s -w" ${GO_BUILD_FLAGS} -o bin/solar-discovery ./cmd/solar-discovery FROM builder AS renderer-builder RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg \ CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -ldflags="-s -w" ${GO_BUILD_FLAGS} -o bin/solar-renderer ./cmd/solar-renderer +FROM --platform=$BUILDPLATFORM node:22-alpine AS ui-frontend-builder +WORKDIR /workspace/web +COPY web/package.json web/pnpm-lock.yaml ./ +RUN corepack enable && pnpm install --frozen-lockfile +COPY web/ . +RUN pnpm build + +FROM builder AS ui-builder +COPY --from=ui-frontend-builder /workspace/web/dist pkg/ui/static/ +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on go build -ldflags="-s -w" ${GO_BUILD_FLAGS} -o bin/solar-ui ./cmd/solar-ui + # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details FROM gcr.io/distroless/static:nonroot AS apiserver @@ -64,8 +78,14 @@ COPY --from=renderer-builder /workspace/bin/solar-renderer . USER 65532:65532 ENTRYPOINT ["/solar-renderer"] -FROM gcr.io/distroless/static:nonroot AS discovery-worker +FROM gcr.io/distroless/static:nonroot AS discovery +WORKDIR / +COPY --from=discovery-builder /workspace/bin/solar-discovery . +USER 65532:65532 +ENTRYPOINT ["/solar-discovery"] + +FROM gcr.io/distroless/static:nonroot AS ui WORKDIR / -COPY --from=webhook-builder /workspace/bin/solar-discovery-worker . +COPY --from=ui-builder /workspace/bin/solar-ui . USER 65532:65532 -ENTRYPOINT ["/solar-discovery-worker"] +ENTRYPOINT ["/solar-ui"] diff --git a/Makefile b/Makefile index f4757d32..3a5db8f5 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ HACK_DIR ?= $(shell cd hack 2>/dev/null && pwd) LOCALBIN ?= $(BUILD_PATH)/bin SOLAR_CHART_DIR ?= $(BUILD_PATH)/charts/solar OCM_DEMO_DIR ?= $(BUILD_PATH)/test/fixtures/ocm-demo-ctf +OCM_DEMO_VERSION ?= v26.4.1 OS := $(shell go env GOOS) ARCH := $(shell go env GOARCH) @@ -55,7 +56,8 @@ export GNOPROXY=*.go.opendefense.cloud/solar APISERVER_IMG ?= solar-apiserver:latest MANAGER_IMG ?= solar-controller-manager:latest RENDERER_IMG ?= solar-renderer:latest -DISCOVERY_WORKER_IMG ?= solar-discovery-worker:latest +DISCOVERY_IMG ?= solar-discovery:latest +UI_IMG ?= solar-ui:latest DOCS_IMG ?= solar-docs:latest TIMESTAMP := $(shell date '+%Y%m%d%H%M%S') @@ -121,6 +123,7 @@ test: setup-envtest ginkgo ocm-transfer-demo ## Run all tests test-e2e: manifests ## Run the e2e tests. Expected an isolated environment using Kind. TAG=e2e OCM=$(OCM) KIND_CLUSTER=$(KIND_CLUSTER_E2E) go test -tags=e2e ./test/e2e/ -v -ginkgo.v + .PHONY: manifests manifests: controller-gen ## Generate ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=manager-role paths="./pkg/controller/...;./api/..." output:rbac:artifacts:config=$(SOLAR_CHART_DIR)/files @@ -130,7 +133,8 @@ kind-load-local-images: $(KIND) load docker-image localhost/local/solar-apiserver:$(TAG) --name $(KIND_CLUSTER) $(KIND) load docker-image localhost/local/solar-controller-manager:$(TAG) --name $(KIND_CLUSTER) $(KIND) load docker-image localhost/local/solar-renderer:$(TAG) --name $(KIND_CLUSTER) - $(KIND) load docker-image localhost/local/solar-discovery-worker:$(TAG) --name $(KIND_CLUSTER) + $(KIND) load docker-image localhost/local/solar-discovery:$(TAG) --name $(KIND_CLUSTER) + $(KIND) load docker-image localhost/local/solar-ui:$(TAG) --name $(KIND_CLUSTER) .PHONY: setup-local-cluster setup-local-cluster: ## Set up a Kind cluster for local development if it does not exist @@ -143,7 +147,7 @@ setup-local-cluster: ## Set up a Kind cluster for local development if it does n echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \ *) \ echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \ - $(KIND) create cluster --name $(KIND_CLUSTER) ;; \ + $(KIND) create cluster --name $(KIND_CLUSTER) $${KIND_CONFIG:+--config $$KIND_CONFIG} ;; \ esac KIND_CLUSTER_E2E ?= solar-test-e2e @@ -159,6 +163,7 @@ e2e-cluster: ocm-transfer-demo ## Create a e2e test cluster (Contains everything cleanup-e2e-cluster: ## Tear down the Kind cluster used for e2e tests @$(KIND) delete cluster --name $(KIND_CLUSTER_E2E) + KIND_CLUSTER_DEV ?= solar-dev .PHONY: dev-cluster @@ -176,15 +181,28 @@ dev-cluster-rebuild: ## Rebuild images from source and load them into the local -f test/fixtures/solar.values.yaml \ --set apiserver.image.tag=$(DEV_TAG) \ --set controller.image.tag=$(DEV_TAG) \ - --set renderer.image.tag=$(DEV_TAG) \ - --set discovery.image.tag=$(DEV_TAG) + --set renderer.image.tag=$(DEV_TAG) + $(HELM) upgrade --install --namespace solar-system solar-discovery charts/solar-discovery \ + -f test/fixtures/solar-discovery-webhook.values.yaml \ + --set image.tag=$(DEV_TAG) \ + --set namespace=solar-system .PHONY: cleanup-dev-cluster cleanup-dev-cluster: ## Tear down the Kind cluster used for local tests @$(KIND) delete cluster --name $(KIND_CLUSTER_DEV) +.PHONY: cleanup-all-clusters +cleanup-all-clusters: ## Tear down all SolAr Kind clusters + @for cluster in $$($(KIND) get clusters 2>/dev/null); do \ + case "$$cluster" in \ + $(KIND_CLUSTER_DEV)|$(KIND_CLUSTER_E2E)|$(KIND_CLUSTER_UI_DEV)|$(KIND_CLUSTER_UI_E2E)) \ + echo "Deleting Kind cluster '$$cluster'..."; \ + $(KIND) delete cluster --name "$$cluster" ;; \ + esac; \ + done + .PHONY: docker-build -docker-build: docker-build-apiserver docker-build-manager docker-build-discovery-worker docker-build-renderer +docker-build: docker-build-apiserver docker-build-manager docker-build-discovery docker-build-renderer docker-build-ui .PHONY: docker-build-local-images docker-build-local-images: @@ -192,7 +210,8 @@ docker-build-local-images: APISERVER_IMG=localhost/local/solar-apiserver:$(TAG) \ MANAGER_IMG=localhost/local/solar-controller-manager:$(TAG) \ RENDERER_IMG=localhost/local/solar-renderer:$(TAG) \ - DISCOVERY_WORKER_IMG=localhost/local/solar-discovery-worker:$(TAG) docker-build + DISCOVERY_IMG=localhost/local/solar-discovery:$(TAG) \ + UI_IMG=localhost/local/solar-ui:$(TAG) docker-build .PHONY: docker-build-apiserver docker-build-apiserver: @@ -202,14 +221,120 @@ docker-build-apiserver: docker-build-manager: $(DOCKER) build --target manager -t ${MANAGER_IMG} . -.PHONY: docker-build-discovery-worker -docker-build-discovery-worker: - $(DOCKER) build --target discovery-worker -t ${DISCOVERY_WORKER_IMG} . +.PHONY: docker-build-discovery +docker-build-discovery: + $(DOCKER) build --target discovery -t ${DISCOVERY_IMG} . .PHONY: docker-build-renderer docker-build-renderer: $(DOCKER) build --target renderer -t ${RENDERER_IMG} . +.PHONY: docker-build-ui +docker-build-ui: + $(DOCKER) build --target ui -t ${UI_IMG} . + +##@ UI + +PNPM ?= pnpm +KIND_CLUSTER_UI_DEV ?= solar-ui-dev +KIND_CLUSTER_UI_E2E ?= solar-test-e2e-ui + +.PHONY: ui-install +ui-install: ## Install frontend dependencies + cd web && $(PNPM) install + +.PHONY: ui-build +ui-build: ui-install ## Build the frontend for production + cd web && $(PNPM) build + rm -rf pkg/ui/static + cp -r web/dist pkg/ui/static + +.PHONY: ui-lint +ui-lint: ## Lint frontend code + cd web && $(PNPM) lint + +.PHONY: ui-dev-cluster +ui-dev-cluster: ocm-transfer-demo ## Create a Kind cluster with SolAr + Dex for UI development + $(HACK_DIR)/generate-dex-certs.sh + KIND_CONFIG=test/fixtures/e2e/kind-config-oidc.yaml $(MAKE) setup-local-cluster KIND_CLUSTER=$(KIND_CLUSTER_UI_DEV) + $(MAKE) docker-build-local-images TAG=$(DEV_TAG) + $(MAKE) kind-load-local-images TAG=$(DEV_TAG) KIND_CLUSTER=$(KIND_CLUSTER_UI_DEV) + TAG=$(DEV_TAG) KIND_CLUSTER=$(KIND_CLUSTER_UI_DEV) $(HACK_DIR)/dev-cluster.sh + KIND_CLUSTER=$(KIND_CLUSTER_UI_DEV) $(HACK_DIR)/setup-dex.sh + +.PHONY: ui-cleanup-dev-cluster +ui-cleanup-dev-cluster: ## Tear down the UI dev cluster + @$(KIND) delete cluster --name $(KIND_CLUSTER_UI_DEV) + +.PHONY: ui-seed-data +ui-seed-data: ## Seed demo resources (targets, releases, components, etc.) into the cluster + $(HACK_DIR)/seed-demo-data.sh + +.PHONY: ui-dev +ui-dev: ui-build ## Start Go backend + Vite dev server against the UI dev cluster + @case "$$($(KIND) get clusters 2>/dev/null)" in \ + *"$(KIND_CLUSTER_UI_DEV)"*) ;; \ + *) echo "UI dev cluster not found. Creating it..."; $(MAKE) ui-dev-cluster ;; \ + esac + @test -f test/fixtures/dex-ca.crt || { echo "Dex CA cert not found. Run 'make ui-dev-cluster' first."; exit 1; } + @echo "Starting Dex port-forward + Vite dev server + solar-ui backend..." + @echo "Open http://localhost:8090 in your browser." + @echo "" + @$(KIND) get kubeconfig --name $(KIND_CLUSTER_UI_DEV) > /tmp/solar-ui-dev-kubeconfig + cd web && $(PNPM) exec concurrently --kill-others --names "dex,vite,bff" --prefix-colors "magenta,cyan,yellow" \ + "KUBECONFIG=/tmp/solar-ui-dev-kubeconfig $(KUBECTL) port-forward -n dex service/dex 5556:5556" \ + "$(PNPM) dev --port 5173" \ + "sleep 2 && cd $(BUILD_PATH) && SSL_CERT_FILE=$(BUILD_PATH)/test/fixtures/dex-ca.crt $(GO) run ./cmd/solar-ui \ + --listen=0.0.0.0:8090 \ + --kubeconfig=/tmp/solar-ui-dev-kubeconfig \ + --oidc-issuer=https://localhost:5556 \ + --oidc-client-id=solar-ui \ + --oidc-client-secret=solar-ui-secret \ + --oidc-redirect-url=http://localhost:8090/api/auth/callback \ + --auth-mode=token \ + --dev-vite-url=http://localhost:5173" + +.PHONY: ui-e2e-cluster +ui-e2e-cluster: ocm-transfer-demo ## Create a Kind cluster with Dex + SolAr for UI e2e testing + $(HACK_DIR)/generate-dex-certs.sh + KIND_CONFIG=test/fixtures/e2e/kind-config-oidc.yaml $(MAKE) setup-local-cluster KIND_CLUSTER=$(KIND_CLUSTER_UI_E2E) + $(MAKE) docker-build-local-images TAG=e2e + $(MAKE) kind-load-local-images TAG=e2e KIND_CLUSTER=$(KIND_CLUSTER_UI_E2E) + TAG=e2e KIND_CLUSTER=$(KIND_CLUSTER_UI_E2E) $(HACK_DIR)/dev-cluster.sh + KIND_CLUSTER=$(KIND_CLUSTER_UI_E2E) $(HACK_DIR)/setup-dex.sh + +.PHONY: ui-cleanup-e2e-cluster +ui-cleanup-e2e-cluster: ## Tear down the UI e2e cluster + @$(KIND) delete cluster --name $(KIND_CLUSTER_UI_E2E) + +.PHONY: ui-test-e2e +ui-test-e2e: ui-build ## Run Playwright UI e2e tests (auto-creates cluster if needed) + @case "$$($(KIND) get clusters 2>/dev/null)" in \ + *"$(KIND_CLUSTER_UI_E2E)"*) ;; \ + *) echo "UI e2e cluster not found. Creating it..."; $(MAKE) ui-e2e-cluster ;; \ + esac + @echo "Starting Dex port-forward + solar-ui backend for e2e tests..." + @$(KIND) get kubeconfig --name $(KIND_CLUSTER_UI_E2E) > /tmp/solar-e2e-ui-kubeconfig && \ + KUBECONFIG=/tmp/solar-e2e-ui-kubeconfig $(KUBECTL) port-forward -n dex service/dex 5556:5556 & \ + PF_PID=$$!; \ + sleep 2 && \ + SSL_CERT_FILE=$(BUILD_PATH)/test/fixtures/dex-ca.crt \ + $(GO) run ./cmd/solar-ui \ + --listen=0.0.0.0:8090 \ + --kubeconfig=/tmp/solar-e2e-ui-kubeconfig \ + --oidc-issuer=https://localhost:5556 \ + --oidc-client-id=solar-ui \ + --oidc-client-secret=solar-ui-secret \ + --oidc-redirect-url=http://localhost:8090/api/auth/callback \ + --auth-mode=token & \ + UI_PID=$$!; \ + sleep 3; \ + cd web && DEX_LOCAL_PORT=5556 $(PNPM) exec playwright test; \ + RC=$$?; \ + kill $$PF_PID $$UI_PID 2>/dev/null; wait $$PF_PID $$UI_PID 2>/dev/null; exit $$RC + +##@ Docs + .PHONY: docs-docker-build docs-docker-build: @$(DOCKER) build -t ${DOCS_IMG} -f mkdocs.Dockerfile . @@ -228,8 +353,10 @@ docs-helm-ref: helm-docs ## Generate Helm Chart reference documentation. .PHONY: ocm-transfer-demo ocm-transfer-demo: ocm ## Transfer the ocm-demo component to the local OCM CTF directory - @test -d $(OCM_DEMO_DIR) || \ - $(OCM) transfer components --latest --copy-resources --type directory ghcr.io/opendefensecloud//opendefense.cloud/ocm-demo:v26.4.0 $(OCM_DEMO_DIR) + @if [ ! -d $(OCM_DEMO_DIR) ] || ! grep -q '"tag":"$(OCM_DEMO_VERSION)"' $(OCM_DEMO_DIR)/artifact-index.json 2>/dev/null; then \ + rm -rf $(OCM_DEMO_DIR); \ + $(OCM) transfer components --latest --copy-resources --type directory ghcr.io/opendefensecloud//opendefense.cloud/ocm-demo:$(OCM_DEMO_VERSION) $(OCM_DEMO_DIR); \ + fi $(LOCALBIN): mkdir -p $(LOCALBIN) diff --git a/README.md b/README.md index 6a7cf377..3f16d476 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ then uses OCM Controllers with fluxCD as a deployer - SolAr follows the Kubernetes Resource Model and thus is entirely configurable via Kubernetes Resources - SolAr has an extensive web ui that exposes all features and functionalities in a consistent and user friendly manner - SolArs web ui ensures that -- SolAr uses next.js for frontend and its apis and tailwind css for styling +- SolAr uses React with Vite, TanStack Router/Query, and shadcn/ui (Tailwind CSS) for the frontend, with a Go Backend-for-Frontend (BFF) handling OIDC auth and K8s API proxying - SolAr creates Docker OCI Images for every component according to best practices for low CVE and minimal secure images - SolAr features a comprehensive Helm chart for deployment using helm 4.x diff --git a/api/solar/bootstrap_rest.go b/api/solar/bootstrap_rest.go deleted file mode 100644 index e1312dff..00000000 --- a/api/solar/bootstrap_rest.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -package solar - -import ( - "context" - - "go.opendefense.cloud/kit/apiserver/resource" - "go.opendefense.cloud/kit/apiserver/rest" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var _ resource.Object = &Bootstrap{} -var _ resource.ObjectWithStatusSubResource = &Bootstrap{} -var _ rest.PrepareForUpdater = &Bootstrap{} -var _ rest.PrepareForCreater = &Bootstrap{} - -func (o *Bootstrap) GetObjectMeta() *metav1.ObjectMeta { - return &o.ObjectMeta -} - -func (o *Bootstrap) NamespaceScoped() bool { - return true -} - -func (o *Bootstrap) New() runtime.Object { - return &Bootstrap{} -} - -func (o *Bootstrap) NewList() runtime.Object { - return &BootstrapList{} -} - -func (o *Bootstrap) GetGroupResource() schema.GroupResource { - return SchemeGroupVersion.WithResource("bootstraps").GroupResource() -} - -func (o *Bootstrap) CopyStatusTo(obj runtime.Object) { - if obj, ok := obj.(*Bootstrap); ok { - obj.Status = o.Status - } -} - -func (o *Bootstrap) PrepareForUpdate(ctx context.Context, old runtime.Object) { - or := old.(*Bootstrap) - incrementGenerationIfNotEqual(o, o.Spec, or.Spec) -} - -func (o *Bootstrap) PrepareForCreate(ctx context.Context) { - o.Generation = 0 -} diff --git a/api/solar/bootstrap_types.go b/api/solar/bootstrap_types.go deleted file mode 100644 index f8b4b953..00000000 --- a/api/solar/bootstrap_types.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -package solar - -import ( - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -// BootstrapSpec defines the desired state of a Bootstrap. -// It contains the concrete releases, profiles, and deployment configuration for a target environment. -type BootstrapSpec struct { - // Releases is a map of release names to their corresponding Release object references. - // Each entry represents a component release that will be deployed to the target. - Releases map[string]corev1.LocalObjectReference `json:"releases"` - // Profiles is a map of profile names to their corresponding Profile object references. - // It points to profiles that match the target, e.g. through the label selector of the Profile - Profiles map[string]corev1.LocalObjectReference `json:"profiles"` - // Userdata contains arbitrary custom data or configuration for the target deployment. - // This allows providing target-specific parameters or settings. - // +optional - Userdata runtime.RawExtension `json:"userdata,omitempty"` -} - -// BootstrapStatus defines the observed state of a Bootstrap. -type BootstrapStatus struct { - // Conditions represent the latest available observations of a Bootstrap's state. - // +optional - // +patchMergeKey=type - // +patchStrategy=merge - Conditions []metav1.Condition `json:"conditions,omitempty" patchMergeKey:"type" patchStrategy:"merge"` - - // RenderTaskRef is a reference to the RenderTask responsible for this Bootstrap. - // +optional - RenderTaskRef *corev1.ObjectReference `json:"renderTaskRef,omitempty"` -} - -// +genclient -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// Bootstrap represents the entrypoint for the gitless gitops configuration. -// It resolves the implicit matching of profiles to produce a concrete set of releases and profiles. -type Bootstrap struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - - Spec BootstrapSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` - Status BootstrapStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// BootstrapList contains a list of Bootstrap resources. -type BootstrapList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - - Items []Bootstrap `json:"items" protobuf:"bytes,2,rep,name=items"` -} - -func (h *Bootstrap) GetSingularName() string { - return "bootstrap" -} - -func (h *Bootstrap) ShortNames() []string { - return []string{"bs"} -} diff --git a/api/solar/component_rest.go b/api/solar/component_rest.go index 4d3887bd..bfe80bd6 100644 --- a/api/solar/component_rest.go +++ b/api/solar/component_rest.go @@ -4,13 +4,18 @@ package solar import ( + "context" + "go.opendefense.cloud/kit/apiserver/resource" + "go.opendefense.cloud/kit/apiserver/rest" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) var _ resource.Object = &Component{} +var _ rest.PrepareForUpdater = &Component{} +var _ rest.PrepareForCreater = &Component{} func (o *Component) GetObjectMeta() *metav1.ObjectMeta { return &o.ObjectMeta @@ -31,3 +36,12 @@ func (o *Component) NewList() runtime.Object { func (o *Component) GetGroupResource() schema.GroupResource { return SchemeGroupVersion.WithResource("components").GroupResource() } + +func (o *Component) PrepareForUpdate(ctx context.Context, old runtime.Object) { + or := old.(*Component) + incrementGenerationIfNotEqual(o, o.Spec, or.Spec) +} + +func (o *Component) PrepareForCreate(ctx context.Context) { + o.Generation = 1 +} diff --git a/api/solar/componentversion_rest.go b/api/solar/componentversion_rest.go index cf2fd74a..40d09bb0 100644 --- a/api/solar/componentversion_rest.go +++ b/api/solar/componentversion_rest.go @@ -4,13 +4,18 @@ package solar import ( + "context" + "go.opendefense.cloud/kit/apiserver/resource" + "go.opendefense.cloud/kit/apiserver/rest" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) var _ resource.Object = &ComponentVersion{} +var _ rest.PrepareForUpdater = &ComponentVersion{} +var _ rest.PrepareForCreater = &ComponentVersion{} func (o *ComponentVersion) GetObjectMeta() *metav1.ObjectMeta { return &o.ObjectMeta @@ -31,3 +36,12 @@ func (o *ComponentVersion) NewList() runtime.Object { func (o *ComponentVersion) GetGroupResource() schema.GroupResource { return SchemeGroupVersion.WithResource("componentversions").GroupResource() } + +func (o *ComponentVersion) PrepareForUpdate(ctx context.Context, old runtime.Object) { + or := old.(*ComponentVersion) + incrementGenerationIfNotEqual(o, o.Spec, or.Spec) +} + +func (o *ComponentVersion) PrepareForCreate(ctx context.Context) { + o.Generation = 1 +} diff --git a/api/solar/discovery_rest.go b/api/solar/discovery_rest.go deleted file mode 100644 index 524d4b54..00000000 --- a/api/solar/discovery_rest.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -package solar - -import ( - "context" - - "go.opendefense.cloud/kit/apiserver/resource" - "go.opendefense.cloud/kit/apiserver/rest" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var _ resource.Object = &Discovery{} -var _ resource.ObjectWithStatusSubResource = &Discovery{} -var _ rest.PrepareForCreater = &Discovery{} -var _ rest.PrepareForUpdater = &Discovery{} - -func (o *Discovery) PrepareForCreate(ctx context.Context) { - o.Generation = 1 -} - -func (o *Discovery) PrepareForUpdate(ctx context.Context, old runtime.Object) { - od := old.(*Discovery) - incrementGenerationIfNotEqual(o, o.Spec, od.Spec) -} - -func (o *Discovery) GetObjectMeta() *metav1.ObjectMeta { - return &o.ObjectMeta -} - -func (o *Discovery) NamespaceScoped() bool { - return true -} - -func (o *Discovery) New() runtime.Object { - return &Discovery{} -} - -func (o *Discovery) NewList() runtime.Object { - return &DiscoveryList{} -} - -func (o *Discovery) GetGroupResource() schema.GroupResource { - return SchemeGroupVersion.WithResource("discoveries").GroupResource() -} - -func (o *Discovery) CopyStatusTo(obj runtime.Object) { - if obj, ok := obj.(*Discovery); ok { - obj.Status = o.Status - } -} diff --git a/api/solar/discovery_types.go b/api/solar/discovery_types.go deleted file mode 100644 index 384abc18..00000000 --- a/api/solar/discovery_types.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -package solar - -import ( - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// AuthenticationType -// +enum -type AuthenticationType string - -const ( - AuthenticationTypeBasic AuthenticationType = "Basic" - AuthenticationTypeToken AuthenticationType = "Token" -) - -type WebhookAuth struct { - // Type represents the type of authentication to use. Currently, only "token" is supported. - Type AuthenticationType `json:"type,omitempty"` - // AuthSecretRef is the reference to the secret which contains the authentication information for the webhook. - AuthSecretRef corev1.LocalObjectReference `json:"authSecretRef,omitempty"` -} - -// Webhook represents the configuration for a webhook. -type Webhook struct { - // Flavor is the webhook implementation to use. - // +kubebuilder:validation:Pattern=`^(@(zot)$` - Flavor string `json:"flavor,omitempty"` - // Path is where the webhook should listen. - Path string `json:"path,omitempty"` - // Auth is the authentication information to use with the webhook. - Auth WebhookAuth `json:"auth,omitempty"` -} - -// Registry defines the configuration for a registry. -type Registry struct { - // Endpoint is the hostname (and optionally port) of the registry, e.g. "registry.example.com" or "registry.example.com:443". - // This must not include a scheme (use PlainHTTP to control HTTP vs HTTPS). - Endpoint string `json:"endpoint"` - - // SecretRef specifies the secret containing the relevant credentials for the registry that should be used during discovery. - // +optional - SecretRef corev1.LocalObjectReference `json:"secretRef"` - - // CAConfigMapRef contains CA bundle for registry connections (e.g., trust-manager's root-bundle). Key is expected to be "trust-bundle.pem". - // +optional - CAConfigMapRef corev1.LocalObjectReference `json:"caConfigMapRef"` - - // PlainHTTP defines whether the registry should be accessed via plain HTTP instead of HTTPS. - // +optional - PlainHTTP bool `json:"plainHTTP,omitempty"` -} - -// Filter defines the filter criteria used to determine which components should be scanned. -type Filter struct { - // RepositoryPatterns defines which repositories should be scanned for components. The default value is empty, which means that all repositories will be scanned. - // Wildcards are supported, e.g. "foo-*" or "*-dev". - RepositoryPatterns []string `json:"repositoryPatterns"` -} - -// DiscoverySpec defines the desired state of a Discovery. -type DiscoverySpec struct { - // Registry specifies the registry that should be scanned by the discovery process. - Registry Registry `json:"registry"` - - // Webhook specifies the configuration for a webhook that is called by the registry on created, updated or deleted images/repositories. - // +optional - Webhook *Webhook `json:"webhook,omitempty"` - - // Filter specifies the filter that should be applied when scanning for components. If not specified, all components will be scanned. - // +kubebuilder:validation:Optional - Filter *Filter `json:"filter,omitempty"` - - // DiscoveryInterval is the amount of time between two full scans of the registry. - // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h" - // May be set to zero to fetch and create it once. Defaults to 24h. - // +kubebuilder:validation:Optional - // +kubebuilder:default:="24h" - // +optional - DiscoveryInterval *metav1.Duration `json:"discoveryInterval,omitempty"` - - // DisableStartupDiscovery defines whether the discovery should not be run on startup of the discovery process. If true it will only run on schedule, see .spec.cron. - // +optional - DisableStartupDiscovery bool `json:"disableStartupDiscovery,omitempty"` -} - -// DiscoveryStatus defines the observed state of a Discovery. -type DiscoveryStatus struct { - // PodGeneration is the generation of the discovery object at the time the worker was instantiated. - PodGeneration int64 `json:"podGeneration"` -} - -// +genclient -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// Discovery represents a configuration for a registry to discover. -type Discovery struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - - Spec DiscoverySpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` - Status DiscoveryStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// DiscoveryList contains a list of Discovery resources. -type DiscoveryList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - - Items []Discovery `json:"items" protobuf:"bytes,2,rep,name=items"` -} - -func (d *Discovery) GetSingularName() string { - return "discovery" -} - -func (d *Discovery) ShortNames() []string { - return []string{"disc"} -} diff --git a/api/solar/profile_rest.go b/api/solar/profile_rest.go index 77367293..a7145fde 100644 --- a/api/solar/profile_rest.go +++ b/api/solar/profile_rest.go @@ -50,5 +50,5 @@ func (o *Profile) PrepareForUpdate(ctx context.Context, old runtime.Object) { } func (o *Profile) PrepareForCreate(ctx context.Context) { - o.Generation = 0 + o.Generation = 1 } diff --git a/api/solar/register.go b/api/solar/register.go index 684c9a75..ebe7a2f5 100644 --- a/api/solar/register.go +++ b/api/solar/register.go @@ -34,18 +34,20 @@ var ( // Adds the list of known types to the given scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, - &Discovery{}, - &DiscoveryList{}, &Component{}, &ComponentList{}, &ComponentVersion{}, &ComponentVersionList{}, &Release{}, &ReleaseList{}, + &ReleaseBinding{}, + &ReleaseBindingList{}, + &Registry{}, + &RegistryList{}, + &RegistryBinding{}, + &RegistryBindingList{}, &Target{}, &TargetList{}, - &Bootstrap{}, - &BootstrapList{}, &RenderTask{}, &RenderTaskList{}, &Profile{}, diff --git a/api/solar/registry_rest.go b/api/solar/registry_rest.go new file mode 100644 index 00000000..32f304bf --- /dev/null +++ b/api/solar/registry_rest.go @@ -0,0 +1,47 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package solar + +import ( + "context" + + "go.opendefense.cloud/kit/apiserver/resource" + "go.opendefense.cloud/kit/apiserver/rest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var _ resource.Object = &Registry{} +var _ rest.PrepareForUpdater = &Registry{} +var _ rest.PrepareForCreater = &Registry{} + +func (o *Registry) GetObjectMeta() *metav1.ObjectMeta { + return &o.ObjectMeta +} + +func (o *Registry) NamespaceScoped() bool { + return true +} + +func (o *Registry) New() runtime.Object { + return &Registry{} +} + +func (o *Registry) NewList() runtime.Object { + return &RegistryList{} +} + +func (o *Registry) GetGroupResource() schema.GroupResource { + return SchemeGroupVersion.WithResource("registries").GroupResource() +} + +func (o *Registry) PrepareForUpdate(ctx context.Context, old runtime.Object) { + or := old.(*Registry) + incrementGenerationIfNotEqual(o, o.Spec, or.Spec) +} + +func (o *Registry) PrepareForCreate(ctx context.Context) { + o.Generation = 1 +} diff --git a/api/solar/registry_types.go b/api/solar/registry_types.go new file mode 100644 index 00000000..25ddf1a6 --- /dev/null +++ b/api/solar/registry_types.go @@ -0,0 +1,76 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package solar + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RegistrySpec defines the desired state of a Registry. +type RegistrySpec struct { + // Hostname is the registry endpoint (e.g. "registry.example.com:5000"). + Hostname string `json:"hostname"` + // PlainHTTP uses HTTP instead of HTTPS for connections to this registry. + // +optional + PlainHTTP bool `json:"plainHTTP,omitempty"` + // SolarSecretRef references a Secret in the same namespace with credentials + // to access this registry from the SolAr cluster. Required if this registry + // is used as a render target. + // +optional + SolarSecretRef *corev1.LocalObjectReference `json:"solarSecretRef,omitempty"` + // TargetSecretRef describes where the credentials secret lives in the target cluster. + // Used by the target agent for pull access. + // +optional + TargetSecretRef *TargetSecretReference `json:"targetSecretRef,omitempty"` +} + +// TargetSecretReference is a reference to a Secret in a target cluster. +type TargetSecretReference struct { + // Name is the name of the Secret. + Name string `json:"name"` + // Namespace is the namespace of the Secret. + Namespace string `json:"namespace"` +} + +// RegistryStatus defines the observed state of a Registry. +type RegistryStatus struct { + // Conditions represent the latest available observations of a Registry's state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchMergeKey:"type" patchStrategy:"merge"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Registry represents an OCI registry that can be used as a source or destination for artifacts. +type Registry struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec RegistrySpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + Status RegistryStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RegistryList contains a list of Registry resources. +type RegistryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Items []Registry `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +func (r *Registry) GetSingularName() string { + return "registry" +} + +func (r *Registry) ShortNames() []string { + return []string{"reg"} +} diff --git a/api/solar/registrybinding_rest.go b/api/solar/registrybinding_rest.go new file mode 100644 index 00000000..a852fe93 --- /dev/null +++ b/api/solar/registrybinding_rest.go @@ -0,0 +1,47 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package solar + +import ( + "context" + + "go.opendefense.cloud/kit/apiserver/resource" + "go.opendefense.cloud/kit/apiserver/rest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var _ resource.Object = &RegistryBinding{} +var _ rest.PrepareForUpdater = &RegistryBinding{} +var _ rest.PrepareForCreater = &RegistryBinding{} + +func (o *RegistryBinding) GetObjectMeta() *metav1.ObjectMeta { + return &o.ObjectMeta +} + +func (o *RegistryBinding) NamespaceScoped() bool { + return true +} + +func (o *RegistryBinding) New() runtime.Object { + return &RegistryBinding{} +} + +func (o *RegistryBinding) NewList() runtime.Object { + return &RegistryBindingList{} +} + +func (o *RegistryBinding) GetGroupResource() schema.GroupResource { + return SchemeGroupVersion.WithResource("registrybindings").GroupResource() +} + +func (o *RegistryBinding) PrepareForUpdate(ctx context.Context, old runtime.Object) { + or := old.(*RegistryBinding) + incrementGenerationIfNotEqual(o, o.Spec, or.Spec) +} + +func (o *RegistryBinding) PrepareForCreate(ctx context.Context) { + o.Generation = 1 +} diff --git a/api/solar/registrybinding_types.go b/api/solar/registrybinding_types.go new file mode 100644 index 00000000..a1112e2f --- /dev/null +++ b/api/solar/registrybinding_types.go @@ -0,0 +1,58 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package solar + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RegistryBindingSpec defines the desired state of a RegistryBinding. +type RegistryBindingSpec struct { + // TargetRef references the Target this binding applies to. + TargetRef corev1.LocalObjectReference `json:"targetRef"` + // RegistryRef references the Registry being bound. + RegistryRef corev1.LocalObjectReference `json:"registryRef"` +} + +// RegistryBindingStatus defines the observed state of a RegistryBinding. +type RegistryBindingStatus struct { + // Conditions represent the latest available observations of a RegistryBinding's state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchMergeKey:"type" patchStrategy:"merge"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RegistryBinding declares that a specific Target is allowed to use a specific Registry. +type RegistryBinding struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec RegistryBindingSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + Status RegistryBindingStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RegistryBindingList contains a list of RegistryBinding resources. +type RegistryBindingList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Items []RegistryBinding `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +func (r *RegistryBinding) GetSingularName() string { + return "registrybinding" +} + +func (r *RegistryBinding) ShortNames() []string { + return []string{"rb"} +} diff --git a/api/solar/release_rest.go b/api/solar/release_rest.go index 6bf5eb54..3243e4a4 100644 --- a/api/solar/release_rest.go +++ b/api/solar/release_rest.go @@ -50,5 +50,5 @@ func (o *Release) PrepareForUpdate(ctx context.Context, old runtime.Object) { } func (o *Release) PrepareForCreate(ctx context.Context) { - o.Generation = 0 + o.Generation = 1 } diff --git a/api/solar/releasebinding_rest.go b/api/solar/releasebinding_rest.go new file mode 100644 index 00000000..9dc08a67 --- /dev/null +++ b/api/solar/releasebinding_rest.go @@ -0,0 +1,47 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package solar + +import ( + "context" + + "go.opendefense.cloud/kit/apiserver/resource" + "go.opendefense.cloud/kit/apiserver/rest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var _ resource.Object = &ReleaseBinding{} +var _ rest.PrepareForUpdater = &ReleaseBinding{} +var _ rest.PrepareForCreater = &ReleaseBinding{} + +func (o *ReleaseBinding) GetObjectMeta() *metav1.ObjectMeta { + return &o.ObjectMeta +} + +func (o *ReleaseBinding) NamespaceScoped() bool { + return true +} + +func (o *ReleaseBinding) New() runtime.Object { + return &ReleaseBinding{} +} + +func (o *ReleaseBinding) NewList() runtime.Object { + return &ReleaseBindingList{} +} + +func (o *ReleaseBinding) GetGroupResource() schema.GroupResource { + return SchemeGroupVersion.WithResource("releasebindings").GroupResource() +} + +func (o *ReleaseBinding) PrepareForUpdate(ctx context.Context, old runtime.Object) { + or := old.(*ReleaseBinding) + incrementGenerationIfNotEqual(o, o.Spec, or.Spec) +} + +func (o *ReleaseBinding) PrepareForCreate(ctx context.Context) { + o.Generation = 1 +} diff --git a/api/solar/releasebinding_types.go b/api/solar/releasebinding_types.go new file mode 100644 index 00000000..f9096428 --- /dev/null +++ b/api/solar/releasebinding_types.go @@ -0,0 +1,58 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package solar + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ReleaseBindingSpec defines the desired state of a ReleaseBinding. +type ReleaseBindingSpec struct { + // TargetRef references the Target this release is bound to. + TargetRef corev1.LocalObjectReference `json:"targetRef"` + // ReleaseRef references the Release to deploy. + ReleaseRef corev1.LocalObjectReference `json:"releaseRef"` +} + +// ReleaseBindingStatus defines the observed state of a ReleaseBinding. +type ReleaseBindingStatus struct { + // Conditions represent the latest available observations of a ReleaseBinding's state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchMergeKey:"type" patchStrategy:"merge"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ReleaseBinding declares that a Release should be deployed to a Target. +type ReleaseBinding struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec ReleaseBindingSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + Status ReleaseBindingStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ReleaseBindingList contains a list of ReleaseBinding resources. +type ReleaseBindingList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Items []ReleaseBinding `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +func (r *ReleaseBinding) GetSingularName() string { + return "releasebinding" +} + +func (r *ReleaseBinding) ShortNames() []string { + return []string{"rlb"} +} diff --git a/api/solar/rendertask_rest.go b/api/solar/rendertask_rest.go index 179f5cb0..191b2aee 100644 --- a/api/solar/rendertask_rest.go +++ b/api/solar/rendertask_rest.go @@ -17,6 +17,8 @@ import ( var _ resource.Object = &RenderTask{} var _ resource.ObjectWithStatusSubResource = &RenderTask{} +var _ rest.PrepareForUpdater = &RenderTask{} +var _ rest.PrepareForCreater = &RenderTask{} var _ rest.ValidateUpdater = &RenderTask{} func (o *RenderTask) GetObjectMeta() *metav1.ObjectMeta { @@ -24,7 +26,7 @@ func (o *RenderTask) GetObjectMeta() *metav1.ObjectMeta { } func (o *RenderTask) NamespaceScoped() bool { - return false + return true } func (o *RenderTask) New() runtime.Object { @@ -45,6 +47,15 @@ func (o *RenderTask) CopyStatusTo(obj runtime.Object) { } } +func (o *RenderTask) PrepareForUpdate(ctx context.Context, old runtime.Object) { + or := old.(*RenderTask) + incrementGenerationIfNotEqual(o, o.Spec, or.Spec) +} + +func (o *RenderTask) PrepareForCreate(ctx context.Context) { + o.Generation = 1 +} + func (o *RenderTask) ValidateUpdate(ctx context.Context, old runtime.Object) field.ErrorList { errors := field.ErrorList{} or := old.(*RenderTask) diff --git a/api/solar/rendertask_types.go b/api/solar/rendertask_types.go index f463e2ce..0e0f34ab 100644 --- a/api/solar/rendertask_types.go +++ b/api/solar/rendertask_types.go @@ -9,13 +9,12 @@ import ( ) // RenderTaskSpec holds the specification for a RenderTask +// +kubebuilder:validation:Required type RenderTaskSpec struct { // RendererConfig is the config used for the renderer job RendererConfig `json:",inline"` // Repository is the Repository where the chart will be pushed to (e.g. charts/mychart) - // Keep in mind that the repository gets automatically prefixed with the - // registry by the rendertask-controller. Repository string `json:"repository"` // Tag is the Tag of the helm chart to be pushed. @@ -23,6 +22,14 @@ type RenderTaskSpec struct { // will error before pushing. Tag string `json:"tag"` + // BaseURL is the registry URL to push the rendered chart to (e.g. "registry.example.com:5000"). + BaseURL string `json:"baseURL"` + + // PushSecretRef references a Secret in the same namespace with registry credentials + // for pushing the rendered chart. + // +optional + PushSecretRef *corev1.LocalObjectReference `json:"pushSecretRef,omitempty"` + // failedJobTTL is the TTL in seconds after which a failed render job and its secrets are cleaned up. // After this duration, the Kubernetes TTL controller will delete the Job and the controller will delete // the Secrets (ConfigSecret, AuthSecret). On success, Job and Secrets are deleted immediately. @@ -38,7 +45,7 @@ type RenderTaskSpec struct { // +kubebuilder:validation:MinLength=1 OwnerNamespace string `json:"ownerNamespace"` - // OwnerKind is the kind of the resource that created this RenderTask (e.g. Release, Bootstrap). + // OwnerKind is the kind of the resource that created this RenderTask (e.g. Release, Target). // +kubebuilder:validation:MinLength=1 OwnerKind string `json:"ownerKind"` } @@ -65,7 +72,6 @@ type RenderTaskStatus struct { } // +genclient -// +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // RenderTask manages a rendering job diff --git a/api/solar/target_rest.go b/api/solar/target_rest.go index e8d731bd..6cf8f788 100644 --- a/api/solar/target_rest.go +++ b/api/solar/target_rest.go @@ -4,13 +4,19 @@ package solar import ( + "context" + "go.opendefense.cloud/kit/apiserver/resource" + "go.opendefense.cloud/kit/apiserver/rest" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) var _ resource.Object = &Target{} +var _ resource.ObjectWithStatusSubResource = &Target{} +var _ rest.PrepareForUpdater = &Target{} +var _ rest.PrepareForCreater = &Target{} func (o *Target) GetObjectMeta() *metav1.ObjectMeta { return &o.ObjectMeta @@ -31,3 +37,18 @@ func (o *Target) NewList() runtime.Object { func (o *Target) GetGroupResource() schema.GroupResource { return SchemeGroupVersion.WithResource("targets").GroupResource() } + +func (o *Target) CopyStatusTo(obj runtime.Object) { + if obj, ok := obj.(*Target); ok { + obj.Status = o.Status + } +} + +func (o *Target) PrepareForUpdate(ctx context.Context, old runtime.Object) { + or := old.(*Target) + incrementGenerationIfNotEqual(o, o.Spec, or.Spec) +} + +func (o *Target) PrepareForCreate(ctx context.Context) { + o.Generation = 1 +} diff --git a/api/solar/target_types.go b/api/solar/target_types.go index 7189720a..5d0d97f4 100644 --- a/api/solar/target_types.go +++ b/api/solar/target_types.go @@ -10,11 +10,11 @@ import ( ) // TargetSpec defines the desired state of a Target. -// It specifies the releases and configuration intended for this deployment target. +// It specifies the render registry and configuration for this deployment target. type TargetSpec struct { - // Releases is a map of release names to their corresponding Release object references. - // Each entry represents a component release intended for deployment on this target. - Releases map[string]corev1.LocalObjectReference `json:"releases"` + // RenderRegistryRef references the Registry to push rendered desired state to. + // The referenced Registry must have SolarSecretRef set for rendering to succeed. + RenderRegistryRef corev1.LocalObjectReference `json:"renderRegistryRef"` // Userdata contains arbitrary custom data or configuration specific to this target. // This enables target-specific customization and deployment parameters. // +optional @@ -23,6 +23,19 @@ type TargetSpec struct { // TargetStatus defines the observed state of a Target. type TargetStatus struct { + // BootstrapVersion is a monotonically increasing counter used as the bootstrap + // chart version. It is incremented each time the bootstrap chart is re-rendered, + // e.g. when the set of bound releases changes. + // +optional + BootstrapVersion int64 `json:"bootstrapVersion,omitempty"` + + // Conditions represent the latest available observations of a Target's state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchMergeKey:"type" patchStrategy:"merge"` } // +genclient diff --git a/api/solar/v1alpha1/bootstrap_types.go b/api/solar/v1alpha1/bootstrap_types.go deleted file mode 100644 index bb1ec2ce..00000000 --- a/api/solar/v1alpha1/bootstrap_types.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -package v1alpha1 - -import ( - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -// BootstrapSpec defines the desired state of a Bootstrap. -// It contains the concrete releases, profiles, and deployment configuration for a target environment. -type BootstrapSpec struct { - // Releases is a map of release names to their corresponding Release object references. - // Each entry represents a component release that will be deployed to the target. - Releases map[string]corev1.LocalObjectReference `json:"releases"` - // Profiles is a map of profile names to their corresponding Profile object references. - // It points to profiles that match the target, e.g. through the label selector of the Profile - Profiles map[string]corev1.LocalObjectReference `json:"profiles"` - // Userdata contains arbitrary custom data or configuration for the target deployment. - // This allows providing target-specific parameters or settings. - // +optional - Userdata runtime.RawExtension `json:"userdata,omitempty"` -} - -// BootstrapStatus defines the observed state of a Bootstrap. -type BootstrapStatus struct { - // Conditions represent the latest available observations of a Bootstrap's state. - // +optional - // +patchMergeKey=type - // +patchStrategy=merge - Conditions []metav1.Condition `json:"conditions,omitempty" patchMergeKey:"type" patchStrategy:"merge"` - - // RenderTaskRef is a reference to the RenderTask responsible for this Bootstrap. - // +optional - RenderTaskRef *corev1.ObjectReference `json:"renderTaskRef,omitempty"` -} - -// +genclient -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// Bootstrap represents the entrypoint for the gitless gitops configuration. -// It resolves the implicit matching of profiles to produce a concrete set of releases and profiles. -type Bootstrap struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - - Spec BootstrapSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` - Status BootstrapStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// BootstrapList contains a list of Bootstrap resources. -type BootstrapList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - - Items []Bootstrap `json:"items" protobuf:"bytes,2,rep,name=items"` -} - -func (h *Bootstrap) GetSingularName() string { - return "bootstrap" -} - -func (h *Bootstrap) ShortNames() []string { - return []string{"bs"} -} diff --git a/api/solar/v1alpha1/discovery_types.go b/api/solar/v1alpha1/discovery_types.go deleted file mode 100644 index 2797a517..00000000 --- a/api/solar/v1alpha1/discovery_types.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -package v1alpha1 - -import ( - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// AuthenticationType -// +enum -type AuthenticationType string - -const ( - AuthenticationTypeBasic AuthenticationType = "Basic" - AuthenticationTypeToken AuthenticationType = "Token" -) - -type WebhookAuth struct { - // Type represents the type of authentication to use. Currently, only "token" is supported. - Type AuthenticationType `json:"type,omitempty"` - // AuthSecretRef is the reference to the secret which contains the authentication information for the webhook. - AuthSecretRef corev1.LocalObjectReference `json:"authSecretRef,omitempty"` -} - -// Webhook represents the configuration for a webhook. -type Webhook struct { - // Flavor is the webhook implementation to use. - // +kubebuilder:validation:Pattern=`^(@(zot)$` - Flavor string `json:"flavor,omitempty"` - // Path is where the webhook should listen. - Path string `json:"path,omitempty"` - // Auth is the authentication information to use with the webhook. - Auth WebhookAuth `json:"auth,omitempty"` -} - -// Registry defines the configuration for a registry. -type Registry struct { - // Endpoint is the hostname (and optionally port) of the registry, e.g. "registry.example.com" or "registry.example.com:443". - // This must not include a scheme (use PlainHTTP to control HTTP vs HTTPS). - Endpoint string `json:"endpoint"` - - // SecretRef specifies the secret containing the relevant credentials for the registry that should be used during discovery. - // +optional - SecretRef corev1.LocalObjectReference `json:"secretRef"` - - // CAConfigMapRef contains CA bundle for registry connections (e.g., trust-manager's root-bundle). Key is expected to be "trust-bundle.pem". - // +optional - CAConfigMapRef corev1.LocalObjectReference `json:"caConfigMapRef"` - - // PlainHTTP defines whether the registry should be accessed via plain HTTP instead of HTTPS. - // +optional - PlainHTTP bool `json:"plainHTTP,omitempty"` -} - -// Filter defines the filter criteria used to determine which components should be scanned. -type Filter struct { - // RepositoryPatterns defines which repositories should be scanned for components. The default value is empty, which means that all repositories will be scanned. - // Wildcards are supported, e.g. "foo-*" or "*-dev". - RepositoryPatterns []string `json:"repositoryPatterns"` -} - -// DiscoverySpec defines the desired state of a Discovery. -type DiscoverySpec struct { - // Registry specifies the registry that should be scanned by the discovery process. - Registry Registry `json:"registry"` - - // Webhook specifies the configuration for a webhook that is called by the registry on created, updated or deleted images/repositories. - // +optional - Webhook *Webhook `json:"webhook,omitempty"` - - // Filter specifies the filter that should be applied when scanning for components. If not specified, all components will be scanned. - // +kubebuilder:validation:Optional - Filter *Filter `json:"filter,omitempty"` - - // DiscoveryInterval is the amount of time between two full scans of the registry. - // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h" - // May be set to zero to fetch and create it once. Defaults to 24h. - // +kubebuilder:validation:Optional - // +kubebuilder:default:="24h" - // +optional - DiscoveryInterval *metav1.Duration `json:"discoveryInterval,omitempty"` - - // DisableStartupDiscovery defines whether the discovery should not be run on startup of the discovery process. If true it will only run on schedule, see .spec.cron. - // +optional - DisableStartupDiscovery bool `json:"disableStartupDiscovery,omitempty"` -} - -// DiscoveryStatus defines the observed state of a Discovery. -type DiscoveryStatus struct { - // PodGeneration is the generation of the discovery object at the time the worker was instantiated. - PodGeneration int64 `json:"podGeneration"` -} - -// +genclient -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// Discovery represents a configuration for a registry to discover. -type Discovery struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - - Spec DiscoverySpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` - Status DiscoveryStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// DiscoveryList contains a list of Discovery resources. -type DiscoveryList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - - Items []Discovery `json:"items" protobuf:"bytes,2,rep,name=items"` -} - -func (d *Discovery) GetSingularName() string { - return "discovery" -} - -func (d *Discovery) ShortNames() []string { - return []string{"disc"} -} diff --git a/api/solar/v1alpha1/register.go b/api/solar/v1alpha1/register.go index fc4aecae..05a035cb 100644 --- a/api/solar/v1alpha1/register.go +++ b/api/solar/v1alpha1/register.go @@ -37,18 +37,20 @@ func init() { // Adds the list of known types to the given scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, - &Discovery{}, - &DiscoveryList{}, &Component{}, &ComponentList{}, &ComponentVersion{}, &ComponentVersionList{}, &Release{}, &ReleaseList{}, + &ReleaseBinding{}, + &ReleaseBindingList{}, + &Registry{}, + &RegistryList{}, + &RegistryBinding{}, + &RegistryBindingList{}, &Target{}, &TargetList{}, - &Bootstrap{}, - &BootstrapList{}, &RenderTask{}, &RenderTaskList{}, &Profile{}, diff --git a/api/solar/v1alpha1/registry_types.go b/api/solar/v1alpha1/registry_types.go new file mode 100644 index 00000000..8b7df7fb --- /dev/null +++ b/api/solar/v1alpha1/registry_types.go @@ -0,0 +1,76 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RegistrySpec defines the desired state of a Registry. +type RegistrySpec struct { + // Hostname is the registry endpoint (e.g. "registry.example.com:5000"). + Hostname string `json:"hostname"` + // PlainHTTP uses HTTP instead of HTTPS for connections to this registry. + // +optional + PlainHTTP bool `json:"plainHTTP,omitempty"` + // SolarSecretRef references a Secret in the same namespace with credentials + // to access this registry from the SolAr cluster. Required if this registry + // is used as a render target. + // +optional + SolarSecretRef *corev1.LocalObjectReference `json:"solarSecretRef,omitempty"` + // TargetSecretRef describes where the credentials secret lives in the target cluster. + // Used by the target agent for pull access. + // +optional + TargetSecretRef *TargetSecretReference `json:"targetSecretRef,omitempty"` +} + +// TargetSecretReference is a reference to a Secret in a target cluster. +type TargetSecretReference struct { + // Name is the name of the Secret. + Name string `json:"name"` + // Namespace is the namespace of the Secret. + Namespace string `json:"namespace"` +} + +// RegistryStatus defines the observed state of a Registry. +type RegistryStatus struct { + // Conditions represent the latest available observations of a Registry's state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchMergeKey:"type" patchStrategy:"merge"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Registry represents an OCI registry that can be used as a source or destination for artifacts. +type Registry struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec RegistrySpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + Status RegistryStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RegistryList contains a list of Registry resources. +type RegistryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Items []Registry `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +func (r *Registry) GetSingularName() string { + return "registry" +} + +func (r *Registry) ShortNames() []string { + return []string{"reg"} +} diff --git a/api/solar/v1alpha1/registrybinding_types.go b/api/solar/v1alpha1/registrybinding_types.go new file mode 100644 index 00000000..da7094c3 --- /dev/null +++ b/api/solar/v1alpha1/registrybinding_types.go @@ -0,0 +1,58 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RegistryBindingSpec defines the desired state of a RegistryBinding. +type RegistryBindingSpec struct { + // TargetRef references the Target this binding applies to. + TargetRef corev1.LocalObjectReference `json:"targetRef"` + // RegistryRef references the Registry being bound. + RegistryRef corev1.LocalObjectReference `json:"registryRef"` +} + +// RegistryBindingStatus defines the observed state of a RegistryBinding. +type RegistryBindingStatus struct { + // Conditions represent the latest available observations of a RegistryBinding's state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchMergeKey:"type" patchStrategy:"merge"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RegistryBinding declares that a specific Target is allowed to use a specific Registry. +type RegistryBinding struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec RegistryBindingSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + Status RegistryBindingStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RegistryBindingList contains a list of RegistryBinding resources. +type RegistryBindingList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Items []RegistryBinding `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +func (r *RegistryBinding) GetSingularName() string { + return "registrybinding" +} + +func (r *RegistryBinding) ShortNames() []string { + return []string{"rb"} +} diff --git a/api/solar/v1alpha1/releasebinding_types.go b/api/solar/v1alpha1/releasebinding_types.go new file mode 100644 index 00000000..a383522d --- /dev/null +++ b/api/solar/v1alpha1/releasebinding_types.go @@ -0,0 +1,58 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ReleaseBindingSpec defines the desired state of a ReleaseBinding. +type ReleaseBindingSpec struct { + // TargetRef references the Target this release is bound to. + TargetRef corev1.LocalObjectReference `json:"targetRef"` + // ReleaseRef references the Release to deploy. + ReleaseRef corev1.LocalObjectReference `json:"releaseRef"` +} + +// ReleaseBindingStatus defines the observed state of a ReleaseBinding. +type ReleaseBindingStatus struct { + // Conditions represent the latest available observations of a ReleaseBinding's state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchMergeKey:"type" patchStrategy:"merge"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ReleaseBinding declares that a Release should be deployed to a Target. +type ReleaseBinding struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec ReleaseBindingSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + Status ReleaseBindingStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ReleaseBindingList contains a list of ReleaseBinding resources. +type ReleaseBindingList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Items []ReleaseBinding `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +func (r *ReleaseBinding) GetSingularName() string { + return "releasebinding" +} + +func (r *ReleaseBinding) ShortNames() []string { + return []string{"rlb"} +} diff --git a/api/solar/v1alpha1/rendertask_types.go b/api/solar/v1alpha1/rendertask_types.go index 109f94f6..c0eab25d 100644 --- a/api/solar/v1alpha1/rendertask_types.go +++ b/api/solar/v1alpha1/rendertask_types.go @@ -14,8 +14,6 @@ type RenderTaskSpec struct { RendererConfig `json:",inline"` // Repository is the Repository where the chart will be pushed to (e.g. charts/mychart) - // Keep in mind that the repository gets automatically prefixed with the - // registry by the rendertask-controller. Repository string `json:"repository"` // Tag is the Tag of the helm chart to be pushed. @@ -23,6 +21,14 @@ type RenderTaskSpec struct { // will error before pushing. Tag string `json:"tag"` + // BaseURL is the registry URL to push the rendered chart to (e.g. "registry.example.com:5000"). + BaseURL string `json:"baseURL"` + + // PushSecretRef references a Secret in the same namespace with registry credentials + // for pushing the rendered chart. + // +optional + PushSecretRef *corev1.LocalObjectReference `json:"pushSecretRef,omitempty"` + // failedJobTTL is the TTL in seconds after which a failed render job and its secrets are cleaned up. // After this duration, the Kubernetes TTL controller will delete the Job and the controller will delete // the Secrets (ConfigSecret, AuthSecret). On success, Job and Secrets are deleted immediately. @@ -38,7 +44,7 @@ type RenderTaskSpec struct { // +kubebuilder:validation:MinLength=1 OwnerNamespace string `json:"ownerNamespace"` - // OwnerKind is the kind of the resource that created this RenderTask (e.g. Release, Bootstrap). + // OwnerKind is the kind of the resource that created this RenderTask (e.g. Release, Target). // +kubebuilder:validation:MinLength=1 OwnerKind string `json:"ownerKind"` } @@ -65,7 +71,6 @@ type RenderTaskStatus struct { } // +genclient -// +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // RenderTask manages a rendering job diff --git a/api/solar/v1alpha1/target_types.go b/api/solar/v1alpha1/target_types.go index d35693c8..319e4ed1 100644 --- a/api/solar/v1alpha1/target_types.go +++ b/api/solar/v1alpha1/target_types.go @@ -10,11 +10,11 @@ import ( ) // TargetSpec defines the desired state of a Target. -// It specifies the releases and configuration intended for this deployment target. +// It specifies the render registry and configuration for this deployment target. type TargetSpec struct { - // Releases is a map of release names to their corresponding Release object references. - // Each entry represents a component release intended for deployment on this target. - Releases map[string]corev1.LocalObjectReference `json:"releases"` + // RenderRegistryRef references the Registry to push rendered desired state to. + // The referenced Registry must have SolarSecretRef set for rendering to succeed. + RenderRegistryRef corev1.LocalObjectReference `json:"renderRegistryRef"` // Userdata contains arbitrary custom data or configuration specific to this target. // This enables target-specific customization and deployment parameters. // +optional @@ -23,6 +23,19 @@ type TargetSpec struct { // TargetStatus defines the observed state of a Target. type TargetStatus struct { + // BootstrapVersion is a monotonically increasing counter used as the bootstrap + // chart version. It is incremented each time the bootstrap chart is re-rendered, + // e.g. when the set of bound releases changes. + // +optional + BootstrapVersion int64 `json:"bootstrapVersion,omitempty"` + + // Conditions represent the latest available observations of a Target's state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchMergeKey:"type" patchStrategy:"merge"` } // +genclient diff --git a/api/solar/v1alpha1/zz_generated.conversion.go b/api/solar/v1alpha1/zz_generated.conversion.go index 193d734b..c9282882 100644 --- a/api/solar/v1alpha1/zz_generated.conversion.go +++ b/api/solar/v1alpha1/zz_generated.conversion.go @@ -12,8 +12,8 @@ import ( unsafe "unsafe" solar "go.opendefense.cloud/solar/api/solar" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -25,16 +25,6 @@ func init() { // RegisterConversions adds conversion functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterConversions(s *runtime.Scheme) error { - if err := s.AddGeneratedConversionFunc((*Bootstrap)(nil), (*solar.Bootstrap)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_Bootstrap_To_solar_Bootstrap(a.(*Bootstrap), b.(*solar.Bootstrap), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*solar.Bootstrap)(nil), (*Bootstrap)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_Bootstrap_To_v1alpha1_Bootstrap(a.(*solar.Bootstrap), b.(*Bootstrap), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*BootstrapConfig)(nil), (*solar.BootstrapConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_BootstrapConfig_To_solar_BootstrapConfig(a.(*BootstrapConfig), b.(*solar.BootstrapConfig), scope) }); err != nil { @@ -55,36 +45,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*BootstrapList)(nil), (*solar.BootstrapList)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_BootstrapList_To_solar_BootstrapList(a.(*BootstrapList), b.(*solar.BootstrapList), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*solar.BootstrapList)(nil), (*BootstrapList)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_BootstrapList_To_v1alpha1_BootstrapList(a.(*solar.BootstrapList), b.(*BootstrapList), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*BootstrapSpec)(nil), (*solar.BootstrapSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_BootstrapSpec_To_solar_BootstrapSpec(a.(*BootstrapSpec), b.(*solar.BootstrapSpec), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*solar.BootstrapSpec)(nil), (*BootstrapSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_BootstrapSpec_To_v1alpha1_BootstrapSpec(a.(*solar.BootstrapSpec), b.(*BootstrapSpec), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*BootstrapStatus)(nil), (*solar.BootstrapStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_BootstrapStatus_To_solar_BootstrapStatus(a.(*BootstrapStatus), b.(*solar.BootstrapStatus), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*solar.BootstrapStatus)(nil), (*BootstrapStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_BootstrapStatus_To_v1alpha1_BootstrapStatus(a.(*solar.BootstrapStatus), b.(*BootstrapStatus), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*ChartConfig)(nil), (*solar.ChartConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_ChartConfig_To_solar_ChartConfig(a.(*ChartConfig), b.(*solar.ChartConfig), scope) }); err != nil { @@ -175,123 +135,143 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Discovery)(nil), (*solar.Discovery)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_Discovery_To_solar_Discovery(a.(*Discovery), b.(*solar.Discovery), scope) + if err := s.AddGeneratedConversionFunc((*Entrypoint)(nil), (*solar.Entrypoint)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Entrypoint_To_solar_Entrypoint(a.(*Entrypoint), b.(*solar.Entrypoint), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.Discovery)(nil), (*Discovery)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_Discovery_To_v1alpha1_Discovery(a.(*solar.Discovery), b.(*Discovery), scope) + if err := s.AddGeneratedConversionFunc((*solar.Entrypoint)(nil), (*Entrypoint)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_Entrypoint_To_v1alpha1_Entrypoint(a.(*solar.Entrypoint), b.(*Entrypoint), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*DiscoveryList)(nil), (*solar.DiscoveryList)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_DiscoveryList_To_solar_DiscoveryList(a.(*DiscoveryList), b.(*solar.DiscoveryList), scope) + if err := s.AddGeneratedConversionFunc((*Profile)(nil), (*solar.Profile)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Profile_To_solar_Profile(a.(*Profile), b.(*solar.Profile), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.DiscoveryList)(nil), (*DiscoveryList)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_DiscoveryList_To_v1alpha1_DiscoveryList(a.(*solar.DiscoveryList), b.(*DiscoveryList), scope) + if err := s.AddGeneratedConversionFunc((*solar.Profile)(nil), (*Profile)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_Profile_To_v1alpha1_Profile(a.(*solar.Profile), b.(*Profile), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*DiscoverySpec)(nil), (*solar.DiscoverySpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_DiscoverySpec_To_solar_DiscoverySpec(a.(*DiscoverySpec), b.(*solar.DiscoverySpec), scope) + if err := s.AddGeneratedConversionFunc((*ProfileList)(nil), (*solar.ProfileList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ProfileList_To_solar_ProfileList(a.(*ProfileList), b.(*solar.ProfileList), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.DiscoverySpec)(nil), (*DiscoverySpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_DiscoverySpec_To_v1alpha1_DiscoverySpec(a.(*solar.DiscoverySpec), b.(*DiscoverySpec), scope) + if err := s.AddGeneratedConversionFunc((*solar.ProfileList)(nil), (*ProfileList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_ProfileList_To_v1alpha1_ProfileList(a.(*solar.ProfileList), b.(*ProfileList), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*DiscoveryStatus)(nil), (*solar.DiscoveryStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_DiscoveryStatus_To_solar_DiscoveryStatus(a.(*DiscoveryStatus), b.(*solar.DiscoveryStatus), scope) + if err := s.AddGeneratedConversionFunc((*ProfileSpec)(nil), (*solar.ProfileSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ProfileSpec_To_solar_ProfileSpec(a.(*ProfileSpec), b.(*solar.ProfileSpec), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.DiscoveryStatus)(nil), (*DiscoveryStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_DiscoveryStatus_To_v1alpha1_DiscoveryStatus(a.(*solar.DiscoveryStatus), b.(*DiscoveryStatus), scope) + if err := s.AddGeneratedConversionFunc((*solar.ProfileSpec)(nil), (*ProfileSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_ProfileSpec_To_v1alpha1_ProfileSpec(a.(*solar.ProfileSpec), b.(*ProfileSpec), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Entrypoint)(nil), (*solar.Entrypoint)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_Entrypoint_To_solar_Entrypoint(a.(*Entrypoint), b.(*solar.Entrypoint), scope) + if err := s.AddGeneratedConversionFunc((*ProfileStatus)(nil), (*solar.ProfileStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ProfileStatus_To_solar_ProfileStatus(a.(*ProfileStatus), b.(*solar.ProfileStatus), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.Entrypoint)(nil), (*Entrypoint)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_Entrypoint_To_v1alpha1_Entrypoint(a.(*solar.Entrypoint), b.(*Entrypoint), scope) + if err := s.AddGeneratedConversionFunc((*solar.ProfileStatus)(nil), (*ProfileStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_ProfileStatus_To_v1alpha1_ProfileStatus(a.(*solar.ProfileStatus), b.(*ProfileStatus), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Filter)(nil), (*solar.Filter)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_Filter_To_solar_Filter(a.(*Filter), b.(*solar.Filter), scope) + if err := s.AddGeneratedConversionFunc((*PushResult)(nil), (*solar.PushResult)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_PushResult_To_solar_PushResult(a.(*PushResult), b.(*solar.PushResult), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.Filter)(nil), (*Filter)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_Filter_To_v1alpha1_Filter(a.(*solar.Filter), b.(*Filter), scope) + if err := s.AddGeneratedConversionFunc((*solar.PushResult)(nil), (*PushResult)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_PushResult_To_v1alpha1_PushResult(a.(*solar.PushResult), b.(*PushResult), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Profile)(nil), (*solar.Profile)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_Profile_To_solar_Profile(a.(*Profile), b.(*solar.Profile), scope) + if err := s.AddGeneratedConversionFunc((*Registry)(nil), (*solar.Registry)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Registry_To_solar_Registry(a.(*Registry), b.(*solar.Registry), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.Profile)(nil), (*Profile)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_Profile_To_v1alpha1_Profile(a.(*solar.Profile), b.(*Profile), scope) + if err := s.AddGeneratedConversionFunc((*solar.Registry)(nil), (*Registry)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_Registry_To_v1alpha1_Registry(a.(*solar.Registry), b.(*Registry), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*ProfileList)(nil), (*solar.ProfileList)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_ProfileList_To_solar_ProfileList(a.(*ProfileList), b.(*solar.ProfileList), scope) + if err := s.AddGeneratedConversionFunc((*RegistryBinding)(nil), (*solar.RegistryBinding)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_RegistryBinding_To_solar_RegistryBinding(a.(*RegistryBinding), b.(*solar.RegistryBinding), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.ProfileList)(nil), (*ProfileList)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_ProfileList_To_v1alpha1_ProfileList(a.(*solar.ProfileList), b.(*ProfileList), scope) + if err := s.AddGeneratedConversionFunc((*solar.RegistryBinding)(nil), (*RegistryBinding)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_RegistryBinding_To_v1alpha1_RegistryBinding(a.(*solar.RegistryBinding), b.(*RegistryBinding), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*ProfileSpec)(nil), (*solar.ProfileSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_ProfileSpec_To_solar_ProfileSpec(a.(*ProfileSpec), b.(*solar.ProfileSpec), scope) + if err := s.AddGeneratedConversionFunc((*RegistryBindingList)(nil), (*solar.RegistryBindingList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_RegistryBindingList_To_solar_RegistryBindingList(a.(*RegistryBindingList), b.(*solar.RegistryBindingList), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.ProfileSpec)(nil), (*ProfileSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_ProfileSpec_To_v1alpha1_ProfileSpec(a.(*solar.ProfileSpec), b.(*ProfileSpec), scope) + if err := s.AddGeneratedConversionFunc((*solar.RegistryBindingList)(nil), (*RegistryBindingList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_RegistryBindingList_To_v1alpha1_RegistryBindingList(a.(*solar.RegistryBindingList), b.(*RegistryBindingList), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*ProfileStatus)(nil), (*solar.ProfileStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_ProfileStatus_To_solar_ProfileStatus(a.(*ProfileStatus), b.(*solar.ProfileStatus), scope) + if err := s.AddGeneratedConversionFunc((*RegistryBindingSpec)(nil), (*solar.RegistryBindingSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_RegistryBindingSpec_To_solar_RegistryBindingSpec(a.(*RegistryBindingSpec), b.(*solar.RegistryBindingSpec), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.ProfileStatus)(nil), (*ProfileStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_ProfileStatus_To_v1alpha1_ProfileStatus(a.(*solar.ProfileStatus), b.(*ProfileStatus), scope) + if err := s.AddGeneratedConversionFunc((*solar.RegistryBindingSpec)(nil), (*RegistryBindingSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_RegistryBindingSpec_To_v1alpha1_RegistryBindingSpec(a.(*solar.RegistryBindingSpec), b.(*RegistryBindingSpec), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*PushResult)(nil), (*solar.PushResult)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_PushResult_To_solar_PushResult(a.(*PushResult), b.(*solar.PushResult), scope) + if err := s.AddGeneratedConversionFunc((*RegistryBindingStatus)(nil), (*solar.RegistryBindingStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_RegistryBindingStatus_To_solar_RegistryBindingStatus(a.(*RegistryBindingStatus), b.(*solar.RegistryBindingStatus), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.PushResult)(nil), (*PushResult)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_PushResult_To_v1alpha1_PushResult(a.(*solar.PushResult), b.(*PushResult), scope) + if err := s.AddGeneratedConversionFunc((*solar.RegistryBindingStatus)(nil), (*RegistryBindingStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_RegistryBindingStatus_To_v1alpha1_RegistryBindingStatus(a.(*solar.RegistryBindingStatus), b.(*RegistryBindingStatus), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Registry)(nil), (*solar.Registry)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_Registry_To_solar_Registry(a.(*Registry), b.(*solar.Registry), scope) + if err := s.AddGeneratedConversionFunc((*RegistryList)(nil), (*solar.RegistryList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_RegistryList_To_solar_RegistryList(a.(*RegistryList), b.(*solar.RegistryList), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*solar.Registry)(nil), (*Registry)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_Registry_To_v1alpha1_Registry(a.(*solar.Registry), b.(*Registry), scope) + if err := s.AddGeneratedConversionFunc((*solar.RegistryList)(nil), (*RegistryList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_RegistryList_To_v1alpha1_RegistryList(a.(*solar.RegistryList), b.(*RegistryList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*RegistrySpec)(nil), (*solar.RegistrySpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_RegistrySpec_To_solar_RegistrySpec(a.(*RegistrySpec), b.(*solar.RegistrySpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*solar.RegistrySpec)(nil), (*RegistrySpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_RegistrySpec_To_v1alpha1_RegistrySpec(a.(*solar.RegistrySpec), b.(*RegistrySpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*RegistryStatus)(nil), (*solar.RegistryStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_RegistryStatus_To_solar_RegistryStatus(a.(*RegistryStatus), b.(*solar.RegistryStatus), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*solar.RegistryStatus)(nil), (*RegistryStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_RegistryStatus_To_v1alpha1_RegistryStatus(a.(*solar.RegistryStatus), b.(*RegistryStatus), scope) }); err != nil { return err } @@ -305,6 +285,46 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*ReleaseBinding)(nil), (*solar.ReleaseBinding)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ReleaseBinding_To_solar_ReleaseBinding(a.(*ReleaseBinding), b.(*solar.ReleaseBinding), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*solar.ReleaseBinding)(nil), (*ReleaseBinding)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_ReleaseBinding_To_v1alpha1_ReleaseBinding(a.(*solar.ReleaseBinding), b.(*ReleaseBinding), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ReleaseBindingList)(nil), (*solar.ReleaseBindingList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ReleaseBindingList_To_solar_ReleaseBindingList(a.(*ReleaseBindingList), b.(*solar.ReleaseBindingList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*solar.ReleaseBindingList)(nil), (*ReleaseBindingList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_ReleaseBindingList_To_v1alpha1_ReleaseBindingList(a.(*solar.ReleaseBindingList), b.(*ReleaseBindingList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ReleaseBindingSpec)(nil), (*solar.ReleaseBindingSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ReleaseBindingSpec_To_solar_ReleaseBindingSpec(a.(*ReleaseBindingSpec), b.(*solar.ReleaseBindingSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*solar.ReleaseBindingSpec)(nil), (*ReleaseBindingSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_ReleaseBindingSpec_To_v1alpha1_ReleaseBindingSpec(a.(*solar.ReleaseBindingSpec), b.(*ReleaseBindingSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ReleaseBindingStatus)(nil), (*solar.ReleaseBindingStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ReleaseBindingStatus_To_solar_ReleaseBindingStatus(a.(*ReleaseBindingStatus), b.(*solar.ReleaseBindingStatus), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*solar.ReleaseBindingStatus)(nil), (*ReleaseBindingStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_ReleaseBindingStatus_To_v1alpha1_ReleaseBindingStatus(a.(*solar.ReleaseBindingStatus), b.(*ReleaseBindingStatus), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*ReleaseComponent)(nil), (*solar.ReleaseComponent)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_ReleaseComponent_To_solar_ReleaseComponent(a.(*ReleaseComponent), b.(*solar.ReleaseComponent), scope) }); err != nil { @@ -455,6 +475,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*TargetSecretReference)(nil), (*solar.TargetSecretReference)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_TargetSecretReference_To_solar_TargetSecretReference(a.(*TargetSecretReference), b.(*solar.TargetSecretReference), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*solar.TargetSecretReference)(nil), (*TargetSecretReference)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_solar_TargetSecretReference_To_v1alpha1_TargetSecretReference(a.(*solar.TargetSecretReference), b.(*TargetSecretReference), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*TargetSpec)(nil), (*solar.TargetSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_TargetSpec_To_solar_TargetSpec(a.(*TargetSpec), b.(*solar.TargetSpec), scope) }); err != nil { @@ -475,61 +505,9 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Webhook)(nil), (*solar.Webhook)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_Webhook_To_solar_Webhook(a.(*Webhook), b.(*solar.Webhook), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*solar.Webhook)(nil), (*Webhook)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_Webhook_To_v1alpha1_Webhook(a.(*solar.Webhook), b.(*Webhook), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*WebhookAuth)(nil), (*solar.WebhookAuth)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_WebhookAuth_To_solar_WebhookAuth(a.(*WebhookAuth), b.(*solar.WebhookAuth), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*solar.WebhookAuth)(nil), (*WebhookAuth)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_solar_WebhookAuth_To_v1alpha1_WebhookAuth(a.(*solar.WebhookAuth), b.(*WebhookAuth), scope) - }); err != nil { - return err - } return nil } -func autoConvert_v1alpha1_Bootstrap_To_solar_Bootstrap(in *Bootstrap, out *solar.Bootstrap, s conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - if err := Convert_v1alpha1_BootstrapSpec_To_solar_BootstrapSpec(&in.Spec, &out.Spec, s); err != nil { - return err - } - if err := Convert_v1alpha1_BootstrapStatus_To_solar_BootstrapStatus(&in.Status, &out.Status, s); err != nil { - return err - } - return nil -} - -// Convert_v1alpha1_Bootstrap_To_solar_Bootstrap is an autogenerated conversion function. -func Convert_v1alpha1_Bootstrap_To_solar_Bootstrap(in *Bootstrap, out *solar.Bootstrap, s conversion.Scope) error { - return autoConvert_v1alpha1_Bootstrap_To_solar_Bootstrap(in, out, s) -} - -func autoConvert_solar_Bootstrap_To_v1alpha1_Bootstrap(in *solar.Bootstrap, out *Bootstrap, s conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - if err := Convert_solar_BootstrapSpec_To_v1alpha1_BootstrapSpec(&in.Spec, &out.Spec, s); err != nil { - return err - } - if err := Convert_solar_BootstrapStatus_To_v1alpha1_BootstrapStatus(&in.Status, &out.Status, s); err != nil { - return err - } - return nil -} - -// Convert_solar_Bootstrap_To_v1alpha1_Bootstrap is an autogenerated conversion function. -func Convert_solar_Bootstrap_To_v1alpha1_Bootstrap(in *solar.Bootstrap, out *Bootstrap, s conversion.Scope) error { - return autoConvert_solar_Bootstrap_To_v1alpha1_Bootstrap(in, out, s) -} - func autoConvert_v1alpha1_BootstrapConfig_To_solar_BootstrapConfig(in *BootstrapConfig, out *solar.BootstrapConfig, s conversion.Scope) error { if err := Convert_v1alpha1_ChartConfig_To_solar_ChartConfig(&in.Chart, &out.Chart, s); err != nil { return err @@ -582,74 +560,6 @@ func Convert_solar_BootstrapInput_To_v1alpha1_BootstrapInput(in *solar.Bootstrap return autoConvert_solar_BootstrapInput_To_v1alpha1_BootstrapInput(in, out, s) } -func autoConvert_v1alpha1_BootstrapList_To_solar_BootstrapList(in *BootstrapList, out *solar.BootstrapList, s conversion.Scope) error { - out.ListMeta = in.ListMeta - out.Items = *(*[]solar.Bootstrap)(unsafe.Pointer(&in.Items)) - return nil -} - -// Convert_v1alpha1_BootstrapList_To_solar_BootstrapList is an autogenerated conversion function. -func Convert_v1alpha1_BootstrapList_To_solar_BootstrapList(in *BootstrapList, out *solar.BootstrapList, s conversion.Scope) error { - return autoConvert_v1alpha1_BootstrapList_To_solar_BootstrapList(in, out, s) -} - -func autoConvert_solar_BootstrapList_To_v1alpha1_BootstrapList(in *solar.BootstrapList, out *BootstrapList, s conversion.Scope) error { - out.ListMeta = in.ListMeta - out.Items = *(*[]Bootstrap)(unsafe.Pointer(&in.Items)) - return nil -} - -// Convert_solar_BootstrapList_To_v1alpha1_BootstrapList is an autogenerated conversion function. -func Convert_solar_BootstrapList_To_v1alpha1_BootstrapList(in *solar.BootstrapList, out *BootstrapList, s conversion.Scope) error { - return autoConvert_solar_BootstrapList_To_v1alpha1_BootstrapList(in, out, s) -} - -func autoConvert_v1alpha1_BootstrapSpec_To_solar_BootstrapSpec(in *BootstrapSpec, out *solar.BootstrapSpec, s conversion.Scope) error { - out.Releases = *(*map[string]v1.LocalObjectReference)(unsafe.Pointer(&in.Releases)) - out.Profiles = *(*map[string]v1.LocalObjectReference)(unsafe.Pointer(&in.Profiles)) - out.Userdata = in.Userdata - return nil -} - -// Convert_v1alpha1_BootstrapSpec_To_solar_BootstrapSpec is an autogenerated conversion function. -func Convert_v1alpha1_BootstrapSpec_To_solar_BootstrapSpec(in *BootstrapSpec, out *solar.BootstrapSpec, s conversion.Scope) error { - return autoConvert_v1alpha1_BootstrapSpec_To_solar_BootstrapSpec(in, out, s) -} - -func autoConvert_solar_BootstrapSpec_To_v1alpha1_BootstrapSpec(in *solar.BootstrapSpec, out *BootstrapSpec, s conversion.Scope) error { - out.Releases = *(*map[string]v1.LocalObjectReference)(unsafe.Pointer(&in.Releases)) - out.Profiles = *(*map[string]v1.LocalObjectReference)(unsafe.Pointer(&in.Profiles)) - out.Userdata = in.Userdata - return nil -} - -// Convert_solar_BootstrapSpec_To_v1alpha1_BootstrapSpec is an autogenerated conversion function. -func Convert_solar_BootstrapSpec_To_v1alpha1_BootstrapSpec(in *solar.BootstrapSpec, out *BootstrapSpec, s conversion.Scope) error { - return autoConvert_solar_BootstrapSpec_To_v1alpha1_BootstrapSpec(in, out, s) -} - -func autoConvert_v1alpha1_BootstrapStatus_To_solar_BootstrapStatus(in *BootstrapStatus, out *solar.BootstrapStatus, s conversion.Scope) error { - out.Conditions = *(*[]metav1.Condition)(unsafe.Pointer(&in.Conditions)) - out.RenderTaskRef = (*v1.ObjectReference)(unsafe.Pointer(in.RenderTaskRef)) - return nil -} - -// Convert_v1alpha1_BootstrapStatus_To_solar_BootstrapStatus is an autogenerated conversion function. -func Convert_v1alpha1_BootstrapStatus_To_solar_BootstrapStatus(in *BootstrapStatus, out *solar.BootstrapStatus, s conversion.Scope) error { - return autoConvert_v1alpha1_BootstrapStatus_To_solar_BootstrapStatus(in, out, s) -} - -func autoConvert_solar_BootstrapStatus_To_v1alpha1_BootstrapStatus(in *solar.BootstrapStatus, out *BootstrapStatus, s conversion.Scope) error { - out.Conditions = *(*[]metav1.Condition)(unsafe.Pointer(&in.Conditions)) - out.RenderTaskRef = (*v1.ObjectReference)(unsafe.Pointer(in.RenderTaskRef)) - return nil -} - -// Convert_solar_BootstrapStatus_To_v1alpha1_BootstrapStatus is an autogenerated conversion function. -func Convert_solar_BootstrapStatus_To_v1alpha1_BootstrapStatus(in *solar.BootstrapStatus, out *BootstrapStatus, s conversion.Scope) error { - return autoConvert_solar_BootstrapStatus_To_v1alpha1_BootstrapStatus(in, out, s) -} - func autoConvert_v1alpha1_ChartConfig_To_solar_ChartConfig(in *ChartConfig, out *solar.ChartConfig, s conversion.Scope) error { out.Name = in.Name out.Description = in.Description @@ -874,112 +784,6 @@ func Convert_solar_ComponentVersionStatus_To_v1alpha1_ComponentVersionStatus(in return autoConvert_solar_ComponentVersionStatus_To_v1alpha1_ComponentVersionStatus(in, out, s) } -func autoConvert_v1alpha1_Discovery_To_solar_Discovery(in *Discovery, out *solar.Discovery, s conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - if err := Convert_v1alpha1_DiscoverySpec_To_solar_DiscoverySpec(&in.Spec, &out.Spec, s); err != nil { - return err - } - if err := Convert_v1alpha1_DiscoveryStatus_To_solar_DiscoveryStatus(&in.Status, &out.Status, s); err != nil { - return err - } - return nil -} - -// Convert_v1alpha1_Discovery_To_solar_Discovery is an autogenerated conversion function. -func Convert_v1alpha1_Discovery_To_solar_Discovery(in *Discovery, out *solar.Discovery, s conversion.Scope) error { - return autoConvert_v1alpha1_Discovery_To_solar_Discovery(in, out, s) -} - -func autoConvert_solar_Discovery_To_v1alpha1_Discovery(in *solar.Discovery, out *Discovery, s conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - if err := Convert_solar_DiscoverySpec_To_v1alpha1_DiscoverySpec(&in.Spec, &out.Spec, s); err != nil { - return err - } - if err := Convert_solar_DiscoveryStatus_To_v1alpha1_DiscoveryStatus(&in.Status, &out.Status, s); err != nil { - return err - } - return nil -} - -// Convert_solar_Discovery_To_v1alpha1_Discovery is an autogenerated conversion function. -func Convert_solar_Discovery_To_v1alpha1_Discovery(in *solar.Discovery, out *Discovery, s conversion.Scope) error { - return autoConvert_solar_Discovery_To_v1alpha1_Discovery(in, out, s) -} - -func autoConvert_v1alpha1_DiscoveryList_To_solar_DiscoveryList(in *DiscoveryList, out *solar.DiscoveryList, s conversion.Scope) error { - out.ListMeta = in.ListMeta - out.Items = *(*[]solar.Discovery)(unsafe.Pointer(&in.Items)) - return nil -} - -// Convert_v1alpha1_DiscoveryList_To_solar_DiscoveryList is an autogenerated conversion function. -func Convert_v1alpha1_DiscoveryList_To_solar_DiscoveryList(in *DiscoveryList, out *solar.DiscoveryList, s conversion.Scope) error { - return autoConvert_v1alpha1_DiscoveryList_To_solar_DiscoveryList(in, out, s) -} - -func autoConvert_solar_DiscoveryList_To_v1alpha1_DiscoveryList(in *solar.DiscoveryList, out *DiscoveryList, s conversion.Scope) error { - out.ListMeta = in.ListMeta - out.Items = *(*[]Discovery)(unsafe.Pointer(&in.Items)) - return nil -} - -// Convert_solar_DiscoveryList_To_v1alpha1_DiscoveryList is an autogenerated conversion function. -func Convert_solar_DiscoveryList_To_v1alpha1_DiscoveryList(in *solar.DiscoveryList, out *DiscoveryList, s conversion.Scope) error { - return autoConvert_solar_DiscoveryList_To_v1alpha1_DiscoveryList(in, out, s) -} - -func autoConvert_v1alpha1_DiscoverySpec_To_solar_DiscoverySpec(in *DiscoverySpec, out *solar.DiscoverySpec, s conversion.Scope) error { - if err := Convert_v1alpha1_Registry_To_solar_Registry(&in.Registry, &out.Registry, s); err != nil { - return err - } - out.Webhook = (*solar.Webhook)(unsafe.Pointer(in.Webhook)) - out.Filter = (*solar.Filter)(unsafe.Pointer(in.Filter)) - out.DiscoveryInterval = (*metav1.Duration)(unsafe.Pointer(in.DiscoveryInterval)) - out.DisableStartupDiscovery = in.DisableStartupDiscovery - return nil -} - -// Convert_v1alpha1_DiscoverySpec_To_solar_DiscoverySpec is an autogenerated conversion function. -func Convert_v1alpha1_DiscoverySpec_To_solar_DiscoverySpec(in *DiscoverySpec, out *solar.DiscoverySpec, s conversion.Scope) error { - return autoConvert_v1alpha1_DiscoverySpec_To_solar_DiscoverySpec(in, out, s) -} - -func autoConvert_solar_DiscoverySpec_To_v1alpha1_DiscoverySpec(in *solar.DiscoverySpec, out *DiscoverySpec, s conversion.Scope) error { - if err := Convert_solar_Registry_To_v1alpha1_Registry(&in.Registry, &out.Registry, s); err != nil { - return err - } - out.Webhook = (*Webhook)(unsafe.Pointer(in.Webhook)) - out.Filter = (*Filter)(unsafe.Pointer(in.Filter)) - out.DiscoveryInterval = (*metav1.Duration)(unsafe.Pointer(in.DiscoveryInterval)) - out.DisableStartupDiscovery = in.DisableStartupDiscovery - return nil -} - -// Convert_solar_DiscoverySpec_To_v1alpha1_DiscoverySpec is an autogenerated conversion function. -func Convert_solar_DiscoverySpec_To_v1alpha1_DiscoverySpec(in *solar.DiscoverySpec, out *DiscoverySpec, s conversion.Scope) error { - return autoConvert_solar_DiscoverySpec_To_v1alpha1_DiscoverySpec(in, out, s) -} - -func autoConvert_v1alpha1_DiscoveryStatus_To_solar_DiscoveryStatus(in *DiscoveryStatus, out *solar.DiscoveryStatus, s conversion.Scope) error { - out.PodGeneration = in.PodGeneration - return nil -} - -// Convert_v1alpha1_DiscoveryStatus_To_solar_DiscoveryStatus is an autogenerated conversion function. -func Convert_v1alpha1_DiscoveryStatus_To_solar_DiscoveryStatus(in *DiscoveryStatus, out *solar.DiscoveryStatus, s conversion.Scope) error { - return autoConvert_v1alpha1_DiscoveryStatus_To_solar_DiscoveryStatus(in, out, s) -} - -func autoConvert_solar_DiscoveryStatus_To_v1alpha1_DiscoveryStatus(in *solar.DiscoveryStatus, out *DiscoveryStatus, s conversion.Scope) error { - out.PodGeneration = in.PodGeneration - return nil -} - -// Convert_solar_DiscoveryStatus_To_v1alpha1_DiscoveryStatus is an autogenerated conversion function. -func Convert_solar_DiscoveryStatus_To_v1alpha1_DiscoveryStatus(in *solar.DiscoveryStatus, out *DiscoveryStatus, s conversion.Scope) error { - return autoConvert_solar_DiscoveryStatus_To_v1alpha1_DiscoveryStatus(in, out, s) -} - func autoConvert_v1alpha1_Entrypoint_To_solar_Entrypoint(in *Entrypoint, out *solar.Entrypoint, s conversion.Scope) error { out.ResourceName = in.ResourceName out.Type = solar.EntrypointType(in.Type) @@ -1002,26 +806,6 @@ func Convert_solar_Entrypoint_To_v1alpha1_Entrypoint(in *solar.Entrypoint, out * return autoConvert_solar_Entrypoint_To_v1alpha1_Entrypoint(in, out, s) } -func autoConvert_v1alpha1_Filter_To_solar_Filter(in *Filter, out *solar.Filter, s conversion.Scope) error { - out.RepositoryPatterns = *(*[]string)(unsafe.Pointer(&in.RepositoryPatterns)) - return nil -} - -// Convert_v1alpha1_Filter_To_solar_Filter is an autogenerated conversion function. -func Convert_v1alpha1_Filter_To_solar_Filter(in *Filter, out *solar.Filter, s conversion.Scope) error { - return autoConvert_v1alpha1_Filter_To_solar_Filter(in, out, s) -} - -func autoConvert_solar_Filter_To_v1alpha1_Filter(in *solar.Filter, out *Filter, s conversion.Scope) error { - out.RepositoryPatterns = *(*[]string)(unsafe.Pointer(&in.RepositoryPatterns)) - return nil -} - -// Convert_solar_Filter_To_v1alpha1_Filter is an autogenerated conversion function. -func Convert_solar_Filter_To_v1alpha1_Filter(in *solar.Filter, out *Filter, s conversion.Scope) error { - return autoConvert_solar_Filter_To_v1alpha1_Filter(in, out, s) -} - func autoConvert_v1alpha1_Profile_To_solar_Profile(in *Profile, out *solar.Profile, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1alpha1_ProfileSpec_To_solar_ProfileSpec(&in.Spec, &out.Spec, s); err != nil { @@ -1102,7 +886,7 @@ func Convert_solar_ProfileSpec_To_v1alpha1_ProfileSpec(in *solar.ProfileSpec, ou func autoConvert_v1alpha1_ProfileStatus_To_solar_ProfileStatus(in *ProfileStatus, out *solar.ProfileStatus, s conversion.Scope) error { out.MatchedTargets = in.MatchedTargets - out.Conditions = *(*[]metav1.Condition)(unsafe.Pointer(&in.Conditions)) + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) return nil } @@ -1113,7 +897,7 @@ func Convert_v1alpha1_ProfileStatus_To_solar_ProfileStatus(in *ProfileStatus, ou func autoConvert_solar_ProfileStatus_To_v1alpha1_ProfileStatus(in *solar.ProfileStatus, out *ProfileStatus, s conversion.Scope) error { out.MatchedTargets = in.MatchedTargets - out.Conditions = *(*[]metav1.Condition)(unsafe.Pointer(&in.Conditions)) + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) return nil } @@ -1143,10 +927,13 @@ func Convert_solar_PushResult_To_v1alpha1_PushResult(in *solar.PushResult, out * } func autoConvert_v1alpha1_Registry_To_solar_Registry(in *Registry, out *solar.Registry, s conversion.Scope) error { - out.Endpoint = in.Endpoint - out.SecretRef = in.SecretRef - out.CAConfigMapRef = in.CAConfigMapRef - out.PlainHTTP = in.PlainHTTP + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1alpha1_RegistrySpec_To_solar_RegistrySpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_v1alpha1_RegistryStatus_To_solar_RegistryStatus(&in.Status, &out.Status, s); err != nil { + return err + } return nil } @@ -1156,10 +943,13 @@ func Convert_v1alpha1_Registry_To_solar_Registry(in *Registry, out *solar.Regist } func autoConvert_solar_Registry_To_v1alpha1_Registry(in *solar.Registry, out *Registry, s conversion.Scope) error { - out.Endpoint = in.Endpoint - out.SecretRef = in.SecretRef - out.CAConfigMapRef = in.CAConfigMapRef - out.PlainHTTP = in.PlainHTTP + out.ObjectMeta = in.ObjectMeta + if err := Convert_solar_RegistrySpec_To_v1alpha1_RegistrySpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_solar_RegistryStatus_To_v1alpha1_RegistryStatus(&in.Status, &out.Status, s); err != nil { + return err + } return nil } @@ -1168,6 +958,170 @@ func Convert_solar_Registry_To_v1alpha1_Registry(in *solar.Registry, out *Regist return autoConvert_solar_Registry_To_v1alpha1_Registry(in, out, s) } +func autoConvert_v1alpha1_RegistryBinding_To_solar_RegistryBinding(in *RegistryBinding, out *solar.RegistryBinding, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1alpha1_RegistryBindingSpec_To_solar_RegistryBindingSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_v1alpha1_RegistryBindingStatus_To_solar_RegistryBindingStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +// Convert_v1alpha1_RegistryBinding_To_solar_RegistryBinding is an autogenerated conversion function. +func Convert_v1alpha1_RegistryBinding_To_solar_RegistryBinding(in *RegistryBinding, out *solar.RegistryBinding, s conversion.Scope) error { + return autoConvert_v1alpha1_RegistryBinding_To_solar_RegistryBinding(in, out, s) +} + +func autoConvert_solar_RegistryBinding_To_v1alpha1_RegistryBinding(in *solar.RegistryBinding, out *RegistryBinding, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_solar_RegistryBindingSpec_To_v1alpha1_RegistryBindingSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_solar_RegistryBindingStatus_To_v1alpha1_RegistryBindingStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +// Convert_solar_RegistryBinding_To_v1alpha1_RegistryBinding is an autogenerated conversion function. +func Convert_solar_RegistryBinding_To_v1alpha1_RegistryBinding(in *solar.RegistryBinding, out *RegistryBinding, s conversion.Scope) error { + return autoConvert_solar_RegistryBinding_To_v1alpha1_RegistryBinding(in, out, s) +} + +func autoConvert_v1alpha1_RegistryBindingList_To_solar_RegistryBindingList(in *RegistryBindingList, out *solar.RegistryBindingList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]solar.RegistryBinding)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1alpha1_RegistryBindingList_To_solar_RegistryBindingList is an autogenerated conversion function. +func Convert_v1alpha1_RegistryBindingList_To_solar_RegistryBindingList(in *RegistryBindingList, out *solar.RegistryBindingList, s conversion.Scope) error { + return autoConvert_v1alpha1_RegistryBindingList_To_solar_RegistryBindingList(in, out, s) +} + +func autoConvert_solar_RegistryBindingList_To_v1alpha1_RegistryBindingList(in *solar.RegistryBindingList, out *RegistryBindingList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]RegistryBinding)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_solar_RegistryBindingList_To_v1alpha1_RegistryBindingList is an autogenerated conversion function. +func Convert_solar_RegistryBindingList_To_v1alpha1_RegistryBindingList(in *solar.RegistryBindingList, out *RegistryBindingList, s conversion.Scope) error { + return autoConvert_solar_RegistryBindingList_To_v1alpha1_RegistryBindingList(in, out, s) +} + +func autoConvert_v1alpha1_RegistryBindingSpec_To_solar_RegistryBindingSpec(in *RegistryBindingSpec, out *solar.RegistryBindingSpec, s conversion.Scope) error { + out.TargetRef = in.TargetRef + out.RegistryRef = in.RegistryRef + return nil +} + +// Convert_v1alpha1_RegistryBindingSpec_To_solar_RegistryBindingSpec is an autogenerated conversion function. +func Convert_v1alpha1_RegistryBindingSpec_To_solar_RegistryBindingSpec(in *RegistryBindingSpec, out *solar.RegistryBindingSpec, s conversion.Scope) error { + return autoConvert_v1alpha1_RegistryBindingSpec_To_solar_RegistryBindingSpec(in, out, s) +} + +func autoConvert_solar_RegistryBindingSpec_To_v1alpha1_RegistryBindingSpec(in *solar.RegistryBindingSpec, out *RegistryBindingSpec, s conversion.Scope) error { + out.TargetRef = in.TargetRef + out.RegistryRef = in.RegistryRef + return nil +} + +// Convert_solar_RegistryBindingSpec_To_v1alpha1_RegistryBindingSpec is an autogenerated conversion function. +func Convert_solar_RegistryBindingSpec_To_v1alpha1_RegistryBindingSpec(in *solar.RegistryBindingSpec, out *RegistryBindingSpec, s conversion.Scope) error { + return autoConvert_solar_RegistryBindingSpec_To_v1alpha1_RegistryBindingSpec(in, out, s) +} + +func autoConvert_v1alpha1_RegistryBindingStatus_To_solar_RegistryBindingStatus(in *RegistryBindingStatus, out *solar.RegistryBindingStatus, s conversion.Scope) error { + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_v1alpha1_RegistryBindingStatus_To_solar_RegistryBindingStatus is an autogenerated conversion function. +func Convert_v1alpha1_RegistryBindingStatus_To_solar_RegistryBindingStatus(in *RegistryBindingStatus, out *solar.RegistryBindingStatus, s conversion.Scope) error { + return autoConvert_v1alpha1_RegistryBindingStatus_To_solar_RegistryBindingStatus(in, out, s) +} + +func autoConvert_solar_RegistryBindingStatus_To_v1alpha1_RegistryBindingStatus(in *solar.RegistryBindingStatus, out *RegistryBindingStatus, s conversion.Scope) error { + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_solar_RegistryBindingStatus_To_v1alpha1_RegistryBindingStatus is an autogenerated conversion function. +func Convert_solar_RegistryBindingStatus_To_v1alpha1_RegistryBindingStatus(in *solar.RegistryBindingStatus, out *RegistryBindingStatus, s conversion.Scope) error { + return autoConvert_solar_RegistryBindingStatus_To_v1alpha1_RegistryBindingStatus(in, out, s) +} + +func autoConvert_v1alpha1_RegistryList_To_solar_RegistryList(in *RegistryList, out *solar.RegistryList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]solar.Registry)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1alpha1_RegistryList_To_solar_RegistryList is an autogenerated conversion function. +func Convert_v1alpha1_RegistryList_To_solar_RegistryList(in *RegistryList, out *solar.RegistryList, s conversion.Scope) error { + return autoConvert_v1alpha1_RegistryList_To_solar_RegistryList(in, out, s) +} + +func autoConvert_solar_RegistryList_To_v1alpha1_RegistryList(in *solar.RegistryList, out *RegistryList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]Registry)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_solar_RegistryList_To_v1alpha1_RegistryList is an autogenerated conversion function. +func Convert_solar_RegistryList_To_v1alpha1_RegistryList(in *solar.RegistryList, out *RegistryList, s conversion.Scope) error { + return autoConvert_solar_RegistryList_To_v1alpha1_RegistryList(in, out, s) +} + +func autoConvert_v1alpha1_RegistrySpec_To_solar_RegistrySpec(in *RegistrySpec, out *solar.RegistrySpec, s conversion.Scope) error { + out.Hostname = in.Hostname + out.PlainHTTP = in.PlainHTTP + out.SolarSecretRef = (*corev1.LocalObjectReference)(unsafe.Pointer(in.SolarSecretRef)) + out.TargetSecretRef = (*solar.TargetSecretReference)(unsafe.Pointer(in.TargetSecretRef)) + return nil +} + +// Convert_v1alpha1_RegistrySpec_To_solar_RegistrySpec is an autogenerated conversion function. +func Convert_v1alpha1_RegistrySpec_To_solar_RegistrySpec(in *RegistrySpec, out *solar.RegistrySpec, s conversion.Scope) error { + return autoConvert_v1alpha1_RegistrySpec_To_solar_RegistrySpec(in, out, s) +} + +func autoConvert_solar_RegistrySpec_To_v1alpha1_RegistrySpec(in *solar.RegistrySpec, out *RegistrySpec, s conversion.Scope) error { + out.Hostname = in.Hostname + out.PlainHTTP = in.PlainHTTP + out.SolarSecretRef = (*corev1.LocalObjectReference)(unsafe.Pointer(in.SolarSecretRef)) + out.TargetSecretRef = (*TargetSecretReference)(unsafe.Pointer(in.TargetSecretRef)) + return nil +} + +// Convert_solar_RegistrySpec_To_v1alpha1_RegistrySpec is an autogenerated conversion function. +func Convert_solar_RegistrySpec_To_v1alpha1_RegistrySpec(in *solar.RegistrySpec, out *RegistrySpec, s conversion.Scope) error { + return autoConvert_solar_RegistrySpec_To_v1alpha1_RegistrySpec(in, out, s) +} + +func autoConvert_v1alpha1_RegistryStatus_To_solar_RegistryStatus(in *RegistryStatus, out *solar.RegistryStatus, s conversion.Scope) error { + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_v1alpha1_RegistryStatus_To_solar_RegistryStatus is an autogenerated conversion function. +func Convert_v1alpha1_RegistryStatus_To_solar_RegistryStatus(in *RegistryStatus, out *solar.RegistryStatus, s conversion.Scope) error { + return autoConvert_v1alpha1_RegistryStatus_To_solar_RegistryStatus(in, out, s) +} + +func autoConvert_solar_RegistryStatus_To_v1alpha1_RegistryStatus(in *solar.RegistryStatus, out *RegistryStatus, s conversion.Scope) error { + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_solar_RegistryStatus_To_v1alpha1_RegistryStatus is an autogenerated conversion function. +func Convert_solar_RegistryStatus_To_v1alpha1_RegistryStatus(in *solar.RegistryStatus, out *RegistryStatus, s conversion.Scope) error { + return autoConvert_solar_RegistryStatus_To_v1alpha1_RegistryStatus(in, out, s) +} + func autoConvert_v1alpha1_Release_To_solar_Release(in *Release, out *solar.Release, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1alpha1_ReleaseSpec_To_solar_ReleaseSpec(&in.Spec, &out.Spec, s); err != nil { @@ -1200,6 +1154,102 @@ func Convert_solar_Release_To_v1alpha1_Release(in *solar.Release, out *Release, return autoConvert_solar_Release_To_v1alpha1_Release(in, out, s) } +func autoConvert_v1alpha1_ReleaseBinding_To_solar_ReleaseBinding(in *ReleaseBinding, out *solar.ReleaseBinding, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1alpha1_ReleaseBindingSpec_To_solar_ReleaseBindingSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_v1alpha1_ReleaseBindingStatus_To_solar_ReleaseBindingStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +// Convert_v1alpha1_ReleaseBinding_To_solar_ReleaseBinding is an autogenerated conversion function. +func Convert_v1alpha1_ReleaseBinding_To_solar_ReleaseBinding(in *ReleaseBinding, out *solar.ReleaseBinding, s conversion.Scope) error { + return autoConvert_v1alpha1_ReleaseBinding_To_solar_ReleaseBinding(in, out, s) +} + +func autoConvert_solar_ReleaseBinding_To_v1alpha1_ReleaseBinding(in *solar.ReleaseBinding, out *ReleaseBinding, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_solar_ReleaseBindingSpec_To_v1alpha1_ReleaseBindingSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_solar_ReleaseBindingStatus_To_v1alpha1_ReleaseBindingStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +// Convert_solar_ReleaseBinding_To_v1alpha1_ReleaseBinding is an autogenerated conversion function. +func Convert_solar_ReleaseBinding_To_v1alpha1_ReleaseBinding(in *solar.ReleaseBinding, out *ReleaseBinding, s conversion.Scope) error { + return autoConvert_solar_ReleaseBinding_To_v1alpha1_ReleaseBinding(in, out, s) +} + +func autoConvert_v1alpha1_ReleaseBindingList_To_solar_ReleaseBindingList(in *ReleaseBindingList, out *solar.ReleaseBindingList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]solar.ReleaseBinding)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1alpha1_ReleaseBindingList_To_solar_ReleaseBindingList is an autogenerated conversion function. +func Convert_v1alpha1_ReleaseBindingList_To_solar_ReleaseBindingList(in *ReleaseBindingList, out *solar.ReleaseBindingList, s conversion.Scope) error { + return autoConvert_v1alpha1_ReleaseBindingList_To_solar_ReleaseBindingList(in, out, s) +} + +func autoConvert_solar_ReleaseBindingList_To_v1alpha1_ReleaseBindingList(in *solar.ReleaseBindingList, out *ReleaseBindingList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]ReleaseBinding)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_solar_ReleaseBindingList_To_v1alpha1_ReleaseBindingList is an autogenerated conversion function. +func Convert_solar_ReleaseBindingList_To_v1alpha1_ReleaseBindingList(in *solar.ReleaseBindingList, out *ReleaseBindingList, s conversion.Scope) error { + return autoConvert_solar_ReleaseBindingList_To_v1alpha1_ReleaseBindingList(in, out, s) +} + +func autoConvert_v1alpha1_ReleaseBindingSpec_To_solar_ReleaseBindingSpec(in *ReleaseBindingSpec, out *solar.ReleaseBindingSpec, s conversion.Scope) error { + out.TargetRef = in.TargetRef + out.ReleaseRef = in.ReleaseRef + return nil +} + +// Convert_v1alpha1_ReleaseBindingSpec_To_solar_ReleaseBindingSpec is an autogenerated conversion function. +func Convert_v1alpha1_ReleaseBindingSpec_To_solar_ReleaseBindingSpec(in *ReleaseBindingSpec, out *solar.ReleaseBindingSpec, s conversion.Scope) error { + return autoConvert_v1alpha1_ReleaseBindingSpec_To_solar_ReleaseBindingSpec(in, out, s) +} + +func autoConvert_solar_ReleaseBindingSpec_To_v1alpha1_ReleaseBindingSpec(in *solar.ReleaseBindingSpec, out *ReleaseBindingSpec, s conversion.Scope) error { + out.TargetRef = in.TargetRef + out.ReleaseRef = in.ReleaseRef + return nil +} + +// Convert_solar_ReleaseBindingSpec_To_v1alpha1_ReleaseBindingSpec is an autogenerated conversion function. +func Convert_solar_ReleaseBindingSpec_To_v1alpha1_ReleaseBindingSpec(in *solar.ReleaseBindingSpec, out *ReleaseBindingSpec, s conversion.Scope) error { + return autoConvert_solar_ReleaseBindingSpec_To_v1alpha1_ReleaseBindingSpec(in, out, s) +} + +func autoConvert_v1alpha1_ReleaseBindingStatus_To_solar_ReleaseBindingStatus(in *ReleaseBindingStatus, out *solar.ReleaseBindingStatus, s conversion.Scope) error { + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_v1alpha1_ReleaseBindingStatus_To_solar_ReleaseBindingStatus is an autogenerated conversion function. +func Convert_v1alpha1_ReleaseBindingStatus_To_solar_ReleaseBindingStatus(in *ReleaseBindingStatus, out *solar.ReleaseBindingStatus, s conversion.Scope) error { + return autoConvert_v1alpha1_ReleaseBindingStatus_To_solar_ReleaseBindingStatus(in, out, s) +} + +func autoConvert_solar_ReleaseBindingStatus_To_v1alpha1_ReleaseBindingStatus(in *solar.ReleaseBindingStatus, out *ReleaseBindingStatus, s conversion.Scope) error { + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_solar_ReleaseBindingStatus_To_v1alpha1_ReleaseBindingStatus is an autogenerated conversion function. +func Convert_solar_ReleaseBindingStatus_To_v1alpha1_ReleaseBindingStatus(in *solar.ReleaseBindingStatus, out *ReleaseBindingStatus, s conversion.Scope) error { + return autoConvert_solar_ReleaseBindingStatus_To_v1alpha1_ReleaseBindingStatus(in, out, s) +} + func autoConvert_v1alpha1_ReleaseComponent_To_solar_ReleaseComponent(in *ReleaseComponent, out *solar.ReleaseComponent, s conversion.Scope) error { out.Name = in.Name return nil @@ -1331,8 +1381,8 @@ func Convert_solar_ReleaseSpec_To_v1alpha1_ReleaseSpec(in *solar.ReleaseSpec, ou } func autoConvert_v1alpha1_ReleaseStatus_To_solar_ReleaseStatus(in *ReleaseStatus, out *solar.ReleaseStatus, s conversion.Scope) error { - out.Conditions = *(*[]metav1.Condition)(unsafe.Pointer(&in.Conditions)) - out.RenderTaskRef = (*v1.ObjectReference)(unsafe.Pointer(in.RenderTaskRef)) + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) + out.RenderTaskRef = (*corev1.ObjectReference)(unsafe.Pointer(in.RenderTaskRef)) out.ChartURL = in.ChartURL return nil } @@ -1343,8 +1393,8 @@ func Convert_v1alpha1_ReleaseStatus_To_solar_ReleaseStatus(in *ReleaseStatus, ou } func autoConvert_solar_ReleaseStatus_To_v1alpha1_ReleaseStatus(in *solar.ReleaseStatus, out *ReleaseStatus, s conversion.Scope) error { - out.Conditions = *(*[]metav1.Condition)(unsafe.Pointer(&in.Conditions)) - out.RenderTaskRef = (*v1.ObjectReference)(unsafe.Pointer(in.RenderTaskRef)) + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) + out.RenderTaskRef = (*corev1.ObjectReference)(unsafe.Pointer(in.RenderTaskRef)) out.ChartURL = in.ChartURL return nil } @@ -1434,6 +1484,8 @@ func autoConvert_v1alpha1_RenderTaskSpec_To_solar_RenderTaskSpec(in *RenderTaskS } out.Repository = in.Repository out.Tag = in.Tag + out.BaseURL = in.BaseURL + out.PushSecretRef = (*corev1.LocalObjectReference)(unsafe.Pointer(in.PushSecretRef)) out.FailedJobTTL = (*int32)(unsafe.Pointer(in.FailedJobTTL)) out.OwnerName = in.OwnerName out.OwnerNamespace = in.OwnerNamespace @@ -1452,6 +1504,8 @@ func autoConvert_solar_RenderTaskSpec_To_v1alpha1_RenderTaskSpec(in *solar.Rende } out.Repository = in.Repository out.Tag = in.Tag + out.BaseURL = in.BaseURL + out.PushSecretRef = (*corev1.LocalObjectReference)(unsafe.Pointer(in.PushSecretRef)) out.FailedJobTTL = (*int32)(unsafe.Pointer(in.FailedJobTTL)) out.OwnerName = in.OwnerName out.OwnerNamespace = in.OwnerNamespace @@ -1465,9 +1519,9 @@ func Convert_solar_RenderTaskSpec_To_v1alpha1_RenderTaskSpec(in *solar.RenderTas } func autoConvert_v1alpha1_RenderTaskStatus_To_solar_RenderTaskStatus(in *RenderTaskStatus, out *solar.RenderTaskStatus, s conversion.Scope) error { - out.Conditions = *(*[]metav1.Condition)(unsafe.Pointer(&in.Conditions)) - out.JobRef = (*v1.ObjectReference)(unsafe.Pointer(in.JobRef)) - out.ConfigSecretRef = (*v1.ObjectReference)(unsafe.Pointer(in.ConfigSecretRef)) + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) + out.JobRef = (*corev1.ObjectReference)(unsafe.Pointer(in.JobRef)) + out.ConfigSecretRef = (*corev1.ObjectReference)(unsafe.Pointer(in.ConfigSecretRef)) out.ChartURL = in.ChartURL return nil } @@ -1478,9 +1532,9 @@ func Convert_v1alpha1_RenderTaskStatus_To_solar_RenderTaskStatus(in *RenderTaskS } func autoConvert_solar_RenderTaskStatus_To_v1alpha1_RenderTaskStatus(in *solar.RenderTaskStatus, out *RenderTaskStatus, s conversion.Scope) error { - out.Conditions = *(*[]metav1.Condition)(unsafe.Pointer(&in.Conditions)) - out.JobRef = (*v1.ObjectReference)(unsafe.Pointer(in.JobRef)) - out.ConfigSecretRef = (*v1.ObjectReference)(unsafe.Pointer(in.ConfigSecretRef)) + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) + out.JobRef = (*corev1.ObjectReference)(unsafe.Pointer(in.JobRef)) + out.ConfigSecretRef = (*corev1.ObjectReference)(unsafe.Pointer(in.ConfigSecretRef)) out.ChartURL = in.ChartURL return nil } @@ -1600,8 +1654,30 @@ func Convert_solar_TargetList_To_v1alpha1_TargetList(in *solar.TargetList, out * return autoConvert_solar_TargetList_To_v1alpha1_TargetList(in, out, s) } +func autoConvert_v1alpha1_TargetSecretReference_To_solar_TargetSecretReference(in *TargetSecretReference, out *solar.TargetSecretReference, s conversion.Scope) error { + out.Name = in.Name + out.Namespace = in.Namespace + return nil +} + +// Convert_v1alpha1_TargetSecretReference_To_solar_TargetSecretReference is an autogenerated conversion function. +func Convert_v1alpha1_TargetSecretReference_To_solar_TargetSecretReference(in *TargetSecretReference, out *solar.TargetSecretReference, s conversion.Scope) error { + return autoConvert_v1alpha1_TargetSecretReference_To_solar_TargetSecretReference(in, out, s) +} + +func autoConvert_solar_TargetSecretReference_To_v1alpha1_TargetSecretReference(in *solar.TargetSecretReference, out *TargetSecretReference, s conversion.Scope) error { + out.Name = in.Name + out.Namespace = in.Namespace + return nil +} + +// Convert_solar_TargetSecretReference_To_v1alpha1_TargetSecretReference is an autogenerated conversion function. +func Convert_solar_TargetSecretReference_To_v1alpha1_TargetSecretReference(in *solar.TargetSecretReference, out *TargetSecretReference, s conversion.Scope) error { + return autoConvert_solar_TargetSecretReference_To_v1alpha1_TargetSecretReference(in, out, s) +} + func autoConvert_v1alpha1_TargetSpec_To_solar_TargetSpec(in *TargetSpec, out *solar.TargetSpec, s conversion.Scope) error { - out.Releases = *(*map[string]v1.LocalObjectReference)(unsafe.Pointer(&in.Releases)) + out.RenderRegistryRef = in.RenderRegistryRef out.Userdata = in.Userdata return nil } @@ -1612,7 +1688,7 @@ func Convert_v1alpha1_TargetSpec_To_solar_TargetSpec(in *TargetSpec, out *solar. } func autoConvert_solar_TargetSpec_To_v1alpha1_TargetSpec(in *solar.TargetSpec, out *TargetSpec, s conversion.Scope) error { - out.Releases = *(*map[string]v1.LocalObjectReference)(unsafe.Pointer(&in.Releases)) + out.RenderRegistryRef = in.RenderRegistryRef out.Userdata = in.Userdata return nil } @@ -1623,6 +1699,8 @@ func Convert_solar_TargetSpec_To_v1alpha1_TargetSpec(in *solar.TargetSpec, out * } func autoConvert_v1alpha1_TargetStatus_To_solar_TargetStatus(in *TargetStatus, out *solar.TargetStatus, s conversion.Scope) error { + out.BootstrapVersion = in.BootstrapVersion + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) return nil } @@ -1632,6 +1710,8 @@ func Convert_v1alpha1_TargetStatus_To_solar_TargetStatus(in *TargetStatus, out * } func autoConvert_solar_TargetStatus_To_v1alpha1_TargetStatus(in *solar.TargetStatus, out *TargetStatus, s conversion.Scope) error { + out.BootstrapVersion = in.BootstrapVersion + out.Conditions = *(*[]v1.Condition)(unsafe.Pointer(&in.Conditions)) return nil } @@ -1639,53 +1719,3 @@ func autoConvert_solar_TargetStatus_To_v1alpha1_TargetStatus(in *solar.TargetSta func Convert_solar_TargetStatus_To_v1alpha1_TargetStatus(in *solar.TargetStatus, out *TargetStatus, s conversion.Scope) error { return autoConvert_solar_TargetStatus_To_v1alpha1_TargetStatus(in, out, s) } - -func autoConvert_v1alpha1_Webhook_To_solar_Webhook(in *Webhook, out *solar.Webhook, s conversion.Scope) error { - out.Flavor = in.Flavor - out.Path = in.Path - if err := Convert_v1alpha1_WebhookAuth_To_solar_WebhookAuth(&in.Auth, &out.Auth, s); err != nil { - return err - } - return nil -} - -// Convert_v1alpha1_Webhook_To_solar_Webhook is an autogenerated conversion function. -func Convert_v1alpha1_Webhook_To_solar_Webhook(in *Webhook, out *solar.Webhook, s conversion.Scope) error { - return autoConvert_v1alpha1_Webhook_To_solar_Webhook(in, out, s) -} - -func autoConvert_solar_Webhook_To_v1alpha1_Webhook(in *solar.Webhook, out *Webhook, s conversion.Scope) error { - out.Flavor = in.Flavor - out.Path = in.Path - if err := Convert_solar_WebhookAuth_To_v1alpha1_WebhookAuth(&in.Auth, &out.Auth, s); err != nil { - return err - } - return nil -} - -// Convert_solar_Webhook_To_v1alpha1_Webhook is an autogenerated conversion function. -func Convert_solar_Webhook_To_v1alpha1_Webhook(in *solar.Webhook, out *Webhook, s conversion.Scope) error { - return autoConvert_solar_Webhook_To_v1alpha1_Webhook(in, out, s) -} - -func autoConvert_v1alpha1_WebhookAuth_To_solar_WebhookAuth(in *WebhookAuth, out *solar.WebhookAuth, s conversion.Scope) error { - out.Type = solar.AuthenticationType(in.Type) - out.AuthSecretRef = in.AuthSecretRef - return nil -} - -// Convert_v1alpha1_WebhookAuth_To_solar_WebhookAuth is an autogenerated conversion function. -func Convert_v1alpha1_WebhookAuth_To_solar_WebhookAuth(in *WebhookAuth, out *solar.WebhookAuth, s conversion.Scope) error { - return autoConvert_v1alpha1_WebhookAuth_To_solar_WebhookAuth(in, out, s) -} - -func autoConvert_solar_WebhookAuth_To_v1alpha1_WebhookAuth(in *solar.WebhookAuth, out *WebhookAuth, s conversion.Scope) error { - out.Type = AuthenticationType(in.Type) - out.AuthSecretRef = in.AuthSecretRef - return nil -} - -// Convert_solar_WebhookAuth_To_v1alpha1_WebhookAuth is an autogenerated conversion function. -func Convert_solar_WebhookAuth_To_v1alpha1_WebhookAuth(in *solar.WebhookAuth, out *WebhookAuth, s conversion.Scope) error { - return autoConvert_solar_WebhookAuth_To_v1alpha1_WebhookAuth(in, out, s) -} diff --git a/api/solar/v1alpha1/zz_generated.deepcopy.go b/api/solar/v1alpha1/zz_generated.deepcopy.go index ea893fd2..0e5dd141 100644 --- a/api/solar/v1alpha1/zz_generated.deepcopy.go +++ b/api/solar/v1alpha1/zz_generated.deepcopy.go @@ -9,39 +9,11 @@ package v1alpha1 import ( - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Bootstrap) DeepCopyInto(out *Bootstrap) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bootstrap. -func (in *Bootstrap) DeepCopy() *Bootstrap { - if in == nil { - return nil - } - out := new(Bootstrap) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Bootstrap) 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 *BootstrapConfig) DeepCopyInto(out *BootstrapConfig) { *out = *in @@ -84,98 +56,6 @@ func (in *BootstrapInput) DeepCopy() *BootstrapInput { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BootstrapList) DeepCopyInto(out *BootstrapList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Bootstrap, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapList. -func (in *BootstrapList) DeepCopy() *BootstrapList { - if in == nil { - return nil - } - out := new(BootstrapList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *BootstrapList) 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 *BootstrapSpec) DeepCopyInto(out *BootstrapSpec) { - *out = *in - if in.Releases != nil { - in, out := &in.Releases, &out.Releases - *out = make(map[string]v1.LocalObjectReference, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Profiles != nil { - in, out := &in.Profiles, &out.Profiles - *out = make(map[string]v1.LocalObjectReference, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - in.Userdata.DeepCopyInto(&out.Userdata) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapSpec. -func (in *BootstrapSpec) DeepCopy() *BootstrapSpec { - if in == nil { - return nil - } - out := new(BootstrapSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BootstrapStatus) DeepCopyInto(out *BootstrapStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.RenderTaskRef != nil { - in, out := &in.RenderTaskRef, &out.RenderTaskRef - *out = new(v1.ObjectReference) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapStatus. -func (in *BootstrapStatus) DeepCopy() *BootstrapStatus { - if in == nil { - return nil - } - out := new(BootstrapStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ChartConfig) DeepCopyInto(out *ChartConfig) { *out = *in @@ -388,27 +268,43 @@ func (in *ComponentVersionStatus) DeepCopy() *ComponentVersionStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Discovery) DeepCopyInto(out *Discovery) { +func (in *Entrypoint) DeepCopyInto(out *Entrypoint) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Entrypoint. +func (in *Entrypoint) DeepCopy() *Entrypoint { + if in == nil { + return nil + } + out := new(Entrypoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Profile) DeepCopyInto(out *Profile) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Discovery. -func (in *Discovery) DeepCopy() *Discovery { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Profile. +func (in *Profile) DeepCopy() *Profile { if in == nil { return nil } - out := new(Discovery) + out := new(Profile) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Discovery) DeepCopyObject() runtime.Object { +func (in *Profile) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -416,13 +312,13 @@ func (in *Discovery) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DiscoveryList) DeepCopyInto(out *DiscoveryList) { +func (in *ProfileList) DeepCopyInto(out *ProfileList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]Discovery, len(*in)) + *out = make([]Profile, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -430,18 +326,18 @@ func (in *DiscoveryList) DeepCopyInto(out *DiscoveryList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoveryList. -func (in *DiscoveryList) DeepCopy() *DiscoveryList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileList. +func (in *ProfileList) DeepCopy() *ProfileList { if in == nil { return nil } - out := new(DiscoveryList) + out := new(ProfileList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DiscoveryList) DeepCopyObject() runtime.Object { +func (in *ProfileList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -449,112 +345,113 @@ func (in *DiscoveryList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DiscoverySpec) DeepCopyInto(out *DiscoverySpec) { +func (in *ProfileSpec) DeepCopyInto(out *ProfileSpec) { *out = *in - out.Registry = in.Registry - if in.Webhook != nil { - in, out := &in.Webhook, &out.Webhook - *out = new(Webhook) - **out = **in - } - if in.Filter != nil { - in, out := &in.Filter, &out.Filter - *out = new(Filter) - (*in).DeepCopyInto(*out) - } - if in.DiscoveryInterval != nil { - in, out := &in.DiscoveryInterval, &out.DiscoveryInterval - *out = new(metav1.Duration) - **out = **in - } + out.ReleaseRef = in.ReleaseRef + in.TargetSelector.DeepCopyInto(&out.TargetSelector) + in.Userdata.DeepCopyInto(&out.Userdata) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoverySpec. -func (in *DiscoverySpec) DeepCopy() *DiscoverySpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileSpec. +func (in *ProfileSpec) DeepCopy() *ProfileSpec { if in == nil { return nil } - out := new(DiscoverySpec) + out := new(ProfileSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DiscoveryStatus) DeepCopyInto(out *DiscoveryStatus) { +func (in *ProfileStatus) DeepCopyInto(out *ProfileStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoveryStatus. -func (in *DiscoveryStatus) DeepCopy() *DiscoveryStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileStatus. +func (in *ProfileStatus) DeepCopy() *ProfileStatus { if in == nil { return nil } - out := new(DiscoveryStatus) + out := new(ProfileStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Entrypoint) DeepCopyInto(out *Entrypoint) { +func (in *PushResult) DeepCopyInto(out *PushResult) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Entrypoint. -func (in *Entrypoint) DeepCopy() *Entrypoint { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushResult. +func (in *PushResult) DeepCopy() *PushResult { if in == nil { return nil } - out := new(Entrypoint) + out := new(PushResult) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Filter) DeepCopyInto(out *Filter) { +func (in *Registry) DeepCopyInto(out *Registry) { *out = *in - if in.RepositoryPatterns != nil { - in, out := &in.RepositoryPatterns, &out.RepositoryPatterns - *out = make([]string, len(*in)) - copy(*out, *in) - } + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Filter. -func (in *Filter) DeepCopy() *Filter { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Registry. +func (in *Registry) DeepCopy() *Registry { if in == nil { return nil } - out := new(Filter) + out := new(Registry) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Registry) 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 *Profile) DeepCopyInto(out *Profile) { +func (in *RegistryBinding) DeepCopyInto(out *RegistryBinding) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Profile. -func (in *Profile) DeepCopy() *Profile { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryBinding. +func (in *RegistryBinding) DeepCopy() *RegistryBinding { if in == nil { return nil } - out := new(Profile) + out := new(RegistryBinding) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Profile) DeepCopyObject() runtime.Object { +func (in *RegistryBinding) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -562,13 +459,13 @@ func (in *Profile) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProfileList) DeepCopyInto(out *ProfileList) { +func (in *RegistryBindingList) DeepCopyInto(out *RegistryBindingList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]Profile, len(*in)) + *out = make([]RegistryBinding, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -576,18 +473,18 @@ func (in *ProfileList) DeepCopyInto(out *ProfileList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileList. -func (in *ProfileList) DeepCopy() *ProfileList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryBindingList. +func (in *RegistryBindingList) DeepCopy() *RegistryBindingList { if in == nil { return nil } - out := new(ProfileList) + out := new(RegistryBindingList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ProfileList) DeepCopyObject() runtime.Object { +func (in *RegistryBindingList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -595,30 +492,29 @@ func (in *ProfileList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProfileSpec) DeepCopyInto(out *ProfileSpec) { +func (in *RegistryBindingSpec) DeepCopyInto(out *RegistryBindingSpec) { *out = *in - out.ReleaseRef = in.ReleaseRef - in.TargetSelector.DeepCopyInto(&out.TargetSelector) - in.Userdata.DeepCopyInto(&out.Userdata) + out.TargetRef = in.TargetRef + out.RegistryRef = in.RegistryRef return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileSpec. -func (in *ProfileSpec) DeepCopy() *ProfileSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryBindingSpec. +func (in *RegistryBindingSpec) DeepCopy() *RegistryBindingSpec { if in == nil { return nil } - out := new(ProfileSpec) + out := new(RegistryBindingSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProfileStatus) DeepCopyInto(out *ProfileStatus) { +func (in *RegistryBindingStatus) DeepCopyInto(out *RegistryBindingStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -626,46 +522,94 @@ func (in *ProfileStatus) DeepCopyInto(out *ProfileStatus) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileStatus. -func (in *ProfileStatus) DeepCopy() *ProfileStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryBindingStatus. +func (in *RegistryBindingStatus) DeepCopy() *RegistryBindingStatus { if in == nil { return nil } - out := new(ProfileStatus) + out := new(RegistryBindingStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PushResult) DeepCopyInto(out *PushResult) { +func (in *RegistryList) DeepCopyInto(out *RegistryList) { *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Registry, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushResult. -func (in *PushResult) DeepCopy() *PushResult { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryList. +func (in *RegistryList) DeepCopy() *RegistryList { if in == nil { return nil } - out := new(PushResult) + out := new(RegistryList) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RegistryList) 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 *Registry) DeepCopyInto(out *Registry) { +func (in *RegistrySpec) DeepCopyInto(out *RegistrySpec) { *out = *in - out.SecretRef = in.SecretRef - out.CAConfigMapRef = in.CAConfigMapRef + if in.SolarSecretRef != nil { + in, out := &in.SolarSecretRef, &out.SolarSecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.TargetSecretRef != nil { + in, out := &in.TargetSecretRef, &out.TargetSecretRef + *out = new(TargetSecretReference) + **out = **in + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Registry. -func (in *Registry) DeepCopy() *Registry { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistrySpec. +func (in *RegistrySpec) DeepCopy() *RegistrySpec { if in == nil { return nil } - out := new(Registry) + out := new(RegistrySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegistryStatus) DeepCopyInto(out *RegistryStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryStatus. +func (in *RegistryStatus) DeepCopy() *RegistryStatus { + if in == nil { + return nil + } + out := new(RegistryStatus) in.DeepCopyInto(out) return out } @@ -698,6 +642,108 @@ func (in *Release) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReleaseBinding) DeepCopyInto(out *ReleaseBinding) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleaseBinding. +func (in *ReleaseBinding) DeepCopy() *ReleaseBinding { + if in == nil { + return nil + } + out := new(ReleaseBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ReleaseBinding) 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 *ReleaseBindingList) DeepCopyInto(out *ReleaseBindingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ReleaseBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleaseBindingList. +func (in *ReleaseBindingList) DeepCopy() *ReleaseBindingList { + if in == nil { + return nil + } + out := new(ReleaseBindingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ReleaseBindingList) 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 *ReleaseBindingSpec) DeepCopyInto(out *ReleaseBindingSpec) { + *out = *in + out.TargetRef = in.TargetRef + out.ReleaseRef = in.ReleaseRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleaseBindingSpec. +func (in *ReleaseBindingSpec) DeepCopy() *ReleaseBindingSpec { + if in == nil { + return nil + } + out := new(ReleaseBindingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReleaseBindingStatus) DeepCopyInto(out *ReleaseBindingStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleaseBindingStatus. +func (in *ReleaseBindingStatus) DeepCopy() *ReleaseBindingStatus { + if in == nil { + return nil + } + out := new(ReleaseBindingStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ReleaseComponent) DeepCopyInto(out *ReleaseComponent) { *out = *in @@ -819,14 +865,14 @@ func (in *ReleaseStatus) DeepCopyInto(out *ReleaseStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.RenderTaskRef != nil { in, out := &in.RenderTaskRef, &out.RenderTaskRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } return @@ -923,6 +969,11 @@ func (in *RenderTaskList) DeepCopyObject() runtime.Object { func (in *RenderTaskSpec) DeepCopyInto(out *RenderTaskSpec) { *out = *in in.RendererConfig.DeepCopyInto(&out.RendererConfig) + if in.PushSecretRef != nil { + in, out := &in.PushSecretRef, &out.PushSecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } if in.FailedJobTTL != nil { in, out := &in.FailedJobTTL, &out.FailedJobTTL *out = new(int32) @@ -946,19 +997,19 @@ func (in *RenderTaskStatus) DeepCopyInto(out *RenderTaskStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.JobRef != nil { in, out := &in.JobRef, &out.JobRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } if in.ConfigSecretRef != nil { in, out := &in.ConfigSecretRef, &out.ConfigSecretRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } return @@ -1014,7 +1065,7 @@ func (in *Target) DeepCopyInto(out *Target) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -1070,75 +1121,58 @@ func (in *TargetList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TargetSpec) DeepCopyInto(out *TargetSpec) { +func (in *TargetSecretReference) DeepCopyInto(out *TargetSecretReference) { *out = *in - if in.Releases != nil { - in, out := &in.Releases, &out.Releases - *out = make(map[string]v1.LocalObjectReference, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - in.Userdata.DeepCopyInto(&out.Userdata) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSpec. -func (in *TargetSpec) DeepCopy() *TargetSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSecretReference. +func (in *TargetSecretReference) DeepCopy() *TargetSecretReference { if in == nil { return nil } - out := new(TargetSpec) + out := new(TargetSecretReference) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TargetStatus) DeepCopyInto(out *TargetStatus) { +func (in *TargetSpec) DeepCopyInto(out *TargetSpec) { *out = *in + out.RenderRegistryRef = in.RenderRegistryRef + in.Userdata.DeepCopyInto(&out.Userdata) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetStatus. -func (in *TargetStatus) DeepCopy() *TargetStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSpec. +func (in *TargetSpec) DeepCopy() *TargetSpec { if in == nil { return nil } - out := new(TargetStatus) + out := new(TargetSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Webhook) DeepCopyInto(out *Webhook) { +func (in *TargetStatus) DeepCopyInto(out *TargetStatus) { *out = *in - out.Auth = in.Auth - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Webhook. -func (in *Webhook) DeepCopy() *Webhook { - if in == nil { - return nil + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } - out := new(Webhook) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookAuth) DeepCopyInto(out *WebhookAuth) { - *out = *in - out.AuthSecretRef = in.AuthSecretRef return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAuth. -func (in *WebhookAuth) DeepCopy() *WebhookAuth { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetStatus. +func (in *TargetStatus) DeepCopy() *TargetStatus { if in == nil { return nil } - out := new(WebhookAuth) + out := new(TargetStatus) in.DeepCopyInto(out) return out } diff --git a/api/solar/v1alpha1/zz_generated.model_name.go b/api/solar/v1alpha1/zz_generated.model_name.go index 7a8016f2..f1530fa9 100644 --- a/api/solar/v1alpha1/zz_generated.model_name.go +++ b/api/solar/v1alpha1/zz_generated.model_name.go @@ -8,11 +8,6 @@ package v1alpha1 -// OpenAPIModelName returns the OpenAPI model name for this type. -func (in Bootstrap) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.Bootstrap" -} - // OpenAPIModelName returns the OpenAPI model name for this type. func (in BootstrapConfig) OpenAPIModelName() string { return "cloud.opendefense.solar.v1alpha1.BootstrapConfig" @@ -23,21 +18,6 @@ func (in BootstrapInput) OpenAPIModelName() string { return "cloud.opendefense.solar.v1alpha1.BootstrapInput" } -// OpenAPIModelName returns the OpenAPI model name for this type. -func (in BootstrapList) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.BootstrapList" -} - -// OpenAPIModelName returns the OpenAPI model name for this type. -func (in BootstrapSpec) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.BootstrapSpec" -} - -// OpenAPIModelName returns the OpenAPI model name for this type. -func (in BootstrapStatus) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.BootstrapStatus" -} - // OpenAPIModelName returns the OpenAPI model name for this type. func (in ChartConfig) OpenAPIModelName() string { return "cloud.opendefense.solar.v1alpha1.ChartConfig" @@ -84,63 +64,73 @@ func (in ComponentVersionStatus) OpenAPIModelName() string { } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in Discovery) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.Discovery" +func (in Entrypoint) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.Entrypoint" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in DiscoveryList) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.DiscoveryList" +func (in Profile) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.Profile" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in DiscoverySpec) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.DiscoverySpec" +func (in ProfileList) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.ProfileList" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in DiscoveryStatus) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.DiscoveryStatus" +func (in ProfileSpec) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.ProfileSpec" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in Entrypoint) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.Entrypoint" +func (in ProfileStatus) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.ProfileStatus" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in Filter) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.Filter" +func (in PushResult) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.PushResult" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in Profile) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.Profile" +func (in Registry) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.Registry" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in ProfileList) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.ProfileList" +func (in RegistryBinding) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.RegistryBinding" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in ProfileSpec) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.ProfileSpec" +func (in RegistryBindingList) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.RegistryBindingList" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in ProfileStatus) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.ProfileStatus" +func (in RegistryBindingSpec) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.RegistryBindingSpec" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in PushResult) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.PushResult" +func (in RegistryBindingStatus) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.RegistryBindingStatus" } // OpenAPIModelName returns the OpenAPI model name for this type. -func (in Registry) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.Registry" +func (in RegistryList) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.RegistryList" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in RegistrySpec) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.RegistrySpec" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in RegistryStatus) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.RegistryStatus" } // OpenAPIModelName returns the OpenAPI model name for this type. @@ -148,6 +138,26 @@ func (in Release) OpenAPIModelName() string { return "cloud.opendefense.solar.v1alpha1.Release" } +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in ReleaseBinding) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.ReleaseBinding" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in ReleaseBindingList) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.ReleaseBindingList" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in ReleaseBindingSpec) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.ReleaseBindingSpec" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in ReleaseBindingStatus) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.ReleaseBindingStatus" +} + // OpenAPIModelName returns the OpenAPI model name for this type. func (in ReleaseComponent) OpenAPIModelName() string { return "cloud.opendefense.solar.v1alpha1.ReleaseComponent" @@ -223,6 +233,11 @@ func (in TargetList) OpenAPIModelName() string { return "cloud.opendefense.solar.v1alpha1.TargetList" } +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in TargetSecretReference) OpenAPIModelName() string { + return "cloud.opendefense.solar.v1alpha1.TargetSecretReference" +} + // OpenAPIModelName returns the OpenAPI model name for this type. func (in TargetSpec) OpenAPIModelName() string { return "cloud.opendefense.solar.v1alpha1.TargetSpec" @@ -232,13 +247,3 @@ func (in TargetSpec) OpenAPIModelName() string { func (in TargetStatus) OpenAPIModelName() string { return "cloud.opendefense.solar.v1alpha1.TargetStatus" } - -// OpenAPIModelName returns the OpenAPI model name for this type. -func (in Webhook) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.Webhook" -} - -// OpenAPIModelName returns the OpenAPI model name for this type. -func (in WebhookAuth) OpenAPIModelName() string { - return "cloud.opendefense.solar.v1alpha1.WebhookAuth" -} diff --git a/api/solar/zz_generated.deepcopy.go b/api/solar/zz_generated.deepcopy.go index 79de05f5..1b88146b 100644 --- a/api/solar/zz_generated.deepcopy.go +++ b/api/solar/zz_generated.deepcopy.go @@ -9,39 +9,11 @@ package solar import ( - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Bootstrap) DeepCopyInto(out *Bootstrap) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bootstrap. -func (in *Bootstrap) DeepCopy() *Bootstrap { - if in == nil { - return nil - } - out := new(Bootstrap) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Bootstrap) 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 *BootstrapConfig) DeepCopyInto(out *BootstrapConfig) { *out = *in @@ -84,98 +56,6 @@ func (in *BootstrapInput) DeepCopy() *BootstrapInput { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BootstrapList) DeepCopyInto(out *BootstrapList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Bootstrap, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapList. -func (in *BootstrapList) DeepCopy() *BootstrapList { - if in == nil { - return nil - } - out := new(BootstrapList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *BootstrapList) 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 *BootstrapSpec) DeepCopyInto(out *BootstrapSpec) { - *out = *in - if in.Releases != nil { - in, out := &in.Releases, &out.Releases - *out = make(map[string]v1.LocalObjectReference, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Profiles != nil { - in, out := &in.Profiles, &out.Profiles - *out = make(map[string]v1.LocalObjectReference, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - in.Userdata.DeepCopyInto(&out.Userdata) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapSpec. -func (in *BootstrapSpec) DeepCopy() *BootstrapSpec { - if in == nil { - return nil - } - out := new(BootstrapSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BootstrapStatus) DeepCopyInto(out *BootstrapStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.RenderTaskRef != nil { - in, out := &in.RenderTaskRef, &out.RenderTaskRef - *out = new(v1.ObjectReference) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapStatus. -func (in *BootstrapStatus) DeepCopy() *BootstrapStatus { - if in == nil { - return nil - } - out := new(BootstrapStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ChartConfig) DeepCopyInto(out *ChartConfig) { *out = *in @@ -388,27 +268,43 @@ func (in *ComponentVersionStatus) DeepCopy() *ComponentVersionStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Discovery) DeepCopyInto(out *Discovery) { +func (in *Entrypoint) DeepCopyInto(out *Entrypoint) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Entrypoint. +func (in *Entrypoint) DeepCopy() *Entrypoint { + if in == nil { + return nil + } + out := new(Entrypoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Profile) DeepCopyInto(out *Profile) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Discovery. -func (in *Discovery) DeepCopy() *Discovery { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Profile. +func (in *Profile) DeepCopy() *Profile { if in == nil { return nil } - out := new(Discovery) + out := new(Profile) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Discovery) DeepCopyObject() runtime.Object { +func (in *Profile) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -416,13 +312,13 @@ func (in *Discovery) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DiscoveryList) DeepCopyInto(out *DiscoveryList) { +func (in *ProfileList) DeepCopyInto(out *ProfileList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]Discovery, len(*in)) + *out = make([]Profile, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -430,18 +326,18 @@ func (in *DiscoveryList) DeepCopyInto(out *DiscoveryList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoveryList. -func (in *DiscoveryList) DeepCopy() *DiscoveryList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileList. +func (in *ProfileList) DeepCopy() *ProfileList { if in == nil { return nil } - out := new(DiscoveryList) + out := new(ProfileList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DiscoveryList) DeepCopyObject() runtime.Object { +func (in *ProfileList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -449,112 +345,113 @@ func (in *DiscoveryList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DiscoverySpec) DeepCopyInto(out *DiscoverySpec) { +func (in *ProfileSpec) DeepCopyInto(out *ProfileSpec) { *out = *in - out.Registry = in.Registry - if in.Webhook != nil { - in, out := &in.Webhook, &out.Webhook - *out = new(Webhook) - **out = **in - } - if in.Filter != nil { - in, out := &in.Filter, &out.Filter - *out = new(Filter) - (*in).DeepCopyInto(*out) - } - if in.DiscoveryInterval != nil { - in, out := &in.DiscoveryInterval, &out.DiscoveryInterval - *out = new(metav1.Duration) - **out = **in - } + out.ReleaseRef = in.ReleaseRef + in.TargetSelector.DeepCopyInto(&out.TargetSelector) + in.Userdata.DeepCopyInto(&out.Userdata) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoverySpec. -func (in *DiscoverySpec) DeepCopy() *DiscoverySpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileSpec. +func (in *ProfileSpec) DeepCopy() *ProfileSpec { if in == nil { return nil } - out := new(DiscoverySpec) + out := new(ProfileSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DiscoveryStatus) DeepCopyInto(out *DiscoveryStatus) { +func (in *ProfileStatus) DeepCopyInto(out *ProfileStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoveryStatus. -func (in *DiscoveryStatus) DeepCopy() *DiscoveryStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileStatus. +func (in *ProfileStatus) DeepCopy() *ProfileStatus { if in == nil { return nil } - out := new(DiscoveryStatus) + out := new(ProfileStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Entrypoint) DeepCopyInto(out *Entrypoint) { +func (in *PushResult) DeepCopyInto(out *PushResult) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Entrypoint. -func (in *Entrypoint) DeepCopy() *Entrypoint { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushResult. +func (in *PushResult) DeepCopy() *PushResult { if in == nil { return nil } - out := new(Entrypoint) + out := new(PushResult) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Filter) DeepCopyInto(out *Filter) { +func (in *Registry) DeepCopyInto(out *Registry) { *out = *in - if in.RepositoryPatterns != nil { - in, out := &in.RepositoryPatterns, &out.RepositoryPatterns - *out = make([]string, len(*in)) - copy(*out, *in) - } + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Filter. -func (in *Filter) DeepCopy() *Filter { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Registry. +func (in *Registry) DeepCopy() *Registry { if in == nil { return nil } - out := new(Filter) + out := new(Registry) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Registry) 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 *Profile) DeepCopyInto(out *Profile) { +func (in *RegistryBinding) DeepCopyInto(out *RegistryBinding) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Profile. -func (in *Profile) DeepCopy() *Profile { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryBinding. +func (in *RegistryBinding) DeepCopy() *RegistryBinding { if in == nil { return nil } - out := new(Profile) + out := new(RegistryBinding) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Profile) DeepCopyObject() runtime.Object { +func (in *RegistryBinding) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -562,13 +459,13 @@ func (in *Profile) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProfileList) DeepCopyInto(out *ProfileList) { +func (in *RegistryBindingList) DeepCopyInto(out *RegistryBindingList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]Profile, len(*in)) + *out = make([]RegistryBinding, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -576,18 +473,18 @@ func (in *ProfileList) DeepCopyInto(out *ProfileList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileList. -func (in *ProfileList) DeepCopy() *ProfileList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryBindingList. +func (in *RegistryBindingList) DeepCopy() *RegistryBindingList { if in == nil { return nil } - out := new(ProfileList) + out := new(RegistryBindingList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ProfileList) DeepCopyObject() runtime.Object { +func (in *RegistryBindingList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -595,30 +492,29 @@ func (in *ProfileList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProfileSpec) DeepCopyInto(out *ProfileSpec) { +func (in *RegistryBindingSpec) DeepCopyInto(out *RegistryBindingSpec) { *out = *in - out.ReleaseRef = in.ReleaseRef - in.TargetSelector.DeepCopyInto(&out.TargetSelector) - in.Userdata.DeepCopyInto(&out.Userdata) + out.TargetRef = in.TargetRef + out.RegistryRef = in.RegistryRef return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileSpec. -func (in *ProfileSpec) DeepCopy() *ProfileSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryBindingSpec. +func (in *RegistryBindingSpec) DeepCopy() *RegistryBindingSpec { if in == nil { return nil } - out := new(ProfileSpec) + out := new(RegistryBindingSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProfileStatus) DeepCopyInto(out *ProfileStatus) { +func (in *RegistryBindingStatus) DeepCopyInto(out *RegistryBindingStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -626,46 +522,94 @@ func (in *ProfileStatus) DeepCopyInto(out *ProfileStatus) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileStatus. -func (in *ProfileStatus) DeepCopy() *ProfileStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryBindingStatus. +func (in *RegistryBindingStatus) DeepCopy() *RegistryBindingStatus { if in == nil { return nil } - out := new(ProfileStatus) + out := new(RegistryBindingStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PushResult) DeepCopyInto(out *PushResult) { +func (in *RegistryList) DeepCopyInto(out *RegistryList) { *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Registry, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushResult. -func (in *PushResult) DeepCopy() *PushResult { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryList. +func (in *RegistryList) DeepCopy() *RegistryList { if in == nil { return nil } - out := new(PushResult) + out := new(RegistryList) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RegistryList) 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 *Registry) DeepCopyInto(out *Registry) { +func (in *RegistrySpec) DeepCopyInto(out *RegistrySpec) { *out = *in - out.SecretRef = in.SecretRef - out.CAConfigMapRef = in.CAConfigMapRef + if in.SolarSecretRef != nil { + in, out := &in.SolarSecretRef, &out.SolarSecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.TargetSecretRef != nil { + in, out := &in.TargetSecretRef, &out.TargetSecretRef + *out = new(TargetSecretReference) + **out = **in + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Registry. -func (in *Registry) DeepCopy() *Registry { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistrySpec. +func (in *RegistrySpec) DeepCopy() *RegistrySpec { if in == nil { return nil } - out := new(Registry) + out := new(RegistrySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegistryStatus) DeepCopyInto(out *RegistryStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryStatus. +func (in *RegistryStatus) DeepCopy() *RegistryStatus { + if in == nil { + return nil + } + out := new(RegistryStatus) in.DeepCopyInto(out) return out } @@ -698,6 +642,108 @@ func (in *Release) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReleaseBinding) DeepCopyInto(out *ReleaseBinding) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleaseBinding. +func (in *ReleaseBinding) DeepCopy() *ReleaseBinding { + if in == nil { + return nil + } + out := new(ReleaseBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ReleaseBinding) 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 *ReleaseBindingList) DeepCopyInto(out *ReleaseBindingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ReleaseBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleaseBindingList. +func (in *ReleaseBindingList) DeepCopy() *ReleaseBindingList { + if in == nil { + return nil + } + out := new(ReleaseBindingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ReleaseBindingList) 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 *ReleaseBindingSpec) DeepCopyInto(out *ReleaseBindingSpec) { + *out = *in + out.TargetRef = in.TargetRef + out.ReleaseRef = in.ReleaseRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleaseBindingSpec. +func (in *ReleaseBindingSpec) DeepCopy() *ReleaseBindingSpec { + if in == nil { + return nil + } + out := new(ReleaseBindingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReleaseBindingStatus) DeepCopyInto(out *ReleaseBindingStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleaseBindingStatus. +func (in *ReleaseBindingStatus) DeepCopy() *ReleaseBindingStatus { + if in == nil { + return nil + } + out := new(ReleaseBindingStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ReleaseComponent) DeepCopyInto(out *ReleaseComponent) { *out = *in @@ -819,14 +865,14 @@ func (in *ReleaseStatus) DeepCopyInto(out *ReleaseStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.RenderTaskRef != nil { in, out := &in.RenderTaskRef, &out.RenderTaskRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } return @@ -923,6 +969,11 @@ func (in *RenderTaskList) DeepCopyObject() runtime.Object { func (in *RenderTaskSpec) DeepCopyInto(out *RenderTaskSpec) { *out = *in in.RendererConfig.DeepCopyInto(&out.RendererConfig) + if in.PushSecretRef != nil { + in, out := &in.PushSecretRef, &out.PushSecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } if in.FailedJobTTL != nil { in, out := &in.FailedJobTTL, &out.FailedJobTTL *out = new(int32) @@ -946,19 +997,19 @@ func (in *RenderTaskStatus) DeepCopyInto(out *RenderTaskStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.JobRef != nil { in, out := &in.JobRef, &out.JobRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } if in.ConfigSecretRef != nil { in, out := &in.ConfigSecretRef, &out.ConfigSecretRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } return @@ -1014,7 +1065,7 @@ func (in *Target) DeepCopyInto(out *Target) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -1070,75 +1121,58 @@ func (in *TargetList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TargetSpec) DeepCopyInto(out *TargetSpec) { +func (in *TargetSecretReference) DeepCopyInto(out *TargetSecretReference) { *out = *in - if in.Releases != nil { - in, out := &in.Releases, &out.Releases - *out = make(map[string]v1.LocalObjectReference, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - in.Userdata.DeepCopyInto(&out.Userdata) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSpec. -func (in *TargetSpec) DeepCopy() *TargetSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSecretReference. +func (in *TargetSecretReference) DeepCopy() *TargetSecretReference { if in == nil { return nil } - out := new(TargetSpec) + out := new(TargetSecretReference) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TargetStatus) DeepCopyInto(out *TargetStatus) { +func (in *TargetSpec) DeepCopyInto(out *TargetSpec) { *out = *in + out.RenderRegistryRef = in.RenderRegistryRef + in.Userdata.DeepCopyInto(&out.Userdata) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetStatus. -func (in *TargetStatus) DeepCopy() *TargetStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSpec. +func (in *TargetSpec) DeepCopy() *TargetSpec { if in == nil { return nil } - out := new(TargetStatus) + out := new(TargetSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Webhook) DeepCopyInto(out *Webhook) { +func (in *TargetStatus) DeepCopyInto(out *TargetStatus) { *out = *in - out.Auth = in.Auth - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Webhook. -func (in *Webhook) DeepCopy() *Webhook { - if in == nil { - return nil + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } - out := new(Webhook) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookAuth) DeepCopyInto(out *WebhookAuth) { - *out = *in - out.AuthSecretRef = in.AuthSecretRef return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAuth. -func (in *WebhookAuth) DeepCopy() *WebhookAuth { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetStatus. +func (in *TargetStatus) DeepCopy() *TargetStatus { if in == nil { return nil } - out := new(WebhookAuth) + out := new(TargetStatus) in.DeepCopyInto(out) return out } diff --git a/charts/solar-discovery/.helmignore b/charts/solar-discovery/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/charts/solar-discovery/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/solar-discovery/Chart.yaml b/charts/solar-discovery/Chart.yaml new file mode 100644 index 00000000..dd552641 --- /dev/null +++ b/charts/solar-discovery/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v2 +name: solar-discovery +description: Helm chart for SolAr Discovery - Standalone OCI registry scanner that discovers OCM packages and populates the SolAr catalog. +type: application +version: 0.1.0 +appVersion: "latest" +home: https://solar.opendefense.cloud +sources: + - https://github.com/opendefensecloud/solution-arsenal +maintainers: + - name: ODD Team + email: odd@opendefense.cloud +keywords: + - ocm + - oci + - discovery + - registry + - catalog + - kubernetes diff --git a/charts/solar-discovery/files/role.yaml b/charts/solar-discovery/files/role.yaml new file mode 100644 index 00000000..f9d27f18 --- /dev/null +++ b/charts/solar-discovery/files/role.yaml @@ -0,0 +1,14 @@ +rules: + - apiGroups: + - solar.opendefense.cloud + resources: + - components + - componentversions + verbs: + - get + - list + - watch + - create + - update + - patch + - delete diff --git a/charts/solar-discovery/templates/NOTES.txt b/charts/solar-discovery/templates/NOTES.txt new file mode 100644 index 00000000..26e2d6c8 --- /dev/null +++ b/charts/solar-discovery/templates/NOTES.txt @@ -0,0 +1,25 @@ +SolAr Discovery has been deployed. + +Chart: {{ .Chart.Name }}-{{ .Chart.Version }} +Image: {{ include "solar-discovery.image" . }} + +Discovery is writing Component and ComponentVersion resources to namespace: {{ .Values.namespace }} + +{{- if .Values.registries }} + +Configured registries: +{{- range .Values.registries }} + - {{ .name }} ({{ .hostname }}) + {{- if .scanInterval }} scan interval: {{ .scanInterval }}{{ end }} + {{- if .webhookPath }} webhook: {{ .webhookPath }}{{ end }} +{{- end }} +{{- else }} + +WARNING: No registries configured. Set .Values.registries to start discovering components. +{{- end }} + +{{- if .Values.service.enabled }} + +Webhook listener available at: + {{ include "solar-discovery.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }} +{{- end }} diff --git a/charts/solar-discovery/templates/_helpers.tpl b/charts/solar-discovery/templates/_helpers.tpl new file mode 100644 index 00000000..0469d1b6 --- /dev/null +++ b/charts/solar-discovery/templates/_helpers.tpl @@ -0,0 +1,68 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "solar-discovery.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "solar-discovery.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 "solar-discovery.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "solar-discovery.labels" -}} +helm.sh/chart: {{ include "solar-discovery.chart" . }} +{{ include "solar-discovery.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "solar-discovery.selectorLabels" -}} +app.kubernetes.io/name: {{ include "solar-discovery.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "solar-discovery.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "solar-discovery.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Discovery image +*/}} +{{- define "solar-discovery.image" -}} +{{- $tag := .Values.image.tag | default .Chart.AppVersion }} +{{- printf "%s:%s" .Values.image.repository $tag }} +{{- end }} diff --git a/charts/solar-discovery/templates/clusterrole.yaml b/charts/solar-discovery/templates/clusterrole.yaml new file mode 100644 index 00000000..8f0a90e3 --- /dev/null +++ b/charts/solar-discovery/templates/clusterrole.yaml @@ -0,0 +1,11 @@ +{{- if .Values.rbac.create -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "solar-discovery.fullname" . }} + labels: + {{- include "solar-discovery.labels" . | nindent 4 }} +{{- $role := .Files.Get "files/role.yaml" | fromYaml }} +rules: + {{- $role.rules | toYaml | nindent 2 }} +{{- end }} diff --git a/charts/solar-discovery/templates/clusterrolebinding.yaml b/charts/solar-discovery/templates/clusterrolebinding.yaml new file mode 100644 index 00000000..4392688f --- /dev/null +++ b/charts/solar-discovery/templates/clusterrolebinding.yaml @@ -0,0 +1,16 @@ +{{- if .Values.rbac.create -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "solar-discovery.fullname" . }} + labels: + {{- include "solar-discovery.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "solar-discovery.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "solar-discovery.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/solar-discovery/templates/configmap.yaml b/charts/solar-discovery/templates/configmap.yaml new file mode 100644 index 00000000..ea06e6e2 --- /dev/null +++ b/charts/solar-discovery/templates/configmap.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "solar-discovery.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "solar-discovery.labels" . | nindent 4 }} +data: + config.yaml: | + registries: + {{- range .Values.registries }} + - name: {{ .name | quote }} + hostname: {{ .hostname | quote }} + {{- if .flavor }} + flavor: {{ .flavor | quote }} + {{- end }} + {{- if hasKey . "plainHTTP" }} + plainHTTP: {{ .plainHTTP }} + {{- end }} + {{- if .scanInterval }} + scanInterval: {{ .scanInterval | quote }} + {{- end }} + {{- if .credentials }} + credentials: + username: {{ .credentials.username | quote }} + password: {{ .credentials.password | quote }} + {{- end }} + {{- if .webhookPath }} + webhookPath: {{ .webhookPath | quote }} + {{- end }} + {{- end }} diff --git a/charts/solar-discovery/templates/deployment.yaml b/charts/solar-discovery/templates/deployment.yaml new file mode 100644 index 00000000..5c3c3c6a --- /dev/null +++ b/charts/solar-discovery/templates/deployment.yaml @@ -0,0 +1,97 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "solar-discovery.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "solar-discovery.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "solar-discovery.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "solar-discovery.labels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "solar-discovery.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + terminationGracePeriodSeconds: 10 + containers: + - name: discovery + image: {{ include "solar-discovery.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - --config + - /etc/solar-discovery/config.yaml + - --namespace + - {{ default .Release.Namespace .Values.namespace }} + - --listen + - 0.0.0.0:{{ .Values.service.port }} + ports: + - name: webhook + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if or .Values.env .Values.caBundle.enabled }} + env: + {{- with .Values.env }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.caBundle.enabled }} + - name: SSL_CERT_FILE + value: /etc/ssl/certs/ca-bundle.pem + {{- end }} + {{- end }} + securityContext: + {{- mustMergeOverwrite (dict "readOnlyRootFilesystem" true) .Values.securityContext | toYaml | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: tmp + mountPath: /tmp + - name: config + mountPath: /etc/solar-discovery + readOnly: true + {{- if .Values.caBundle.enabled }} + - name: ca-bundle + mountPath: /etc/ssl/certs + readOnly: true + {{- end }} + volumes: + - name: tmp + emptyDir: {} + - name: config + configMap: + name: {{ include "solar-discovery.fullname" . }} + {{- if .Values.caBundle.enabled }} + - name: ca-bundle + configMap: + name: {{ required "caBundle.configMapName must be set when caBundle.enabled=true" .Values.caBundle.configMapName }} + items: + - key: {{ .Values.caBundle.key }} + path: ca-bundle.pem + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/solar-discovery/templates/service.yaml b/charts/solar-discovery/templates/service.yaml new file mode 100644 index 00000000..852599d2 --- /dev/null +++ b/charts/solar-discovery/templates/service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.service.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "solar-discovery.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "solar-discovery.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - name: webhook + port: {{ .Values.service.port }} + targetPort: webhook + protocol: TCP + selector: + {{- include "solar-discovery.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/solar-discovery/templates/serviceaccount.yaml b/charts/solar-discovery/templates/serviceaccount.yaml new file mode 100644 index 00000000..bf7b67dc --- /dev/null +++ b/charts/solar-discovery/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "solar-discovery.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "solar-discovery.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/solar-discovery/values.yaml b/charts/solar-discovery/values.yaml new file mode 100644 index 00000000..5bc59ae7 --- /dev/null +++ b/charts/solar-discovery/values.yaml @@ -0,0 +1,118 @@ +# solar-discovery Helm chart values + +# -- Image configuration for solar-discovery +image: + repository: ghcr.io/opendefensecloud/solar-discovery + # -- Overrides the image tag (default is the chart appVersion) + tag: "" + pullPolicy: IfNotPresent + +# -- Image pull secrets (list of secret names) +imagePullSecrets: [] + +# -- Override the full name of resources +fullnameOverride: "" + +# -- Override the chart name +nameOverride: "" + +# -- Registry configurations to scan. +# Each entry describes a registry and its operating mode (scan, webhook, or both). +# Environment variables can be referenced via $VAR or ${VAR} syntax and will be +# expanded at startup. This allows injecting credentials from Secrets mounted as +# environment variables (see envFrom below). +# +# Example: +# registries: +# - name: my-registry +# hostname: registry.example.com +# scanInterval: 24h +# credentials: +# username: ${REGISTRY_USERNAME} +# password: ${REGISTRY_PASSWORD} +# webhookPath: events # enables webhook mode +# flavor: zot # webhook implementation +registries: [] + +# -- Kubernetes namespace where discovered Component and ComponentVersion +# resources will be created. +namespace: "" + +# -- Webhook listener configuration +service: + # -- Create a Service for the webhook listener + enabled: true + # -- Service type + type: ClusterIP + # -- Webhook listener port + port: 8080 + +# -- Additional environment variables from Secrets or ConfigMaps. +# Use this to inject registry credentials as environment variables that +# can be referenced in the registries config via ${VAR} syntax. +# +# Example: +# envFrom: +# - secretRef: +# name: registry-credentials +envFrom: [] + +# -- Additional environment variables (name/value pairs) +env: [] + +# -- Number of replicas +replicaCount: 1 + +serviceAccount: + # -- Create a ServiceAccount + create: true + # -- Override the ServiceAccount name + name: "" + # -- Annotations for the ServiceAccount + annotations: {} + +rbac: + # -- Create ClusterRole and ClusterRoleBinding for discovery RBAC + create: true + +# -- Pod security context +podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +# -- Container security context +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + +# -- Resource requests and limits +resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + +# -- Node selector +nodeSelector: {} + +# -- Tolerations +tolerations: [] + +# -- Affinity rules +affinity: {} + +# -- CA certificate bundle configuration. +# Mount a ConfigMap containing a CA bundle for TLS connections to registries. +caBundle: + # -- Enable CA bundle mounting + enabled: false + # -- Name of the ConfigMap containing the CA bundle + configMapName: "" + # -- Key in the ConfigMap (default: trust-bundle.pem) + key: "trust-bundle.pem" diff --git a/charts/solar/files/role.yaml b/charts/solar/files/role.yaml index 1fdb15d3..bd7d9471 100644 --- a/charts/solar/files/role.yaml +++ b/charts/solar/files/role.yaml @@ -7,10 +7,7 @@ rules: - apiGroups: - "" resources: - - pods - secrets - - serviceaccounts - - services verbs: - create - delete @@ -40,25 +37,30 @@ rules: - update - watch - apiGroups: - - rbac.authorization.k8s.io + - solar.opendefense.cloud resources: - - rolebindings - - roles + - componentversions + - profiles + - registries verbs: - - create - - delete - get - list + - watch +- apiGroups: + - solar.opendefense.cloud + resources: + - profiles/status + - releases/status + - rendertasks/status + - targets/status + verbs: + - get - patch - update - - watch - apiGroups: - solar.opendefense.cloud resources: - - bootstraps - - components - - componentversions - - discoveries + - releasebindings - releases - rendertasks - targets @@ -73,30 +75,7 @@ rules: - apiGroups: - solar.opendefense.cloud resources: - - bootstraps/finalizers - - discoveries/finalizers - - releases/finalizers - rendertasks/finalizers - targets/finalizers verbs: - update -- apiGroups: - - solar.opendefense.cloud - resources: - - bootstraps/status - - discoveries/status - - releases/status - - rendertasks/status - - targets/status - verbs: - - get - - patch - - update -- apiGroups: - - solar.opendefense.cloud - resources: - - profiles - verbs: - - get - - list - - watch diff --git a/charts/solar/templates/_helpers.tpl b/charts/solar/templates/_helpers.tpl index f7acc59f..a80fcb17 100644 --- a/charts/solar/templates/_helpers.tpl +++ b/charts/solar/templates/_helpers.tpl @@ -224,14 +224,6 @@ Renderer image {{- printf "%s:%s" .Values.renderer.image.repository $tag }} {{- end }} -{{/* -Discovery image -*/}} -{{- define "solar.discovery.image" -}} -{{- $tag := .Values.discovery.image.tag | default .Chart.AppVersion }} -{{- printf "%s:%s" .Values.discovery.image.repository $tag }} -{{- end }} - {{/* cert-manager Issuer name */}} diff --git a/charts/solar/templates/controller/deployment.yaml b/charts/solar/templates/controller/deployment.yaml index 916b9560..47234aa7 100644 --- a/charts/solar/templates/controller/deployment.yaml +++ b/charts/solar/templates/controller/deployment.yaml @@ -70,21 +70,15 @@ spec: - --pprof-bind-address={{ .Values.controller.args.pprofBindAddress }} {{- end }} - --renderer-image={{ include "solar.renderer.image" . }} - - --renderer-base-url={{ .Values.renderer.baseURL }} {{- if .Values.renderer.caConfigMap }} - --renderer-ca-configmap={{ .Values.renderer.caConfigMap }} {{- end }} - - --namespace={{ include "solar.namespace" . }} {{- with .Values.renderer.command }} - --renderer-command={{ . }} {{- end }} - {{- with .Values.renderer.pushSecretName }} - - --renderer-push-secret-name={{ . }} - {{- end }} {{- with .Values.renderer.extraArgs }} - --renderer-args="{{ . | join "," }}" {{- end }} - - --discovery-worker-image={{ include "solar.discovery.image" . }} {{- range $key, $value := .Values.controller.extraArgs }} - --{{ $key }}={{ $value }} {{- end }} diff --git a/charts/solar/templates/ui/admin-clusterrolebinding.yaml b/charts/solar/templates/ui/admin-clusterrolebinding.yaml new file mode 100644 index 00000000..319ecc05 --- /dev/null +++ b/charts/solar/templates/ui/admin-clusterrolebinding.yaml @@ -0,0 +1,41 @@ +{{- /* +Create the solar-ui:admin ClusterRole (labeled solar.opendefense.cloud/admin=true, +no rules — it is a marker role) and, when ui.admin.subjects is non-empty, a +ClusterRoleBinding that binds those subjects to it. + +The solar-ui BFF lists ClusterRoleBindings with this label at runtime using its own +service-account credentials to determine which logged-in OIDC users have admin access +(i.e. can reach the /auth/impersonation-targets and /auth/impersonate endpoints). + +To grant admin access without Helm, create your own ClusterRoleBinding with + roleRef.name: solar-ui:admin + labels: + solar.opendefense.cloud/admin: "true" +*/ -}} +{{- if .Values.rbac.create }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: solar-ui:admin + labels: + {{- include "solar.labels" . | nindent 4 }} + solar.opendefense.cloud/admin: "true" +rules: [] +{{- if .Values.ui.admin.subjects }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: solar-ui:admin + labels: + {{- include "solar.labels" . | nindent 4 }} + solar.opendefense.cloud/admin: "true" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: solar-ui:admin +subjects: + {{- toYaml .Values.ui.admin.subjects | nindent 2 }} +{{- end }} +{{- end }} diff --git a/charts/solar/templates/ui/impersonation-clusterroles.yaml b/charts/solar/templates/ui/impersonation-clusterroles.yaml new file mode 100644 index 00000000..54842f8e --- /dev/null +++ b/charts/solar/templates/ui/impersonation-clusterroles.yaml @@ -0,0 +1,54 @@ +{{- /* +For each entry in .Values.ui.impersonation.targets, create: + 1. A ClusterRole labeled solar.opendefense.cloud/impersonatable=true with + impersonate rules for the user and their groups. + 2. A ClusterRoleBinding granting that role to the solar-ui ServiceAccount. + +The solar-ui BFF lists these ClusterRoles at runtime to discover the available "preview as" options +*/ -}} +{{- if and .Values.rbac.create .Values.ui.impersonation.targets }} +{{- $saNamespace := tpl .Values.ui.impersonation.serviceAccountNamespace . }} +{{- range .Values.ui.impersonation.targets }} +{{- $hash := sha256sum .username | trunc 6 }} +{{- $name := printf "solar-ui:impersonate:%s-%s" (.username | lower | replace "@" "-" | replace "." "-" | trunc 56 | trimSuffix "-") $hash }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + {{- /* Derive a safe name from the username: lowercase, replace @ and . with - */}} + name: {{ $name }} + labels: + {{- include "solar.labels" $ | nindent 4 }} + solar.opendefense.cloud/impersonatable: "true" +rules: + - apiGroups: [""] + resources: ["users"] + verbs: ["impersonate"] + resourceNames: + - {{ .username | quote }} + {{- if .groups }} + - apiGroups: [""] + resources: ["groups"] + verbs: ["impersonate"] + resourceNames: + {{- range .groups }} + - {{ . | quote }} + {{- end }} + {{- end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ $name }} + labels: + {{- include "solar.labels" $ | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ $name }} +subjects: + - kind: ServiceAccount + name: {{ $.Values.ui.impersonation.serviceAccountName }} + namespace: {{ $saNamespace }} +{{- end }} +{{- end }} diff --git a/charts/solar/templates/ui/rbac.yaml b/charts/solar/templates/ui/rbac.yaml new file mode 100644 index 00000000..818f2940 --- /dev/null +++ b/charts/solar/templates/ui/rbac.yaml @@ -0,0 +1,37 @@ +{{- /* +Grant the solar-ui BFF service account read-only access to ClusterRoles and +ClusterRoleBindings so it can: + - list ClusterRoles labeled solar.opendefense.cloud/impersonatable=true + (used by listImpersonationTargets to discover available "preview as" personas) + - list ClusterRoleBindings labeled solar.opendefense.cloud/admin=true + (used by isAdminUser to decide whether the logged-in OIDC user has admin access) +*/ -}} +{{- if and .Values.rbac.create .Values.ui.impersonation.serviceAccountName }} +{{- $saNamespace := tpl .Values.ui.impersonation.serviceAccountNamespace . }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: solar-ui:read-rbac + labels: + {{- include "solar.labels" . | nindent 4 }} +rules: + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterroles", "clusterrolebindings"] + verbs: ["list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: solar-ui:read-rbac + labels: + {{- include "solar.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: solar-ui:read-rbac +subjects: + - kind: ServiceAccount + name: {{ .Values.ui.impersonation.serviceAccountName }} + namespace: {{ $saNamespace }} +{{- end }} diff --git a/charts/solar/values.yaml b/charts/solar/values.yaml index 41d38383..bef5da30 100644 --- a/charts/solar/values.yaml +++ b/charts/solar/values.yaml @@ -158,28 +158,14 @@ renderer: image: repository: ghcr.io/opendefensecloud/solar-renderer tag: "" - # -- Base URL to push rendered charts to - baseURL: "" # -- ConfigMap name containing CA bundle for registry connections (e.g., trust-manager's root-bundle) caConfigMap: "" - - # Optional renderer parameters: - # -- Name of a secret in the controller's namespace used to authenticate against the registry for push operations. - # Secret must be either of type kubernetes.io/dockerconfigjson or kubernetes.io/basicauth. - # Leaving Name empty will attempt to push without authenticating. - pushSecretName: "" # -- Command to execute in the solar-renderer job command: "" # -- Additional args for the renderer extraArgs: [] # - --plain-http -# Discovery configuration -discovery: - image: - repository: ghcr.io/opendefensecloud/solar-discovery-worker - tag: "" - # Controller Manager configuration controller: # -- Enable Controller Manager deployment @@ -489,3 +475,48 @@ rbac: # - apiGroups: [""] # resources: ["secrets"] # verbs: ["get", "list"] + +# UI / BFF configuration +ui: + # -- Admin users who can access impersonation-management endpoints. + # For each entry in subjects, a ClusterRoleBinding labeled + # solar.opendefense.cloud/admin=true is created. The BFF checks this label + # at runtime using its own service-account credentials to authorise + # GET /auth/impersonation-targets, PUT /auth/impersonate, and + # DELETE /auth/impersonate. The check is immune to active impersonation state. + admin: + # -- Subjects (Kind: User or Group) that are granted solar-ui admin access. + subjects: [] + # - kind: User + # name: admin@solar.local + # apiGroup: rbac.authorization.k8s.io + # - kind: Group + # name: solar-admins + # apiGroup: rbac.authorization.k8s.io + + # -- Impersonation personas available in the admin "preview as" feature. + # For each entry a ClusterRole labeled solar.opendefense.cloud/impersonatable=true + # is created that grants the solar-ui service account the right to impersonate + # the given user and groups. The solar-ui BFF discovers these ClusterRoles at + # runtime, so adding or removing entries takes effect after a Helm upgrade + # without restarting the BFF. + # + # Each entry must have: + # username – the exact OIDC subject / e-mail of the user to impersonate + # groups – the list of OIDC groups to present when impersonating + # + # The serviceAccountName/Namespace fields tell the binding which service account + # to grant these impersonation rights to. + impersonation: + # -- Name of the solar-ui ServiceAccount that receives impersonation rights. + serviceAccountName: solar-ui + # -- Namespace of the solar-ui ServiceAccount. + serviceAccountNamespace: "{{ .Release.Namespace }}" + # -- List of user personas that can be impersonated. + targets: [] + # - username: maintainer@solar.local + # groups: + # - maintainer + # - username: coordinator@solar.local + # groups: + # - coordinator diff --git a/client-go/applyconfigurations/solar/v1alpha1/bootstrapspec.go b/client-go/applyconfigurations/solar/v1alpha1/bootstrapspec.go deleted file mode 100644 index 40105942..00000000 --- a/client-go/applyconfigurations/solar/v1alpha1/bootstrapspec.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by applyconfiguration-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - v1 "k8s.io/api/core/v1" - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// BootstrapSpecApplyConfiguration represents a declarative configuration of the BootstrapSpec type for use -// with apply. -// -// BootstrapSpec defines the desired state of a Bootstrap. -// It contains the concrete releases, profiles, and deployment configuration for a target environment. -type BootstrapSpecApplyConfiguration struct { - // Releases is a map of release names to their corresponding Release object references. - // Each entry represents a component release that will be deployed to the target. - Releases map[string]v1.LocalObjectReference `json:"releases,omitempty"` - // Profiles is a map of profile names to their corresponding Profile object references. - // It points to profiles that match the target, e.g. through the label selector of the Profile - Profiles map[string]v1.LocalObjectReference `json:"profiles,omitempty"` - // Userdata contains arbitrary custom data or configuration for the target deployment. - // This allows providing target-specific parameters or settings. - Userdata *runtime.RawExtension `json:"userdata,omitempty"` -} - -// BootstrapSpecApplyConfiguration constructs a declarative configuration of the BootstrapSpec type for use with -// apply. -func BootstrapSpec() *BootstrapSpecApplyConfiguration { - return &BootstrapSpecApplyConfiguration{} -} - -// WithReleases puts the entries into the Releases field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, the entries provided by each call will be put on the Releases field, -// overwriting an existing map entries in Releases field with the same key. -func (b *BootstrapSpecApplyConfiguration) WithReleases(entries map[string]v1.LocalObjectReference) *BootstrapSpecApplyConfiguration { - if b.Releases == nil && len(entries) > 0 { - b.Releases = make(map[string]v1.LocalObjectReference, len(entries)) - } - for k, v := range entries { - b.Releases[k] = v - } - return b -} - -// WithProfiles puts the entries into the Profiles field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, the entries provided by each call will be put on the Profiles field, -// overwriting an existing map entries in Profiles field with the same key. -func (b *BootstrapSpecApplyConfiguration) WithProfiles(entries map[string]v1.LocalObjectReference) *BootstrapSpecApplyConfiguration { - if b.Profiles == nil && len(entries) > 0 { - b.Profiles = make(map[string]v1.LocalObjectReference, len(entries)) - } - for k, v := range entries { - b.Profiles[k] = v - } - return b -} - -// WithUserdata sets the Userdata field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Userdata field is set to the value of the last call. -func (b *BootstrapSpecApplyConfiguration) WithUserdata(value runtime.RawExtension) *BootstrapSpecApplyConfiguration { - b.Userdata = &value - return b -} diff --git a/client-go/applyconfigurations/solar/v1alpha1/bootstrapstatus.go b/client-go/applyconfigurations/solar/v1alpha1/bootstrapstatus.go deleted file mode 100644 index 374cb516..00000000 --- a/client-go/applyconfigurations/solar/v1alpha1/bootstrapstatus.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by applyconfiguration-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/client-go/applyconfigurations/meta/v1" -) - -// BootstrapStatusApplyConfiguration represents a declarative configuration of the BootstrapStatus type for use -// with apply. -// -// BootstrapStatus defines the observed state of a Bootstrap. -type BootstrapStatusApplyConfiguration struct { - // Conditions represent the latest available observations of a Bootstrap's state. - Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` - // RenderTaskRef is a reference to the RenderTask responsible for this Bootstrap. - RenderTaskRef *corev1.ObjectReference `json:"renderTaskRef,omitempty"` -} - -// BootstrapStatusApplyConfiguration constructs a declarative configuration of the BootstrapStatus type for use with -// apply. -func BootstrapStatus() *BootstrapStatusApplyConfiguration { - return &BootstrapStatusApplyConfiguration{} -} - -// WithConditions adds the given value to the Conditions field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, values provided by each call will be appended to the Conditions field. -func (b *BootstrapStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *BootstrapStatusApplyConfiguration { - for i := range values { - if values[i] == nil { - panic("nil value passed to WithConditions") - } - b.Conditions = append(b.Conditions, *values[i]) - } - return b -} - -// WithRenderTaskRef sets the RenderTaskRef field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the RenderTaskRef field is set to the value of the last call. -func (b *BootstrapStatusApplyConfiguration) WithRenderTaskRef(value corev1.ObjectReference) *BootstrapStatusApplyConfiguration { - b.RenderTaskRef = &value - return b -} diff --git a/client-go/applyconfigurations/solar/v1alpha1/discoveryspec.go b/client-go/applyconfigurations/solar/v1alpha1/discoveryspec.go deleted file mode 100644 index 0e2744c9..00000000 --- a/client-go/applyconfigurations/solar/v1alpha1/discoveryspec.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by applyconfiguration-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// DiscoverySpecApplyConfiguration represents a declarative configuration of the DiscoverySpec type for use -// with apply. -// -// DiscoverySpec defines the desired state of a Discovery. -type DiscoverySpecApplyConfiguration struct { - // Registry specifies the registry that should be scanned by the discovery process. - Registry *RegistryApplyConfiguration `json:"registry,omitempty"` - // Webhook specifies the configuration for a webhook that is called by the registry on created, updated or deleted images/repositories. - Webhook *WebhookApplyConfiguration `json:"webhook,omitempty"` - // Filter specifies the filter that should be applied when scanning for components. If not specified, all components will be scanned. - Filter *FilterApplyConfiguration `json:"filter,omitempty"` - // DiscoveryInterval is the amount of time between two full scans of the registry. - // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h" - // May be set to zero to fetch and create it once. Defaults to 24h. - DiscoveryInterval *v1.Duration `json:"discoveryInterval,omitempty"` - // DisableStartupDiscovery defines whether the discovery should not be run on startup of the discovery process. If true it will only run on schedule, see .spec.cron. - DisableStartupDiscovery *bool `json:"disableStartupDiscovery,omitempty"` -} - -// DiscoverySpecApplyConfiguration constructs a declarative configuration of the DiscoverySpec type for use with -// apply. -func DiscoverySpec() *DiscoverySpecApplyConfiguration { - return &DiscoverySpecApplyConfiguration{} -} - -// WithRegistry sets the Registry field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Registry field is set to the value of the last call. -func (b *DiscoverySpecApplyConfiguration) WithRegistry(value *RegistryApplyConfiguration) *DiscoverySpecApplyConfiguration { - b.Registry = value - return b -} - -// WithWebhook sets the Webhook field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Webhook field is set to the value of the last call. -func (b *DiscoverySpecApplyConfiguration) WithWebhook(value *WebhookApplyConfiguration) *DiscoverySpecApplyConfiguration { - b.Webhook = value - return b -} - -// WithFilter sets the Filter field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Filter field is set to the value of the last call. -func (b *DiscoverySpecApplyConfiguration) WithFilter(value *FilterApplyConfiguration) *DiscoverySpecApplyConfiguration { - b.Filter = value - return b -} - -// WithDiscoveryInterval sets the DiscoveryInterval field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the DiscoveryInterval field is set to the value of the last call. -func (b *DiscoverySpecApplyConfiguration) WithDiscoveryInterval(value v1.Duration) *DiscoverySpecApplyConfiguration { - b.DiscoveryInterval = &value - return b -} - -// WithDisableStartupDiscovery sets the DisableStartupDiscovery field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the DisableStartupDiscovery field is set to the value of the last call. -func (b *DiscoverySpecApplyConfiguration) WithDisableStartupDiscovery(value bool) *DiscoverySpecApplyConfiguration { - b.DisableStartupDiscovery = &value - return b -} diff --git a/client-go/applyconfigurations/solar/v1alpha1/discoverystatus.go b/client-go/applyconfigurations/solar/v1alpha1/discoverystatus.go deleted file mode 100644 index eabc82ec..00000000 --- a/client-go/applyconfigurations/solar/v1alpha1/discoverystatus.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by applyconfiguration-gen. DO NOT EDIT. - -package v1alpha1 - -// DiscoveryStatusApplyConfiguration represents a declarative configuration of the DiscoveryStatus type for use -// with apply. -// -// DiscoveryStatus defines the observed state of a Discovery. -type DiscoveryStatusApplyConfiguration struct { - // PodGeneration is the generation of the discovery object at the time the worker was instantiated. - PodGeneration *int64 `json:"podGeneration,omitempty"` -} - -// DiscoveryStatusApplyConfiguration constructs a declarative configuration of the DiscoveryStatus type for use with -// apply. -func DiscoveryStatus() *DiscoveryStatusApplyConfiguration { - return &DiscoveryStatusApplyConfiguration{} -} - -// WithPodGeneration sets the PodGeneration field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the PodGeneration field is set to the value of the last call. -func (b *DiscoveryStatusApplyConfiguration) WithPodGeneration(value int64) *DiscoveryStatusApplyConfiguration { - b.PodGeneration = &value - return b -} diff --git a/client-go/applyconfigurations/solar/v1alpha1/filter.go b/client-go/applyconfigurations/solar/v1alpha1/filter.go deleted file mode 100644 index dae287bc..00000000 --- a/client-go/applyconfigurations/solar/v1alpha1/filter.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by applyconfiguration-gen. DO NOT EDIT. - -package v1alpha1 - -// FilterApplyConfiguration represents a declarative configuration of the Filter type for use -// with apply. -// -// Filter defines the filter criteria used to determine which components should be scanned. -type FilterApplyConfiguration struct { - // RepositoryPatterns defines which repositories should be scanned for components. The default value is empty, which means that all repositories will be scanned. - // Wildcards are supported, e.g. "foo-*" or "*-dev". - RepositoryPatterns []string `json:"repositoryPatterns,omitempty"` -} - -// FilterApplyConfiguration constructs a declarative configuration of the Filter type for use with -// apply. -func Filter() *FilterApplyConfiguration { - return &FilterApplyConfiguration{} -} - -// WithRepositoryPatterns adds the given value to the RepositoryPatterns field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, values provided by each call will be appended to the RepositoryPatterns field. -func (b *FilterApplyConfiguration) WithRepositoryPatterns(values ...string) *FilterApplyConfiguration { - for i := range values { - b.RepositoryPatterns = append(b.RepositoryPatterns, values[i]) - } - return b -} diff --git a/client-go/applyconfigurations/solar/v1alpha1/registry.go b/client-go/applyconfigurations/solar/v1alpha1/registry.go index d79ac814..44efba08 100644 --- a/client-go/applyconfigurations/solar/v1alpha1/registry.go +++ b/client-go/applyconfigurations/solar/v1alpha1/registry.go @@ -6,59 +6,227 @@ package v1alpha1 import ( - v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" ) // RegistryApplyConfiguration represents a declarative configuration of the Registry type for use // with apply. // -// Registry defines the configuration for a registry. +// Registry represents an OCI registry that can be used as a source or destination for artifacts. type RegistryApplyConfiguration struct { - // Endpoint is the hostname (and optionally port) of the registry, e.g. "registry.example.com" or "registry.example.com:443". - // This must not include a scheme (use PlainHTTP to control HTTP vs HTTPS). - Endpoint *string `json:"endpoint,omitempty"` - // SecretRef specifies the secret containing the relevant credentials for the registry that should be used during discovery. - SecretRef *v1.LocalObjectReference `json:"secretRef,omitempty"` - // CAConfigMapRef contains CA bundle for registry connections (e.g., trust-manager's root-bundle). Key is expected to be "trust-bundle.pem". - CAConfigMapRef *v1.LocalObjectReference `json:"caConfigMapRef,omitempty"` - // PlainHTTP defines whether the registry should be accessed via plain HTTP instead of HTTPS. - PlainHTTP *bool `json:"plainHTTP,omitempty"` -} - -// RegistryApplyConfiguration constructs a declarative configuration of the Registry type for use with + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *RegistrySpecApplyConfiguration `json:"spec,omitempty"` + Status *RegistryStatusApplyConfiguration `json:"status,omitempty"` +} + +// Registry constructs a declarative configuration of the Registry type for use with // apply. -func Registry() *RegistryApplyConfiguration { - return &RegistryApplyConfiguration{} +func Registry(name, namespace string) *RegistryApplyConfiguration { + b := &RegistryApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("Registry") + b.WithAPIVersion("solar.opendefense.cloud/v1alpha1") + return b +} + +func (b RegistryApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithKind(value string) *RegistryApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithAPIVersion(value string) *RegistryApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithName(value string) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithGenerateName(value string) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithNamespace(value string) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithUID(value types.UID) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithResourceVersion(value string) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b } -// WithEndpoint sets the Endpoint field in the declarative configuration to the given value +// WithGeneration sets the Generation field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Endpoint field is set to the value of the last call. -func (b *RegistryApplyConfiguration) WithEndpoint(value string) *RegistryApplyConfiguration { - b.Endpoint = &value +// If called multiple times, the Generation field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithGeneration(value int64) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value return b } -// WithSecretRef sets the SecretRef field in the declarative configuration to the given value +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the SecretRef field is set to the value of the last call. -func (b *RegistryApplyConfiguration) WithSecretRef(value v1.LocalObjectReference) *RegistryApplyConfiguration { - b.SecretRef = &value +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithCreationTimestamp(value metav1.Time) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value return b } -// WithCAConfigMapRef sets the CAConfigMapRef field in the declarative configuration to the given value +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the CAConfigMapRef field is set to the value of the last call. -func (b *RegistryApplyConfiguration) WithCAConfigMapRef(value v1.LocalObjectReference) *RegistryApplyConfiguration { - b.CAConfigMapRef = &value +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value return b } -// WithPlainHTTP sets the PlainHTTP field in the declarative configuration to the given value +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the PlainHTTP field is set to the value of the last call. -func (b *RegistryApplyConfiguration) WithPlainHTTP(value bool) *RegistryApplyConfiguration { - b.PlainHTTP = &value +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *RegistryApplyConfiguration) WithLabels(entries map[string]string) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *RegistryApplyConfiguration) WithAnnotations(entries map[string]string) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *RegistryApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *RegistryApplyConfiguration) WithFinalizers(values ...string) *RegistryApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } return b } + +func (b *RegistryApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithSpec(value *RegistrySpecApplyConfiguration) *RegistryApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *RegistryApplyConfiguration) WithStatus(value *RegistryStatusApplyConfiguration) *RegistryApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *RegistryApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *RegistryApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *RegistryApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *RegistryApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/client-go/applyconfigurations/solar/v1alpha1/discovery.go b/client-go/applyconfigurations/solar/v1alpha1/registrybinding.go similarity index 73% rename from client-go/applyconfigurations/solar/v1alpha1/discovery.go rename to client-go/applyconfigurations/solar/v1alpha1/registrybinding.go index 0989309a..239a025e 100644 --- a/client-go/applyconfigurations/solar/v1alpha1/discovery.go +++ b/client-go/applyconfigurations/solar/v1alpha1/registrybinding.go @@ -11,34 +11,34 @@ import ( v1 "k8s.io/client-go/applyconfigurations/meta/v1" ) -// DiscoveryApplyConfiguration represents a declarative configuration of the Discovery type for use +// RegistryBindingApplyConfiguration represents a declarative configuration of the RegistryBinding type for use // with apply. // -// Discovery represents a configuration for a registry to discover. -type DiscoveryApplyConfiguration struct { +// RegistryBinding declares that a specific Target is allowed to use a specific Registry. +type RegistryBindingApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` - Spec *DiscoverySpecApplyConfiguration `json:"spec,omitempty"` - Status *DiscoveryStatusApplyConfiguration `json:"status,omitempty"` + Spec *RegistryBindingSpecApplyConfiguration `json:"spec,omitempty"` + Status *RegistryBindingStatusApplyConfiguration `json:"status,omitempty"` } -// Discovery constructs a declarative configuration of the Discovery type for use with +// RegistryBinding constructs a declarative configuration of the RegistryBinding type for use with // apply. -func Discovery(name, namespace string) *DiscoveryApplyConfiguration { - b := &DiscoveryApplyConfiguration{} +func RegistryBinding(name, namespace string) *RegistryBindingApplyConfiguration { + b := &RegistryBindingApplyConfiguration{} b.WithName(name) b.WithNamespace(namespace) - b.WithKind("Discovery") + b.WithKind("RegistryBinding") b.WithAPIVersion("solar.opendefense.cloud/v1alpha1") return b } -func (b DiscoveryApplyConfiguration) IsApplyConfiguration() {} +func (b RegistryBindingApplyConfiguration) IsApplyConfiguration() {} // WithKind sets the Kind field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Kind field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithKind(value string) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithKind(value string) *RegistryBindingApplyConfiguration { b.TypeMetaApplyConfiguration.Kind = &value return b } @@ -46,7 +46,7 @@ func (b *DiscoveryApplyConfiguration) WithKind(value string) *DiscoveryApplyConf // WithAPIVersion sets the APIVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the APIVersion field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithAPIVersion(value string) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithAPIVersion(value string) *RegistryBindingApplyConfiguration { b.TypeMetaApplyConfiguration.APIVersion = &value return b } @@ -54,7 +54,7 @@ func (b *DiscoveryApplyConfiguration) WithAPIVersion(value string) *DiscoveryApp // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithName(value string) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithName(value string) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Name = &value return b @@ -63,7 +63,7 @@ func (b *DiscoveryApplyConfiguration) WithName(value string) *DiscoveryApplyConf // WithGenerateName sets the GenerateName field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the GenerateName field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithGenerateName(value string) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithGenerateName(value string) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.GenerateName = &value return b @@ -72,7 +72,7 @@ func (b *DiscoveryApplyConfiguration) WithGenerateName(value string) *DiscoveryA // WithNamespace sets the Namespace field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Namespace field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithNamespace(value string) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithNamespace(value string) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Namespace = &value return b @@ -81,7 +81,7 @@ func (b *DiscoveryApplyConfiguration) WithNamespace(value string) *DiscoveryAppl // WithUID sets the UID field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the UID field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithUID(value types.UID) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithUID(value types.UID) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.UID = &value return b @@ -90,7 +90,7 @@ func (b *DiscoveryApplyConfiguration) WithUID(value types.UID) *DiscoveryApplyCo // WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ResourceVersion field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithResourceVersion(value string) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithResourceVersion(value string) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.ResourceVersion = &value return b @@ -99,7 +99,7 @@ func (b *DiscoveryApplyConfiguration) WithResourceVersion(value string) *Discove // WithGeneration sets the Generation field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Generation field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithGeneration(value int64) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithGeneration(value int64) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Generation = &value return b @@ -108,7 +108,7 @@ func (b *DiscoveryApplyConfiguration) WithGeneration(value int64) *DiscoveryAppl // WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the CreationTimestamp field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithCreationTimestamp(value metav1.Time) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithCreationTimestamp(value metav1.Time) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.CreationTimestamp = &value return b @@ -117,7 +117,7 @@ func (b *DiscoveryApplyConfiguration) WithCreationTimestamp(value metav1.Time) * // WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionTimestamp field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value return b @@ -126,7 +126,7 @@ func (b *DiscoveryApplyConfiguration) WithDeletionTimestamp(value metav1.Time) * // WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value return b @@ -136,7 +136,7 @@ func (b *DiscoveryApplyConfiguration) WithDeletionGracePeriodSeconds(value int64 // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Labels field, // overwriting an existing map entries in Labels field with the same key. -func (b *DiscoveryApplyConfiguration) WithLabels(entries map[string]string) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithLabels(entries map[string]string) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) @@ -151,7 +151,7 @@ func (b *DiscoveryApplyConfiguration) WithLabels(entries map[string]string) *Dis // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Annotations field, // overwriting an existing map entries in Annotations field with the same key. -func (b *DiscoveryApplyConfiguration) WithAnnotations(entries map[string]string) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithAnnotations(entries map[string]string) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) @@ -165,7 +165,7 @@ func (b *DiscoveryApplyConfiguration) WithAnnotations(entries map[string]string) // WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the OwnerReferences field. -func (b *DiscoveryApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { if values[i] == nil { @@ -179,7 +179,7 @@ func (b *DiscoveryApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerRef // WithFinalizers adds the given value to the Finalizers field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Finalizers field. -func (b *DiscoveryApplyConfiguration) WithFinalizers(values ...string) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithFinalizers(values ...string) *RegistryBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) @@ -187,7 +187,7 @@ func (b *DiscoveryApplyConfiguration) WithFinalizers(values ...string) *Discover return b } -func (b *DiscoveryApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { +func (b *RegistryBindingApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { if b.ObjectMetaApplyConfiguration == nil { b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} } @@ -196,7 +196,7 @@ func (b *DiscoveryApplyConfiguration) ensureObjectMetaApplyConfigurationExists() // WithSpec sets the Spec field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Spec field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithSpec(value *DiscoverySpecApplyConfiguration) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithSpec(value *RegistryBindingSpecApplyConfiguration) *RegistryBindingApplyConfiguration { b.Spec = value return b } @@ -204,29 +204,29 @@ func (b *DiscoveryApplyConfiguration) WithSpec(value *DiscoverySpecApplyConfigur // WithStatus sets the Status field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Status field is set to the value of the last call. -func (b *DiscoveryApplyConfiguration) WithStatus(value *DiscoveryStatusApplyConfiguration) *DiscoveryApplyConfiguration { +func (b *RegistryBindingApplyConfiguration) WithStatus(value *RegistryBindingStatusApplyConfiguration) *RegistryBindingApplyConfiguration { b.Status = value return b } // GetKind retrieves the value of the Kind field in the declarative configuration. -func (b *DiscoveryApplyConfiguration) GetKind() *string { +func (b *RegistryBindingApplyConfiguration) GetKind() *string { return b.TypeMetaApplyConfiguration.Kind } // GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. -func (b *DiscoveryApplyConfiguration) GetAPIVersion() *string { +func (b *RegistryBindingApplyConfiguration) GetAPIVersion() *string { return b.TypeMetaApplyConfiguration.APIVersion } // GetName retrieves the value of the Name field in the declarative configuration. -func (b *DiscoveryApplyConfiguration) GetName() *string { +func (b *RegistryBindingApplyConfiguration) GetName() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Name } // GetNamespace retrieves the value of the Namespace field in the declarative configuration. -func (b *DiscoveryApplyConfiguration) GetNamespace() *string { +func (b *RegistryBindingApplyConfiguration) GetNamespace() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Namespace } diff --git a/client-go/applyconfigurations/solar/v1alpha1/registrybindingspec.go b/client-go/applyconfigurations/solar/v1alpha1/registrybindingspec.go new file mode 100644 index 00000000..b75bf528 --- /dev/null +++ b/client-go/applyconfigurations/solar/v1alpha1/registrybindingspec.go @@ -0,0 +1,43 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" +) + +// RegistryBindingSpecApplyConfiguration represents a declarative configuration of the RegistryBindingSpec type for use +// with apply. +// +// RegistryBindingSpec defines the desired state of a RegistryBinding. +type RegistryBindingSpecApplyConfiguration struct { + // TargetRef references the Target this binding applies to. + TargetRef *v1.LocalObjectReference `json:"targetRef,omitempty"` + // RegistryRef references the Registry being bound. + RegistryRef *v1.LocalObjectReference `json:"registryRef,omitempty"` +} + +// RegistryBindingSpecApplyConfiguration constructs a declarative configuration of the RegistryBindingSpec type for use with +// apply. +func RegistryBindingSpec() *RegistryBindingSpecApplyConfiguration { + return &RegistryBindingSpecApplyConfiguration{} +} + +// WithTargetRef sets the TargetRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TargetRef field is set to the value of the last call. +func (b *RegistryBindingSpecApplyConfiguration) WithTargetRef(value v1.LocalObjectReference) *RegistryBindingSpecApplyConfiguration { + b.TargetRef = &value + return b +} + +// WithRegistryRef sets the RegistryRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RegistryRef field is set to the value of the last call. +func (b *RegistryBindingSpecApplyConfiguration) WithRegistryRef(value v1.LocalObjectReference) *RegistryBindingSpecApplyConfiguration { + b.RegistryRef = &value + return b +} diff --git a/client-go/applyconfigurations/solar/v1alpha1/registrybindingstatus.go b/client-go/applyconfigurations/solar/v1alpha1/registrybindingstatus.go new file mode 100644 index 00000000..f649d592 --- /dev/null +++ b/client-go/applyconfigurations/solar/v1alpha1/registrybindingstatus.go @@ -0,0 +1,38 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// RegistryBindingStatusApplyConfiguration represents a declarative configuration of the RegistryBindingStatus type for use +// with apply. +// +// RegistryBindingStatus defines the observed state of a RegistryBinding. +type RegistryBindingStatusApplyConfiguration struct { + // Conditions represent the latest available observations of a RegistryBinding's state. + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` +} + +// RegistryBindingStatusApplyConfiguration constructs a declarative configuration of the RegistryBindingStatus type for use with +// apply. +func RegistryBindingStatus() *RegistryBindingStatusApplyConfiguration { + return &RegistryBindingStatusApplyConfiguration{} +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *RegistryBindingStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *RegistryBindingStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} diff --git a/client-go/applyconfigurations/solar/v1alpha1/registryspec.go b/client-go/applyconfigurations/solar/v1alpha1/registryspec.go new file mode 100644 index 00000000..a2224a3b --- /dev/null +++ b/client-go/applyconfigurations/solar/v1alpha1/registryspec.go @@ -0,0 +1,66 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" +) + +// RegistrySpecApplyConfiguration represents a declarative configuration of the RegistrySpec type for use +// with apply. +// +// RegistrySpec defines the desired state of a Registry. +type RegistrySpecApplyConfiguration struct { + // Hostname is the registry endpoint (e.g. "registry.example.com:5000"). + Hostname *string `json:"hostname,omitempty"` + // PlainHTTP uses HTTP instead of HTTPS for connections to this registry. + PlainHTTP *bool `json:"plainHTTP,omitempty"` + // SolarSecretRef references a Secret in the same namespace with credentials + // to access this registry from the SolAr cluster. Required if this registry + // is used as a render target. + SolarSecretRef *v1.LocalObjectReference `json:"solarSecretRef,omitempty"` + // TargetSecretRef describes where the credentials secret lives in the target cluster. + // Used by the target agent for pull access. + TargetSecretRef *TargetSecretReferenceApplyConfiguration `json:"targetSecretRef,omitempty"` +} + +// RegistrySpecApplyConfiguration constructs a declarative configuration of the RegistrySpec type for use with +// apply. +func RegistrySpec() *RegistrySpecApplyConfiguration { + return &RegistrySpecApplyConfiguration{} +} + +// WithHostname sets the Hostname field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Hostname field is set to the value of the last call. +func (b *RegistrySpecApplyConfiguration) WithHostname(value string) *RegistrySpecApplyConfiguration { + b.Hostname = &value + return b +} + +// WithPlainHTTP sets the PlainHTTP field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PlainHTTP field is set to the value of the last call. +func (b *RegistrySpecApplyConfiguration) WithPlainHTTP(value bool) *RegistrySpecApplyConfiguration { + b.PlainHTTP = &value + return b +} + +// WithSolarSecretRef sets the SolarSecretRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SolarSecretRef field is set to the value of the last call. +func (b *RegistrySpecApplyConfiguration) WithSolarSecretRef(value v1.LocalObjectReference) *RegistrySpecApplyConfiguration { + b.SolarSecretRef = &value + return b +} + +// WithTargetSecretRef sets the TargetSecretRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TargetSecretRef field is set to the value of the last call. +func (b *RegistrySpecApplyConfiguration) WithTargetSecretRef(value *TargetSecretReferenceApplyConfiguration) *RegistrySpecApplyConfiguration { + b.TargetSecretRef = value + return b +} diff --git a/client-go/applyconfigurations/solar/v1alpha1/registrystatus.go b/client-go/applyconfigurations/solar/v1alpha1/registrystatus.go new file mode 100644 index 00000000..f87436fb --- /dev/null +++ b/client-go/applyconfigurations/solar/v1alpha1/registrystatus.go @@ -0,0 +1,38 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// RegistryStatusApplyConfiguration represents a declarative configuration of the RegistryStatus type for use +// with apply. +// +// RegistryStatus defines the observed state of a Registry. +type RegistryStatusApplyConfiguration struct { + // Conditions represent the latest available observations of a Registry's state. + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` +} + +// RegistryStatusApplyConfiguration constructs a declarative configuration of the RegistryStatus type for use with +// apply. +func RegistryStatus() *RegistryStatusApplyConfiguration { + return &RegistryStatusApplyConfiguration{} +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *RegistryStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *RegistryStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} diff --git a/client-go/applyconfigurations/solar/v1alpha1/bootstrap.go b/client-go/applyconfigurations/solar/v1alpha1/releasebinding.go similarity index 73% rename from client-go/applyconfigurations/solar/v1alpha1/bootstrap.go rename to client-go/applyconfigurations/solar/v1alpha1/releasebinding.go index bd47fba6..3a83419e 100644 --- a/client-go/applyconfigurations/solar/v1alpha1/bootstrap.go +++ b/client-go/applyconfigurations/solar/v1alpha1/releasebinding.go @@ -11,35 +11,34 @@ import ( v1 "k8s.io/client-go/applyconfigurations/meta/v1" ) -// BootstrapApplyConfiguration represents a declarative configuration of the Bootstrap type for use +// ReleaseBindingApplyConfiguration represents a declarative configuration of the ReleaseBinding type for use // with apply. // -// Bootstrap represents the entrypoint for the gitless gitops configuration. -// It resolves the implicit matching of profiles to produce a concrete set of releases and profiles. -type BootstrapApplyConfiguration struct { +// ReleaseBinding declares that a Release should be deployed to a Target. +type ReleaseBindingApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` - Spec *BootstrapSpecApplyConfiguration `json:"spec,omitempty"` - Status *BootstrapStatusApplyConfiguration `json:"status,omitempty"` + Spec *ReleaseBindingSpecApplyConfiguration `json:"spec,omitempty"` + Status *ReleaseBindingStatusApplyConfiguration `json:"status,omitempty"` } -// Bootstrap constructs a declarative configuration of the Bootstrap type for use with +// ReleaseBinding constructs a declarative configuration of the ReleaseBinding type for use with // apply. -func Bootstrap(name, namespace string) *BootstrapApplyConfiguration { - b := &BootstrapApplyConfiguration{} +func ReleaseBinding(name, namespace string) *ReleaseBindingApplyConfiguration { + b := &ReleaseBindingApplyConfiguration{} b.WithName(name) b.WithNamespace(namespace) - b.WithKind("Bootstrap") + b.WithKind("ReleaseBinding") b.WithAPIVersion("solar.opendefense.cloud/v1alpha1") return b } -func (b BootstrapApplyConfiguration) IsApplyConfiguration() {} +func (b ReleaseBindingApplyConfiguration) IsApplyConfiguration() {} // WithKind sets the Kind field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Kind field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithKind(value string) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithKind(value string) *ReleaseBindingApplyConfiguration { b.TypeMetaApplyConfiguration.Kind = &value return b } @@ -47,7 +46,7 @@ func (b *BootstrapApplyConfiguration) WithKind(value string) *BootstrapApplyConf // WithAPIVersion sets the APIVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the APIVersion field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithAPIVersion(value string) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithAPIVersion(value string) *ReleaseBindingApplyConfiguration { b.TypeMetaApplyConfiguration.APIVersion = &value return b } @@ -55,7 +54,7 @@ func (b *BootstrapApplyConfiguration) WithAPIVersion(value string) *BootstrapApp // WithName sets the Name field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Name field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithName(value string) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithName(value string) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Name = &value return b @@ -64,7 +63,7 @@ func (b *BootstrapApplyConfiguration) WithName(value string) *BootstrapApplyConf // WithGenerateName sets the GenerateName field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the GenerateName field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithGenerateName(value string) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithGenerateName(value string) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.GenerateName = &value return b @@ -73,7 +72,7 @@ func (b *BootstrapApplyConfiguration) WithGenerateName(value string) *BootstrapA // WithNamespace sets the Namespace field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Namespace field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithNamespace(value string) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithNamespace(value string) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Namespace = &value return b @@ -82,7 +81,7 @@ func (b *BootstrapApplyConfiguration) WithNamespace(value string) *BootstrapAppl // WithUID sets the UID field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the UID field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithUID(value types.UID) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithUID(value types.UID) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.UID = &value return b @@ -91,7 +90,7 @@ func (b *BootstrapApplyConfiguration) WithUID(value types.UID) *BootstrapApplyCo // WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the ResourceVersion field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithResourceVersion(value string) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithResourceVersion(value string) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.ResourceVersion = &value return b @@ -100,7 +99,7 @@ func (b *BootstrapApplyConfiguration) WithResourceVersion(value string) *Bootstr // WithGeneration sets the Generation field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Generation field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithGeneration(value int64) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithGeneration(value int64) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.Generation = &value return b @@ -109,7 +108,7 @@ func (b *BootstrapApplyConfiguration) WithGeneration(value int64) *BootstrapAppl // WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the CreationTimestamp field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithCreationTimestamp(value metav1.Time) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithCreationTimestamp(value metav1.Time) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.CreationTimestamp = &value return b @@ -118,7 +117,7 @@ func (b *BootstrapApplyConfiguration) WithCreationTimestamp(value metav1.Time) * // WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionTimestamp field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value return b @@ -127,7 +126,7 @@ func (b *BootstrapApplyConfiguration) WithDeletionTimestamp(value metav1.Time) * // WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value return b @@ -137,7 +136,7 @@ func (b *BootstrapApplyConfiguration) WithDeletionGracePeriodSeconds(value int64 // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Labels field, // overwriting an existing map entries in Labels field with the same key. -func (b *BootstrapApplyConfiguration) WithLabels(entries map[string]string) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithLabels(entries map[string]string) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) @@ -152,7 +151,7 @@ func (b *BootstrapApplyConfiguration) WithLabels(entries map[string]string) *Boo // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Annotations field, // overwriting an existing map entries in Annotations field with the same key. -func (b *BootstrapApplyConfiguration) WithAnnotations(entries map[string]string) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithAnnotations(entries map[string]string) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) @@ -166,7 +165,7 @@ func (b *BootstrapApplyConfiguration) WithAnnotations(entries map[string]string) // WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the OwnerReferences field. -func (b *BootstrapApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { if values[i] == nil { @@ -180,7 +179,7 @@ func (b *BootstrapApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerRef // WithFinalizers adds the given value to the Finalizers field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Finalizers field. -func (b *BootstrapApplyConfiguration) WithFinalizers(values ...string) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithFinalizers(values ...string) *ReleaseBindingApplyConfiguration { b.ensureObjectMetaApplyConfigurationExists() for i := range values { b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) @@ -188,7 +187,7 @@ func (b *BootstrapApplyConfiguration) WithFinalizers(values ...string) *Bootstra return b } -func (b *BootstrapApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { +func (b *ReleaseBindingApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { if b.ObjectMetaApplyConfiguration == nil { b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} } @@ -197,7 +196,7 @@ func (b *BootstrapApplyConfiguration) ensureObjectMetaApplyConfigurationExists() // WithSpec sets the Spec field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Spec field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithSpec(value *BootstrapSpecApplyConfiguration) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithSpec(value *ReleaseBindingSpecApplyConfiguration) *ReleaseBindingApplyConfiguration { b.Spec = value return b } @@ -205,29 +204,29 @@ func (b *BootstrapApplyConfiguration) WithSpec(value *BootstrapSpecApplyConfigur // WithStatus sets the Status field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Status field is set to the value of the last call. -func (b *BootstrapApplyConfiguration) WithStatus(value *BootstrapStatusApplyConfiguration) *BootstrapApplyConfiguration { +func (b *ReleaseBindingApplyConfiguration) WithStatus(value *ReleaseBindingStatusApplyConfiguration) *ReleaseBindingApplyConfiguration { b.Status = value return b } // GetKind retrieves the value of the Kind field in the declarative configuration. -func (b *BootstrapApplyConfiguration) GetKind() *string { +func (b *ReleaseBindingApplyConfiguration) GetKind() *string { return b.TypeMetaApplyConfiguration.Kind } // GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. -func (b *BootstrapApplyConfiguration) GetAPIVersion() *string { +func (b *ReleaseBindingApplyConfiguration) GetAPIVersion() *string { return b.TypeMetaApplyConfiguration.APIVersion } // GetName retrieves the value of the Name field in the declarative configuration. -func (b *BootstrapApplyConfiguration) GetName() *string { +func (b *ReleaseBindingApplyConfiguration) GetName() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Name } // GetNamespace retrieves the value of the Namespace field in the declarative configuration. -func (b *BootstrapApplyConfiguration) GetNamespace() *string { +func (b *ReleaseBindingApplyConfiguration) GetNamespace() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Namespace } diff --git a/client-go/applyconfigurations/solar/v1alpha1/releasebindingspec.go b/client-go/applyconfigurations/solar/v1alpha1/releasebindingspec.go new file mode 100644 index 00000000..b540943a --- /dev/null +++ b/client-go/applyconfigurations/solar/v1alpha1/releasebindingspec.go @@ -0,0 +1,43 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" +) + +// ReleaseBindingSpecApplyConfiguration represents a declarative configuration of the ReleaseBindingSpec type for use +// with apply. +// +// ReleaseBindingSpec defines the desired state of a ReleaseBinding. +type ReleaseBindingSpecApplyConfiguration struct { + // TargetRef references the Target this release is bound to. + TargetRef *v1.LocalObjectReference `json:"targetRef,omitempty"` + // ReleaseRef references the Release to deploy. + ReleaseRef *v1.LocalObjectReference `json:"releaseRef,omitempty"` +} + +// ReleaseBindingSpecApplyConfiguration constructs a declarative configuration of the ReleaseBindingSpec type for use with +// apply. +func ReleaseBindingSpec() *ReleaseBindingSpecApplyConfiguration { + return &ReleaseBindingSpecApplyConfiguration{} +} + +// WithTargetRef sets the TargetRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TargetRef field is set to the value of the last call. +func (b *ReleaseBindingSpecApplyConfiguration) WithTargetRef(value v1.LocalObjectReference) *ReleaseBindingSpecApplyConfiguration { + b.TargetRef = &value + return b +} + +// WithReleaseRef sets the ReleaseRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ReleaseRef field is set to the value of the last call. +func (b *ReleaseBindingSpecApplyConfiguration) WithReleaseRef(value v1.LocalObjectReference) *ReleaseBindingSpecApplyConfiguration { + b.ReleaseRef = &value + return b +} diff --git a/client-go/applyconfigurations/solar/v1alpha1/releasebindingstatus.go b/client-go/applyconfigurations/solar/v1alpha1/releasebindingstatus.go new file mode 100644 index 00000000..2c472b69 --- /dev/null +++ b/client-go/applyconfigurations/solar/v1alpha1/releasebindingstatus.go @@ -0,0 +1,38 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// ReleaseBindingStatusApplyConfiguration represents a declarative configuration of the ReleaseBindingStatus type for use +// with apply. +// +// ReleaseBindingStatus defines the observed state of a ReleaseBinding. +type ReleaseBindingStatusApplyConfiguration struct { + // Conditions represent the latest available observations of a ReleaseBinding's state. + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` +} + +// ReleaseBindingStatusApplyConfiguration constructs a declarative configuration of the ReleaseBindingStatus type for use with +// apply. +func ReleaseBindingStatus() *ReleaseBindingStatusApplyConfiguration { + return &ReleaseBindingStatusApplyConfiguration{} +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *ReleaseBindingStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *ReleaseBindingStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} diff --git a/client-go/applyconfigurations/solar/v1alpha1/rendertask.go b/client-go/applyconfigurations/solar/v1alpha1/rendertask.go index b59f93e0..1e3ddebf 100644 --- a/client-go/applyconfigurations/solar/v1alpha1/rendertask.go +++ b/client-go/applyconfigurations/solar/v1alpha1/rendertask.go @@ -24,9 +24,10 @@ type RenderTaskApplyConfiguration struct { // RenderTask constructs a declarative configuration of the RenderTask type for use with // apply. -func RenderTask(name string) *RenderTaskApplyConfiguration { +func RenderTask(name, namespace string) *RenderTaskApplyConfiguration { b := &RenderTaskApplyConfiguration{} b.WithName(name) + b.WithNamespace(namespace) b.WithKind("RenderTask") b.WithAPIVersion("solar.opendefense.cloud/v1alpha1") return b diff --git a/client-go/applyconfigurations/solar/v1alpha1/rendertaskspec.go b/client-go/applyconfigurations/solar/v1alpha1/rendertaskspec.go index 298921c3..498045fe 100644 --- a/client-go/applyconfigurations/solar/v1alpha1/rendertaskspec.go +++ b/client-go/applyconfigurations/solar/v1alpha1/rendertaskspec.go @@ -7,6 +7,7 @@ package v1alpha1 import ( solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + v1 "k8s.io/api/core/v1" ) // RenderTaskSpecApplyConfiguration represents a declarative configuration of the RenderTaskSpec type for use @@ -17,13 +18,16 @@ type RenderTaskSpecApplyConfiguration struct { // RendererConfig is the config used for the renderer job RendererConfigApplyConfiguration `json:",inline"` // Repository is the Repository where the chart will be pushed to (e.g. charts/mychart) - // Keep in mind that the repository gets automatically prefixed with the - // registry by the rendertask-controller. Repository *string `json:"repository,omitempty"` // Tag is the Tag of the helm chart to be pushed. // Make sure that the tag matches the version in Chart.yaml, otherwise helm // will error before pushing. Tag *string `json:"tag,omitempty"` + // BaseURL is the registry URL to push the rendered chart to (e.g. "registry.example.com:5000"). + BaseURL *string `json:"baseURL,omitempty"` + // PushSecretRef references a Secret in the same namespace with registry credentials + // for pushing the rendered chart. + PushSecretRef *v1.LocalObjectReference `json:"pushSecretRef,omitempty"` // failedJobTTL is the TTL in seconds after which a failed render job and its secrets are cleaned up. // After this duration, the Kubernetes TTL controller will delete the Job and the controller will delete // the Secrets (ConfigSecret, AuthSecret). On success, Job and Secrets are deleted immediately. @@ -33,7 +37,7 @@ type RenderTaskSpecApplyConfiguration struct { OwnerName *string `json:"ownerName,omitempty"` // OwnerNamespace is the namespace of the resource that created this RenderTask. OwnerNamespace *string `json:"ownerNamespace,omitempty"` - // OwnerKind is the kind of the resource that created this RenderTask (e.g. Release, Bootstrap). + // OwnerKind is the kind of the resource that created this RenderTask (e.g. Release, Target). OwnerKind *string `json:"ownerKind,omitempty"` } @@ -83,6 +87,22 @@ func (b *RenderTaskSpecApplyConfiguration) WithTag(value string) *RenderTaskSpec return b } +// WithBaseURL sets the BaseURL field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BaseURL field is set to the value of the last call. +func (b *RenderTaskSpecApplyConfiguration) WithBaseURL(value string) *RenderTaskSpecApplyConfiguration { + b.BaseURL = &value + return b +} + +// WithPushSecretRef sets the PushSecretRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PushSecretRef field is set to the value of the last call. +func (b *RenderTaskSpecApplyConfiguration) WithPushSecretRef(value v1.LocalObjectReference) *RenderTaskSpecApplyConfiguration { + b.PushSecretRef = &value + return b +} + // WithFailedJobTTL sets the FailedJobTTL field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the FailedJobTTL field is set to the value of the last call. diff --git a/client-go/applyconfigurations/solar/v1alpha1/target.go b/client-go/applyconfigurations/solar/v1alpha1/target.go index c251d5ff..35c3da05 100644 --- a/client-go/applyconfigurations/solar/v1alpha1/target.go +++ b/client-go/applyconfigurations/solar/v1alpha1/target.go @@ -6,7 +6,6 @@ package v1alpha1 import ( - solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" v1 "k8s.io/client-go/applyconfigurations/meta/v1" @@ -21,8 +20,8 @@ import ( type TargetApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` - Spec *TargetSpecApplyConfiguration `json:"spec,omitempty"` - Status *solarv1alpha1.TargetStatus `json:"status,omitempty"` + Spec *TargetSpecApplyConfiguration `json:"spec,omitempty"` + Status *TargetStatusApplyConfiguration `json:"status,omitempty"` } // Target constructs a declarative configuration of the Target type for use with @@ -207,8 +206,8 @@ func (b *TargetApplyConfiguration) WithSpec(value *TargetSpecApplyConfiguration) // WithStatus sets the Status field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Status field is set to the value of the last call. -func (b *TargetApplyConfiguration) WithStatus(value solarv1alpha1.TargetStatus) *TargetApplyConfiguration { - b.Status = &value +func (b *TargetApplyConfiguration) WithStatus(value *TargetStatusApplyConfiguration) *TargetApplyConfiguration { + b.Status = value return b } diff --git a/client-go/applyconfigurations/solar/v1alpha1/targetsecretreference.go b/client-go/applyconfigurations/solar/v1alpha1/targetsecretreference.go new file mode 100644 index 00000000..633927b5 --- /dev/null +++ b/client-go/applyconfigurations/solar/v1alpha1/targetsecretreference.go @@ -0,0 +1,39 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// TargetSecretReferenceApplyConfiguration represents a declarative configuration of the TargetSecretReference type for use +// with apply. +// +// TargetSecretReference is a reference to a Secret in a target cluster. +type TargetSecretReferenceApplyConfiguration struct { + // Name is the name of the Secret. + Name *string `json:"name,omitempty"` + // Namespace is the namespace of the Secret. + Namespace *string `json:"namespace,omitempty"` +} + +// TargetSecretReferenceApplyConfiguration constructs a declarative configuration of the TargetSecretReference type for use with +// apply. +func TargetSecretReference() *TargetSecretReferenceApplyConfiguration { + return &TargetSecretReferenceApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *TargetSecretReferenceApplyConfiguration) WithName(value string) *TargetSecretReferenceApplyConfiguration { + b.Name = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *TargetSecretReferenceApplyConfiguration) WithNamespace(value string) *TargetSecretReferenceApplyConfiguration { + b.Namespace = &value + return b +} diff --git a/client-go/applyconfigurations/solar/v1alpha1/targetspec.go b/client-go/applyconfigurations/solar/v1alpha1/targetspec.go index 93f9efee..0ee2de06 100644 --- a/client-go/applyconfigurations/solar/v1alpha1/targetspec.go +++ b/client-go/applyconfigurations/solar/v1alpha1/targetspec.go @@ -14,11 +14,11 @@ import ( // with apply. // // TargetSpec defines the desired state of a Target. -// It specifies the releases and configuration intended for this deployment target. +// It specifies the render registry and configuration for this deployment target. type TargetSpecApplyConfiguration struct { - // Releases is a map of release names to their corresponding Release object references. - // Each entry represents a component release intended for deployment on this target. - Releases map[string]v1.LocalObjectReference `json:"releases,omitempty"` + // RenderRegistryRef references the Registry to push rendered desired state to. + // The referenced Registry must have SolarSecretRef set for rendering to succeed. + RenderRegistryRef *v1.LocalObjectReference `json:"renderRegistryRef,omitempty"` // Userdata contains arbitrary custom data or configuration specific to this target. // This enables target-specific customization and deployment parameters. Userdata *runtime.RawExtension `json:"userdata,omitempty"` @@ -30,17 +30,11 @@ func TargetSpec() *TargetSpecApplyConfiguration { return &TargetSpecApplyConfiguration{} } -// WithReleases puts the entries into the Releases field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, the entries provided by each call will be put on the Releases field, -// overwriting an existing map entries in Releases field with the same key. -func (b *TargetSpecApplyConfiguration) WithReleases(entries map[string]v1.LocalObjectReference) *TargetSpecApplyConfiguration { - if b.Releases == nil && len(entries) > 0 { - b.Releases = make(map[string]v1.LocalObjectReference, len(entries)) - } - for k, v := range entries { - b.Releases[k] = v - } +// WithRenderRegistryRef sets the RenderRegistryRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RenderRegistryRef field is set to the value of the last call. +func (b *TargetSpecApplyConfiguration) WithRenderRegistryRef(value v1.LocalObjectReference) *TargetSpecApplyConfiguration { + b.RenderRegistryRef = &value return b } diff --git a/client-go/applyconfigurations/solar/v1alpha1/targetstatus.go b/client-go/applyconfigurations/solar/v1alpha1/targetstatus.go new file mode 100644 index 00000000..2a95f16f --- /dev/null +++ b/client-go/applyconfigurations/solar/v1alpha1/targetstatus.go @@ -0,0 +1,50 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// TargetStatusApplyConfiguration represents a declarative configuration of the TargetStatus type for use +// with apply. +// +// TargetStatus defines the observed state of a Target. +type TargetStatusApplyConfiguration struct { + // BootstrapVersion is a monotonically increasing counter used as the bootstrap + // chart version. It is incremented each time the bootstrap chart is re-rendered, + // e.g. when the set of bound releases changes. + BootstrapVersion *int64 `json:"bootstrapVersion,omitempty"` + // Conditions represent the latest available observations of a Target's state. + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` +} + +// TargetStatusApplyConfiguration constructs a declarative configuration of the TargetStatus type for use with +// apply. +func TargetStatus() *TargetStatusApplyConfiguration { + return &TargetStatusApplyConfiguration{} +} + +// WithBootstrapVersion sets the BootstrapVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BootstrapVersion field is set to the value of the last call. +func (b *TargetStatusApplyConfiguration) WithBootstrapVersion(value int64) *TargetStatusApplyConfiguration { + b.BootstrapVersion = &value + return b +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *TargetStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *TargetStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} diff --git a/client-go/applyconfigurations/solar/v1alpha1/webhook.go b/client-go/applyconfigurations/solar/v1alpha1/webhook.go deleted file mode 100644 index a6f87c35..00000000 --- a/client-go/applyconfigurations/solar/v1alpha1/webhook.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by applyconfiguration-gen. DO NOT EDIT. - -package v1alpha1 - -// WebhookApplyConfiguration represents a declarative configuration of the Webhook type for use -// with apply. -// -// Webhook represents the configuration for a webhook. -type WebhookApplyConfiguration struct { - // Flavor is the webhook implementation to use. - Flavor *string `json:"flavor,omitempty"` - // Path is where the webhook should listen. - Path *string `json:"path,omitempty"` - // Auth is the authentication information to use with the webhook. - Auth *WebhookAuthApplyConfiguration `json:"auth,omitempty"` -} - -// WebhookApplyConfiguration constructs a declarative configuration of the Webhook type for use with -// apply. -func Webhook() *WebhookApplyConfiguration { - return &WebhookApplyConfiguration{} -} - -// WithFlavor sets the Flavor field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Flavor field is set to the value of the last call. -func (b *WebhookApplyConfiguration) WithFlavor(value string) *WebhookApplyConfiguration { - b.Flavor = &value - return b -} - -// WithPath sets the Path field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Path field is set to the value of the last call. -func (b *WebhookApplyConfiguration) WithPath(value string) *WebhookApplyConfiguration { - b.Path = &value - return b -} - -// WithAuth sets the Auth field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Auth field is set to the value of the last call. -func (b *WebhookApplyConfiguration) WithAuth(value *WebhookAuthApplyConfiguration) *WebhookApplyConfiguration { - b.Auth = value - return b -} diff --git a/client-go/applyconfigurations/solar/v1alpha1/webhookauth.go b/client-go/applyconfigurations/solar/v1alpha1/webhookauth.go deleted file mode 100644 index 557df8dc..00000000 --- a/client-go/applyconfigurations/solar/v1alpha1/webhookauth.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by applyconfiguration-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" - v1 "k8s.io/api/core/v1" -) - -// WebhookAuthApplyConfiguration represents a declarative configuration of the WebhookAuth type for use -// with apply. -type WebhookAuthApplyConfiguration struct { - // Type represents the type of authentication to use. Currently, only "token" is supported. - Type *solarv1alpha1.AuthenticationType `json:"type,omitempty"` - // AuthSecretRef is the reference to the secret which contains the authentication information for the webhook. - AuthSecretRef *v1.LocalObjectReference `json:"authSecretRef,omitempty"` -} - -// WebhookAuthApplyConfiguration constructs a declarative configuration of the WebhookAuth type for use with -// apply. -func WebhookAuth() *WebhookAuthApplyConfiguration { - return &WebhookAuthApplyConfiguration{} -} - -// WithType sets the Type field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the Type field is set to the value of the last call. -func (b *WebhookAuthApplyConfiguration) WithType(value solarv1alpha1.AuthenticationType) *WebhookAuthApplyConfiguration { - b.Type = &value - return b -} - -// WithAuthSecretRef sets the AuthSecretRef field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the AuthSecretRef field is set to the value of the last call. -func (b *WebhookAuthApplyConfiguration) WithAuthSecretRef(value v1.LocalObjectReference) *WebhookAuthApplyConfiguration { - b.AuthSecretRef = &value - return b -} diff --git a/client-go/applyconfigurations/utils.go b/client-go/applyconfigurations/utils.go index 7f6f0a81..8450380f 100644 --- a/client-go/applyconfigurations/utils.go +++ b/client-go/applyconfigurations/utils.go @@ -19,16 +19,10 @@ import ( func ForKind(kind schema.GroupVersionKind) interface{} { switch kind { // Group=solar.opendefense.cloud, Version=v1alpha1 - case v1alpha1.SchemeGroupVersion.WithKind("Bootstrap"): - return &solarv1alpha1.BootstrapApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("BootstrapConfig"): return &solarv1alpha1.BootstrapConfigApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("BootstrapInput"): return &solarv1alpha1.BootstrapInputApplyConfiguration{} - case v1alpha1.SchemeGroupVersion.WithKind("BootstrapSpec"): - return &solarv1alpha1.BootstrapSpecApplyConfiguration{} - case v1alpha1.SchemeGroupVersion.WithKind("BootstrapStatus"): - return &solarv1alpha1.BootstrapStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ChartConfig"): return &solarv1alpha1.ChartConfigApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Component"): @@ -39,16 +33,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &solarv1alpha1.ComponentVersionApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ComponentVersionSpec"): return &solarv1alpha1.ComponentVersionSpecApplyConfiguration{} - case v1alpha1.SchemeGroupVersion.WithKind("Discovery"): - return &solarv1alpha1.DiscoveryApplyConfiguration{} - case v1alpha1.SchemeGroupVersion.WithKind("DiscoverySpec"): - return &solarv1alpha1.DiscoverySpecApplyConfiguration{} - case v1alpha1.SchemeGroupVersion.WithKind("DiscoveryStatus"): - return &solarv1alpha1.DiscoveryStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Entrypoint"): return &solarv1alpha1.EntrypointApplyConfiguration{} - case v1alpha1.SchemeGroupVersion.WithKind("Filter"): - return &solarv1alpha1.FilterApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Profile"): return &solarv1alpha1.ProfileApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ProfileSpec"): @@ -57,8 +43,24 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &solarv1alpha1.ProfileStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Registry"): return &solarv1alpha1.RegistryApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("RegistryBinding"): + return &solarv1alpha1.RegistryBindingApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("RegistryBindingSpec"): + return &solarv1alpha1.RegistryBindingSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("RegistryBindingStatus"): + return &solarv1alpha1.RegistryBindingStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("RegistrySpec"): + return &solarv1alpha1.RegistrySpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("RegistryStatus"): + return &solarv1alpha1.RegistryStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Release"): return &solarv1alpha1.ReleaseApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ReleaseBinding"): + return &solarv1alpha1.ReleaseBindingApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ReleaseBindingSpec"): + return &solarv1alpha1.ReleaseBindingSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ReleaseBindingStatus"): + return &solarv1alpha1.ReleaseBindingStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ReleaseComponent"): return &solarv1alpha1.ReleaseComponentApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ReleaseConfig"): @@ -81,12 +83,12 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &solarv1alpha1.ResourceAccessApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Target"): return &solarv1alpha1.TargetApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("TargetSecretReference"): + return &solarv1alpha1.TargetSecretReferenceApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("TargetSpec"): return &solarv1alpha1.TargetSpecApplyConfiguration{} - case v1alpha1.SchemeGroupVersion.WithKind("Webhook"): - return &solarv1alpha1.WebhookApplyConfiguration{} - case v1alpha1.SchemeGroupVersion.WithKind("WebhookAuth"): - return &solarv1alpha1.WebhookAuthApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("TargetStatus"): + return &solarv1alpha1.TargetStatusApplyConfiguration{} } return nil diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/bootstrap.go b/client-go/clientset/versioned/typed/solar/v1alpha1/bootstrap.go deleted file mode 100644 index 1b3b695d..00000000 --- a/client-go/clientset/versioned/typed/solar/v1alpha1/bootstrap.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by client-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - context "context" - - solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" - applyconfigurationssolarv1alpha1 "go.opendefense.cloud/solar/client-go/applyconfigurations/solar/v1alpha1" - scheme "go.opendefense.cloud/solar/client-go/clientset/versioned/scheme" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - gentype "k8s.io/client-go/gentype" -) - -// BootstrapsGetter has a method to return a BootstrapInterface. -// A group's client should implement this interface. -type BootstrapsGetter interface { - Bootstraps(namespace string) BootstrapInterface -} - -// BootstrapInterface has methods to work with Bootstrap resources. -type BootstrapInterface interface { - Create(ctx context.Context, bootstrap *solarv1alpha1.Bootstrap, opts v1.CreateOptions) (*solarv1alpha1.Bootstrap, error) - Update(ctx context.Context, bootstrap *solarv1alpha1.Bootstrap, opts v1.UpdateOptions) (*solarv1alpha1.Bootstrap, error) - // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). - UpdateStatus(ctx context.Context, bootstrap *solarv1alpha1.Bootstrap, opts v1.UpdateOptions) (*solarv1alpha1.Bootstrap, error) - Delete(ctx context.Context, name string, opts v1.DeleteOptions) error - DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error - Get(ctx context.Context, name string, opts v1.GetOptions) (*solarv1alpha1.Bootstrap, error) - List(ctx context.Context, opts v1.ListOptions) (*solarv1alpha1.BootstrapList, error) - Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *solarv1alpha1.Bootstrap, err error) - Apply(ctx context.Context, bootstrap *applyconfigurationssolarv1alpha1.BootstrapApplyConfiguration, opts v1.ApplyOptions) (result *solarv1alpha1.Bootstrap, err error) - // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). - ApplyStatus(ctx context.Context, bootstrap *applyconfigurationssolarv1alpha1.BootstrapApplyConfiguration, opts v1.ApplyOptions) (result *solarv1alpha1.Bootstrap, err error) - BootstrapExpansion -} - -// bootstraps implements BootstrapInterface -type bootstraps struct { - *gentype.ClientWithListAndApply[*solarv1alpha1.Bootstrap, *solarv1alpha1.BootstrapList, *applyconfigurationssolarv1alpha1.BootstrapApplyConfiguration] -} - -// newBootstraps returns a Bootstraps -func newBootstraps(c *SolarV1alpha1Client, namespace string) *bootstraps { - return &bootstraps{ - gentype.NewClientWithListAndApply[*solarv1alpha1.Bootstrap, *solarv1alpha1.BootstrapList, *applyconfigurationssolarv1alpha1.BootstrapApplyConfiguration]( - "bootstraps", - c.RESTClient(), - scheme.ParameterCodec, - namespace, - func() *solarv1alpha1.Bootstrap { return &solarv1alpha1.Bootstrap{} }, - func() *solarv1alpha1.BootstrapList { return &solarv1alpha1.BootstrapList{} }, - ), - } -} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/discovery.go b/client-go/clientset/versioned/typed/solar/v1alpha1/discovery.go deleted file mode 100644 index 66bf4b9b..00000000 --- a/client-go/clientset/versioned/typed/solar/v1alpha1/discovery.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by client-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - context "context" - - solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" - applyconfigurationssolarv1alpha1 "go.opendefense.cloud/solar/client-go/applyconfigurations/solar/v1alpha1" - scheme "go.opendefense.cloud/solar/client-go/clientset/versioned/scheme" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - gentype "k8s.io/client-go/gentype" -) - -// DiscoveriesGetter has a method to return a DiscoveryInterface. -// A group's client should implement this interface. -type DiscoveriesGetter interface { - Discoveries(namespace string) DiscoveryInterface -} - -// DiscoveryInterface has methods to work with Discovery resources. -type DiscoveryInterface interface { - Create(ctx context.Context, discovery *solarv1alpha1.Discovery, opts v1.CreateOptions) (*solarv1alpha1.Discovery, error) - Update(ctx context.Context, discovery *solarv1alpha1.Discovery, opts v1.UpdateOptions) (*solarv1alpha1.Discovery, error) - // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). - UpdateStatus(ctx context.Context, discovery *solarv1alpha1.Discovery, opts v1.UpdateOptions) (*solarv1alpha1.Discovery, error) - Delete(ctx context.Context, name string, opts v1.DeleteOptions) error - DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error - Get(ctx context.Context, name string, opts v1.GetOptions) (*solarv1alpha1.Discovery, error) - List(ctx context.Context, opts v1.ListOptions) (*solarv1alpha1.DiscoveryList, error) - Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *solarv1alpha1.Discovery, err error) - Apply(ctx context.Context, discovery *applyconfigurationssolarv1alpha1.DiscoveryApplyConfiguration, opts v1.ApplyOptions) (result *solarv1alpha1.Discovery, err error) - // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). - ApplyStatus(ctx context.Context, discovery *applyconfigurationssolarv1alpha1.DiscoveryApplyConfiguration, opts v1.ApplyOptions) (result *solarv1alpha1.Discovery, err error) - DiscoveryExpansion -} - -// discoveries implements DiscoveryInterface -type discoveries struct { - *gentype.ClientWithListAndApply[*solarv1alpha1.Discovery, *solarv1alpha1.DiscoveryList, *applyconfigurationssolarv1alpha1.DiscoveryApplyConfiguration] -} - -// newDiscoveries returns a Discoveries -func newDiscoveries(c *SolarV1alpha1Client, namespace string) *discoveries { - return &discoveries{ - gentype.NewClientWithListAndApply[*solarv1alpha1.Discovery, *solarv1alpha1.DiscoveryList, *applyconfigurationssolarv1alpha1.DiscoveryApplyConfiguration]( - "discoveries", - c.RESTClient(), - scheme.ParameterCodec, - namespace, - func() *solarv1alpha1.Discovery { return &solarv1alpha1.Discovery{} }, - func() *solarv1alpha1.DiscoveryList { return &solarv1alpha1.DiscoveryList{} }, - ), - } -} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_bootstrap.go b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_bootstrap.go deleted file mode 100644 index f9cda928..00000000 --- a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_bootstrap.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by client-gen. DO NOT EDIT. - -package fake - -import ( - v1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" - solarv1alpha1 "go.opendefense.cloud/solar/client-go/applyconfigurations/solar/v1alpha1" - typedsolarv1alpha1 "go.opendefense.cloud/solar/client-go/clientset/versioned/typed/solar/v1alpha1" - gentype "k8s.io/client-go/gentype" -) - -// fakeBootstraps implements BootstrapInterface -type fakeBootstraps struct { - *gentype.FakeClientWithListAndApply[*v1alpha1.Bootstrap, *v1alpha1.BootstrapList, *solarv1alpha1.BootstrapApplyConfiguration] - Fake *FakeSolarV1alpha1 -} - -func newFakeBootstraps(fake *FakeSolarV1alpha1, namespace string) typedsolarv1alpha1.BootstrapInterface { - return &fakeBootstraps{ - gentype.NewFakeClientWithListAndApply[*v1alpha1.Bootstrap, *v1alpha1.BootstrapList, *solarv1alpha1.BootstrapApplyConfiguration]( - fake.Fake, - namespace, - v1alpha1.SchemeGroupVersion.WithResource("bootstraps"), - v1alpha1.SchemeGroupVersion.WithKind("Bootstrap"), - func() *v1alpha1.Bootstrap { return &v1alpha1.Bootstrap{} }, - func() *v1alpha1.BootstrapList { return &v1alpha1.BootstrapList{} }, - func(dst, src *v1alpha1.BootstrapList) { dst.ListMeta = src.ListMeta }, - func(list *v1alpha1.BootstrapList) []*v1alpha1.Bootstrap { return gentype.ToPointerSlice(list.Items) }, - func(list *v1alpha1.BootstrapList, items []*v1alpha1.Bootstrap) { - list.Items = gentype.FromPointerSlice(items) - }, - ), - fake, - } -} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_discovery.go b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_discovery.go deleted file mode 100644 index 719645e5..00000000 --- a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_discovery.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by client-gen. DO NOT EDIT. - -package fake - -import ( - v1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" - solarv1alpha1 "go.opendefense.cloud/solar/client-go/applyconfigurations/solar/v1alpha1" - typedsolarv1alpha1 "go.opendefense.cloud/solar/client-go/clientset/versioned/typed/solar/v1alpha1" - gentype "k8s.io/client-go/gentype" -) - -// fakeDiscoveries implements DiscoveryInterface -type fakeDiscoveries struct { - *gentype.FakeClientWithListAndApply[*v1alpha1.Discovery, *v1alpha1.DiscoveryList, *solarv1alpha1.DiscoveryApplyConfiguration] - Fake *FakeSolarV1alpha1 -} - -func newFakeDiscoveries(fake *FakeSolarV1alpha1, namespace string) typedsolarv1alpha1.DiscoveryInterface { - return &fakeDiscoveries{ - gentype.NewFakeClientWithListAndApply[*v1alpha1.Discovery, *v1alpha1.DiscoveryList, *solarv1alpha1.DiscoveryApplyConfiguration]( - fake.Fake, - namespace, - v1alpha1.SchemeGroupVersion.WithResource("discoveries"), - v1alpha1.SchemeGroupVersion.WithKind("Discovery"), - func() *v1alpha1.Discovery { return &v1alpha1.Discovery{} }, - func() *v1alpha1.DiscoveryList { return &v1alpha1.DiscoveryList{} }, - func(dst, src *v1alpha1.DiscoveryList) { dst.ListMeta = src.ListMeta }, - func(list *v1alpha1.DiscoveryList) []*v1alpha1.Discovery { return gentype.ToPointerSlice(list.Items) }, - func(list *v1alpha1.DiscoveryList, items []*v1alpha1.Discovery) { - list.Items = gentype.FromPointerSlice(items) - }, - ), - fake, - } -} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_registry.go b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_registry.go new file mode 100644 index 00000000..f33826de --- /dev/null +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_registry.go @@ -0,0 +1,38 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + solarv1alpha1 "go.opendefense.cloud/solar/client-go/applyconfigurations/solar/v1alpha1" + typedsolarv1alpha1 "go.opendefense.cloud/solar/client-go/clientset/versioned/typed/solar/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeRegistries implements RegistryInterface +type fakeRegistries struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.Registry, *v1alpha1.RegistryList, *solarv1alpha1.RegistryApplyConfiguration] + Fake *FakeSolarV1alpha1 +} + +func newFakeRegistries(fake *FakeSolarV1alpha1, namespace string) typedsolarv1alpha1.RegistryInterface { + return &fakeRegistries{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.Registry, *v1alpha1.RegistryList, *solarv1alpha1.RegistryApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("registries"), + v1alpha1.SchemeGroupVersion.WithKind("Registry"), + func() *v1alpha1.Registry { return &v1alpha1.Registry{} }, + func() *v1alpha1.RegistryList { return &v1alpha1.RegistryList{} }, + func(dst, src *v1alpha1.RegistryList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.RegistryList) []*v1alpha1.Registry { return gentype.ToPointerSlice(list.Items) }, + func(list *v1alpha1.RegistryList, items []*v1alpha1.Registry) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_registrybinding.go b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_registrybinding.go new file mode 100644 index 00000000..36893af9 --- /dev/null +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_registrybinding.go @@ -0,0 +1,40 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + solarv1alpha1 "go.opendefense.cloud/solar/client-go/applyconfigurations/solar/v1alpha1" + typedsolarv1alpha1 "go.opendefense.cloud/solar/client-go/clientset/versioned/typed/solar/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeRegistryBindings implements RegistryBindingInterface +type fakeRegistryBindings struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.RegistryBinding, *v1alpha1.RegistryBindingList, *solarv1alpha1.RegistryBindingApplyConfiguration] + Fake *FakeSolarV1alpha1 +} + +func newFakeRegistryBindings(fake *FakeSolarV1alpha1, namespace string) typedsolarv1alpha1.RegistryBindingInterface { + return &fakeRegistryBindings{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.RegistryBinding, *v1alpha1.RegistryBindingList, *solarv1alpha1.RegistryBindingApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("registrybindings"), + v1alpha1.SchemeGroupVersion.WithKind("RegistryBinding"), + func() *v1alpha1.RegistryBinding { return &v1alpha1.RegistryBinding{} }, + func() *v1alpha1.RegistryBindingList { return &v1alpha1.RegistryBindingList{} }, + func(dst, src *v1alpha1.RegistryBindingList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.RegistryBindingList) []*v1alpha1.RegistryBinding { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.RegistryBindingList, items []*v1alpha1.RegistryBinding) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_releasebinding.go b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_releasebinding.go new file mode 100644 index 00000000..9f50a450 --- /dev/null +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_releasebinding.go @@ -0,0 +1,40 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + solarv1alpha1 "go.opendefense.cloud/solar/client-go/applyconfigurations/solar/v1alpha1" + typedsolarv1alpha1 "go.opendefense.cloud/solar/client-go/clientset/versioned/typed/solar/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeReleaseBindings implements ReleaseBindingInterface +type fakeReleaseBindings struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.ReleaseBinding, *v1alpha1.ReleaseBindingList, *solarv1alpha1.ReleaseBindingApplyConfiguration] + Fake *FakeSolarV1alpha1 +} + +func newFakeReleaseBindings(fake *FakeSolarV1alpha1, namespace string) typedsolarv1alpha1.ReleaseBindingInterface { + return &fakeReleaseBindings{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.ReleaseBinding, *v1alpha1.ReleaseBindingList, *solarv1alpha1.ReleaseBindingApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("releasebindings"), + v1alpha1.SchemeGroupVersion.WithKind("ReleaseBinding"), + func() *v1alpha1.ReleaseBinding { return &v1alpha1.ReleaseBinding{} }, + func() *v1alpha1.ReleaseBindingList { return &v1alpha1.ReleaseBindingList{} }, + func(dst, src *v1alpha1.ReleaseBindingList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.ReleaseBindingList) []*v1alpha1.ReleaseBinding { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.ReleaseBindingList, items []*v1alpha1.ReleaseBinding) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_rendertask.go b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_rendertask.go index 64faadbb..3fca8874 100644 --- a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_rendertask.go +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_rendertask.go @@ -18,11 +18,11 @@ type fakeRenderTasks struct { Fake *FakeSolarV1alpha1 } -func newFakeRenderTasks(fake *FakeSolarV1alpha1) typedsolarv1alpha1.RenderTaskInterface { +func newFakeRenderTasks(fake *FakeSolarV1alpha1, namespace string) typedsolarv1alpha1.RenderTaskInterface { return &fakeRenderTasks{ gentype.NewFakeClientWithListAndApply[*v1alpha1.RenderTask, *v1alpha1.RenderTaskList, *solarv1alpha1.RenderTaskApplyConfiguration]( fake.Fake, - "", + namespace, v1alpha1.SchemeGroupVersion.WithResource("rendertasks"), v1alpha1.SchemeGroupVersion.WithKind("RenderTask"), func() *v1alpha1.RenderTask { return &v1alpha1.RenderTask{} }, diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_solar_client.go b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_solar_client.go index 68bedace..f875508e 100644 --- a/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_solar_client.go +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/fake/fake_solar_client.go @@ -15,10 +15,6 @@ type FakeSolarV1alpha1 struct { *testing.Fake } -func (c *FakeSolarV1alpha1) Bootstraps(namespace string) v1alpha1.BootstrapInterface { - return newFakeBootstraps(c, namespace) -} - func (c *FakeSolarV1alpha1) Components(namespace string) v1alpha1.ComponentInterface { return newFakeComponents(c, namespace) } @@ -27,20 +23,28 @@ func (c *FakeSolarV1alpha1) ComponentVersions(namespace string) v1alpha1.Compone return newFakeComponentVersions(c, namespace) } -func (c *FakeSolarV1alpha1) Discoveries(namespace string) v1alpha1.DiscoveryInterface { - return newFakeDiscoveries(c, namespace) -} - func (c *FakeSolarV1alpha1) Profiles(namespace string) v1alpha1.ProfileInterface { return newFakeProfiles(c, namespace) } +func (c *FakeSolarV1alpha1) Registries(namespace string) v1alpha1.RegistryInterface { + return newFakeRegistries(c, namespace) +} + +func (c *FakeSolarV1alpha1) RegistryBindings(namespace string) v1alpha1.RegistryBindingInterface { + return newFakeRegistryBindings(c, namespace) +} + func (c *FakeSolarV1alpha1) Releases(namespace string) v1alpha1.ReleaseInterface { return newFakeReleases(c, namespace) } -func (c *FakeSolarV1alpha1) RenderTasks() v1alpha1.RenderTaskInterface { - return newFakeRenderTasks(c) +func (c *FakeSolarV1alpha1) ReleaseBindings(namespace string) v1alpha1.ReleaseBindingInterface { + return newFakeReleaseBindings(c, namespace) +} + +func (c *FakeSolarV1alpha1) RenderTasks(namespace string) v1alpha1.RenderTaskInterface { + return newFakeRenderTasks(c, namespace) } func (c *FakeSolarV1alpha1) Targets(namespace string) v1alpha1.TargetInterface { diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/generated_expansion.go b/client-go/clientset/versioned/typed/solar/v1alpha1/generated_expansion.go index 9d45fa0e..7185f2e8 100644 --- a/client-go/clientset/versioned/typed/solar/v1alpha1/generated_expansion.go +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/generated_expansion.go @@ -5,18 +5,20 @@ package v1alpha1 -type BootstrapExpansion interface{} - type ComponentExpansion interface{} type ComponentVersionExpansion interface{} -type DiscoveryExpansion interface{} - type ProfileExpansion interface{} +type RegistryExpansion interface{} + +type RegistryBindingExpansion interface{} + type ReleaseExpansion interface{} +type ReleaseBindingExpansion interface{} + type RenderTaskExpansion interface{} type TargetExpansion interface{} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/registry.go b/client-go/clientset/versioned/typed/solar/v1alpha1/registry.go new file mode 100644 index 00000000..c69c7dfc --- /dev/null +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/registry.go @@ -0,0 +1,61 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + applyconfigurationssolarv1alpha1 "go.opendefense.cloud/solar/client-go/applyconfigurations/solar/v1alpha1" + scheme "go.opendefense.cloud/solar/client-go/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// RegistriesGetter has a method to return a RegistryInterface. +// A group's client should implement this interface. +type RegistriesGetter interface { + Registries(namespace string) RegistryInterface +} + +// RegistryInterface has methods to work with Registry resources. +type RegistryInterface interface { + Create(ctx context.Context, registry *solarv1alpha1.Registry, opts v1.CreateOptions) (*solarv1alpha1.Registry, error) + Update(ctx context.Context, registry *solarv1alpha1.Registry, opts v1.UpdateOptions) (*solarv1alpha1.Registry, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, registry *solarv1alpha1.Registry, opts v1.UpdateOptions) (*solarv1alpha1.Registry, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*solarv1alpha1.Registry, error) + List(ctx context.Context, opts v1.ListOptions) (*solarv1alpha1.RegistryList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *solarv1alpha1.Registry, err error) + Apply(ctx context.Context, registry *applyconfigurationssolarv1alpha1.RegistryApplyConfiguration, opts v1.ApplyOptions) (result *solarv1alpha1.Registry, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, registry *applyconfigurationssolarv1alpha1.RegistryApplyConfiguration, opts v1.ApplyOptions) (result *solarv1alpha1.Registry, err error) + RegistryExpansion +} + +// registries implements RegistryInterface +type registries struct { + *gentype.ClientWithListAndApply[*solarv1alpha1.Registry, *solarv1alpha1.RegistryList, *applyconfigurationssolarv1alpha1.RegistryApplyConfiguration] +} + +// newRegistries returns a Registries +func newRegistries(c *SolarV1alpha1Client, namespace string) *registries { + return ®istries{ + gentype.NewClientWithListAndApply[*solarv1alpha1.Registry, *solarv1alpha1.RegistryList, *applyconfigurationssolarv1alpha1.RegistryApplyConfiguration]( + "registries", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *solarv1alpha1.Registry { return &solarv1alpha1.Registry{} }, + func() *solarv1alpha1.RegistryList { return &solarv1alpha1.RegistryList{} }, + ), + } +} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/registrybinding.go b/client-go/clientset/versioned/typed/solar/v1alpha1/registrybinding.go new file mode 100644 index 00000000..68fff661 --- /dev/null +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/registrybinding.go @@ -0,0 +1,61 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + applyconfigurationssolarv1alpha1 "go.opendefense.cloud/solar/client-go/applyconfigurations/solar/v1alpha1" + scheme "go.opendefense.cloud/solar/client-go/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// RegistryBindingsGetter has a method to return a RegistryBindingInterface. +// A group's client should implement this interface. +type RegistryBindingsGetter interface { + RegistryBindings(namespace string) RegistryBindingInterface +} + +// RegistryBindingInterface has methods to work with RegistryBinding resources. +type RegistryBindingInterface interface { + Create(ctx context.Context, registryBinding *solarv1alpha1.RegistryBinding, opts v1.CreateOptions) (*solarv1alpha1.RegistryBinding, error) + Update(ctx context.Context, registryBinding *solarv1alpha1.RegistryBinding, opts v1.UpdateOptions) (*solarv1alpha1.RegistryBinding, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, registryBinding *solarv1alpha1.RegistryBinding, opts v1.UpdateOptions) (*solarv1alpha1.RegistryBinding, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*solarv1alpha1.RegistryBinding, error) + List(ctx context.Context, opts v1.ListOptions) (*solarv1alpha1.RegistryBindingList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *solarv1alpha1.RegistryBinding, err error) + Apply(ctx context.Context, registryBinding *applyconfigurationssolarv1alpha1.RegistryBindingApplyConfiguration, opts v1.ApplyOptions) (result *solarv1alpha1.RegistryBinding, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, registryBinding *applyconfigurationssolarv1alpha1.RegistryBindingApplyConfiguration, opts v1.ApplyOptions) (result *solarv1alpha1.RegistryBinding, err error) + RegistryBindingExpansion +} + +// registryBindings implements RegistryBindingInterface +type registryBindings struct { + *gentype.ClientWithListAndApply[*solarv1alpha1.RegistryBinding, *solarv1alpha1.RegistryBindingList, *applyconfigurationssolarv1alpha1.RegistryBindingApplyConfiguration] +} + +// newRegistryBindings returns a RegistryBindings +func newRegistryBindings(c *SolarV1alpha1Client, namespace string) *registryBindings { + return ®istryBindings{ + gentype.NewClientWithListAndApply[*solarv1alpha1.RegistryBinding, *solarv1alpha1.RegistryBindingList, *applyconfigurationssolarv1alpha1.RegistryBindingApplyConfiguration]( + "registrybindings", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *solarv1alpha1.RegistryBinding { return &solarv1alpha1.RegistryBinding{} }, + func() *solarv1alpha1.RegistryBindingList { return &solarv1alpha1.RegistryBindingList{} }, + ), + } +} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/releasebinding.go b/client-go/clientset/versioned/typed/solar/v1alpha1/releasebinding.go new file mode 100644 index 00000000..59690d74 --- /dev/null +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/releasebinding.go @@ -0,0 +1,61 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + applyconfigurationssolarv1alpha1 "go.opendefense.cloud/solar/client-go/applyconfigurations/solar/v1alpha1" + scheme "go.opendefense.cloud/solar/client-go/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// ReleaseBindingsGetter has a method to return a ReleaseBindingInterface. +// A group's client should implement this interface. +type ReleaseBindingsGetter interface { + ReleaseBindings(namespace string) ReleaseBindingInterface +} + +// ReleaseBindingInterface has methods to work with ReleaseBinding resources. +type ReleaseBindingInterface interface { + Create(ctx context.Context, releaseBinding *solarv1alpha1.ReleaseBinding, opts v1.CreateOptions) (*solarv1alpha1.ReleaseBinding, error) + Update(ctx context.Context, releaseBinding *solarv1alpha1.ReleaseBinding, opts v1.UpdateOptions) (*solarv1alpha1.ReleaseBinding, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, releaseBinding *solarv1alpha1.ReleaseBinding, opts v1.UpdateOptions) (*solarv1alpha1.ReleaseBinding, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*solarv1alpha1.ReleaseBinding, error) + List(ctx context.Context, opts v1.ListOptions) (*solarv1alpha1.ReleaseBindingList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *solarv1alpha1.ReleaseBinding, err error) + Apply(ctx context.Context, releaseBinding *applyconfigurationssolarv1alpha1.ReleaseBindingApplyConfiguration, opts v1.ApplyOptions) (result *solarv1alpha1.ReleaseBinding, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, releaseBinding *applyconfigurationssolarv1alpha1.ReleaseBindingApplyConfiguration, opts v1.ApplyOptions) (result *solarv1alpha1.ReleaseBinding, err error) + ReleaseBindingExpansion +} + +// releaseBindings implements ReleaseBindingInterface +type releaseBindings struct { + *gentype.ClientWithListAndApply[*solarv1alpha1.ReleaseBinding, *solarv1alpha1.ReleaseBindingList, *applyconfigurationssolarv1alpha1.ReleaseBindingApplyConfiguration] +} + +// newReleaseBindings returns a ReleaseBindings +func newReleaseBindings(c *SolarV1alpha1Client, namespace string) *releaseBindings { + return &releaseBindings{ + gentype.NewClientWithListAndApply[*solarv1alpha1.ReleaseBinding, *solarv1alpha1.ReleaseBindingList, *applyconfigurationssolarv1alpha1.ReleaseBindingApplyConfiguration]( + "releasebindings", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *solarv1alpha1.ReleaseBinding { return &solarv1alpha1.ReleaseBinding{} }, + func() *solarv1alpha1.ReleaseBindingList { return &solarv1alpha1.ReleaseBindingList{} }, + ), + } +} diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/rendertask.go b/client-go/clientset/versioned/typed/solar/v1alpha1/rendertask.go index 77b0107c..5a4e34a6 100644 --- a/client-go/clientset/versioned/typed/solar/v1alpha1/rendertask.go +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/rendertask.go @@ -20,7 +20,7 @@ import ( // RenderTasksGetter has a method to return a RenderTaskInterface. // A group's client should implement this interface. type RenderTasksGetter interface { - RenderTasks() RenderTaskInterface + RenderTasks(namespace string) RenderTaskInterface } // RenderTaskInterface has methods to work with RenderTask resources. @@ -47,13 +47,13 @@ type renderTasks struct { } // newRenderTasks returns a RenderTasks -func newRenderTasks(c *SolarV1alpha1Client) *renderTasks { +func newRenderTasks(c *SolarV1alpha1Client, namespace string) *renderTasks { return &renderTasks{ gentype.NewClientWithListAndApply[*solarv1alpha1.RenderTask, *solarv1alpha1.RenderTaskList, *applyconfigurationssolarv1alpha1.RenderTaskApplyConfiguration]( "rendertasks", c.RESTClient(), scheme.ParameterCodec, - "", + namespace, func() *solarv1alpha1.RenderTask { return &solarv1alpha1.RenderTask{} }, func() *solarv1alpha1.RenderTaskList { return &solarv1alpha1.RenderTaskList{} }, ), diff --git a/client-go/clientset/versioned/typed/solar/v1alpha1/solar_client.go b/client-go/clientset/versioned/typed/solar/v1alpha1/solar_client.go index cb0b7f65..451e82f6 100644 --- a/client-go/clientset/versioned/typed/solar/v1alpha1/solar_client.go +++ b/client-go/clientset/versioned/typed/solar/v1alpha1/solar_client.go @@ -15,12 +15,13 @@ import ( type SolarV1alpha1Interface interface { RESTClient() rest.Interface - BootstrapsGetter ComponentsGetter ComponentVersionsGetter - DiscoveriesGetter ProfilesGetter + RegistriesGetter + RegistryBindingsGetter ReleasesGetter + ReleaseBindingsGetter RenderTasksGetter TargetsGetter } @@ -30,10 +31,6 @@ type SolarV1alpha1Client struct { restClient rest.Interface } -func (c *SolarV1alpha1Client) Bootstraps(namespace string) BootstrapInterface { - return newBootstraps(c, namespace) -} - func (c *SolarV1alpha1Client) Components(namespace string) ComponentInterface { return newComponents(c, namespace) } @@ -42,20 +39,28 @@ func (c *SolarV1alpha1Client) ComponentVersions(namespace string) ComponentVersi return newComponentVersions(c, namespace) } -func (c *SolarV1alpha1Client) Discoveries(namespace string) DiscoveryInterface { - return newDiscoveries(c, namespace) -} - func (c *SolarV1alpha1Client) Profiles(namespace string) ProfileInterface { return newProfiles(c, namespace) } +func (c *SolarV1alpha1Client) Registries(namespace string) RegistryInterface { + return newRegistries(c, namespace) +} + +func (c *SolarV1alpha1Client) RegistryBindings(namespace string) RegistryBindingInterface { + return newRegistryBindings(c, namespace) +} + func (c *SolarV1alpha1Client) Releases(namespace string) ReleaseInterface { return newReleases(c, namespace) } -func (c *SolarV1alpha1Client) RenderTasks() RenderTaskInterface { - return newRenderTasks(c) +func (c *SolarV1alpha1Client) ReleaseBindings(namespace string) ReleaseBindingInterface { + return newReleaseBindings(c, namespace) +} + +func (c *SolarV1alpha1Client) RenderTasks(namespace string) RenderTaskInterface { + return newRenderTasks(c, namespace) } func (c *SolarV1alpha1Client) Targets(namespace string) TargetInterface { diff --git a/client-go/informers/externalversions/generic.go b/client-go/informers/externalversions/generic.go index fa5ff7f1..0035b02a 100644 --- a/client-go/informers/externalversions/generic.go +++ b/client-go/informers/externalversions/generic.go @@ -40,18 +40,20 @@ func (f *genericInformer) Lister() cache.GenericLister { func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=solar.opendefense.cloud, Version=v1alpha1 - case v1alpha1.SchemeGroupVersion.WithResource("bootstraps"): - return &genericInformer{resource: resource.GroupResource(), informer: f.Solar().V1alpha1().Bootstraps().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("components"): return &genericInformer{resource: resource.GroupResource(), informer: f.Solar().V1alpha1().Components().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("componentversions"): return &genericInformer{resource: resource.GroupResource(), informer: f.Solar().V1alpha1().ComponentVersions().Informer()}, nil - case v1alpha1.SchemeGroupVersion.WithResource("discoveries"): - return &genericInformer{resource: resource.GroupResource(), informer: f.Solar().V1alpha1().Discoveries().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("profiles"): return &genericInformer{resource: resource.GroupResource(), informer: f.Solar().V1alpha1().Profiles().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("registries"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Solar().V1alpha1().Registries().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("registrybindings"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Solar().V1alpha1().RegistryBindings().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("releases"): return &genericInformer{resource: resource.GroupResource(), informer: f.Solar().V1alpha1().Releases().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("releasebindings"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Solar().V1alpha1().ReleaseBindings().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("rendertasks"): return &genericInformer{resource: resource.GroupResource(), informer: f.Solar().V1alpha1().RenderTasks().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("targets"): diff --git a/client-go/informers/externalversions/solar/v1alpha1/interface.go b/client-go/informers/externalversions/solar/v1alpha1/interface.go index e2c17a3f..5d6e46d0 100644 --- a/client-go/informers/externalversions/solar/v1alpha1/interface.go +++ b/client-go/informers/externalversions/solar/v1alpha1/interface.go @@ -11,18 +11,20 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { - // Bootstraps returns a BootstrapInformer. - Bootstraps() BootstrapInformer // Components returns a ComponentInformer. Components() ComponentInformer // ComponentVersions returns a ComponentVersionInformer. ComponentVersions() ComponentVersionInformer - // Discoveries returns a DiscoveryInformer. - Discoveries() DiscoveryInformer // Profiles returns a ProfileInformer. Profiles() ProfileInformer + // Registries returns a RegistryInformer. + Registries() RegistryInformer + // RegistryBindings returns a RegistryBindingInformer. + RegistryBindings() RegistryBindingInformer // Releases returns a ReleaseInformer. Releases() ReleaseInformer + // ReleaseBindings returns a ReleaseBindingInformer. + ReleaseBindings() ReleaseBindingInformer // RenderTasks returns a RenderTaskInformer. RenderTasks() RenderTaskInformer // Targets returns a TargetInformer. @@ -40,11 +42,6 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } -// Bootstraps returns a BootstrapInformer. -func (v *version) Bootstraps() BootstrapInformer { - return &bootstrapInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} -} - // Components returns a ComponentInformer. func (v *version) Components() ComponentInformer { return &componentInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} @@ -55,24 +52,34 @@ func (v *version) ComponentVersions() ComponentVersionInformer { return &componentVersionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } -// Discoveries returns a DiscoveryInformer. -func (v *version) Discoveries() DiscoveryInformer { - return &discoveryInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} -} - // Profiles returns a ProfileInformer. func (v *version) Profiles() ProfileInformer { return &profileInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// Registries returns a RegistryInformer. +func (v *version) Registries() RegistryInformer { + return ®istryInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// RegistryBindings returns a RegistryBindingInformer. +func (v *version) RegistryBindings() RegistryBindingInformer { + return ®istryBindingInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // Releases returns a ReleaseInformer. func (v *version) Releases() ReleaseInformer { return &releaseInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// ReleaseBindings returns a ReleaseBindingInformer. +func (v *version) ReleaseBindings() ReleaseBindingInformer { + return &releaseBindingInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // RenderTasks returns a RenderTaskInformer. func (v *version) RenderTasks() RenderTaskInformer { - return &renderTaskInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} + return &renderTaskInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } // Targets returns a TargetInformer. diff --git a/client-go/informers/externalversions/solar/v1alpha1/bootstrap.go b/client-go/informers/externalversions/solar/v1alpha1/registry.go similarity index 56% rename from client-go/informers/externalversions/solar/v1alpha1/bootstrap.go rename to client-go/informers/externalversions/solar/v1alpha1/registry.go index 83b3ca75..19afe755 100644 --- a/client-go/informers/externalversions/solar/v1alpha1/bootstrap.go +++ b/client-go/informers/externalversions/solar/v1alpha1/registry.go @@ -19,71 +19,71 @@ import ( cache "k8s.io/client-go/tools/cache" ) -// BootstrapInformer provides access to a shared informer and lister for -// Bootstraps. -type BootstrapInformer interface { +// RegistryInformer provides access to a shared informer and lister for +// Registries. +type RegistryInformer interface { Informer() cache.SharedIndexInformer - Lister() solarv1alpha1.BootstrapLister + Lister() solarv1alpha1.RegistryLister } -type bootstrapInformer struct { +type registryInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } -// NewBootstrapInformer constructs a new informer for Bootstrap type. +// NewRegistryInformer constructs a new informer for Registry type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewBootstrapInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredBootstrapInformer(client, namespace, resyncPeriod, indexers, nil) +func NewRegistryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredRegistryInformer(client, namespace, resyncPeriod, indexers, nil) } -// NewFilteredBootstrapInformer constructs a new informer for Bootstrap type. +// NewFilteredRegistryInformer constructs a new informer for Registry type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewFilteredBootstrapInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { +func NewFilteredRegistryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().Bootstraps(namespace).List(context.Background(), options) + return client.SolarV1alpha1().Registries(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().Bootstraps(namespace).Watch(context.Background(), options) + return client.SolarV1alpha1().Registries(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().Bootstraps(namespace).List(ctx, options) + return client.SolarV1alpha1().Registries(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().Bootstraps(namespace).Watch(ctx, options) + return client.SolarV1alpha1().Registries(namespace).Watch(ctx, options) }, }, client), - &apisolarv1alpha1.Bootstrap{}, + &apisolarv1alpha1.Registry{}, resyncPeriod, indexers, ) } -func (f *bootstrapInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredBootstrapInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +func (f *registryInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredRegistryInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } -func (f *bootstrapInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&apisolarv1alpha1.Bootstrap{}, f.defaultInformer) +func (f *registryInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisolarv1alpha1.Registry{}, f.defaultInformer) } -func (f *bootstrapInformer) Lister() solarv1alpha1.BootstrapLister { - return solarv1alpha1.NewBootstrapLister(f.Informer().GetIndexer()) +func (f *registryInformer) Lister() solarv1alpha1.RegistryLister { + return solarv1alpha1.NewRegistryLister(f.Informer().GetIndexer()) } diff --git a/client-go/informers/externalversions/solar/v1alpha1/registrybinding.go b/client-go/informers/externalversions/solar/v1alpha1/registrybinding.go new file mode 100644 index 00000000..1880b984 --- /dev/null +++ b/client-go/informers/externalversions/solar/v1alpha1/registrybinding.go @@ -0,0 +1,89 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + apisolarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + versioned "go.opendefense.cloud/solar/client-go/clientset/versioned" + internalinterfaces "go.opendefense.cloud/solar/client-go/informers/externalversions/internalinterfaces" + solarv1alpha1 "go.opendefense.cloud/solar/client-go/listers/solar/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// RegistryBindingInformer provides access to a shared informer and lister for +// RegistryBindings. +type RegistryBindingInformer interface { + Informer() cache.SharedIndexInformer + Lister() solarv1alpha1.RegistryBindingLister +} + +type registryBindingInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewRegistryBindingInformer constructs a new informer for RegistryBinding type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewRegistryBindingInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredRegistryBindingInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredRegistryBindingInformer constructs a new informer for RegistryBinding type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredRegistryBindingInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SolarV1alpha1().RegistryBindings(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SolarV1alpha1().RegistryBindings(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SolarV1alpha1().RegistryBindings(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SolarV1alpha1().RegistryBindings(namespace).Watch(ctx, options) + }, + }, client), + &apisolarv1alpha1.RegistryBinding{}, + resyncPeriod, + indexers, + ) +} + +func (f *registryBindingInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredRegistryBindingInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *registryBindingInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisolarv1alpha1.RegistryBinding{}, f.defaultInformer) +} + +func (f *registryBindingInformer) Lister() solarv1alpha1.RegistryBindingLister { + return solarv1alpha1.NewRegistryBindingLister(f.Informer().GetIndexer()) +} diff --git a/client-go/informers/externalversions/solar/v1alpha1/discovery.go b/client-go/informers/externalversions/solar/v1alpha1/releasebinding.go similarity index 52% rename from client-go/informers/externalversions/solar/v1alpha1/discovery.go rename to client-go/informers/externalversions/solar/v1alpha1/releasebinding.go index 5536f3ac..2d631cab 100644 --- a/client-go/informers/externalversions/solar/v1alpha1/discovery.go +++ b/client-go/informers/externalversions/solar/v1alpha1/releasebinding.go @@ -19,71 +19,71 @@ import ( cache "k8s.io/client-go/tools/cache" ) -// DiscoveryInformer provides access to a shared informer and lister for -// Discoveries. -type DiscoveryInformer interface { +// ReleaseBindingInformer provides access to a shared informer and lister for +// ReleaseBindings. +type ReleaseBindingInformer interface { Informer() cache.SharedIndexInformer - Lister() solarv1alpha1.DiscoveryLister + Lister() solarv1alpha1.ReleaseBindingLister } -type discoveryInformer struct { +type releaseBindingInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } -// NewDiscoveryInformer constructs a new informer for Discovery type. +// NewReleaseBindingInformer constructs a new informer for ReleaseBinding type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewDiscoveryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredDiscoveryInformer(client, namespace, resyncPeriod, indexers, nil) +func NewReleaseBindingInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredReleaseBindingInformer(client, namespace, resyncPeriod, indexers, nil) } -// NewFilteredDiscoveryInformer constructs a new informer for Discovery type. +// NewFilteredReleaseBindingInformer constructs a new informer for ReleaseBinding type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewFilteredDiscoveryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { +func NewFilteredReleaseBindingInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().Discoveries(namespace).List(context.Background(), options) + return client.SolarV1alpha1().ReleaseBindings(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().Discoveries(namespace).Watch(context.Background(), options) + return client.SolarV1alpha1().ReleaseBindings(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().Discoveries(namespace).List(ctx, options) + return client.SolarV1alpha1().ReleaseBindings(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().Discoveries(namespace).Watch(ctx, options) + return client.SolarV1alpha1().ReleaseBindings(namespace).Watch(ctx, options) }, }, client), - &apisolarv1alpha1.Discovery{}, + &apisolarv1alpha1.ReleaseBinding{}, resyncPeriod, indexers, ) } -func (f *discoveryInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredDiscoveryInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +func (f *releaseBindingInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredReleaseBindingInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } -func (f *discoveryInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&apisolarv1alpha1.Discovery{}, f.defaultInformer) +func (f *releaseBindingInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisolarv1alpha1.ReleaseBinding{}, f.defaultInformer) } -func (f *discoveryInformer) Lister() solarv1alpha1.DiscoveryLister { - return solarv1alpha1.NewDiscoveryLister(f.Informer().GetIndexer()) +func (f *releaseBindingInformer) Lister() solarv1alpha1.ReleaseBindingLister { + return solarv1alpha1.NewReleaseBindingLister(f.Informer().GetIndexer()) } diff --git a/client-go/informers/externalversions/solar/v1alpha1/rendertask.go b/client-go/informers/externalversions/solar/v1alpha1/rendertask.go index 6623aab7..1f84cd2b 100644 --- a/client-go/informers/externalversions/solar/v1alpha1/rendertask.go +++ b/client-go/informers/externalversions/solar/v1alpha1/rendertask.go @@ -29,44 +29,45 @@ type RenderTaskInformer interface { type renderTaskInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string } // NewRenderTaskInformer constructs a new informer for RenderTask type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewRenderTaskInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredRenderTaskInformer(client, resyncPeriod, indexers, nil) +func NewRenderTaskInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredRenderTaskInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredRenderTaskInformer constructs a new informer for RenderTask type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewFilteredRenderTaskInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { +func NewFilteredRenderTaskInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().RenderTasks().List(context.Background(), options) + return client.SolarV1alpha1().RenderTasks(namespace).List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().RenderTasks().Watch(context.Background(), options) + return client.SolarV1alpha1().RenderTasks(namespace).Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().RenderTasks().List(ctx, options) + return client.SolarV1alpha1().RenderTasks(namespace).List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SolarV1alpha1().RenderTasks().Watch(ctx, options) + return client.SolarV1alpha1().RenderTasks(namespace).Watch(ctx, options) }, }, client), &apisolarv1alpha1.RenderTask{}, @@ -76,7 +77,7 @@ func NewFilteredRenderTaskInformer(client versioned.Interface, resyncPeriod time } func (f *renderTaskInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredRenderTaskInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + return NewFilteredRenderTaskInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *renderTaskInformer) Informer() cache.SharedIndexInformer { diff --git a/client-go/listers/solar/v1alpha1/bootstrap.go b/client-go/listers/solar/v1alpha1/bootstrap.go deleted file mode 100644 index 8e4a518b..00000000 --- a/client-go/listers/solar/v1alpha1/bootstrap.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" - labels "k8s.io/apimachinery/pkg/labels" - listers "k8s.io/client-go/listers" - cache "k8s.io/client-go/tools/cache" -) - -// BootstrapLister helps list Bootstraps. -// All objects returned here must be treated as read-only. -type BootstrapLister interface { - // List lists all Bootstraps in the indexer. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*solarv1alpha1.Bootstrap, err error) - // Bootstraps returns an object that can list and get Bootstraps. - Bootstraps(namespace string) BootstrapNamespaceLister - BootstrapListerExpansion -} - -// bootstrapLister implements the BootstrapLister interface. -type bootstrapLister struct { - listers.ResourceIndexer[*solarv1alpha1.Bootstrap] -} - -// NewBootstrapLister returns a new BootstrapLister. -func NewBootstrapLister(indexer cache.Indexer) BootstrapLister { - return &bootstrapLister{listers.New[*solarv1alpha1.Bootstrap](indexer, solarv1alpha1.Resource("bootstrap"))} -} - -// Bootstraps returns an object that can list and get Bootstraps. -func (s *bootstrapLister) Bootstraps(namespace string) BootstrapNamespaceLister { - return bootstrapNamespaceLister{listers.NewNamespaced[*solarv1alpha1.Bootstrap](s.ResourceIndexer, namespace)} -} - -// BootstrapNamespaceLister helps list and get Bootstraps. -// All objects returned here must be treated as read-only. -type BootstrapNamespaceLister interface { - // List lists all Bootstraps in the indexer for a given namespace. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*solarv1alpha1.Bootstrap, err error) - // Get retrieves the Bootstrap from the indexer for a given namespace and name. - // Objects returned here must be treated as read-only. - Get(name string) (*solarv1alpha1.Bootstrap, error) - BootstrapNamespaceListerExpansion -} - -// bootstrapNamespaceLister implements the BootstrapNamespaceLister -// interface. -type bootstrapNamespaceLister struct { - listers.ResourceIndexer[*solarv1alpha1.Bootstrap] -} diff --git a/client-go/listers/solar/v1alpha1/discovery.go b/client-go/listers/solar/v1alpha1/discovery.go deleted file mode 100644 index 97b31094..00000000 --- a/client-go/listers/solar/v1alpha1/discovery.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" - labels "k8s.io/apimachinery/pkg/labels" - listers "k8s.io/client-go/listers" - cache "k8s.io/client-go/tools/cache" -) - -// DiscoveryLister helps list Discoveries. -// All objects returned here must be treated as read-only. -type DiscoveryLister interface { - // List lists all Discoveries in the indexer. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*solarv1alpha1.Discovery, err error) - // Discoveries returns an object that can list and get Discoveries. - Discoveries(namespace string) DiscoveryNamespaceLister - DiscoveryListerExpansion -} - -// discoveryLister implements the DiscoveryLister interface. -type discoveryLister struct { - listers.ResourceIndexer[*solarv1alpha1.Discovery] -} - -// NewDiscoveryLister returns a new DiscoveryLister. -func NewDiscoveryLister(indexer cache.Indexer) DiscoveryLister { - return &discoveryLister{listers.New[*solarv1alpha1.Discovery](indexer, solarv1alpha1.Resource("discovery"))} -} - -// Discoveries returns an object that can list and get Discoveries. -func (s *discoveryLister) Discoveries(namespace string) DiscoveryNamespaceLister { - return discoveryNamespaceLister{listers.NewNamespaced[*solarv1alpha1.Discovery](s.ResourceIndexer, namespace)} -} - -// DiscoveryNamespaceLister helps list and get Discoveries. -// All objects returned here must be treated as read-only. -type DiscoveryNamespaceLister interface { - // List lists all Discoveries in the indexer for a given namespace. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*solarv1alpha1.Discovery, err error) - // Get retrieves the Discovery from the indexer for a given namespace and name. - // Objects returned here must be treated as read-only. - Get(name string) (*solarv1alpha1.Discovery, error) - DiscoveryNamespaceListerExpansion -} - -// discoveryNamespaceLister implements the DiscoveryNamespaceLister -// interface. -type discoveryNamespaceLister struct { - listers.ResourceIndexer[*solarv1alpha1.Discovery] -} diff --git a/client-go/listers/solar/v1alpha1/expansion_generated.go b/client-go/listers/solar/v1alpha1/expansion_generated.go index 29622620..4d31597b 100644 --- a/client-go/listers/solar/v1alpha1/expansion_generated.go +++ b/client-go/listers/solar/v1alpha1/expansion_generated.go @@ -5,14 +5,6 @@ package v1alpha1 -// BootstrapListerExpansion allows custom methods to be added to -// BootstrapLister. -type BootstrapListerExpansion interface{} - -// BootstrapNamespaceListerExpansion allows custom methods to be added to -// BootstrapNamespaceLister. -type BootstrapNamespaceListerExpansion interface{} - // ComponentListerExpansion allows custom methods to be added to // ComponentLister. type ComponentListerExpansion interface{} @@ -29,14 +21,6 @@ type ComponentVersionListerExpansion interface{} // ComponentVersionNamespaceLister. type ComponentVersionNamespaceListerExpansion interface{} -// DiscoveryListerExpansion allows custom methods to be added to -// DiscoveryLister. -type DiscoveryListerExpansion interface{} - -// DiscoveryNamespaceListerExpansion allows custom methods to be added to -// DiscoveryNamespaceLister. -type DiscoveryNamespaceListerExpansion interface{} - // ProfileListerExpansion allows custom methods to be added to // ProfileLister. type ProfileListerExpansion interface{} @@ -45,6 +29,22 @@ type ProfileListerExpansion interface{} // ProfileNamespaceLister. type ProfileNamespaceListerExpansion interface{} +// RegistryListerExpansion allows custom methods to be added to +// RegistryLister. +type RegistryListerExpansion interface{} + +// RegistryNamespaceListerExpansion allows custom methods to be added to +// RegistryNamespaceLister. +type RegistryNamespaceListerExpansion interface{} + +// RegistryBindingListerExpansion allows custom methods to be added to +// RegistryBindingLister. +type RegistryBindingListerExpansion interface{} + +// RegistryBindingNamespaceListerExpansion allows custom methods to be added to +// RegistryBindingNamespaceLister. +type RegistryBindingNamespaceListerExpansion interface{} + // ReleaseListerExpansion allows custom methods to be added to // ReleaseLister. type ReleaseListerExpansion interface{} @@ -53,10 +53,22 @@ type ReleaseListerExpansion interface{} // ReleaseNamespaceLister. type ReleaseNamespaceListerExpansion interface{} +// ReleaseBindingListerExpansion allows custom methods to be added to +// ReleaseBindingLister. +type ReleaseBindingListerExpansion interface{} + +// ReleaseBindingNamespaceListerExpansion allows custom methods to be added to +// ReleaseBindingNamespaceLister. +type ReleaseBindingNamespaceListerExpansion interface{} + // RenderTaskListerExpansion allows custom methods to be added to // RenderTaskLister. type RenderTaskListerExpansion interface{} +// RenderTaskNamespaceListerExpansion allows custom methods to be added to +// RenderTaskNamespaceLister. +type RenderTaskNamespaceListerExpansion interface{} + // TargetListerExpansion allows custom methods to be added to // TargetLister. type TargetListerExpansion interface{} diff --git a/client-go/listers/solar/v1alpha1/registry.go b/client-go/listers/solar/v1alpha1/registry.go new file mode 100644 index 00000000..75ba921c --- /dev/null +++ b/client-go/listers/solar/v1alpha1/registry.go @@ -0,0 +1,57 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// RegistryLister helps list Registries. +// All objects returned here must be treated as read-only. +type RegistryLister interface { + // List lists all Registries in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*solarv1alpha1.Registry, err error) + // Registries returns an object that can list and get Registries. + Registries(namespace string) RegistryNamespaceLister + RegistryListerExpansion +} + +// registryLister implements the RegistryLister interface. +type registryLister struct { + listers.ResourceIndexer[*solarv1alpha1.Registry] +} + +// NewRegistryLister returns a new RegistryLister. +func NewRegistryLister(indexer cache.Indexer) RegistryLister { + return ®istryLister{listers.New[*solarv1alpha1.Registry](indexer, solarv1alpha1.Resource("registry"))} +} + +// Registries returns an object that can list and get Registries. +func (s *registryLister) Registries(namespace string) RegistryNamespaceLister { + return registryNamespaceLister{listers.NewNamespaced[*solarv1alpha1.Registry](s.ResourceIndexer, namespace)} +} + +// RegistryNamespaceLister helps list and get Registries. +// All objects returned here must be treated as read-only. +type RegistryNamespaceLister interface { + // List lists all Registries in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*solarv1alpha1.Registry, err error) + // Get retrieves the Registry from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*solarv1alpha1.Registry, error) + RegistryNamespaceListerExpansion +} + +// registryNamespaceLister implements the RegistryNamespaceLister +// interface. +type registryNamespaceLister struct { + listers.ResourceIndexer[*solarv1alpha1.Registry] +} diff --git a/client-go/listers/solar/v1alpha1/registrybinding.go b/client-go/listers/solar/v1alpha1/registrybinding.go new file mode 100644 index 00000000..5e7f13d9 --- /dev/null +++ b/client-go/listers/solar/v1alpha1/registrybinding.go @@ -0,0 +1,57 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// RegistryBindingLister helps list RegistryBindings. +// All objects returned here must be treated as read-only. +type RegistryBindingLister interface { + // List lists all RegistryBindings in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*solarv1alpha1.RegistryBinding, err error) + // RegistryBindings returns an object that can list and get RegistryBindings. + RegistryBindings(namespace string) RegistryBindingNamespaceLister + RegistryBindingListerExpansion +} + +// registryBindingLister implements the RegistryBindingLister interface. +type registryBindingLister struct { + listers.ResourceIndexer[*solarv1alpha1.RegistryBinding] +} + +// NewRegistryBindingLister returns a new RegistryBindingLister. +func NewRegistryBindingLister(indexer cache.Indexer) RegistryBindingLister { + return ®istryBindingLister{listers.New[*solarv1alpha1.RegistryBinding](indexer, solarv1alpha1.Resource("registrybinding"))} +} + +// RegistryBindings returns an object that can list and get RegistryBindings. +func (s *registryBindingLister) RegistryBindings(namespace string) RegistryBindingNamespaceLister { + return registryBindingNamespaceLister{listers.NewNamespaced[*solarv1alpha1.RegistryBinding](s.ResourceIndexer, namespace)} +} + +// RegistryBindingNamespaceLister helps list and get RegistryBindings. +// All objects returned here must be treated as read-only. +type RegistryBindingNamespaceLister interface { + // List lists all RegistryBindings in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*solarv1alpha1.RegistryBinding, err error) + // Get retrieves the RegistryBinding from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*solarv1alpha1.RegistryBinding, error) + RegistryBindingNamespaceListerExpansion +} + +// registryBindingNamespaceLister implements the RegistryBindingNamespaceLister +// interface. +type registryBindingNamespaceLister struct { + listers.ResourceIndexer[*solarv1alpha1.RegistryBinding] +} diff --git a/client-go/listers/solar/v1alpha1/releasebinding.go b/client-go/listers/solar/v1alpha1/releasebinding.go new file mode 100644 index 00000000..b330e058 --- /dev/null +++ b/client-go/listers/solar/v1alpha1/releasebinding.go @@ -0,0 +1,57 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// ReleaseBindingLister helps list ReleaseBindings. +// All objects returned here must be treated as read-only. +type ReleaseBindingLister interface { + // List lists all ReleaseBindings in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*solarv1alpha1.ReleaseBinding, err error) + // ReleaseBindings returns an object that can list and get ReleaseBindings. + ReleaseBindings(namespace string) ReleaseBindingNamespaceLister + ReleaseBindingListerExpansion +} + +// releaseBindingLister implements the ReleaseBindingLister interface. +type releaseBindingLister struct { + listers.ResourceIndexer[*solarv1alpha1.ReleaseBinding] +} + +// NewReleaseBindingLister returns a new ReleaseBindingLister. +func NewReleaseBindingLister(indexer cache.Indexer) ReleaseBindingLister { + return &releaseBindingLister{listers.New[*solarv1alpha1.ReleaseBinding](indexer, solarv1alpha1.Resource("releasebinding"))} +} + +// ReleaseBindings returns an object that can list and get ReleaseBindings. +func (s *releaseBindingLister) ReleaseBindings(namespace string) ReleaseBindingNamespaceLister { + return releaseBindingNamespaceLister{listers.NewNamespaced[*solarv1alpha1.ReleaseBinding](s.ResourceIndexer, namespace)} +} + +// ReleaseBindingNamespaceLister helps list and get ReleaseBindings. +// All objects returned here must be treated as read-only. +type ReleaseBindingNamespaceLister interface { + // List lists all ReleaseBindings in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*solarv1alpha1.ReleaseBinding, err error) + // Get retrieves the ReleaseBinding from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*solarv1alpha1.ReleaseBinding, error) + ReleaseBindingNamespaceListerExpansion +} + +// releaseBindingNamespaceLister implements the ReleaseBindingNamespaceLister +// interface. +type releaseBindingNamespaceLister struct { + listers.ResourceIndexer[*solarv1alpha1.ReleaseBinding] +} diff --git a/client-go/listers/solar/v1alpha1/rendertask.go b/client-go/listers/solar/v1alpha1/rendertask.go index f9fca157..8cb50857 100644 --- a/client-go/listers/solar/v1alpha1/rendertask.go +++ b/client-go/listers/solar/v1alpha1/rendertask.go @@ -18,9 +18,8 @@ type RenderTaskLister interface { // List lists all RenderTasks in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*solarv1alpha1.RenderTask, err error) - // Get retrieves the RenderTask from the index for a given name. - // Objects returned here must be treated as read-only. - Get(name string) (*solarv1alpha1.RenderTask, error) + // RenderTasks returns an object that can list and get RenderTasks. + RenderTasks(namespace string) RenderTaskNamespaceLister RenderTaskListerExpansion } @@ -33,3 +32,26 @@ type renderTaskLister struct { func NewRenderTaskLister(indexer cache.Indexer) RenderTaskLister { return &renderTaskLister{listers.New[*solarv1alpha1.RenderTask](indexer, solarv1alpha1.Resource("rendertask"))} } + +// RenderTasks returns an object that can list and get RenderTasks. +func (s *renderTaskLister) RenderTasks(namespace string) RenderTaskNamespaceLister { + return renderTaskNamespaceLister{listers.NewNamespaced[*solarv1alpha1.RenderTask](s.ResourceIndexer, namespace)} +} + +// RenderTaskNamespaceLister helps list and get RenderTasks. +// All objects returned here must be treated as read-only. +type RenderTaskNamespaceLister interface { + // List lists all RenderTasks in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*solarv1alpha1.RenderTask, err error) + // Get retrieves the RenderTask from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*solarv1alpha1.RenderTask, error) + RenderTaskNamespaceListerExpansion +} + +// renderTaskNamespaceLister implements the RenderTaskNamespaceLister +// interface. +type renderTaskNamespaceLister struct { + listers.ResourceIndexer[*solarv1alpha1.RenderTask] +} diff --git a/client-go/openapi/api_violations.report b/client-go/openapi/api_violations.report index a4bd186e..d4061e2b 100644 --- a/client-go/openapi/api_violations.report +++ b/client-go/openapi/api_violations.report @@ -1,5 +1,3 @@ -API rule violation: list_type_missing,go.opendefense.cloud/solar/api/solar/v1alpha1,BootstrapStatus,Conditions -API rule violation: list_type_missing,go.opendefense.cloud/solar/api/solar/v1alpha1,Filter,RepositoryPatterns API rule violation: list_type_missing,go.opendefense.cloud/solar/api/solar/v1alpha1,ReleaseStatus,Conditions API rule violation: list_type_missing,go.opendefense.cloud/solar/api/solar/v1alpha1,RenderTaskStatus,Conditions API rule violation: names_match,go.opendefense.cloud/solar/api/solar/v1alpha1,RendererConfig,BootstrapConfig diff --git a/client-go/openapi/zz_generated.openapi.go b/client-go/openapi/zz_generated.openapi.go index 2c2f4c52..16372ee2 100644 --- a/client-go/openapi/zz_generated.openapi.go +++ b/client-go/openapi/zz_generated.openapi.go @@ -21,12 +21,8 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - v1alpha1.Bootstrap{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_Bootstrap(ref), v1alpha1.BootstrapConfig{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_BootstrapConfig(ref), v1alpha1.BootstrapInput{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_BootstrapInput(ref), - v1alpha1.BootstrapList{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_BootstrapList(ref), - v1alpha1.BootstrapSpec{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_BootstrapSpec(ref), - v1alpha1.BootstrapStatus{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_BootstrapStatus(ref), v1alpha1.ChartConfig{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ChartConfig(ref), v1alpha1.Component{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_Component(ref), v1alpha1.ComponentList{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ComponentList(ref), @@ -36,19 +32,25 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA v1alpha1.ComponentVersionList{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ComponentVersionList(ref), v1alpha1.ComponentVersionSpec{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ComponentVersionSpec(ref), v1alpha1.ComponentVersionStatus{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ComponentVersionStatus(ref), - v1alpha1.Discovery{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_Discovery(ref), - v1alpha1.DiscoveryList{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_DiscoveryList(ref), - v1alpha1.DiscoverySpec{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_DiscoverySpec(ref), - v1alpha1.DiscoveryStatus{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_DiscoveryStatus(ref), v1alpha1.Entrypoint{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_Entrypoint(ref), - v1alpha1.Filter{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_Filter(ref), v1alpha1.Profile{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_Profile(ref), v1alpha1.ProfileList{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ProfileList(ref), v1alpha1.ProfileSpec{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ProfileSpec(ref), v1alpha1.ProfileStatus{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ProfileStatus(ref), v1alpha1.PushResult{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_PushResult(ref), v1alpha1.Registry{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_Registry(ref), + v1alpha1.RegistryBinding{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_RegistryBinding(ref), + v1alpha1.RegistryBindingList{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_RegistryBindingList(ref), + v1alpha1.RegistryBindingSpec{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_RegistryBindingSpec(ref), + v1alpha1.RegistryBindingStatus{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_RegistryBindingStatus(ref), + v1alpha1.RegistryList{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_RegistryList(ref), + v1alpha1.RegistrySpec{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_RegistrySpec(ref), + v1alpha1.RegistryStatus{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_RegistryStatus(ref), v1alpha1.Release{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_Release(ref), + v1alpha1.ReleaseBinding{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ReleaseBinding(ref), + v1alpha1.ReleaseBindingList{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ReleaseBindingList(ref), + v1alpha1.ReleaseBindingSpec{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ReleaseBindingSpec(ref), + v1alpha1.ReleaseBindingStatus{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ReleaseBindingStatus(ref), v1alpha1.ReleaseComponent{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ReleaseComponent(ref), v1alpha1.ReleaseConfig{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ReleaseConfig(ref), v1alpha1.ReleaseInput{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ReleaseInput(ref), @@ -64,10 +66,9 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA v1alpha1.ResourceAccess{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_ResourceAccess(ref), v1alpha1.Target{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_Target(ref), v1alpha1.TargetList{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_TargetList(ref), + v1alpha1.TargetSecretReference{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_TargetSecretReference(ref), v1alpha1.TargetSpec{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_TargetSpec(ref), v1alpha1.TargetStatus{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_TargetStatus(ref), - v1alpha1.Webhook{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_Webhook(ref), - v1alpha1.WebhookAuth{}.OpenAPIModelName(): schema_solar_api_solar_v1alpha1_WebhookAuth(ref), v1.AWSElasticBlockStoreVolumeSource{}.OpenAPIModelName(): schema_k8sio_api_core_v1_AWSElasticBlockStoreVolumeSource(ref), v1.Affinity{}.OpenAPIModelName(): schema_k8sio_api_core_v1_Affinity(ref), v1.AppArmorProfile{}.OpenAPIModelName(): schema_k8sio_api_core_v1_AppArmorProfile(ref), @@ -362,53 +363,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA } } -func schema_solar_api_solar_v1alpha1_Bootstrap(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Bootstrap represents the entrypoint for the gitless gitops configuration. It resolves the implicit matching of profiles to produce a concrete set of releases and profiles.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - 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{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - 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{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(metav1.ObjectMeta{}.OpenAPIModelName()), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1alpha1.BootstrapSpec{}.OpenAPIModelName()), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1alpha1.BootstrapStatus{}.OpenAPIModelName()), - }, - }, - }, - }, - }, - Dependencies: []string{ - v1alpha1.BootstrapSpec{}.OpenAPIModelName(), v1alpha1.BootstrapStatus{}.OpenAPIModelName(), metav1.ObjectMeta{}.OpenAPIModelName()}, - } -} - func schema_solar_api_solar_v1alpha1_BootstrapConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -475,148 +429,6 @@ func schema_solar_api_solar_v1alpha1_BootstrapInput(ref common.ReferenceCallback } } -func schema_solar_api_solar_v1alpha1_BootstrapList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "BootstrapList contains a list of Bootstrap resources.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - 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{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - 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{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(metav1.ListMeta{}.OpenAPIModelName()), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1alpha1.Bootstrap{}.OpenAPIModelName()), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - v1alpha1.Bootstrap{}.OpenAPIModelName(), metav1.ListMeta{}.OpenAPIModelName()}, - } -} - -func schema_solar_api_solar_v1alpha1_BootstrapSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "BootstrapSpec defines the desired state of a Bootstrap. It contains the concrete releases, profiles, and deployment configuration for a target environment.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "releases": { - SchemaProps: spec.SchemaProps{ - Description: "Releases is a map of release names to their corresponding Release object references. Each entry represents a component release that will be deployed to the target.", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), - }, - }, - }, - }, - }, - "profiles": { - SchemaProps: spec.SchemaProps{ - Description: "Profiles is a map of profile names to their corresponding Profile object references. It points to profiles that match the target, e.g. through the label selector of the Profile", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), - }, - }, - }, - }, - }, - "userdata": { - SchemaProps: spec.SchemaProps{ - Description: "Userdata contains arbitrary custom data or configuration for the target deployment. This allows providing target-specific parameters or settings.", - Ref: ref(runtime.RawExtension{}.OpenAPIModelName()), - }, - }, - }, - Required: []string{"releases", "profiles"}, - }, - }, - Dependencies: []string{ - v1.LocalObjectReference{}.OpenAPIModelName(), runtime.RawExtension{}.OpenAPIModelName()}, - } -} - -func schema_solar_api_solar_v1alpha1_BootstrapStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "BootstrapStatus defines the observed state of a Bootstrap.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "conditions": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-patch-merge-key": "type", - "x-kubernetes-patch-strategy": "merge", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "Conditions represent the latest available observations of a Bootstrap's state.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(metav1.Condition{}.OpenAPIModelName()), - }, - }, - }, - }, - }, - "renderTaskRef": { - SchemaProps: spec.SchemaProps{ - Description: "RenderTaskRef is a reference to the RenderTask responsible for this Bootstrap.", - Ref: ref(v1.ObjectReference{}.OpenAPIModelName()), - }, - }, - }, - }, - }, - Dependencies: []string{ - v1.ObjectReference{}.OpenAPIModelName(), metav1.Condition{}.OpenAPIModelName()}, - } -} - func schema_solar_api_solar_v1alpha1_ChartConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -968,11 +780,42 @@ func schema_solar_api_solar_v1alpha1_ComponentVersionStatus(ref common.Reference } } -func schema_solar_api_solar_v1alpha1_Discovery(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_Entrypoint(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Entrypoint defines the entrypoint for deploying a ComponentVersion.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "resourceName": { + SchemaProps: spec.SchemaProps{ + Description: "ResourceName is the Name of the Resource to use as the entrypoint.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Type of entrypoint.\n\nPossible enum values:\n - `\"helm\"`\n - `\"kro\"`", + Default: "", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"helm", "kro"}, + }, + }, + }, + Required: []string{"resourceName", "type"}, + }, + }, + } +} + +func schema_solar_api_solar_v1alpha1_Profile(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "Discovery represents a configuration for a registry to discover.", + Description: "Profile represents the link between a Release and a set of matching Targets the Release is intended to be deployed to.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { @@ -998,28 +841,28 @@ func schema_solar_api_solar_v1alpha1_Discovery(ref common.ReferenceCallback) com "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref(v1alpha1.DiscoverySpec{}.OpenAPIModelName()), + Ref: ref(v1alpha1.ProfileSpec{}.OpenAPIModelName()), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref(v1alpha1.DiscoveryStatus{}.OpenAPIModelName()), + Ref: ref(v1alpha1.ProfileStatus{}.OpenAPIModelName()), }, }, }, }, }, Dependencies: []string{ - v1alpha1.DiscoverySpec{}.OpenAPIModelName(), v1alpha1.DiscoveryStatus{}.OpenAPIModelName(), metav1.ObjectMeta{}.OpenAPIModelName()}, + v1alpha1.ProfileSpec{}.OpenAPIModelName(), v1alpha1.ProfileStatus{}.OpenAPIModelName(), metav1.ObjectMeta{}.OpenAPIModelName()}, } } -func schema_solar_api_solar_v1alpha1_DiscoveryList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_ProfileList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "DiscoveryList contains a list of Discovery resources.", + Description: "ProfileList contains a list of Profile resources.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { @@ -1049,7 +892,7 @@ func schema_solar_api_solar_v1alpha1_DiscoveryList(ref common.ReferenceCallback) Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref(v1alpha1.Discovery{}.OpenAPIModelName()), + Ref: ref(v1alpha1.Profile{}.OpenAPIModelName()), }, }, }, @@ -1060,145 +903,509 @@ func schema_solar_api_solar_v1alpha1_DiscoveryList(ref common.ReferenceCallback) }, }, Dependencies: []string{ - v1alpha1.Discovery{}.OpenAPIModelName(), metav1.ListMeta{}.OpenAPIModelName()}, + v1alpha1.Profile{}.OpenAPIModelName(), metav1.ListMeta{}.OpenAPIModelName()}, } } -func schema_solar_api_solar_v1alpha1_DiscoverySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_ProfileSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "DiscoverySpec defines the desired state of a Discovery.", + Description: "ProfileSpec defines the desired state of a Profile. It points to a Release and defines target selection criteria for Targets this Release is intended to be deployed to.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "registry": { + "releaseRef": { SchemaProps: spec.SchemaProps{ - Description: "Registry specifies the registry that should be scanned by the discovery process.", + Description: "ReleaseRef is a reference to a Release. It points to the Release that is intended to be deployed to all Targets identified by the TargetSelector.", Default: map[string]interface{}{}, - Ref: ref(v1alpha1.Registry{}.OpenAPIModelName()), - }, - }, - "webhook": { - SchemaProps: spec.SchemaProps{ - Description: "Webhook specifies the configuration for a webhook that is called by the registry on created, updated or deleted images/repositories.", - Ref: ref(v1alpha1.Webhook{}.OpenAPIModelName()), - }, - }, - "filter": { - SchemaProps: spec.SchemaProps{ - Description: "Filter specifies the filter that should be applied when scanning for components. If not specified, all components will be scanned.", - Ref: ref(v1alpha1.Filter{}.OpenAPIModelName()), + Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), }, }, - "discoveryInterval": { + "targetSelector": { SchemaProps: spec.SchemaProps{ - Description: "DiscoveryInterval is the amount of time between two full scans of the registry. Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\" May be set to zero to fetch and create it once. Defaults to 24h.", - Ref: ref(metav1.Duration{}.OpenAPIModelName()), + Description: "TargetSelector is a label-based filter to identify the Targets this Release is intended to be deployed to.", + Default: map[string]interface{}{}, + Ref: ref(metav1.LabelSelector{}.OpenAPIModelName()), }, }, - "disableStartupDiscovery": { + "userdata": { SchemaProps: spec.SchemaProps{ - Description: "DisableStartupDiscovery defines whether the discovery should not be run on startup of the discovery process. If true it will only run on schedule, see .spec.cron.", - Type: []string{"boolean"}, - Format: "", + Description: "Userdata contains arbitrary custom data or configuration which is passed to all Targets associated with this Profile.", + Ref: ref(runtime.RawExtension{}.OpenAPIModelName()), }, }, }, - Required: []string{"registry"}, + Required: []string{"releaseRef"}, }, }, Dependencies: []string{ - v1alpha1.Filter{}.OpenAPIModelName(), v1alpha1.Registry{}.OpenAPIModelName(), v1alpha1.Webhook{}.OpenAPIModelName(), metav1.Duration{}.OpenAPIModelName()}, + v1.LocalObjectReference{}.OpenAPIModelName(), metav1.LabelSelector{}.OpenAPIModelName(), runtime.RawExtension{}.OpenAPIModelName()}, } } -func schema_solar_api_solar_v1alpha1_DiscoveryStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_ProfileStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "DiscoveryStatus defines the observed state of a Discovery.", + Description: "ProfileStatus defines the observed state of a Profile.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "podGeneration": { + "matchedTargets": { SchemaProps: spec.SchemaProps{ - Description: "PodGeneration is the generation of the discovery object at the time the worker was instantiated.", - Default: 0, + Description: "MatchedTargets is the total number of Targets matching the target selection criteria.", Type: []string{"integer"}, - Format: "int64", + Format: "int32", }, }, - }, - Required: []string{"podGeneration"}, - }, - }, - } -} - -func schema_solar_api_solar_v1alpha1_Entrypoint(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Entrypoint defines the entrypoint for deploying a ComponentVersion.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "resourceName": { - SchemaProps: spec.SchemaProps{ - Description: "ResourceName is the Name of the Resource to use as the entrypoint.", - Default: "", + "conditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "type", + }, + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "type", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Conditions represent the latest available observations of the Profile's state.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(metav1.Condition{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + metav1.Condition{}.OpenAPIModelName()}, + } +} + +func schema_solar_api_solar_v1alpha1_PushResult(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PushResult contains the result of a push operation.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "ref": { + SchemaProps: spec.SchemaProps{ + Description: "Ref is the full OCI reference of the pushed chart", + Default: "", Type: []string{"string"}, Format: "", }, }, - "type": { + }, + Required: []string{"ref"}, + }, + }, + } +} + +func schema_solar_api_solar_v1alpha1_Registry(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Registry represents an OCI registry that can be used as a source or destination for artifacts.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { SchemaProps: spec.SchemaProps{ - Description: "Type of entrypoint.\n\nPossible enum values:\n - `\"helm\"`\n - `\"kro\"`", + 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{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(metav1.ObjectMeta{}.OpenAPIModelName()), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1alpha1.RegistrySpec{}.OpenAPIModelName()), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1alpha1.RegistryStatus{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + Dependencies: []string{ + v1alpha1.RegistrySpec{}.OpenAPIModelName(), v1alpha1.RegistryStatus{}.OpenAPIModelName(), metav1.ObjectMeta{}.OpenAPIModelName()}, + } +} + +func schema_solar_api_solar_v1alpha1_RegistryBinding(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "RegistryBinding declares that a specific Target is allowed to use a specific Registry.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(metav1.ObjectMeta{}.OpenAPIModelName()), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1alpha1.RegistryBindingSpec{}.OpenAPIModelName()), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1alpha1.RegistryBindingStatus{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + Dependencies: []string{ + v1alpha1.RegistryBindingSpec{}.OpenAPIModelName(), v1alpha1.RegistryBindingStatus{}.OpenAPIModelName(), metav1.ObjectMeta{}.OpenAPIModelName()}, + } +} + +func schema_solar_api_solar_v1alpha1_RegistryBindingList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "RegistryBindingList contains a list of RegistryBinding resources.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(metav1.ListMeta{}.OpenAPIModelName()), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1alpha1.RegistryBinding{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + v1alpha1.RegistryBinding{}.OpenAPIModelName(), metav1.ListMeta{}.OpenAPIModelName()}, + } +} + +func schema_solar_api_solar_v1alpha1_RegistryBindingSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "RegistryBindingSpec defines the desired state of a RegistryBinding.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "targetRef": { + SchemaProps: spec.SchemaProps{ + Description: "TargetRef references the Target this binding applies to.", + Default: map[string]interface{}{}, + Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), + }, + }, + "registryRef": { + SchemaProps: spec.SchemaProps{ + Description: "RegistryRef references the Registry being bound.", + Default: map[string]interface{}{}, + Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), + }, + }, + }, + Required: []string{"targetRef", "registryRef"}, + }, + }, + Dependencies: []string{ + v1.LocalObjectReference{}.OpenAPIModelName()}, + } +} + +func schema_solar_api_solar_v1alpha1_RegistryBindingStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "RegistryBindingStatus defines the observed state of a RegistryBinding.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "conditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "type", + }, + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "type", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Conditions represent the latest available observations of a RegistryBinding's state.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(metav1.Condition{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + metav1.Condition{}.OpenAPIModelName()}, + } +} + +func schema_solar_api_solar_v1alpha1_RegistryList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "RegistryList contains a list of Registry resources.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(metav1.ListMeta{}.OpenAPIModelName()), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1alpha1.Registry{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + v1alpha1.Registry{}.OpenAPIModelName(), metav1.ListMeta{}.OpenAPIModelName()}, + } +} + +func schema_solar_api_solar_v1alpha1_RegistrySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "RegistrySpec defines the desired state of a Registry.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "hostname": { + SchemaProps: spec.SchemaProps{ + Description: "Hostname is the registry endpoint (e.g. \"registry.example.com:5000\").", Default: "", Type: []string{"string"}, Format: "", - Enum: []interface{}{"helm", "kro"}, + }, + }, + "plainHTTP": { + SchemaProps: spec.SchemaProps{ + Description: "PlainHTTP uses HTTP instead of HTTPS for connections to this registry.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "solarSecretRef": { + SchemaProps: spec.SchemaProps{ + Description: "SolarSecretRef references a Secret in the same namespace with credentials to access this registry from the SolAr cluster. Required if this registry is used as a render target.", + Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), + }, + }, + "targetSecretRef": { + SchemaProps: spec.SchemaProps{ + Description: "TargetSecretRef describes where the credentials secret lives in the target cluster. Used by the target agent for pull access.", + Ref: ref(v1alpha1.TargetSecretReference{}.OpenAPIModelName()), + }, + }, + }, + Required: []string{"hostname"}, + }, + }, + Dependencies: []string{ + v1alpha1.TargetSecretReference{}.OpenAPIModelName(), v1.LocalObjectReference{}.OpenAPIModelName()}, + } +} + +func schema_solar_api_solar_v1alpha1_RegistryStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "RegistryStatus defines the observed state of a Registry.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "conditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "type", + }, + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "type", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Conditions represent the latest available observations of a Registry's state.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(metav1.Condition{}.OpenAPIModelName()), + }, + }, + }, }, }, }, - Required: []string{"resourceName", "type"}, }, }, + Dependencies: []string{ + metav1.Condition{}.OpenAPIModelName()}, } } -func schema_solar_api_solar_v1alpha1_Filter(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_Release(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "Filter defines the filter criteria used to determine which components should be scanned.", + Description: "Release represents a specific deployment instance of a component. It combines a component version with deployment values and configuration for a particular use case.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "repositoryPatterns": { + "kind": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(metav1.ObjectMeta{}.OpenAPIModelName()), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1alpha1.ReleaseSpec{}.OpenAPIModelName()), + }, + }, + "status": { SchemaProps: spec.SchemaProps{ - Description: "RepositoryPatterns defines which repositories should be scanned for components. The default value is empty, which means that all repositories will be scanned. Wildcards are supported, e.g. \"foo-*\" or \"*-dev\".", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, + Default: map[string]interface{}{}, + Ref: ref(v1alpha1.ReleaseStatus{}.OpenAPIModelName()), }, }, }, - Required: []string{"repositoryPatterns"}, }, }, + Dependencies: []string{ + v1alpha1.ReleaseSpec{}.OpenAPIModelName(), v1alpha1.ReleaseStatus{}.OpenAPIModelName(), metav1.ObjectMeta{}.OpenAPIModelName()}, } } -func schema_solar_api_solar_v1alpha1_Profile(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_ReleaseBinding(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "Profile represents the link between a Release and a set of matching Targets the Release is intended to be deployed to.", + Description: "ReleaseBinding declares that a Release should be deployed to a Target.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { @@ -1224,28 +1431,28 @@ func schema_solar_api_solar_v1alpha1_Profile(ref common.ReferenceCallback) commo "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref(v1alpha1.ProfileSpec{}.OpenAPIModelName()), + Ref: ref(v1alpha1.ReleaseBindingSpec{}.OpenAPIModelName()), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref(v1alpha1.ProfileStatus{}.OpenAPIModelName()), + Ref: ref(v1alpha1.ReleaseBindingStatus{}.OpenAPIModelName()), }, }, }, }, }, Dependencies: []string{ - v1alpha1.ProfileSpec{}.OpenAPIModelName(), v1alpha1.ProfileStatus{}.OpenAPIModelName(), metav1.ObjectMeta{}.OpenAPIModelName()}, + v1alpha1.ReleaseBindingSpec{}.OpenAPIModelName(), v1alpha1.ReleaseBindingStatus{}.OpenAPIModelName(), metav1.ObjectMeta{}.OpenAPIModelName()}, } } -func schema_solar_api_solar_v1alpha1_ProfileList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_ReleaseBindingList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "ProfileList contains a list of Profile resources.", + Description: "ReleaseBindingList contains a list of ReleaseBinding resources.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { @@ -1275,7 +1482,7 @@ func schema_solar_api_solar_v1alpha1_ProfileList(ref common.ReferenceCallback) c Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref(v1alpha1.Profile{}.OpenAPIModelName()), + Ref: ref(v1alpha1.ReleaseBinding{}.OpenAPIModelName()), }, }, }, @@ -1286,60 +1493,47 @@ func schema_solar_api_solar_v1alpha1_ProfileList(ref common.ReferenceCallback) c }, }, Dependencies: []string{ - v1alpha1.Profile{}.OpenAPIModelName(), metav1.ListMeta{}.OpenAPIModelName()}, + v1alpha1.ReleaseBinding{}.OpenAPIModelName(), metav1.ListMeta{}.OpenAPIModelName()}, } } -func schema_solar_api_solar_v1alpha1_ProfileSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_ReleaseBindingSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "ProfileSpec defines the desired state of a Profile. It points to a Release and defines target selection criteria for Targets this Release is intended to be deployed to.", + Description: "ReleaseBindingSpec defines the desired state of a ReleaseBinding.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "releaseRef": { + "targetRef": { SchemaProps: spec.SchemaProps{ - Description: "ReleaseRef is a reference to a Release. It points to the Release that is intended to be deployed to all Targets identified by the TargetSelector.", + Description: "TargetRef references the Target this release is bound to.", Default: map[string]interface{}{}, Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), }, }, - "targetSelector": { + "releaseRef": { SchemaProps: spec.SchemaProps{ - Description: "TargetSelector is a label-based filter to identify the Targets this Release is intended to be deployed to.", + Description: "ReleaseRef references the Release to deploy.", Default: map[string]interface{}{}, - Ref: ref(metav1.LabelSelector{}.OpenAPIModelName()), - }, - }, - "userdata": { - SchemaProps: spec.SchemaProps{ - Description: "Userdata contains arbitrary custom data or configuration which is passed to all Targets associated with this Profile.", - Ref: ref(runtime.RawExtension{}.OpenAPIModelName()), + Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), }, }, }, - Required: []string{"releaseRef"}, + Required: []string{"targetRef", "releaseRef"}, }, }, Dependencies: []string{ - v1.LocalObjectReference{}.OpenAPIModelName(), metav1.LabelSelector{}.OpenAPIModelName(), runtime.RawExtension{}.OpenAPIModelName()}, + v1.LocalObjectReference{}.OpenAPIModelName()}, } } -func schema_solar_api_solar_v1alpha1_ProfileStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_ReleaseBindingStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "ProfileStatus defines the observed state of a Profile.", + Description: "ReleaseBindingStatus defines the observed state of a ReleaseBinding.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "matchedTargets": { - SchemaProps: spec.SchemaProps{ - Description: "MatchedTargets is the total number of Targets matching the target selection criteria.", - Type: []string{"integer"}, - Format: "int32", - }, - }, "conditions": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ @@ -1352,7 +1546,7 @@ func schema_solar_api_solar_v1alpha1_ProfileStatus(ref common.ReferenceCallback) }, }, SchemaProps: spec.SchemaProps{ - Description: "Conditions represent the latest available observations of the Profile's state.", + Description: "Conditions represent the latest available observations of a ReleaseBinding's state.", Type: []string{"array"}, Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ @@ -1372,120 +1566,6 @@ func schema_solar_api_solar_v1alpha1_ProfileStatus(ref common.ReferenceCallback) } } -func schema_solar_api_solar_v1alpha1_PushResult(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "PushResult contains the result of a push operation.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "ref": { - SchemaProps: spec.SchemaProps{ - Description: "Ref is the full OCI reference of the pushed chart", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"ref"}, - }, - }, - } -} - -func schema_solar_api_solar_v1alpha1_Registry(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Registry defines the configuration for a registry.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "endpoint": { - SchemaProps: spec.SchemaProps{ - Description: "Endpoint is the hostname (and optionally port) of the registry, e.g. \"registry.example.com\" or \"registry.example.com:443\". This must not include a scheme (use PlainHTTP to control HTTP vs HTTPS).", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "secretRef": { - SchemaProps: spec.SchemaProps{ - Description: "SecretRef specifies the secret containing the relevant credentials for the registry that should be used during discovery.", - Default: map[string]interface{}{}, - Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), - }, - }, - "caConfigMapRef": { - SchemaProps: spec.SchemaProps{ - Description: "CAConfigMapRef contains CA bundle for registry connections (e.g., trust-manager's root-bundle). Key is expected to be \"trust-bundle.pem\".", - Default: map[string]interface{}{}, - Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), - }, - }, - "plainHTTP": { - SchemaProps: spec.SchemaProps{ - Description: "PlainHTTP defines whether the registry should be accessed via plain HTTP instead of HTTPS.", - Type: []string{"boolean"}, - Format: "", - }, - }, - }, - Required: []string{"endpoint"}, - }, - }, - Dependencies: []string{ - v1.LocalObjectReference{}.OpenAPIModelName()}, - } -} - -func schema_solar_api_solar_v1alpha1_Release(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Release represents a specific deployment instance of a component. It combines a component version with deployment values and configuration for a particular use case.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - 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{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - 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{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(metav1.ObjectMeta{}.OpenAPIModelName()), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1alpha1.ReleaseSpec{}.OpenAPIModelName()), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1alpha1.ReleaseStatus{}.OpenAPIModelName()), - }, - }, - }, - }, - }, - Dependencies: []string{ - v1alpha1.ReleaseSpec{}.OpenAPIModelName(), v1alpha1.ReleaseStatus{}.OpenAPIModelName(), metav1.ObjectMeta{}.OpenAPIModelName()}, - } -} - func schema_solar_api_solar_v1alpha1_ReleaseComponent(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1872,7 +1952,7 @@ func schema_solar_api_solar_v1alpha1_RenderTaskSpec(ref common.ReferenceCallback }, "repository": { SchemaProps: spec.SchemaProps{ - Description: "Repository is the Repository where the chart will be pushed to (e.g. charts/mychart) Keep in mind that the repository gets automatically prefixed with the registry by the rendertask-controller.", + Description: "Repository is the Repository where the chart will be pushed to (e.g. charts/mychart)", Default: "", Type: []string{"string"}, Format: "", @@ -1886,6 +1966,20 @@ func schema_solar_api_solar_v1alpha1_RenderTaskSpec(ref common.ReferenceCallback Format: "", }, }, + "baseURL": { + SchemaProps: spec.SchemaProps{ + Description: "BaseURL is the registry URL to push the rendered chart to (e.g. \"registry.example.com:5000\").", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "pushSecretRef": { + SchemaProps: spec.SchemaProps{ + Description: "PushSecretRef references a Secret in the same namespace with registry credentials for pushing the rendered chart.", + Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), + }, + }, "failedJobTTL": { SchemaProps: spec.SchemaProps{ Description: "failedJobTTL is the TTL in seconds after which a failed render job and its secrets are cleaned up. After this duration, the Kubernetes TTL controller will delete the Job and the controller will delete the Secrets (ConfigSecret, AuthSecret). On success, Job and Secrets are deleted immediately. If not set, defaults to 3600 (1 hour).", @@ -1911,18 +2005,18 @@ func schema_solar_api_solar_v1alpha1_RenderTaskSpec(ref common.ReferenceCallback }, "ownerKind": { SchemaProps: spec.SchemaProps{ - Description: "OwnerKind is the kind of the resource that created this RenderTask (e.g. Release, Bootstrap).", + Description: "OwnerKind is the kind of the resource that created this RenderTask (e.g. Release, Target).", Default: "", Type: []string{"string"}, Format: "", }, }, }, - Required: []string{"type", "release", "bootstrap", "repository", "tag", "ownerName", "ownerNamespace", "ownerKind"}, + Required: []string{"type", "release", "bootstrap", "repository", "tag", "baseURL", "ownerName", "ownerNamespace", "ownerKind"}, }, }, Dependencies: []string{ - v1alpha1.BootstrapConfig{}.OpenAPIModelName(), v1alpha1.ReleaseConfig{}.OpenAPIModelName()}, + v1alpha1.BootstrapConfig{}.OpenAPIModelName(), v1alpha1.ReleaseConfig{}.OpenAPIModelName(), v1.LocalObjectReference{}.OpenAPIModelName()}, } } @@ -2154,116 +2248,108 @@ func schema_solar_api_solar_v1alpha1_TargetList(ref common.ReferenceCallback) co } } -func schema_solar_api_solar_v1alpha1_TargetSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_TargetSecretReference(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "TargetSpec defines the desired state of a Target. It specifies the releases and configuration intended for this deployment target.", + Description: "TargetSecretReference is a reference to a Secret in a target cluster.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "releases": { + "name": { SchemaProps: spec.SchemaProps{ - Description: "Releases is a map of release names to their corresponding Release object references. Each entry represents a component release intended for deployment on this target.", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), - }, - }, - }, + Description: "Name is the name of the Secret.", + Default: "", + Type: []string{"string"}, + Format: "", }, }, - "userdata": { + "namespace": { SchemaProps: spec.SchemaProps{ - Description: "Userdata contains arbitrary custom data or configuration specific to this target. This enables target-specific customization and deployment parameters.", - Ref: ref(runtime.RawExtension{}.OpenAPIModelName()), + Description: "Namespace is the namespace of the Secret.", + Default: "", + Type: []string{"string"}, + Format: "", }, }, }, - Required: []string{"releases"}, - }, - }, - Dependencies: []string{ - v1.LocalObjectReference{}.OpenAPIModelName(), runtime.RawExtension{}.OpenAPIModelName()}, - } -} - -func schema_solar_api_solar_v1alpha1_TargetStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "TargetStatus defines the observed state of a Target.", - Type: []string{"object"}, + Required: []string{"name", "namespace"}, }, }, } } -func schema_solar_api_solar_v1alpha1_Webhook(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_TargetSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "Webhook represents the configuration for a webhook.", + Description: "TargetSpec defines the desired state of a Target. It specifies the render registry and configuration for this deployment target.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "flavor": { - SchemaProps: spec.SchemaProps{ - Description: "Flavor is the webhook implementation to use.", - Type: []string{"string"}, - Format: "", - }, - }, - "path": { + "renderRegistryRef": { SchemaProps: spec.SchemaProps{ - Description: "Path is where the webhook should listen.", - Type: []string{"string"}, - Format: "", + Description: "RenderRegistryRef references the Registry to push rendered desired state to. The referenced Registry must have SolarSecretRef set for rendering to succeed.", + Default: map[string]interface{}{}, + Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), }, }, - "auth": { + "userdata": { SchemaProps: spec.SchemaProps{ - Description: "Auth is the authentication information to use with the webhook.", - Default: map[string]interface{}{}, - Ref: ref(v1alpha1.WebhookAuth{}.OpenAPIModelName()), + Description: "Userdata contains arbitrary custom data or configuration specific to this target. This enables target-specific customization and deployment parameters.", + Ref: ref(runtime.RawExtension{}.OpenAPIModelName()), }, }, }, + Required: []string{"renderRegistryRef"}, }, }, Dependencies: []string{ - v1alpha1.WebhookAuth{}.OpenAPIModelName()}, + v1.LocalObjectReference{}.OpenAPIModelName(), runtime.RawExtension{}.OpenAPIModelName()}, } } -func schema_solar_api_solar_v1alpha1_WebhookAuth(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_solar_api_solar_v1alpha1_TargetStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, + Description: "TargetStatus defines the observed state of a Target.", + Type: []string{"object"}, Properties: map[string]spec.Schema{ - "type": { + "bootstrapVersion": { SchemaProps: spec.SchemaProps{ - Description: "Type represents the type of authentication to use. Currently, only \"token\" is supported.\n\nPossible enum values:\n - `\"Basic\"`\n - `\"Token\"`", - Type: []string{"string"}, - Format: "", - Enum: []interface{}{"Basic", "Token"}, + Description: "BootstrapVersion is a monotonically increasing counter used as the bootstrap chart version. It is incremented each time the bootstrap chart is re-rendered, e.g. when the set of bound releases changes.", + Type: []string{"integer"}, + Format: "int64", }, }, - "authSecretRef": { + "conditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "type", + }, + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "type", + "x-kubernetes-patch-strategy": "merge", + }, + }, SchemaProps: spec.SchemaProps{ - Description: "AuthSecretRef is the reference to the secret which contains the authentication information for the webhook.", - Default: map[string]interface{}{}, - Ref: ref(v1.LocalObjectReference{}.OpenAPIModelName()), + Description: "Conditions represent the latest available observations of a Target's state.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(metav1.Condition{}.OpenAPIModelName()), + }, + }, + }, }, }, }, }, }, Dependencies: []string{ - v1.LocalObjectReference{}.OpenAPIModelName()}, + metav1.Condition{}.OpenAPIModelName()}, } } diff --git a/cmd/solar-apiserver/apiserver_test.go b/cmd/solar-apiserver/apiserver_test.go index 53fd31a0..d52385e3 100644 --- a/cmd/solar-apiserver/apiserver_test.go +++ b/cmd/solar-apiserver/apiserver_test.go @@ -122,29 +122,85 @@ var _ = Describe("Target", func() { }) }) -var _ = Describe("Bootstrap", func() { +var _ = Describe("Registry", func() { var ( - ctx = envtest.Context() - ns = SetupTest(ctx) - bootstrap = &solarv1alpha1.Bootstrap{} + ctx = envtest.Context() + ns = SetupTest(ctx) + reg = &solarv1alpha1.Registry{} + ) + + Context("Registry", func() { + It("should allow creating a registry", func() { + By("creating a test registry") + reg = &solarv1alpha1.Registry{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + GenerateName: "test-", + }, + Spec: solarv1alpha1.RegistrySpec{ + Hostname: "registry.example.com", + }, + } + Expect(k8sClient.Create(ctx, reg)).To(Succeed()) + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(reg), reg)).To(Succeed()) + }) + It("should allow deleting a registry", func() { + By("deleting a test registry") + Expect(k8sClient.Delete(ctx, reg)).To(Succeed()) + }) + }) +}) + +var _ = Describe("RegistryBinding", func() { + var ( + ctx = envtest.Context() + ns = SetupTest(ctx) + rb = &solarv1alpha1.RegistryBinding{} + ) + + Context("RegistryBinding", func() { + It("should allow creating a registry binding", func() { + By("creating a test registry binding") + rb = &solarv1alpha1.RegistryBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + GenerateName: "test-", + }, + Spec: solarv1alpha1.RegistryBindingSpec{}, + } + Expect(k8sClient.Create(ctx, rb)).To(Succeed()) + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(rb), rb)).To(Succeed()) + }) + It("should allow deleting a registry binding", func() { + By("deleting a test registry binding") + Expect(k8sClient.Delete(ctx, rb)).To(Succeed()) + }) + }) +}) + +var _ = Describe("ReleaseBinding", func() { + var ( + ctx = envtest.Context() + ns = SetupTest(ctx) + rlb = &solarv1alpha1.ReleaseBinding{} ) - Context("Bootstrap", func() { - It("should allow creating a bootstrap", func() { - By("creating a test bootstrap") - bootstrap = &solarv1alpha1.Bootstrap{ + Context("ReleaseBinding", func() { + It("should allow creating a release binding", func() { + By("creating a test release binding") + rlb = &solarv1alpha1.ReleaseBinding{ ObjectMeta: metav1.ObjectMeta{ Namespace: ns.Name, GenerateName: "test-", }, - Spec: solarv1alpha1.BootstrapSpec{}, + Spec: solarv1alpha1.ReleaseBindingSpec{}, } - Expect(k8sClient.Create(ctx, bootstrap)).To(Succeed()) - Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(bootstrap), bootstrap)).To(Succeed()) + Expect(k8sClient.Create(ctx, rlb)).To(Succeed()) + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(rlb), rlb)).To(Succeed()) }) - It("should allow deleting a bootstrap", func() { - By("deleting a test bootstrap") - Expect(k8sClient.Delete(ctx, bootstrap)).To(Succeed()) + It("should allow deleting a release binding", func() { + By("deleting a test release binding") + Expect(k8sClient.Delete(ctx, rlb)).To(Succeed()) }) }) }) diff --git a/cmd/solar-apiserver/main.go b/cmd/solar-apiserver/main.go index 35680125..6a611086 100644 --- a/cmd/solar-apiserver/main.go +++ b/cmd/solar-apiserver/main.go @@ -46,12 +46,13 @@ func main() { code := apiserver.NewBuilder(scheme). WithComponentName(componentName). WithOpenAPIDefinitions(componentName, "v0.1.0", openapi.GetOpenAPIDefinitions). - With(apiserver.Resource(&solar.Discovery{}, solarv1alpha1.SchemeGroupVersion)). With(apiserver.Resource(&solar.Component{}, solarv1alpha1.SchemeGroupVersion)). With(apiserver.Resource(&solar.ComponentVersion{}, solarv1alpha1.SchemeGroupVersion)). With(apiserver.Resource(&solar.Release{}, solarv1alpha1.SchemeGroupVersion)). + With(apiserver.Resource(&solar.ReleaseBinding{}, solarv1alpha1.SchemeGroupVersion)). + With(apiserver.Resource(&solar.Registry{}, solarv1alpha1.SchemeGroupVersion)). + With(apiserver.Resource(&solar.RegistryBinding{}, solarv1alpha1.SchemeGroupVersion)). With(apiserver.Resource(&solar.Target{}, solarv1alpha1.SchemeGroupVersion)). - With(apiserver.Resource(&solar.Bootstrap{}, solarv1alpha1.SchemeGroupVersion)). With(apiserver.Resource(&solar.RenderTask{}, solarv1alpha1.SchemeGroupVersion)). With(apiserver.Resource(&solar.Profile{}, solarv1alpha1.SchemeGroupVersion)). Execute() diff --git a/cmd/solar-controller-manager/main.go b/cmd/solar-controller-manager/main.go index b0d50b8e..60679b61 100644 --- a/cmd/solar-controller-manager/main.go +++ b/cmd/solar-controller-manager/main.go @@ -12,7 +12,6 @@ import ( "strings" "time" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -55,13 +54,9 @@ func main() { virtualIPBindTimeout time.Duration networkInterfaceBindTimeout time.Duration tlsOpts []func(*tls.Config) - workerImage, workerCommand string rendererImage, rendererCommand string rendererArgs string - rendererBaseURL string rendererCAConfigMap string - rendererPushSecretName string - podNS string ) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ @@ -91,24 +86,14 @@ func main() { "Time to wait until considering a virtual ip bind to be failed.") flag.DurationVar(&networkInterfaceBindTimeout, "network-interface-bind-timeout", 10*time.Second, "Time to wait until considering a network interface bind to be failed.") - flag.StringVar(&workerImage, "discovery-worker-image", "ghcr.io/opendefensecloud/solar-discovery-worker:latest", - "The image of the discovery worker container.") - flag.StringVar(&workerCommand, "discovery-worker-command", "/solar-discovery-worker", - "The command of the discovery worker container.") flag.StringVar(&rendererImage, "renderer-image", "ghcr.io/opendefensecloud/solar-renderer:latest", "The image for renderer containers.") flag.StringVar(&rendererCommand, "renderer-command", "/solar-renderer", "The command for renderer containers.") - flag.StringVar(&rendererBaseURL, "renderer-base-url", "", - "The url to push rendered objects to.") flag.StringVar(&rendererCAConfigMap, "renderer-ca-configmap", "", "ConfigMap name containing CA bundle for registry connections.") flag.StringVar(&rendererArgs, "renderer-args", "", "Comma separated list of additional args for the renderer cli.") - flag.StringVar(&rendererPushSecretName, "renderer-push-secret-name", "", - "Name of the secret in each namespace containing credential information.") - flag.StringVar(&podNS, "namespace", "default", - "Namespace the controller-manager pod is running in.") flag.Parse() opts := zap.Options{ @@ -210,22 +195,12 @@ func main() { } // Register field indexers (must be done before controller setup) - if err := controller.IndexRenderTaskOwnerFields(context.Background(), mgr); err != nil { + if err := controller.IndexFields(context.Background(), mgr); err != nil { setupLog.Error(err, "unable to register field indexers") os.Exit(1) } // Register controllers - if err := (&controller.DiscoveryReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorder("discovery-controller"), - WorkerImage: workerImage, - WorkerCommand: workerCommand, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "discovery") - os.Exit(1) - } if err := (&controller.TargetReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -234,14 +209,6 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "target") os.Exit(1) } - if err := (&controller.BootstrapReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorder("bootstrap-controller"), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "bootstrap") - os.Exit(1) - } if err := (&controller.ReleaseReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -251,15 +218,6 @@ func main() { os.Exit(1) } - var rendererPushSecretRef *corev1.SecretReference - if rendererPushSecretName != "" { - rendererPushSecretRef = &corev1.SecretReference{ - Name: rendererPushSecretName, - Namespace: podNS, - } - } else { - setupLog.Info("no push credentials were configured, continuing to start the controller without authentication", "controller", "rendertask") - } // strings.Split("", ",") returns [""], not [], so we need to handle empty string specially // to avoid passing an empty arg to the renderer CLI var rendererArgsSlice []string @@ -273,15 +231,21 @@ func main() { RendererImage: rendererImage, RendererCommand: rendererCommand, RendererArgs: rendererArgsSlice, - PushSecretRef: rendererPushSecretRef, - BaseURL: rendererBaseURL, RendererCAConfigMap: rendererCAConfigMap, - Namespace: podNS, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "rendertask") os.Exit(1) } + if err := (&controller.ProfileReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorder("profile-controller"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "profile") + os.Exit(1) + } + // healthz / readyz setup if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/cmd/solar-discovery-worker/main.go b/cmd/solar-discovery/main.go similarity index 100% rename from cmd/solar-discovery-worker/main.go rename to cmd/solar-discovery/main.go diff --git a/cmd/solar-renderer/main.go b/cmd/solar-renderer/main.go index 47ed8b9b..d3fed883 100644 --- a/cmd/solar-renderer/main.go +++ b/cmd/solar-renderer/main.go @@ -37,38 +37,75 @@ func rootFunc(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to parse config-file: %w", err) } - var result *solarv1alpha1.RenderResult + if skipPush { + return renderOnly(cmd, config) + } + + if passwordStdIn { + if _, err := fmt.Scanln(&password); err != nil { + return err + } + } + + pushOpts := buildPushOptions() + + // Check if the chart already exists in the registry before doing any work. + // This allows multiple targets sharing the same release to create their own + // RenderTasks without redundant rendering and pushing. + exists, err := renderer.ChartExists(pushOpts) + if err != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Could not check for existing chart, proceeding with render: %v\n", err) + } else if exists { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Chart already exists at %s, skipping render and push\n", url) + + return nil + } + result, err := render(config) + if err != nil { + return err + } + defer func() { _ = result.Close() }() + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Rendered %s to %s\n", config.Type, result.Dir) + + pushResult, err := renderer.PushChart(result, pushOpts) + if err != nil { + return fmt.Errorf("failed to push result: %w", err) + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Pushed result to %s\n", pushResult.Ref) + + return nil +} + +func render(config solarv1alpha1.RendererConfig) (*solarv1alpha1.RenderResult, error) { switch config.Type { case solarv1alpha1.RendererConfigTypeRelease: - result, err = renderer.RenderRelease(config.ReleaseConfig) + return renderer.RenderRelease(config.ReleaseConfig) case solarv1alpha1.RendererConfigTypeBootstrap: - result, err = renderer.RenderBootstrap(config.BootstrapConfig) + return renderer.RenderBootstrap(config.BootstrapConfig) default: - return fmt.Errorf("unknown type specified in config: %s", config.Type) + return nil, fmt.Errorf("unknown type specified in config: %s", config.Type) } +} + +func renderOnly(cmd *cobra.Command, config solarv1alpha1.RendererConfig) error { + result, err := render(config) if err != nil { return fmt.Errorf("failed to render %s: %w", config.Type, err) } defer func() { _ = result.Close() }() - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Rendered %s to %s\n", config.Type, result.Dir) - if skipPush { - return nil - } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Rendered %s to %s (skip-push)\n", config.Type, result.Dir) - if passwordStdIn { - if _, err := fmt.Scanln(&password); err != nil { - return err - } - } + return nil +} +func buildPushOptions() renderer.PushOptions { dockerconfig, _ = os.LookupEnv("DOCKER_CONFIG") if dockerconfig == "" { - home, err := os.UserHomeDir() - if err != nil { - return err - } + home, _ := os.UserHomeDir() dockerconfig = path.Join(home, ".docker", "config.json") } @@ -78,11 +115,11 @@ func rootFunc(cmd *cobra.Command, args []string) error { clientOpts = append(clientOpts, registry.ClientOptPlainHTTP()) } - // Decide authentication method // CLI flags take precedence over env vars if username == "" { username = os.Getenv("REGISTRY_USERNAME") } + if password == "" { password = os.Getenv("REGISTRY_PASSWORD") } @@ -94,19 +131,10 @@ func rootFunc(cmd *cobra.Command, args []string) error { clientOpts = append(clientOpts, registry.ClientOptCredentialsFile(dockerconfig)) } - po := renderer.PushOptions{ + return renderer.PushOptions{ Reference: url, ClientOptions: clientOpts, } - - pushResult, err := renderer.PushChart(result, po) - if err != nil { - return fmt.Errorf("failed to push result: %w", err) - } - - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Pushed result to %s\n", pushResult.Ref) - - return nil } func newRootCmd() *cobra.Command { diff --git a/cmd/solar-ui/main.go b/cmd/solar-ui/main.go new file mode 100644 index 00000000..31bab6bb --- /dev/null +++ b/cmd/solar-ui/main.go @@ -0,0 +1,88 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "github.com/spf13/cobra" + "go.uber.org/zap" + + "go.opendefense.cloud/solar/pkg/ui" +) + +var cmd = &cobra.Command{ + Use: "solar-ui", + Short: "SolAr UI — web frontend and BFF for the SolAr Kubernetes extension API", + RunE: runE, +} + +func init() { + cmd.Flags().StringP("listen", "l", "0.0.0.0:8090", "Address to listen on") + cmd.Flags().String("oidc-issuer", "", "OIDC issuer URL (e.g. https://dex.example.com)") + cmd.Flags().String("oidc-client-id", "solar-ui", "OIDC client ID") + cmd.Flags().String("oidc-client-secret", "", "OIDC client secret") + cmd.Flags().String("oidc-redirect-url", "http://localhost:8090/api/auth/callback", "OIDC redirect URL") + cmd.Flags().String("session-key", "", "Session encryption key (32 bytes, hex-encoded). Generated if empty.") + cmd.Flags().String("kubeconfig", "", "Path to kubeconfig (defaults to in-cluster config)") + cmd.Flags().String("auth-mode", "token", "How to convey OIDC identity to K8s: 'token' (forward id_token) or 'impersonate'") + cmd.Flags().String("dev-vite-url", "", "Proxy non-API requests to Vite dev server (e.g. http://localhost:5173)") +} + +func runE(cmd *cobra.Command, _ []string) error { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) + defer stop() + + var log logr.Logger + + zapLog, err := zap.NewDevelopment() + if err != nil { + panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) + } + log = zapr.NewLogger(zapLog) + + addr, _ := cmd.Flags().GetString("listen") + oidcIssuer, _ := cmd.Flags().GetString("oidc-issuer") + oidcClientID, _ := cmd.Flags().GetString("oidc-client-id") + oidcClientSecret, _ := cmd.Flags().GetString("oidc-client-secret") + oidcRedirectURL, _ := cmd.Flags().GetString("oidc-redirect-url") + sessionKey, _ := cmd.Flags().GetString("session-key") + kubeconfig, _ := cmd.Flags().GetString("kubeconfig") + authMode, _ := cmd.Flags().GetString("auth-mode") + devViteURL, _ := cmd.Flags().GetString("dev-vite-url") + + cfg := ui.Config{ + ListenAddr: addr, + OIDCIssuer: oidcIssuer, + OIDCClientID: oidcClientID, + OIDCClientSecret: oidcClientSecret, + OIDCRedirectURL: oidcRedirectURL, + SessionKey: sessionKey, + Kubeconfig: kubeconfig, + AuthMode: authMode, + DevViteURL: devViteURL, + } + + server, err := ui.NewServer(cfg, log) + if err != nil { + return fmt.Errorf("failed to create server: %w", err) + } + + return server.Run(ctx) +} + +func main() { + if err := cmd.Execute(); err != nil { + if _, err := fmt.Fprintln(os.Stderr, err); err != nil { + panic(err) + } + + os.Exit(1) + } +} diff --git a/devenv.nix b/devenv.nix index 37b7672d..47886b1f 100644 --- a/devenv.nix +++ b/devenv.nix @@ -9,10 +9,16 @@ pkgs.kind pkgs.kubectl pkgs.kubernetes-helm + pkgs.nodejs_22 + pkgs.pnpm + pkgs.chromium pkgs.shellcheck pkgs.yq-go ]; + env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"; + env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH = "${pkgs.chromium}/bin/chromium"; + # https://devenv.sh/languages/ languages.go.enable = true; languages.go.version = "1.26.2"; diff --git a/docs/.nav.yml b/docs/.nav.yml index 610d2684..abdbeb09 100644 --- a/docs/.nav.yml +++ b/docs/.nav.yml @@ -11,6 +11,7 @@ nav: - Installation: - operator-manual/installation/installation.md - operator-manual/installation/*.md + - operator-manual/ui-access-control.md - operator-manual/*.md - Developer Guide: - CODE_OF_CONDUCT.md diff --git a/docs/developer-guide/adrs/010-UI-Architecture.md b/docs/developer-guide/adrs/010-UI-Architecture.md new file mode 100644 index 00000000..f4218c08 --- /dev/null +++ b/docs/developer-guide/adrs/010-UI-Architecture.md @@ -0,0 +1,254 @@ +--- +status: proposed +date: 2026-04-11 +--- + +# UI Architecture: Go Backend-for-Frontend with React SPA + +## Context and Problem Statement + +SolAr currently provides a fully Kubernetes-native API — all interaction happens via `kubectl`, GitOps tooling, or direct API calls. While this is sufficient for platform operators and developers, several user stories require a graphical interface for catalog browsing, target management, and deployment visibility. Non-CLI users (deployment coordinators, solution consumers, administrators) need a WYSIWYG experience that surfaces the relationships between Components, Releases, Targets, Profiles, and their bindings without requiring Kubernetes expertise. + +The central challenge is how a UI should interact with the Kubernetes API while preserving the existing RBAC model — users in the UI must have the same permissions as they would via `kubectl`. This is complicated by the variety of authentication setups across Kubernetes clusters (OIDC, client certificates, service account tokens, cloud provider IAM) and the fact that SolAr is designed to work across different environments. + +## Decision Drivers + +- Users must have the same permissions in the UI as in `kubectl` — no privilege escalation +- Support multiple authentication mechanisms to work across different cluster setups +- Minimize operational complexity — few additional moving parts to deploy +- Leverage existing Go codebase and generated client-go clients +- Enable real-time visibility into resource state changes +- Keep the frontend decoupled from Kubernetes API specifics +- Support single-binary deployment for simplicity + +## Considered Options + +### Option 1: SPA interacting directly with the Kubernetes API + +The React SPA talks directly to the Kubernetes API server. Authentication tokens are obtained client-side (e.g. via OIDC implicit/PKCE flow) and sent as Bearer tokens. + +**Advantages:** +- No backend to build or maintain +- Direct use of Kubernetes RBAC — no proxy layer +- Lowest latency for API calls + +**Disadvantages:** +- Kubernetes API servers do not serve CORS headers by default — requires a reverse proxy or API server configuration changes +- Tokens must be stored client-side (localStorage, sessionStorage, or cookies), increasing the attack surface +- No aggregation layer — the frontend must make multiple calls and stitch data together (e.g. Target + ReleaseBindings + RenderTasks for a single target view) +- Every Kubernetes API change (field renames, version bumps) directly impacts the frontend +- Supporting multiple auth mechanisms client-side is complex and fragile +- WebSocket-based watches require direct connectivity to the API server +- Cannot support kubeconfig upload without a backend to process it + +### Option 2: GraphQL gateway (e.g. kubernetes-graphql-gateway) + +A GraphQL middleware translates frontend queries into Kubernetes API calls. The frontend uses GraphQL for flexible, aggregated queries. + +**Advantages:** +- Flexible query model — frontend can request exactly the data it needs in a single call +- Can aggregate related resources (Target + bindings + status) in one query +- Growing ecosystem of Kubernetes-to-GraphQL bridges + +**Disadvantages:** +- Existing K8s-to-GraphQL bridges are immature and not production-hardened +- Schema maintenance overhead — must be kept in sync with SolAr API types +- Still requires a separate authentication layer in front of the gateway +- Adds operational complexity (another service to deploy, monitor, upgrade) +- GraphQL subscription support for K8s watches is not well established +- Overkill for an MVP — the query flexibility is not needed when the resource model is well-defined + +### Option 3: Dedicated Go backend (Backend-for-Frontend) + +A purpose-built Go HTTP server serves a REST API tailored to the UI's needs. It handles authentication, session management, and translates frontend requests into Kubernetes API calls using the generated client-go. The React SPA is embedded in the binary via `go:embed` and served as static files. + +**Advantages:** +- Full control over authentication flows — can support OIDC, kubeconfig upload, and impersonation in one place +- Credentials never reach the browser — tokens and kubeconfigs stay server-side +- Aggregation layer — can combine multiple K8s resources into frontend-friendly responses +- Uses existing generated client-go directly — no schema translation +- Single binary deployment (`go:embed` for static SPA assets) +- Server-Sent Events for real-time updates backed by Kubernetes watches +- Can add validation, rate limiting, and audit logging without K8s API server changes +- Natural fit for the team's Go expertise + +**Disadvantages:** +- More code to write and maintain than option 1 +- An additional component to deploy (though single-binary mitigates this) +- API surface must be designed and versioned + +## Decision Outcome + +Chosen option: **Option 3 — Dedicated Go Backend-for-Frontend with React SPA.** + +The authentication requirements alone make a backend mandatory. Supporting OIDC, kubeconfig upload, and impersonation across different cluster setups cannot be handled purely client-side. Since the project already has generated Go clients and deep Go expertise, a Go BFF is the natural choice over introducing a Node.js layer or an immature GraphQL bridge. + +### Architecture + +``` +┌─────────────────┐ ┌───────────────────────┐ ┌──────────────────────┐ +│ React SPA │──────→│ solar-ui (Go) │──────→│ K8s API Server │ +│ (embedded) │ REST │ BFF + Auth + SSE │ │ + SolAr Extension │ +└─────────────────┘ └───────────────────────┘ └──────────────────────┘ + ├─ Session store (cookie)│ + ├─ Auth providers │ + └─ K8s client-go │ +``` + +### Authentication Strategy + +The backend supports multiple authentication providers, selectable per deployment or per session. All providers implement a common interface that returns a per-request `rest.Config`: + +| Provider | Flow | Use Case | +|----------|------|----------| +| **OIDC (Authorization Code + PKCE)** | Backend performs OAuth2 code exchange with the cluster's IdP, stores tokens in httpOnly secure cookie, refreshes transparently. Uses obtained ID/access token against K8s API. | Production clusters with OIDC-capable API server | +| **Impersonation** | Backend authenticates with its own ServiceAccount, sets `Impersonate-User` and `Impersonate-Group` headers based on the session's authenticated identity (from OIDC or other). | Clusters where the backend ServiceAccount is granted impersonation rights | +| **Kubeconfig upload** | User uploads/pastes kubeconfig in the UI. Backend extracts credentials (token, client cert, exec-based), stores them in the encrypted session, uses them for K8s API calls. | Development, local clusters, quick onboarding, air-gapped environments | + +The OIDC and impersonation providers can be combined: OIDC establishes the user's identity, and impersonation is used if the cluster's API server does not accept the OIDC token directly (e.g. the IdP is not configured as an API server OIDC issuer, but the backend is trusted to impersonate). + +### Backend Design + +**Binary:** `cmd/solar-ui/main.go` + +**Key packages:** + +``` +pkg/ui/ + server.go — HTTP server, middleware, router + auth/ + provider.go — AuthProvider interface + oidc.go — OIDC authorization code flow + impersonation.go — ServiceAccount + impersonation headers + kubeconfig.go — Kubeconfig upload and extraction + session/ + store.go — Encrypted cookie-based session management + api/ + targets.go — Target list/detail (aggregated with bindings and status) + releases.go — Release list/detail + components.go — Component and ComponentVersion catalog + profiles.go — Profile list/detail with target match preview + registries.go — Registry list/detail + rendertasks.go — RenderTask status + events.go — SSE endpoint backed by K8s watches +``` + +**API surface (REST):** + +``` +# Authentication +POST /api/auth/login — initiate OIDC flow (redirects to IdP) +GET /api/auth/callback — OIDC callback +POST /api/auth/kubeconfig — upload kubeconfig +DELETE /api/auth/session — logout +GET /api/auth/me — current user info + +# Resources (read) +GET /api/targets — list targets with aggregated status +GET /api/targets/:name — target detail (+ bindings + render status) +GET /api/releases — list releases +GET /api/releases/:name — release detail (+ component version info) +GET /api/components — list components +GET /api/components/:name — component detail (+ versions) +GET /api/profiles — list profiles +GET /api/profiles/:name — profile detail (+ matched targets preview) +GET /api/registries — list registries +GET /api/rendertasks — list render tasks + +# Resources (write) — MVP may start read-only +POST /api/targets — create target +PUT /api/targets/:name — update target +DELETE /api/targets/:name — delete target +# (same pattern for releases, profiles, bindings) + +# Real-time +GET /api/events — SSE stream (resource watch events) +``` + +The backend aggregates related resources server-side. For example, `GET /api/targets/:name` returns the target, its ReleaseBindings, RegistryBindings, referenced Registry, and current RenderTask status in a single response — avoiding N+1 queries from the frontend. + +### Frontend Design + +**Stack:** + +- **React 19** + TypeScript +- **Vite** for build tooling +- **TanStack Query** for data fetching, caching, and SSE-driven cache invalidation +- **TanStack Router** for type-safe file-based routing +- **shadcn/ui** (Radix primitives + Tailwind CSS) for UI components — customizable, accessible, no heavy framework lock-in +- **SSE** via `EventSource` for live updates, feeding into TanStack Query cache + +**Directory structure:** + +``` +ui/ + src/ + api/ — typed API client (hand-written or generated from OpenAPI) + components/ — shared UI components + pages/ — route-based page components + dashboard/ + targets/ + releases/ + components/ + profiles/ + hooks/ — custom React hooks (useSSE, useAuth, etc.) + lib/ — utilities + vite.config.ts + tsconfig.json + package.json +``` + +**Key views (MVP):** + +1. **Dashboard** — overview cards showing target count, release count, active render tasks, recent errors +2. **Targets** — list with status indicators; detail view showing bound releases, registries, render status +3. **Releases** — list/detail; shows referenced ComponentVersion, which targets use it +4. **Components** — catalog browser; component versions discovered by solar-discovery +5. **Profiles** — list/detail; shows target selector, matched targets, created ReleaseBindings + +**Embedding:** + +The built SPA is embedded into the Go binary via `//go:embed ui/dist`, making `solar-ui` a single binary that serves both the API and the frontend. No separate web server or CDN needed. + +### Deployment + +- **Helm chart**: new deployment in `charts/solar/` or a standalone `charts/solar-ui/` chart +- **ServiceAccount**: needs `get`, `list`, `watch` on all SolAr resources; needs `impersonate` verb on `users` and `groups` if using impersonation mode +- **Ingress/Route**: requires external access for browser clients; TLS termination at ingress +- **Configuration**: auth provider selection, OIDC issuer/client config, session encryption key + +### Consequences + +**Positive:** + +- Single Go binary with embedded SPA — simple to build, deploy, and operate +- Authentication complexity is handled server-side; credentials never reach the browser +- Aggregated API responses reduce frontend complexity and improve performance +- SSE provides real-time updates without WebSocket complexity +- Reuses existing generated client-go — no schema translation or sync needed +- Same permission model as `kubectl` — no privilege escalation +- Can be deployed alongside the existing SolAr components with minimal changes + +**Negative:** + +- Additional component to develop and maintain (Go backend + React frontend) +- REST API surface must be designed, documented, and versioned +- Session management adds state to an otherwise stateless system +- OIDC configuration requires coordination with the cluster's IdP setup +- Frontend build tooling (Node.js, npm/pnpm) added to the project's dev dependencies + +## Open Questions + +- **Multi-cluster**: Should the UI support connecting to multiple clusters simultaneously, or is it one cluster per UI instance? Multi-cluster significantly complicates session management and resource aggregation. The simpler model (one instance per cluster, or one instance per SolAr control plane) is recommended for MVP. +- **Write operations**: Should the MVP include create/edit/delete, or start read-only? Read-only MVP reduces scope and risk; write operations can be added incrementally. +- **OpenAPI generation**: Should the BFF's REST API be defined OpenAPI-first (and generate server/client stubs), or code-first (and generate OpenAPI from code)? Code-first is faster for MVP; OpenAPI-first is better for long-term client generation. +- **Namespace scoping**: How does the UI handle multi-tenancy? Should users see all namespaces or only those they have access to? The backend can use SelfSubjectAccessReview to determine visibility. +- **Standalone or co-located chart**: Should `solar-ui` be a separate Helm chart or part of the main `charts/solar/` chart? Separate chart allows independent deployment lifecycle. + +## Spike Recommendations + +Before committing to full implementation, two focused spikes are recommended: + +1. **Auth spike** (~2-3 days): Minimal Go HTTP server that performs OIDC authorization code flow against the cluster's IdP, stores the token in an encrypted cookie, and makes a `list namespaces` call using the user's identity. Validates the auth flow end-to-end. Stretch: also test impersonation mode. + +2. **Frontend spike** (~2-3 days): React app with Vite + TanStack Query that renders a target list from a mock API, with live updates via SSE. Validates the frontend stack choice and real-time UX. Stretch: integrate with the auth spike's real backend. diff --git a/docs/developer-guide/architecture.md b/docs/developer-guide/architecture.md index 8f188086..0dd5182e 100644 --- a/docs/developer-guide/architecture.md +++ b/docs/developer-guide/architecture.md @@ -8,6 +8,7 @@ graph TB User["User/Operator"] Kubectl["kubectl CLI"] GitOps["GitOps Tools"] + WebUI["SolAr Web UI
(solar-ui)"] end subgraph "Kubernetes Control Plane" @@ -21,32 +22,37 @@ graph TB end subgraph "SolAr Controller Manager" - DiscoveryCtrl["Discovery Controller
Manages Discovery resources
Creates Pod"] - TargetCtrl["Controller
Manages
Creates"] - ReleaseCtrl["Controller
Manages
Creates"] - BootstrapCtrl["Controller
Manages
Creates"] - RenderTaskCtrl["Controller
Manages
Creates"] + TargetCtrl["Target Controller"] + ReleaseCtrl["Release Controller"] + ProfileCtrl["Profile Controller"] + RenderTaskCtrl["RenderTask Controller"] + end + + subgraph "SolAr Discovery (standalone)" + Discovery["solar-discovery
Scans OCI registries
for OCM packages"] end subgraph "External Systems" SrcReg["Source Systems
OCI Registries, S3,
Helm Repos, HTTP"] - DstReg["Destination Systems
Private Registries,
Secure Storage"] + DstReg["Render Registries
OCI Registries for
rendered charts"] end - User -->|"Creates Releases"| Kubectl + User -->|"Creates Releases,
Targets, Profiles"| Kubectl + User -->|"Browses catalog,
manages deployments"| WebUI GitOps -->|"Declarative Config"| Kubectl Kubectl -->|"API Requests"| K8sAPI + WebUI -->|"OIDC Auth +
K8s API Proxy"| K8sAPI K8sAPI <-->|"Routes solar.opendefense.cloud"| APIAgg APIAgg <-->|"Custom Resources"| SOLARAPI SOLARAPI <-->|"Persists"| SOLARETCD - - Release -->|"Watched by"| ReleaseCtrl ``` **Architecture: SolAr System Components and Data Flow** -The system follows a layered architecture where users interact through `kubectl` (or GitOps tools), requests flow through the Kubernetes API aggregation layer to the SolAr API Server. +The system follows a layered architecture where users interact through `kubectl`, GitOps tools, or the **SolAr Web UI**. Requests flow through the Kubernetes API aggregation layer to the SolAr API Server. Controllers reconcile the declared resources and drive the rendering pipeline. + +The Web UI (`solar-ui`) is a Go Backend-for-Frontend (BFF) that serves a React SPA and proxies authenticated requests to the Kubernetes API. It handles OIDC authentication via Dex, forwarding the user's identity token as a K8s bearer token. See [ADR-010](adrs/010-UI-Architecture.md) for architectural details. **Key Design Decisions:** @@ -57,33 +63,55 @@ The system follows a layered architecture where users interact through `kubectl` ```mermaid graph TB - subgraph "User-Facing Resources" + subgraph "Catalog Resources" + Component["Component"] + ComponentVersion["ComponentVersion"] + end + + subgraph "Deployment Resources" Release["Release"] - Profile["Profile"] Target["Target"] + Profile["Profile"] end - subgraph "Configuration Resources" - Secret["Kubernetes Secret
Credentials for RenderTask and Discovery Worker"] - Component["Component
An ocm component"] - ComponentVersion["ComponentVersion
A Version of an ocm component"] - DiscoveryWorker["Discovery Worker
A kubernetes Pod executing the discovery pipeline"] + subgraph "Binding Resources" + ReleaseBinding["ReleaseBinding"] + Registry["Registry"] end - Discovery --> |"creates"| DiscoveryWorker - - DiscoveryWorker --> |"discovers"| ComponentVersion - DiscoveryWorker --> |"discovers"| Component + subgraph "Internal Resources" + RenderTask["RenderTask"] + end - ComponentVersion --> |"references"| Component + SolArDiscovery["solar-discovery
(standalone)"] -->|"discovers"| ComponentVersion + SolArDiscovery -->|"discovers"| Component + ComponentVersion -->|"references"| Component Release -->|"references"| ComponentVersion - Profile -->|"references one or more"| Release + Profile -->|"references"| Release + Profile -->|"creates"| ReleaseBinding + + ReleaseBinding -->|"binds"| Release + ReleaseBinding -->|"binds"| Target - Bootstrap -->|"references one or more"| Profile + Target -->|"references"| Registry + Target -->|"creates"| RenderTask + + Registry -->|"provides credentials
and hostname"| RenderTask ``` +### Resource Roles + +- **Component / ComponentVersion** — catalog entries discovered from OCI registries by solar-discovery. +- **Release** — declares which ComponentVersion to deploy and with what configuration. +- **Target** — represents a deployment target (cluster). References a Registry via `renderRegistryRef` for pushing rendered charts. +- **Registry** — stores OCI registry hostname and push credentials (`solarSecretRef`). +- **ReleaseBinding** — declares that a Release should be deployed to a Target. Created manually or automatically by the Profile controller. +- **Profile** — matches Targets by label selector and automatically creates ReleaseBindings for a given Release. +- **RenderTask** — internal resource created by the Target controller to drive chart rendering jobs. + ## Controllers -[RenderTask controller](./rendertask_controller.md) +- [Rendering pipeline](./rendering-pipeline.md) — how Targets, Releases, and RenderTasks produce deployable Helm charts +- [RenderTask controller](./rendertask_controller.md) — lifecycle of individual RenderTask resources diff --git a/docs/developer-guide/rendering-pipeline.md b/docs/developer-guide/rendering-pipeline.md new file mode 100644 index 00000000..55d11270 --- /dev/null +++ b/docs/developer-guide/rendering-pipeline.md @@ -0,0 +1,150 @@ +# Rendering Pipeline + +This document describes how SolAr renders Helm charts for deployment targets and how the resulting FluxCD resources relate to each other. + +## Overview + +When a Target has ReleaseBindings (created manually or via Profiles), the Target controller orchestrates a two-stage rendering pipeline: + +1. **Release rendering** — each bound Release is rendered into a standalone Helm chart and pushed to the render registry. +2. **Bootstrap rendering** — all rendered release charts are bundled into a single bootstrap Helm chart per target, which FluxCD installs on the target cluster. + +```mermaid +flowchart TD + subgraph "User Resources" + Target + Release1["Release A"] + Release2["Release B"] + RB1["ReleaseBinding A→Target"] + RB2["ReleaseBinding B→Target"] + Profile["Profile"] + end + + subgraph "Render Pipeline" + RT1["RenderTask
render-rel-a-*"] + RT2["RenderTask
render-rel-b-*"] + RTB["RenderTask
render-tgt-target-N"] + end + + subgraph "OCI Registry" + ChartA["release-a chart"] + ChartB["release-b chart"] + Bootstrap["bootstrap-target chart"] + end + + Profile -->|creates| RB2 + RB1 -->|binds| Release1 + RB1 -->|binds| Target + RB2 -->|binds| Release2 + RB2 -->|binds| Target + + Target -->|creates| RT1 + Target -->|creates| RT2 + RT1 -->|pushes| ChartA + RT2 -->|pushes| ChartB + + Target -->|creates| RTB + RTB -->|bundles| ChartA + RTB -->|bundles| ChartB + RTB -->|pushes| Bootstrap +``` + +## Stage 1: Release RenderTasks + +For each ReleaseBinding on a Target, the Target controller creates a per-release RenderTask. These are deduplicated by release name and registry — if two targets share the same render registry, they reuse the same release RenderTask. + +The release RenderTask name is deterministic: `render-rel--`, where the hash is derived from the release and registry names. + +The renderer container produces a Helm chart that wraps the original OCM component's entrypoint chart. The rendered chart template contains: + +- A FluxCD **OCIRepository** pointing to the original chart in the source registry +- A FluxCD **HelmRelease** that installs the chart from that OCIRepository + +This is the **inner release** — a HelmRelease managed by the bootstrap chart (see Stage 2). + +## Stage 2: Bootstrap RenderTask + +Once all release RenderTasks have succeeded, the Target controller creates a bootstrap RenderTask (`render-tgt--`). This bundles all rendered release charts into a single bootstrap Helm chart. + +The bootstrap chart template iterates over all releases and creates, for each one: + +- A FluxCD **OCIRepository** pointing to the rendered release chart in the render registry +- A FluxCD **HelmRelease** that installs the rendered release chart + +These are the **inner HelmReleases** — they are managed by the **outer HelmRelease** (the bootstrap itself). + +### Bootstrap Versioning + +The bootstrap chart version is `v0.0.`, where `bootstrapVersion` is a counter stored in `target.status.bootstrapVersion`. It is incremented each time the set of bound releases changes (e.g. a Profile creates a new ReleaseBinding). This ensures a new chart version is pushed to the registry whenever the bootstrap content changes. + +## HelmRelease Hierarchy + +When FluxCD installs the bootstrap chart on a target cluster, the result is a three-level hierarchy: + +``` +Outer HelmRelease (solar-bootstrap) + └── Bootstrap chart (bootstrap-) + ├── Inner HelmRelease (solar-bootstrap-) + │ └── Release chart (release-) + │ └── Innermost HelmRelease + │ └── Original application chart (e.g. demo) + └── Inner HelmRelease (solar-bootstrap-) + └── Release chart (release-) + └── Innermost HelmRelease + └── Original application chart +``` + +| Level | Resource | Created By | Purpose | +|-------|----------|-----------|---------| +| Outer | HelmRelease `solar-bootstrap` | User / GitOps | Installs the bootstrap chart from the render registry | +| Inner | HelmRelease `solar-bootstrap-` | Bootstrap chart template | Installs each rendered release chart | +| Innermost | HelmRelease `-` | Release chart template | Installs the original application chart from the source registry | + +### Name Truncation + +Inner HelmRelease names are constructed as `-`. Since Kubernetes object names are limited to 253 characters (and Helm release names to 53), names exceeding 53 characters are truncated and suffixed with a short SHA-256 hash to preserve uniqueness. + +## Data Flow: From ReleaseBinding to Deployment + +```mermaid +sequenceDiagram + participant User + participant Target Controller + participant RenderTask Controller + participant Registry + participant FluxCD + + User->>Target Controller: Create ReleaseBinding + Target Controller->>Target Controller: Resolve Release → ComponentVersion + Target Controller->>RenderTask Controller: Create release RenderTask + RenderTask Controller->>Registry: Push rendered release chart + RenderTask Controller->>Target Controller: RenderTask succeeded + + Target Controller->>Target Controller: All releases rendered, bump bootstrapVersion + Target Controller->>RenderTask Controller: Create bootstrap RenderTask + RenderTask Controller->>Registry: Push bootstrap chart (v0.0.N) + RenderTask Controller->>Target Controller: Bootstrap RenderTask succeeded + + FluxCD->>Registry: Poll OCIRepository, detect new version + FluxCD->>FluxCD: Install/upgrade bootstrap HelmRelease + FluxCD->>FluxCD: Bootstrap chart creates inner HelmReleases + FluxCD->>FluxCD: Inner releases install application workloads +``` + +## Registry Layout + +For a target `cluster-1` in namespace `prod` with two releases, the render registry contains: + +``` +/ + prod/ + release-my-app-release # Rendered release chart (v0.0.0) + release-monitoring-release # Rendered release chart (v0.0.0) + bootstrap-cluster-1 # Bootstrap chart (v0.0.0, v0.0.1, ...) +``` + +## Profiles and Indirect Binding + +Profiles automate ReleaseBinding creation. A Profile references a Release and a target label selector. The Profile controller watches for matching Targets and creates ReleaseBindings with owner references back to the Profile. + +When a Profile creates a new ReleaseBinding for a Target that already has a bootstrap chart, the Target controller detects the changed release set, increments `bootstrapVersion`, and triggers a new bootstrap render that includes the additional release. diff --git a/docs/developer-guide/rendertask_controller.md b/docs/developer-guide/rendertask_controller.md index 7e48f415..415d8500 100644 --- a/docs/developer-guide/rendertask_controller.md +++ b/docs/developer-guide/rendertask_controller.md @@ -97,14 +97,24 @@ stateDiagram-v2 Configuration of the controller is managed by the controller manager. The RenderTask controller can be configured with the following parameters: -| Parameter | Type | Description -| --- | --- | --- -| `RendererImage` | `string` | Image to be used for the render Job / Pod -| `RendererCommand` | `string` | Command for the render Job / Pod -| `RendererArgs` | `[]string` | Additional args for the render Job / Pod -| `BaseURL` | `string` | URL of the registry to which rendered charts get pushed to -| `PushSecretRef` | `*corev1.SecretReference` | (Optional) Reference to a secret containing credentials for the registry - -If PushSecretRef is set, the controller copies the secret to the Job's -Namespace so it can be mounted by the Pod. The secret gets cleaned up together -with the other RenderTask Resources. +| Parameter | Type | Description | +| --- | --- | --- | +| `RendererImage` | `string` | Image to be used for the render Job / Pod | +| `RendererCommand` | `string` | Command for the render Job / Pod | +| `RendererArgs` | `[]string` | Additional args for the render Job / Pod | + +## Per-Task Registry Credentials + +Each RenderTask carries its own `baseURL` and `pushSecretRef`, which are +resolved by the Target controller from the Target's `renderRegistryRef`: + +1. The Target references a **Registry** resource via `spec.renderRegistryRef`. +2. The Registry provides the OCI hostname (`spec.hostname`) and a secret + reference (`spec.solarSecretRef`) containing push credentials. +3. When creating a RenderTask, the Target controller sets these values on the + RenderTask spec so the renderer Job can authenticate to the registry. + +If `pushSecretRef` is set on the RenderTask, the controller copies the +referenced secret into the RenderTask's namespace so it can be mounted by the +renderer Pod. The copied secret is cleaned up together with the other +RenderTask resources. diff --git a/docs/index.md b/docs/index.md index 2d0838e9..e04b6ccd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,11 +15,14 @@ For a detailed architecture overview, see the [Architecture documentation](./dev SolAr manages software delivery through several key resources: -- **Component/ComponentVersion** - OCM components representing deployable software packages -- **Release** - A specific deployment instance of a component with configuration -- **Target** - A deployment target environment (cluster/namespace) -- **Bootstrap** - A fully resolved target with concrete releases and configuration -- **Discovery** - Automated scanning of registries for new components +- **Component / ComponentVersion** — OCM components representing deployable software packages, discovered automatically by solar-discovery +- **Release** — a deployment configuration for a ComponentVersion +- **Target** — a deployment target environment (e.g. a cluster), references a render Registry +- **Registry** — an OCI registry configuration with hostname and push credentials +- **ReleaseBinding** — declares that a Release should be deployed to a Target +- **Profile** — matches Targets by label selector and automatically creates ReleaseBindings for a Release +- **RenderTask** — internal resource that drives Helm chart rendering jobs +- **Discovery** — automated scanning of OCI registries for new OCM components For the complete API specification, see the [API Reference](./user-guide/api-reference.md). diff --git a/docs/operator-manual/ui-access-control.md b/docs/operator-manual/ui-access-control.md new file mode 100644 index 00000000..cb81e236 --- /dev/null +++ b/docs/operator-manual/ui-access-control.md @@ -0,0 +1,59 @@ +# UI Access Control + +The SolAr UI uses Kubernetes RBAC to control which features are available to logged-in users. There are two distinct access levels: + +- **Admin** — can use the "Preview as" impersonation feature to browse the UI as another persona +- **Regular user** — sees only the resources their own OIDC identity is permitted to access + +## Granting Admin Access + +Admin access is determined by membership in a `ClusterRoleBinding` labeled `solar.opendefense.cloud/admin=true`. The BFF checks this label using its own service-account credentials, so the check is independent of any active impersonation state. + +Use the `ui.admin.subjects` Helm value to declare which users or groups are admins: + +```yaml +ui: + admin: + subjects: + - kind: User + name: admin@example.com + apiGroup: rbac.authorization.k8s.io + - kind: Group + name: platform-admins + apiGroup: rbac.authorization.k8s.io +``` + +This creates a `ClusterRoleBinding` named `solar-ui:admin` with the required label. You can also manage the binding manually — any `ClusterRoleBinding` with the label `solar.opendefense.cloud/admin=true` that lists the user as a subject will grant admin access. + +## Configuring Impersonation Personas + +Admins can preview the UI as another persona via the "Preview as" dropdown. Personas are defined with `ui.impersonation.targets`: + +```yaml +ui: + impersonation: + targets: + - username: maintainer@example.com + groups: + - maintainer + - username: coordinator@example.com + groups: + - coordinator +``` + +For each entry the chart creates a `ClusterRole` labeled `solar.opendefense.cloud/impersonatable=true` and a `ClusterRoleBinding` that grants the BFF service account the right to impersonate that user. The BFF lists these roles at runtime, so adding or removing personas only requires a `helm upgrade` — no restart needed. + +!!! note + Personas do not need to be real OIDC users. They are K8s impersonation targets only — give them appropriate `ClusterRole` bindings to define what they can see. + +## Required BFF Service Account Permissions + +The chart automatically creates a `ClusterRole` named `solar-ui:read-rbac` and binds it to the BFF service account. It grants `list` on `clusterroles` and `clusterrolebindings` — the minimum required for admin checks and persona discovery. No additional setup is needed. + +## Summary + +| Helm value | Effect | +|---|---| +| `ui.admin.subjects` | Users/groups that can access impersonation management | +| `ui.impersonation.targets` | Personas available in the "Preview as" dropdown | +| `ui.impersonation.serviceAccountName` | BFF service account that receives impersonation rights (default: `solar-ui`) | diff --git a/docs/user-guide/api-reference.md b/docs/user-guide/api-reference.md index a5621ab1..ab174489 100644 --- a/docs/user-guide/api-reference.md +++ b/docs/user-guide/api-reference.md @@ -10,44 +10,6 @@ Package v1alpha1 is the v1alpha1 version of the API. -#### AuthenticationType - -_Underlying type:_ _string_ - -AuthenticationType - - - -_Appears in:_ -- [WebhookAuth](#webhookauth) - -| Field | Description | -| --- | --- | -| `Basic` | | -| `Token` | | - - -#### Bootstrap - - - -Bootstrap represents the entrypoint for the gitless gitops configuration. -It resolves the implicit matching of profiles to produce a concrete set of releases and profiles. - - - -_Appears in:_ -- [BootstrapList](#bootstraplist) - -| Field | Description | Default | Validation | -| --- | --- | --- | --- | -| `kind` _string_ | 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 | | | -| `apiVersion` _string_ | 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 | | | -| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | -| `spec` _[BootstrapSpec](#bootstrapspec)_ | | | | -| `status` _[BootstrapStatus](#bootstrapstatus)_ | | | | - - #### BootstrapConfig @@ -83,44 +45,6 @@ _Appears in:_ | `userdata` _[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#rawextension-runtime-pkg)_ | Userdata is additional data to be rendered into the bootstrap chart values. | | | - - -#### BootstrapSpec - - - -BootstrapSpec defines the desired state of a Bootstrap. -It contains the concrete releases, profiles, and deployment configuration for a target environment. - - - -_Appears in:_ -- [Bootstrap](#bootstrap) - -| Field | Description | Default | Validation | -| --- | --- | --- | --- | -| `releases` _object (keys:string, values:[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core))_ | Releases is a map of release names to their corresponding Release object references.
Each entry represents a component release that will be deployed to the target. | | | -| `profiles` _object (keys:string, values:[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core))_ | Profiles is a map of profile names to their corresponding Profile object references.
It points to profiles that match the target, e.g. through the label selector of the Profile | | | -| `userdata` _[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#rawextension-runtime-pkg)_ | Userdata contains arbitrary custom data or configuration for the target deployment.
This allows providing target-specific parameters or settings. | | | - - -#### BootstrapStatus - - - -BootstrapStatus defines the observed state of a Bootstrap. - - - -_Appears in:_ -- [Bootstrap](#bootstrap) - -| Field | Description | Default | Validation | -| --- | --- | --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#condition-v1-meta) array_ | Conditions represent the latest available observations of a Bootstrap's state. | | | -| `renderTaskRef` _[ObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#objectreference-v1-core)_ | RenderTaskRef is a reference to the RenderTask responsible for this Bootstrap. | | | - - #### ChartConfig @@ -249,194 +173,213 @@ _Appears in:_ -#### Discovery +#### Entrypoint -Discovery represents a configuration for a registry to discover. +Entrypoint defines the entrypoint for deploying a ComponentVersion. _Appears in:_ -- [DiscoveryList](#discoverylist) +- [ComponentVersionSpec](#componentversionspec) +- [ReleaseInput](#releaseinput) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `kind` _string_ | 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 | | | -| `apiVersion` _string_ | 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 | | | -| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | -| `spec` _[DiscoverySpec](#discoveryspec)_ | | | | -| `status` _[DiscoveryStatus](#discoverystatus)_ | | | | - - - +| `resourceName` _string_ | ResourceName is the Name of the Resource to use as the entrypoint. | | | +| `type` _[EntrypointType](#entrypointtype)_ | Type of entrypoint. | | | -#### DiscoverySpec +#### EntrypointType +_Underlying type:_ _string_ -DiscoverySpec defines the desired state of a Discovery. +EntrypointType is the Type of Entrypoint. _Appears in:_ -- [Discovery](#discovery) +- [Entrypoint](#entrypoint) -| Field | Description | Default | Validation | -| --- | --- | --- | --- | -| `registry` _[Registry](#registry)_ | Registry specifies the registry that should be scanned by the discovery process. | | | -| `webhook` _[Webhook](#webhook)_ | Webhook specifies the configuration for a webhook that is called by the registry on created, updated or deleted images/repositories. | | | -| `filter` _[Filter](#filter)_ | Filter specifies the filter that should be applied when scanning for components. If not specified, all components will be scanned. | | Optional: \{\}
| -| `discoveryInterval` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#duration-v1-meta)_ | DiscoveryInterval is the amount of time between two full scans of the registry.
Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"
May be set to zero to fetch and create it once. Defaults to 24h. | 24h | Optional: \{\}
| -| `disableStartupDiscovery` _boolean_ | DisableStartupDiscovery defines whether the discovery should not be run on startup of the discovery process. If true it will only run on schedule, see .spec.cron. | | | +| Field | Description | +| --- | --- | +| `kro` | | +| `helm` | | -#### DiscoveryStatus +#### Profile -DiscoveryStatus defines the observed state of a Discovery. +Profile represents the link between a Release and a set of matching Targets the Release is +intended to be deployed to. _Appears in:_ -- [Discovery](#discovery) +- [ProfileList](#profilelist) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `podGeneration` _integer_ | PodGeneration is the generation of the discovery object at the time the worker was instantiated. | | | +| `kind` _string_ | 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 | | | +| `apiVersion` _string_ | 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 | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[ProfileSpec](#profilespec)_ | | | | +| `status` _[ProfileStatus](#profilestatus)_ | | | | -#### Entrypoint +#### ProfileSpec -Entrypoint defines the entrypoint for deploying a ComponentVersion. + + +ProfileSpec defines the desired state of a Profile. +It points to a Release and defines target selection criteria for +Targets this Release is intended to be deployed to. _Appears in:_ -- [ComponentVersionSpec](#componentversionspec) -- [ReleaseInput](#releaseinput) +- [Profile](#profile) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `resourceName` _string_ | ResourceName is the Name of the Resource to use as the entrypoint. | | | -| `type` _[EntrypointType](#entrypointtype)_ | Type of entrypoint. | | | +| `releaseRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | ReleaseRef is a reference to a Release.
It points to the Release that is intended to be deployed to all Targets identified
by the TargetSelector. | | Required: \{\}
| +| `targetSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#labelselector-v1-meta)_ | TargetSelector is a label-based filter to identify the Targets this Release is
intended to be deployed to. | | | +| `userdata` _[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#rawextension-runtime-pkg)_ | Userdata contains arbitrary custom data or configuration which is passed to all
Targets associated with this Profile. | | | -#### EntrypointType +#### ProfileStatus -_Underlying type:_ _string_ -EntrypointType is the Type of Entrypoint. + +ProfileStatus defines the observed state of a Profile. _Appears in:_ -- [Entrypoint](#entrypoint) +- [Profile](#profile) -| Field | Description | -| --- | --- | -| `kro` | | -| `helm` | | +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `matchedTargets` _integer_ | MatchedTargets is the total number of Targets matching the target selection criteria. | | | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#condition-v1-meta) array_ | Conditions represent the latest available observations of the Profile's state. | | | -#### Filter +#### Registry + -Filter defines the filter criteria used to determine which components should be scanned. + +Registry represents an OCI registry that can be used as a source or destination for artifacts. _Appears in:_ -- [DiscoverySpec](#discoveryspec) +- [RegistryList](#registrylist) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `repositoryPatterns` _string array_ | RepositoryPatterns defines which repositories should be scanned for components. The default value is empty, which means that all repositories will be scanned.
Wildcards are supported, e.g. "foo-*" or "*-dev". | | | +| `kind` _string_ | 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 | | | +| `apiVersion` _string_ | 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 | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[RegistrySpec](#registryspec)_ | | | | +| `status` _[RegistryStatus](#registrystatus)_ | | | | -#### Profile +#### RegistryBinding -Profile represents the link between a Release and a set of matching Targets the Release is -intended to be deployed to. +RegistryBinding declares that a specific Target is allowed to use a specific Registry. _Appears in:_ -- [ProfileList](#profilelist) +- [RegistryBindingList](#registrybindinglist) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `kind` _string_ | 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 | | | | `apiVersion` _string_ | 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 | | | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | -| `spec` _[ProfileSpec](#profilespec)_ | | | | -| `status` _[ProfileStatus](#profilestatus)_ | | | | +| `spec` _[RegistryBindingSpec](#registrybindingspec)_ | | | | +| `status` _[RegistryBindingStatus](#registrybindingstatus)_ | | | | -#### ProfileSpec +#### RegistryBindingSpec -ProfileSpec defines the desired state of a Profile. -It points to a Release and defines target selection criteria for -Targets this Release is intended to be deployed to. +RegistryBindingSpec defines the desired state of a RegistryBinding. _Appears in:_ -- [Profile](#profile) +- [RegistryBinding](#registrybinding) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `releaseRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | ReleaseRef is a reference to a Release.
It points to the Release that is intended to be deployed to all Targets identified
by the TargetSelector. | | Required: \{\}
| -| `targetSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#labelselector-v1-meta)_ | TargetSelector is a label-based filter to identify the Targets this Release is
intended to be deployed to. | | | -| `userdata` _[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#rawextension-runtime-pkg)_ | Userdata contains arbitrary custom data or configuration which is passed to all
Targets associated with this Profile. | | | +| `targetRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | TargetRef references the Target this binding applies to. | | | +| `registryRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | RegistryRef references the Registry being bound. | | | -#### ProfileStatus +#### RegistryBindingStatus -ProfileStatus defines the observed state of a Profile. +RegistryBindingStatus defines the observed state of a RegistryBinding. _Appears in:_ -- [Profile](#profile) +- [RegistryBinding](#registrybinding) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `matchedTargets` _integer_ | MatchedTargets is the total number of Targets matching the target selection criteria. | | | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#condition-v1-meta) array_ | Conditions represent the latest available observations of the Profile's state. | | | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#condition-v1-meta) array_ | Conditions represent the latest available observations of a RegistryBinding's state. | | | -#### Registry +#### RegistrySpec -Registry defines the configuration for a registry. +RegistrySpec defines the desired state of a Registry. _Appears in:_ -- [DiscoverySpec](#discoveryspec) +- [Registry](#registry) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `endpoint` _string_ | Endpoint is the hostname (and optionally port) of the registry, e.g. "registry.example.com" or "registry.example.com:443".
This must not include a scheme (use PlainHTTP to control HTTP vs HTTPS). | | | -| `secretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | SecretRef specifies the secret containing the relevant credentials for the registry that should be used during discovery. | | | -| `caConfigMapRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | CAConfigMapRef contains CA bundle for registry connections (e.g., trust-manager's root-bundle). Key is expected to be "trust-bundle.pem". | | | -| `plainHTTP` _boolean_ | PlainHTTP defines whether the registry should be accessed via plain HTTP instead of HTTPS. | | | +| `hostname` _string_ | Hostname is the registry endpoint (e.g. "registry.example.com:5000"). | | | +| `plainHTTP` _boolean_ | PlainHTTP uses HTTP instead of HTTPS for connections to this registry. | | | +| `solarSecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | SolarSecretRef references a Secret in the same namespace with credentials
to access this registry from the SolAr cluster. Required if this registry
is used as a render target. | | | +| `targetSecretRef` _[TargetSecretReference](#targetsecretreference)_ | TargetSecretRef describes where the credentials secret lives in the target cluster.
Used by the target agent for pull access. | | | + + +#### RegistryStatus + + + +RegistryStatus defines the observed state of a Registry. + + + +_Appears in:_ +- [Registry](#registry) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#condition-v1-meta) array_ | Conditions represent the latest available observations of a Registry's state. | | | #### Release @@ -460,6 +403,61 @@ _Appears in:_ | `status` _[ReleaseStatus](#releasestatus)_ | | | | +#### ReleaseBinding + + + +ReleaseBinding declares that a Release should be deployed to a Target. + + + +_Appears in:_ +- [ReleaseBindingList](#releasebindinglist) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `kind` _string_ | 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 | | | +| `apiVersion` _string_ | 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 | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[ReleaseBindingSpec](#releasebindingspec)_ | | | | +| `status` _[ReleaseBindingStatus](#releasebindingstatus)_ | | | | + + + + +#### ReleaseBindingSpec + + + +ReleaseBindingSpec defines the desired state of a ReleaseBinding. + + + +_Appears in:_ +- [ReleaseBinding](#releasebinding) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `targetRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | TargetRef references the Target this release is bound to. | | | +| `releaseRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | ReleaseRef references the Release to deploy. | | | + + +#### ReleaseBindingStatus + + + +ReleaseBindingStatus defines the observed state of a ReleaseBinding. + + + +_Appears in:_ +- [ReleaseBinding](#releasebinding) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#condition-v1-meta) array_ | Conditions represent the latest available observations of a ReleaseBinding's state. | | | + + #### ReleaseComponent @@ -592,12 +590,14 @@ _Appears in:_ | `type` _[RendererConfigType](#rendererconfigtype)_ | Type defines the output type of the renderer. | | | | `release` _[ReleaseConfig](#releaseconfig)_ | ReleaseConfig is a config for a release. | | | | `bootstrap` _[BootstrapConfig](#bootstrapconfig)_ | BootstrapConfig is a config for a bootstrap. | | | -| `repository` _string_ | Repository is the Repository where the chart will be pushed to (e.g. charts/mychart)
Keep in mind that the repository gets automatically prefixed with the
registry by the rendertask-controller. | | | +| `repository` _string_ | Repository is the Repository where the chart will be pushed to (e.g. charts/mychart) | | | | `tag` _string_ | Tag is the Tag of the helm chart to be pushed.
Make sure that the tag matches the version in Chart.yaml, otherwise helm
will error before pushing. | | | +| `baseURL` _string_ | BaseURL is the registry URL to push the rendered chart to (e.g. "registry.example.com:5000"). | | | +| `pushSecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | PushSecretRef references a Secret in the same namespace with registry credentials
for pushing the rendered chart. | | | | `failedJobTTL` _integer_ | failedJobTTL is the TTL in seconds after which a failed render job and its secrets are cleaned up.
After this duration, the Kubernetes TTL controller will delete the Job and the controller will delete
the Secrets (ConfigSecret, AuthSecret). On success, Job and Secrets are deleted immediately.
If not set, defaults to 3600 (1 hour). | | | | `ownerName` _string_ | OwnerName is the name of the resource that created this RenderTask. | | MinLength: 1
| | `ownerNamespace` _string_ | OwnerNamespace is the namespace of the resource that created this RenderTask. | | MinLength: 1
| -| `ownerKind` _string_ | OwnerKind is the kind of the resource that created this RenderTask (e.g. Release, Bootstrap). | | MinLength: 1
| +| `ownerKind` _string_ | OwnerKind is the kind of the resource that created this RenderTask (e.g. Release, Target). | | MinLength: 1
| #### RenderTaskStatus @@ -700,69 +700,55 @@ _Appears in:_ -#### TargetSpec +#### TargetSecretReference -TargetSpec defines the desired state of a Target. -It specifies the releases and configuration intended for this deployment target. +TargetSecretReference is a reference to a Secret in a target cluster. _Appears in:_ -- [Target](#target) +- [RegistrySpec](#registryspec) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `releases` _object (keys:string, values:[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core))_ | Releases is a map of release names to their corresponding Release object references.
Each entry represents a component release intended for deployment on this target. | | | -| `userdata` _[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#rawextension-runtime-pkg)_ | Userdata contains arbitrary custom data or configuration specific to this target.
This enables target-specific customization and deployment parameters. | | | +| `name` _string_ | Name is the name of the Secret. | | | +| `namespace` _string_ | Namespace is the namespace of the Secret. | | | -#### TargetStatus +#### TargetSpec -TargetStatus defines the observed state of a Target. +TargetSpec defines the desired state of a Target. +It specifies the render registry and configuration for this deployment target. _Appears in:_ - [Target](#target) - - -#### Webhook - - - -Webhook represents the configuration for a webhook. - - - -_Appears in:_ -- [DiscoverySpec](#discoveryspec) - | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `flavor` _string_ | Flavor is the webhook implementation to use. | | Pattern: `^(@(zot)$`
| -| `path` _string_ | Path is where the webhook should listen. | | | -| `auth` _[WebhookAuth](#webhookauth)_ | Auth is the authentication information to use with the webhook. | | | - +| `renderRegistryRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | RenderRegistryRef references the Registry to push rendered desired state to.
The referenced Registry must have SolarSecretRef set for rendering to succeed. | | | +| `userdata` _[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#rawextension-runtime-pkg)_ | Userdata contains arbitrary custom data or configuration specific to this target.
This enables target-specific customization and deployment parameters. | | | -#### WebhookAuth +#### TargetStatus +TargetStatus defines the observed state of a Target. _Appears in:_ -- [Webhook](#webhook) +- [Target](#target) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `type` _[AuthenticationType](#authenticationtype)_ | Type represents the type of authentication to use. Currently, only "token" is supported. | | | -| `authSecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#localobjectreference-v1-core)_ | AuthSecretRef is the reference to the secret which contains the authentication information for the webhook. | | | +| `bootstrapVersion` _integer_ | BootstrapVersion is a monotonically increasing counter used as the bootstrap
chart version. It is incremented each time the bootstrap chart is re-rendered,
e.g. when the set of bound releases changes. | | | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#condition-v1-meta) array_ | Conditions represent the latest available observations of a Target's state. | | | diff --git a/docs/user-guide/discovery.md b/docs/user-guide/discovery.md new file mode 100644 index 00000000..e1702f71 --- /dev/null +++ b/docs/user-guide/discovery.md @@ -0,0 +1,238 @@ +# SolAr Discovery + +SolAr Discovery is a standalone tool that scans OCI registries for +[Open Component Model (OCM)](https://ocm.software) packages and populates the +SolAr catalog by creating `Component` and `ComponentVersion` resources in a +Kubernetes cluster. + +Discovery is **fully optional** — the SolAr catalog can be populated through +other means (direct API calls, GitOps, catalog chaining). See +[ADR-008](../developer-guide/adrs/008-No-Auth-Architecture.md), principle 6. + +## Operating Modes + +Discovery supports two operating modes that can be used independently or +combined on the same registry. + +### Scan Mode + +In scan mode, discovery periodically performs a full scan of the registry, +walking all repositories to find OCM component descriptors. This is the +simplest mode and works with any OCI registry. + +```yaml +registries: + - name: my-registry + hostname: registry.example.com + scanInterval: 24h +``` + +### Webhook Mode + +In webhook mode, discovery listens for HTTP notifications from the registry. +When a new image is pushed or deleted, the registry sends an event and +discovery processes it immediately. This provides near-real-time catalog +updates. + +Webhook mode requires a registry that supports event notifications (e.g. Zot). + +```yaml +registries: + - name: my-registry + hostname: registry.example.com + webhookPath: events + flavor: zot +``` + +### Combined Mode + +Both modes can be enabled on the same registry. The scan provides a baseline +and catches anything the webhook might miss; the webhook provides real-time +updates between scans. + +```yaml +registries: + - name: my-registry + hostname: registry.example.com + scanInterval: 24h + webhookPath: events + flavor: zot +``` + +## Installation + +### Helm Chart + +The recommended way to deploy discovery in a Kubernetes cluster: + +```bash +helm upgrade --install solar-discovery oci://ghcr.io/opendefensecloud/charts/solar-discovery \ + --namespace solar-system \ + --set namespace=solar-system \ + --values my-values.yaml +``` + +### Binary + +Discovery can also be run as a standalone binary outside a cluster: + +```bash +solar-discovery --config config.yaml --namespace default +``` + +When running outside a cluster, set the `KUBECONFIG` environment variable to +point to a kubeconfig file with access to the target cluster's SolAr API. + +## Configuration + +### Config File Format + +The config file is a YAML file listing the registries to scan: + +```yaml +registries: + - name: production + hostname: registry.example.com + scanInterval: 24h + credentials: + username: ${REGISTRY_USERNAME} + password: ${REGISTRY_PASSWORD} + + - name: staging + hostname: staging-registry.example.com + scanInterval: 1h + plainHTTP: true +``` + +### Environment Variable Substitution + +The config file supports `$VAR` and `${VAR}` syntax for environment variable +expansion. This is the recommended way to inject credentials without storing +them in plaintext: + +```yaml +registries: + - name: my-registry + hostname: registry.example.com + credentials: + username: ${REGISTRY_USERNAME} + password: ${REGISTRY_PASSWORD} +``` + +### Registry Fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | yes | — | Unique local identifier for this registry | +| `hostname` | string | yes | — | Registry hostname and optional port | +| `scanInterval` | duration | no | `24h` | How often to run a full scan (set to `0` to disable scan mode) | +| `webhookPath` | string | no | — | Webhook endpoint path (enables webhook mode) | +| `flavor` | string | no | — | Webhook implementation (e.g. `zot`) | +| `plainHTTP` | bool | no | `false` | Use HTTP instead of HTTPS | +| `credentials.username` | string | no | — | Registry username | +| `credentials.password` | string | no | — | Registry password | + +### CLI Flags + +| Flag | Short | Default | Description | +|------|-------|---------|-------------| +| `--config` | `-c` | — | Path to the registry config file (required) | +| `--namespace` | `-n` | `default` | Kubernetes namespace for Component/ComponentVersion resources | +| `--listen` | `-l` | `0.0.0.0:8080` | Address for the webhook HTTP listener | + +### Helm Chart Values + +See `charts/solar-discovery/values.yaml` for the full list of configurable +values. Key settings: + +| Value | Description | +|-------|-------------| +| `registries` | List of registry configurations (rendered into the ConfigMap) | +| `namespace` | Target namespace for discovered resources | +| `envFrom` | Secret/ConfigMap references for environment variables (credentials) | +| `caBundle.enabled` | Mount a CA bundle ConfigMap for TLS connections | +| `caBundle.configMapName` | Name of the CA bundle ConfigMap | +| `service.enabled` | Create a Service for webhook mode | +| `rbac.create` | Create ClusterRole/ClusterRoleBinding for API access | + +## Examples + +### Minimal Scan-Only Setup + +```yaml +# values.yaml +registries: + - name: main + hostname: registry.example.com + scanInterval: 1h + +namespace: solar-system +service: + enabled: false +``` + +### Webhook with Zot Registry + +```yaml +# values.yaml +registries: + - name: zot + hostname: zot.internal:5000 + webhookPath: events + flavor: zot + credentials: + username: ${username} + password: ${password} + +namespace: solar-system + +envFrom: + - secretRef: + name: zot-credentials + +caBundle: + enabled: true + configMapName: root-bundle +``` + +### Multiple Registries + +```yaml +# values.yaml +registries: + - name: production + hostname: prod-registry.example.com + scanInterval: 24h + credentials: + username: ${PROD_USERNAME} + password: ${PROD_PASSWORD} + + - name: staging + hostname: staging-registry.example.com + scanInterval: 30m + webhookPath: events + flavor: zot + credentials: + username: ${STAGING_USERNAME} + password: ${STAGING_PASSWORD} + +namespace: solar-system + +envFrom: + - secretRef: + name: registry-credentials +``` + +### Running Outside a Cluster + +```bash +# Set kubeconfig for API access +export KUBECONFIG=~/.kube/config + +# Set registry credentials +export REGISTRY_USERNAME=admin +export REGISTRY_PASSWORD=secret + +# Run discovery +solar-discovery --config config.yaml --namespace solar-system +``` diff --git a/go.mod b/go.mod index 62c137e0..27285944 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/cenkalti/backoff/v4 v4.3.0 github.com/cloudevents/sdk-go/v2 v2.16.2 + github.com/coreos/go-oidc/v3 v3.17.0 github.com/creasty/defaults v1.8.0 github.com/evanphx/json-patch/v5 v5.9.11 github.com/go-logr/logr v1.4.3 @@ -19,6 +20,7 @@ require ( github.com/spf13/cobra v1.10.2 go.opendefense.cloud/kit v0.3.2 go.uber.org/zap v1.27.1 + golang.org/x/oauth2 v0.36.0 golang.org/x/time v0.15.0 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v4 v4.1.4 @@ -132,7 +134,6 @@ require ( github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.2.1 // indirect github.com/containers/storage v1.59.1 // indirect - github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect @@ -373,7 +374,6 @@ require ( golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.52.0 // indirect - golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect diff --git a/hack/dev-cluster.sh b/hack/dev-cluster.sh index bd0cc28e..86860d24 100755 --- a/hack/dev-cluster.sh +++ b/hack/dev-cluster.sh @@ -174,12 +174,31 @@ setup_solar() { -f test/fixtures/solar.values.yaml \ --set apiserver.image.tag="$TAG" \ --set controller.image.tag="$TAG" \ - --set renderer.image.tag="$TAG" \ - --set discovery.image.tag="$TAG" + --set renderer.image.tag="$TAG" $KUBECTL apply --namespace=solar-system \ -f test/fixtures/e2e/zot-deploy-auth.yaml } +# setup_discovery installs the solar-discovery Helm chart into the solar-system namespace. +setup_discovery() { + echo -e "\nSETTING UP SOLAR-DISCOVERY:\n" + $KUBECTL apply --namespace=solar-system -f test/fixtures/e2e/zot-discovery-auth.yaml + $HELM upgrade --install \ + --namespace=solar-system \ + solar-discovery charts/solar-discovery \ + -f test/fixtures/solar-discovery-webhook.values.yaml \ + --set image.tag="$TAG" \ + --set namespace=solar-system + $KUBECTL wait deployment \ + --namespace solar-system \ + -l app.kubernetes.io/instance=solar-discovery \ + --for condition=Available \ + --timeout 5m + # Update discovery webhook pointer service to point to the discovery service + $KUBECTL apply --namespace zot \ + -f test/fixtures/discovery-webhook-ptr-svc.yaml +} + # main orchestrates cluster setup by invoking cert-manager, trust-manager, Zot components, Flux, and (unless SKIP_SOLAR is "true") Solar, then prints DONE. main() { echo "Switching kubectl context to kind-${KIND_CLUSTER}..." @@ -191,7 +210,10 @@ main() { setup_flux if [[ "$SKIP_SOLAR" != "true" ]]; then + $KUBECTL create namespace solar-system 2>/dev/null || true + $KUBECTL label namespace solar-system trust=enabled --overwrite setup_solar + setup_discovery fi echo -e "\nDONE" diff --git a/hack/generate-dex-certs.sh b/hack/generate-dex-certs.sh new file mode 100755 index 00000000..c3516e24 --- /dev/null +++ b/hack/generate-dex-certs.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CERT_DIR="${CERT_DIR:-$PROJECT_DIR/test/fixtures}" + +DEX_CA_CERT="$CERT_DIR/dex-ca.crt" +DEX_CA_KEY="$CERT_DIR/dex-ca.key" +DEX_TLS_CERT="$CERT_DIR/dex-tls.crt" +DEX_TLS_KEY="$CERT_DIR/dex-tls.key" +AUTH_CONFIG="/tmp/solar-dex-auth-config.yaml" + +if [[ -f "$DEX_CA_CERT" && -f "$DEX_TLS_CERT" ]]; then + echo "Dex certificates already exist, skipping generation." +else + echo "Generating Dex CA certificate..." + openssl req -x509 -newkey rsa:2048 -keyout "$DEX_CA_KEY" -out "$DEX_CA_CERT" \ + -days 3650 -nodes -subj "/CN=Dex Test CA" 2>/dev/null + + echo "Generating Dex TLS certificate..." + openssl req -newkey rsa:2048 -keyout "$DEX_TLS_KEY" -out /tmp/dex-tls.csr \ + -nodes -subj "/CN=localhost" 2>/dev/null + + openssl x509 -req -in /tmp/dex-tls.csr \ + -CA "$DEX_CA_CERT" -CAkey "$DEX_CA_KEY" -CAcreateserial \ + -out "$DEX_TLS_CERT" -days 3650 \ + -extfile <(printf "subjectAltName=DNS:localhost,DNS:dex.dex.svc.cluster.local,DNS:dex.dex.svc,DNS:dex") \ + 2>/dev/null + + rm -f /tmp/dex-tls.csr "$CERT_DIR/dex-ca.srl" + + echo "Dex certificates generated:" + echo " CA cert: $DEX_CA_CERT" + echo " TLS cert: $DEX_TLS_CERT" +fi + +# Generate K8s AuthenticationConfiguration with CA cert inlined. +# This is mounted into the Kind node for the API server to use. +echo "Generating K8s OIDC AuthenticationConfiguration..." +CA_CONTENT=$(sed 's/^/ /' "$DEX_CA_CERT") +cat > "$AUTH_CONFIG" </dev/null || true + +# Create TLS secret from generated certificates +echo "Creating Dex TLS secret..." +$KUBECTL create secret tls dex-tls -n dex \ + --cert="$CERT_DIR/dex-tls.crt" \ + --key="$CERT_DIR/dex-tls.key" \ + --dry-run=client -o yaml | $KUBECTL apply -f - + +# Deploy Dex config and deployment +$KUBECTL apply -f "$PROJECT_DIR/test/fixtures/e2e/dex/dex-config.yaml" +$KUBECTL apply -f "$PROJECT_DIR/test/fixtures/e2e/dex/dex-deployment.yaml" + +echo "Waiting for Dex deployment to be available..." +$KUBECTL wait deployment/dex -n dex --for=condition=Available --timeout=120s + +# Grant the static Dex user cluster-admin for dev/test +$KUBECTL apply -f "$PROJECT_DIR/test/fixtures/e2e/dex/dex-rbac.yaml" + +# Apply dev RBAC: solar roles for impersonation targets + impersonatable ClusterRoles +$KUBECTL apply -f "$PROJECT_DIR/test/fixtures/e2e/dex/dev-solar-rbac.yaml" + +echo "Dex setup complete." diff --git a/pkg/controller/bootstrap_controller.go b/pkg/controller/bootstrap_controller.go deleted file mode 100644 index cd6ba569..00000000 --- a/pkg/controller/bootstrap_controller.go +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -package controller - -import ( - "context" - "errors" - "fmt" - "net/url" - "slices" - "strings" - "time" - - ociname "github.com/google/go-containerregistry/pkg/name" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/events" - 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/handler" - - solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" -) - -const ( - bootstrapFinalizer = "solar.opendefense.cloud/bootstrap-finalizer" -) - -// BootstrapReconciler reconciles a Bootstrap object -type BootstrapReconciler struct { - client.Client - Scheme *runtime.Scheme - Recorder events.EventRecorder - // WatchNamespace restricts reconciliation to this namespace. - // Should be empty in production (watches all namespaces). - // Intended for use in integration tests only. - // See: https://book.kubebuilder.io/reference/envtest#testing-considerations - WatchNamespace string -} - -var ErrReleaseNotRenderedYet = errors.New("release is not rendered yet") - -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=bootstraps,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=bootstraps/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=bootstraps/finalizers,verbs=update -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=profiles,verbs=get;list;watch -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch -//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch - -// Reconcile moves the current state of the cluster closer to the desired state -// -// Reconciliation Flow: -// -// Bootstrap created -// ↓ -// Add finalizer -// ↓ -// Check if already succeeded → YES → Return (no-op) -// ↓ NO -// Get or create RenderTask -// ↓ -// Update status from RenderTask - -func (r *BootstrapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := ctrl.LoggerFrom(ctx) - ctrlResult := ctrl.Result{} - - log.V(1).Info("Bootstrap is being reconciled", "req", req) - - if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace { - return ctrlResult, nil - } - - // Fetch the Bootstrap instance - res := &solarv1alpha1.Bootstrap{} - if err := r.Get(ctx, req.NamespacedName, res); err != nil { - if apierrors.IsNotFound(err) { - // Object not found, return. Created objects are automatically garbage collected. - return ctrlResult, nil - } - - return ctrlResult, errLogAndWrap(log, err, "failed to get object") - } - - // Handle deletion: cleanup rendertask, then remove finalizer - if !res.DeletionTimestamp.IsZero() { - log.V(1).Info("Bootstrap is being deleted") - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "Bootstrap is being deleted, cleaning up resources") - - if err := r.deleteRenderTask(ctx, res); client.IgnoreNotFound(err) != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to delete render task") - } - - // Remove finalizer - if slices.Contains(res.Finalizers, bootstrapFinalizer) { - log.V(1).Info("Removing finalizer from resource") - res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool { - return f == bootstrapFinalizer - }) - if err := r.Update(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer") - } - } - - return ctrlResult, nil - } - - // Add finalizer if not present and not deleting - if res.DeletionTimestamp.IsZero() { - if !slices.Contains(res.Finalizers, bootstrapFinalizer) { - log.V(1).Info("Adding finalizer to resource") - res.Finalizers = append(res.Finalizers, bootstrapFinalizer) - if err := r.Update(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer") - } - // Return without requeue; the Update event will trigger reconciliation again - return ctrlResult, nil - } - } - - // Check if rendertask has already completed successfully - sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeTaskCompleted) - if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue { - log.V(1).Info("RenderTask has already completed successfully, no further action needed") - return ctrlResult, nil - } - - // Check if rendertask has already failed - fc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeTaskFailed) - if fc != nil && fc.ObservedGeneration >= res.Generation && fc.Status == metav1.ConditionTrue { - log.V(1).Info("RenderTask has already failed, no further action needed") - return ctrlResult, nil - } - - // Reconcile RenderTask - rt := &solarv1alpha1.RenderTask{} - err := r.Get(ctx, client.ObjectKey{Name: renderTaskName(res)}, rt) - if client.IgnoreNotFound(err) != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to get RenderTask") - } - - if apierrors.IsNotFound(err) { - if err := r.createRenderTask(ctx, res); err != nil { - if errors.Is(err, ErrReleaseNotRenderedYet) { - return ctrl.Result{RequeueAfter: 30 * time.Second}, nil - } - log.V(1).Error(err, "Failed to create RenderTask") - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", fmt.Sprintf("failed to create RenderTask: %q", err)) - - if apierrors.IsNotFound(err) { - return ctrl.Result{RequeueAfter: 30 * time.Second}, nil - } - - return ctrlResult, errLogAndWrap(log, err, "failed to create RenderTask") - } - log.V(1).Info("Created RenderTask", "res", res) - r.Recorder.Eventf(res, rt, corev1.EventTypeNormal, "Created", "Create", "RenderTask was created") - } - - if changed := r.updateStatusConditionsFromRenderTask(ctx, res, rt); changed { - if err := r.Status().Update(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to update status") - } - } - - // RenderTask still running - return ctrlResult, nil -} - -func (r *BootstrapReconciler) updateStatusConditionsFromRenderTask(ctx context.Context, res *solarv1alpha1.Bootstrap, rt *solarv1alpha1.RenderTask) (changed bool) { - if rt == nil || res == nil { - return false - } - - log := ctrl.LoggerFrom(ctx) - - if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) { - changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{ - Type: ConditionTypeTaskFailed, - Status: metav1.ConditionTrue, - ObservedGeneration: res.Generation, - Reason: "TaskFailed", - Message: "RenderTask failed", - }) - - log.V(1).Info("RenderTask failed", "name", rt.Name) - r.Recorder.Eventf(res, rt, corev1.EventTypeWarning, "TaskFailed", "RunTask", "RenderTask failed") - - return changed - } - - if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) { - changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{ - Type: ConditionTypeTaskCompleted, - Status: metav1.ConditionTrue, - ObservedGeneration: res.Generation, - Reason: "TaskCompleted", - Message: "RenderTask completed", - }) - - log.V(1).Info("RenderTask completed", "name", rt.Name) - r.Recorder.Eventf(res, rt, corev1.EventTypeWarning, "TaskCompleted", "RunTask", "RenderTask completed successfully") - - return changed - } - - log.V(1).Info("RenderTask has no final condtions yet", "name", rt.Name) - - return false -} - -func (r *BootstrapReconciler) createRenderTask(ctx context.Context, res *solarv1alpha1.Bootstrap) error { - log := ctrl.LoggerFrom(ctx) - - // Check if we need to cleanup an old task - if res.Status.RenderTaskRef != nil && res.Status.RenderTaskRef.Name != "" { - if err := r.deleteRenderTask(ctx, res); err != nil { - return errLogAndWrap(log, err, "failed to cleanup old task") - } - } - - spec, err := r.computeRenderTaskSpec(ctx, res) - if err != nil { - return err - } - rt := &solarv1alpha1.RenderTask{ - ObjectMeta: metav1.ObjectMeta{ - Name: renderTaskName(res), - }, - Spec: spec, - } - rt.Spec.OwnerName = res.Name - rt.Spec.OwnerNamespace = res.Namespace - rt.Spec.OwnerKind = "Bootstrap" - - if err := r.Create(ctx, rt); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create RenderTask", err) - return errLogAndWrap(log, err, "failed to create RenderTask") - } - - // Set Reference in Status - res.Status.RenderTaskRef = &corev1.ObjectReference{ - APIVersion: solarv1alpha1.SchemeGroupVersion.String(), - Kind: "RenderTask", - Name: rt.Name, - } - - if err := r.Status().Update(ctx, res); err != nil { - return errLogAndWrap(log, err, "failed to update status") - } - - return nil -} - -func (r *BootstrapReconciler) deleteRenderTask(ctx context.Context, res *solarv1alpha1.Bootstrap) error { - if res.Status.RenderTaskRef == nil { - return nil - } - - rt := &solarv1alpha1.RenderTask{} - if err := r.Get(ctx, client.ObjectKey{Name: res.Status.RenderTaskRef.Name}, rt); client.IgnoreNotFound(err) != nil { - return err - } else if err == nil { - return r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)) - } - - return nil -} - -func (r *BootstrapReconciler) computeRenderTaskSpec(ctx context.Context, res *solarv1alpha1.Bootstrap) (solarv1alpha1.RenderTaskSpec, error) { - spec := solarv1alpha1.RenderTaskSpec{} - - releases := map[string]*solarv1alpha1.Release{} - for _, v := range res.Spec.Releases { - rel := &solarv1alpha1.Release{} - if err := r.Get(ctx, client.ObjectKey{Name: v.Name, Namespace: res.Namespace}, rel); err != nil { - return spec, err - } - releases[rel.Name] = rel - } - for _, v := range res.Spec.Profiles { - prf := &solarv1alpha1.Profile{} - if err := r.Get(ctx, client.ObjectKey{Name: v.Name, Namespace: res.Namespace}, prf); err != nil { - return spec, err - } - if _, exists := releases[prf.Spec.ReleaseRef.Name]; exists { - continue - } - rel := &solarv1alpha1.Release{} - if err := r.Get(ctx, client.ObjectKey{Name: prf.Spec.ReleaseRef.Name, Namespace: res.Namespace}, rel); err != nil { - return spec, err - } - releases[rel.Name] = rel - } - - isReleaseRendered := func(rel *solarv1alpha1.Release) (bool, error) { - condFailed := apimeta.FindStatusCondition(rel.Status.Conditions, ConditionTypeTaskFailed) - if condFailed != nil && - condFailed.Status == metav1.ConditionTrue && - condFailed.ObservedGeneration >= rel.Generation { - return false, fmt.Errorf("rendering release %s has failed", rel.Name) - } - - condCompleted := apimeta.FindStatusCondition(rel.Status.Conditions, ConditionTypeTaskCompleted) - - return condCompleted != nil && - condCompleted.Status == metav1.ConditionTrue && - condCompleted.ObservedGeneration >= rel.Generation, nil - } - - resolvedReleases := map[string]solarv1alpha1.ResourceAccess{} - for k, v := range releases { - - if ok, err := isReleaseRendered(v); !ok { - if err != nil { - return spec, err - } - - return spec, fmt.Errorf("release %s: %w", k, ErrReleaseNotRenderedYet) - } - - if v.Status.ChartURL == "" { - return spec, fmt.Errorf("chartURL of release %s was empty, check the release's status", k) - } - - ref, err := ociname.ParseReference(v.Status.ChartURL) - if err != nil { - return spec, err - } - - repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr()) - if err != nil { - return spec, err - } - - resolvedReleases[k] = solarv1alpha1.ResourceAccess{ - Repository: strings.TrimPrefix(repo, "oci://"), - Tag: ref.Identifier(), - } - } - - resolvedReleaseNames := make([]string, 0, len(resolvedReleases)) - for k := range resolvedReleases { - resolvedReleaseNames = append(resolvedReleaseNames, k) - } - - chartName := fmt.Sprintf("bootstrap-%s", res.Name) - repo, err := url.JoinPath(res.Namespace, chartName) - if err != nil { - return spec, err - } - - tag := fmt.Sprintf("v0.0.%d", res.GetGeneration()) - - spec.RendererConfig = solarv1alpha1.RendererConfig{ - Type: solarv1alpha1.RendererConfigTypeBootstrap, - BootstrapConfig: solarv1alpha1.BootstrapConfig{ - Chart: solarv1alpha1.ChartConfig{ - Name: chartName, - Description: fmt.Sprintf("Bootstrap of %v", resolvedReleaseNames), - Version: tag, - AppVersion: tag, - }, - Input: solarv1alpha1.BootstrapInput{ - Releases: resolvedReleases, - Userdata: res.Spec.Userdata, - }, - }, - } - spec.Repository = repo - spec.Tag = tag - - return spec, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *BootstrapReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&solarv1alpha1.Bootstrap{}). - Watches(&solarv1alpha1.RenderTask{}, - handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Bootstrap")), - builder.WithPredicates(renderTaskStatusChangePredicate()), - ). - Complete(r) -} diff --git a/pkg/controller/bootstrap_controller_test.go b/pkg/controller/bootstrap_controller_test.go deleted file mode 100644 index 374c384a..00000000 --- a/pkg/controller/bootstrap_controller_test.go +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -package controller - -import ( - "fmt" - "slices" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("BootstrapController", Ordered, func() { - var ( - validBootstrap = func(name string) *solarv1alpha1.Bootstrap { - return &solarv1alpha1.Bootstrap{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns.Name, - }, - Spec: solarv1alpha1.BootstrapSpec{ - Releases: map[string]corev1.LocalObjectReference{ - "rel": { - Name: "my-release-1", - }, - }, - Profiles: map[string]corev1.LocalObjectReference{ - "prf-1": { - Name: "my-profile-1", - }, - "prf-2": { - Name: "my-profile-2", - }, - }, - Userdata: runtime.RawExtension{ - Raw: []byte(`{"key": "value"}`), - }, - }, - } - } - validRelease = func(name string) *solarv1alpha1.Release { - return &solarv1alpha1.Release{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns.Name, - }, - Spec: solarv1alpha1.ReleaseSpec{ - ComponentVersionRef: corev1.LocalObjectReference{ - Name: "my-component-v1", - }, - Values: runtime.RawExtension{ - Raw: []byte(`{"key": "value"}`), - }, - }, - } - } - validProfile = func(name string, releaseName string) *solarv1alpha1.Profile { - return &solarv1alpha1.Profile{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns.Name, - }, - Spec: solarv1alpha1.ProfileSpec{ - ReleaseRef: corev1.LocalObjectReference{ - Name: releaseName, - }, - }, - } - } - setReleaseStatus = func(rel *solarv1alpha1.Release, tag string) { - rel.Status.ChartURL = fmt.Sprintf("oci://%s/%s:%s", ns.Name, rel.Name, tag) - apimeta.SetStatusCondition(&rel.Status.Conditions, metav1.Condition{ - Type: ConditionTypeTaskCompleted, - Status: metav1.ConditionTrue, - ObservedGeneration: rel.Generation, - }) - } - ) - - BeforeEach(func() { - // Create the referenced Releases and Profiles - rel1 := validRelease("my-release-1") - Expect(k8sClient.Create(ctx, rel1)).To(Succeed()) - patch1 := client.MergeFrom(rel1.DeepCopy()) - setReleaseStatus(rel1, "v1.1.1") - Expect(k8sClient.Status().Patch(ctx, rel1, patch1)).To(Succeed()) - - rel2 := validRelease("my-release-2") - Expect(k8sClient.Create(ctx, rel2)).To(Succeed()) - patch2 := client.MergeFrom(rel2.DeepCopy()) - setReleaseStatus(rel2, "v2.2.2") - Expect(k8sClient.Status().Patch(ctx, rel2, patch2)).To(Succeed()) - - prf1 := validProfile("my-profile-1", "my-release-1") - Expect(k8sClient.Create(ctx, prf1)).To(Succeed()) - - prf2 := validProfile("my-profile-2", "my-release-2") - Expect(k8sClient.Create(ctx, prf2)).To(Succeed()) - }) - - Describe("Bootstrap creation and RenderTask creation", func() { - It("should create a Bootstrap and create a RenderTask", func() { - // Create a Bootstrap - bs := validBootstrap("test-bootstrap") - Expect(k8sClient.Create(ctx, bs)).To(Succeed()) - - // Verify the Bootstrap was created - createdBootstrap := &solarv1alpha1.Bootstrap{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap", Namespace: ns.Name}, createdBootstrap) - }).Should(Succeed()) - - // Verify finalizer was added after a reconciliation cycle - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap", Namespace: ns.Name}, createdBootstrap) - if err != nil { - return false - } - - return len(createdBootstrap.Finalizers) > 0 && slices.Contains(createdBootstrap.Finalizers, bootstrapFinalizer) - }, eventuallyTimeout).Should(BeTrue(), "finalizer should be added by reconciler") - - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-bootstrap-0", ns.Name)}, task) - }, eventuallyTimeout).Should(Succeed()) - - Expect(task.Spec.RendererConfig.Type).To(Equal(solarv1alpha1.RendererConfigTypeBootstrap)) - Expect(task.Spec.RendererConfig.BootstrapConfig.Chart.Name).To(Equal("bootstrap-test-bootstrap")) - Expect(task.Spec.RendererConfig.BootstrapConfig.Chart.Version).To(Equal("v0.0.0")) - - checkRelease := func(name string, repo string, tag string) { - Expect(task.Spec.RendererConfig.BootstrapConfig.Input.Releases).To(HaveKey(name)) - Expect(task.Spec.RendererConfig.BootstrapConfig.Input.Releases[name].Repository).To(Equal(repo)) - Expect(task.Spec.RendererConfig.BootstrapConfig.Input.Releases[name].Tag).To(Equal(tag)) - } - checkRelease("my-release-1", fmt.Sprintf("%s/my-release-1", ns.Name), "v1.1.1") - checkRelease("my-release-2", fmt.Sprintf("%s/my-release-2", ns.Name), "v2.2.2") - - Expect(task.Spec.Repository).To(Equal(fmt.Sprintf("%s/bootstrap-test-bootstrap", ns.Name))) - Expect(task.Spec.Tag).To(Equal("v0.0.0")) - }) - }) - - Describe("Bootstrap RenderTask completion", func() { - It("should represent completion when RenderTask completes successfully", func() { - // Create a Bootstrap - bs := validBootstrap("test-bootstrap-success") - Expect(k8sClient.Create(ctx, bs)).To(Succeed()) - - // Wait for RenderTask to be created - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-bootstrap-success-0", ns.Name)}, task) - }).Should(Succeed()) - - // Manipulate Task to be Successful - Expect(apimeta.SetStatusCondition(&task.Status.Conditions, metav1.Condition{ - Type: ConditionTypeJobSucceeded, - Status: metav1.ConditionTrue, - ObservedGeneration: task.Generation, - Reason: ConditionTypeJobSucceeded, - })).To(BeTrue()) - Expect(k8sClient.Status().Update(ctx, task)).To(Succeed()) - - // Verify Bootstrap has Status Conditions - updatedBootstrap := &solarv1alpha1.Bootstrap{} - Eventually(func() bool { - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap-success", Namespace: ns.Name}, updatedBootstrap); err != nil { - return false - } - - return apimeta.IsStatusConditionTrue(updatedBootstrap.Status.Conditions, ConditionTypeTaskCompleted) - }, eventuallyTimeout).Should(BeTrue()) - - condition := apimeta.FindStatusCondition(updatedBootstrap.Status.Conditions, ConditionTypeTaskCompleted) - Expect(condition).NotTo(BeNil()) - Expect(condition.Status).To(Equal(metav1.ConditionTrue)) - Expect(condition.Reason).To(Equal("TaskCompleted")) - }) - - It("should represent failure when RenderTask failed", func() { - // Create a Bootstrap - bs := validBootstrap("test-bootstrap-failed") - Expect(k8sClient.Create(ctx, bs)).To(Succeed()) - - // Wait for RenderTask to be created - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-bootstrap-failed-0", ns.Name)}, task) - }).Should(Succeed()) - - // Manipulate Task to be Failed - Expect(apimeta.SetStatusCondition(&task.Status.Conditions, metav1.Condition{ - Type: ConditionTypeJobFailed, - Status: metav1.ConditionTrue, - ObservedGeneration: task.Generation, - Reason: ConditionTypeJobFailed, - })).To(BeTrue()) - Expect(k8sClient.Status().Update(ctx, task)).To(Succeed()) - - // Verify Bootstrap has Status Conditions - updatedBootstrap := &solarv1alpha1.Bootstrap{} - Eventually(func() bool { - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap-failed", Namespace: ns.Name}, updatedBootstrap); err != nil { - return false - } - - return apimeta.IsStatusConditionTrue(updatedBootstrap.Status.Conditions, ConditionTypeTaskFailed) - }, eventuallyTimeout).Should(BeTrue()) - - condition := apimeta.FindStatusCondition(updatedBootstrap.Status.Conditions, ConditionTypeTaskFailed) - Expect(condition).NotTo(BeNil()) - Expect(condition.Status).To(Equal(metav1.ConditionTrue)) - Expect(condition.Reason).To(Equal("TaskFailed")) - }) - }) - - Describe("Bootstrap deletion", func() { - It("should cleanup RenderTask when Bootstrap is deleted", func() { - // Create a Bootstrap - bs := validBootstrap("test-bootstrap-delete") - Expect(k8sClient.Create(ctx, bs)).To(Succeed()) - - // Wait for RenderTask to be created - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-bootstrap-delete-0", ns.Name)}, task) - }).Should(Succeed()) - - // Delete the Bootstrap - createdBootstrap := &solarv1alpha1.Bootstrap{} - Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap-delete", Namespace: ns.Name}, createdBootstrap)).To(Succeed()) - Expect(k8sClient.Delete(ctx, createdBootstrap)).To(Succeed()) - - // Verify RenderTask is deleted - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-bootstrap-delete-0", ns.Name)}, task) - }).Should(MatchError(ContainSubstring("not found"))) - - // Verify Bootstrap is deleted (finalizer removed) - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap-delete", Namespace: ns.Name}, createdBootstrap) - }).Should(MatchError(ContainSubstring("not found"))) - }) - }) - - Describe("Bootstrap status references", func() { - It("should maintain references to created RenderTask in Bootstrap status", func() { - // Create a Bootstrap - bs := validBootstrap("test-bootstrap-refs") - Expect(k8sClient.Create(ctx, bs)).To(Succeed()) - - // Wait for RenderTask to be created - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-bootstrap-refs-0", ns.Name)}, task) - }).Should(Succeed()) - - // Verify Bootstrap status has references - updatedBootstrap := &solarv1alpha1.Bootstrap{} - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap-refs", Namespace: ns.Name}, updatedBootstrap) - if err != nil { - return false - } - - return updatedBootstrap.Status.RenderTaskRef != nil - }).Should(BeTrue()) - - // Verify RenderTaskRef details - Expect(updatedBootstrap.Status.RenderTaskRef.Name).To(Equal(fmt.Sprintf("%s-test-bootstrap-refs-0", ns.Name))) - Expect(updatedBootstrap.Status.RenderTaskRef.Kind).To(Equal("RenderTask")) - Expect(updatedBootstrap.Status.RenderTaskRef.APIVersion).To(Equal("solar.opendefense.cloud/v1alpha1")) - }) - }) - - Describe("Bootstrap updates", func() { - It("should increase the Generation when the Spec changes", func() { - // Create a Bootstrap - bs := validBootstrap("test-bootstrap-gen") - Expect(k8sClient.Create(ctx, bs)).To(Succeed()) - - // Verify the Bootstrap was created - createdBootstrap := &solarv1alpha1.Bootstrap{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap-gen", Namespace: ns.Name}, createdBootstrap) - }).Should(Succeed()) - - Expect(createdBootstrap.Generation).To(Equal(int64(0))) - - // Update the Bootstrap - Eventually(func() error { - latest := &solarv1alpha1.Bootstrap{} - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap-gen", Namespace: ns.Name}, latest); err != nil { - return err - } - latest.Spec.Userdata.Raw = []byte(`{"new-shiny-value": true}`) - - return k8sClient.Update(ctx, latest) - }).Should(Succeed()) - - // Check Bootstrap after Update - updatedBootstrap := &solarv1alpha1.Bootstrap{} - Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap-gen", Namespace: ns.Name}, updatedBootstrap)).To(Succeed()) - - Expect(updatedBootstrap.Generation).To(Equal(int64(1))) - }) - - It("should create a RenderTask for the latest Generation only", func() { - // Create a Bootstrap - bs := validBootstrap("test-bootstrap-update") - Expect(k8sClient.Create(ctx, bs)).To(Succeed()) - - // Verify the RenderTask was created - initialTask := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-bootstrap-update-0", ns.Name)}, initialTask) - }).Should(Succeed()) - - // Update the Bootstrap - Eventually(func() error { - latest := &solarv1alpha1.Bootstrap{} - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-bootstrap-update", Namespace: ns.Name}, latest); err != nil { - return err - } - latest.Spec.Userdata.Raw = []byte(`{"new-shiny-value": true}`) - - return k8sClient.Update(ctx, latest) - }).Should(Succeed()) - - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-bootstrap-update-0", ns.Name)}, initialTask) - return apierrors.IsNotFound(err) - }).Should(BeTrue()) - - newTask := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-bootstrap-update-1", ns.Name)}, newTask) - }).Should(Succeed()) - }) - }) -}) diff --git a/pkg/controller/discovery_controller.go b/pkg/controller/discovery_controller.go deleted file mode 100644 index 62e6d389..00000000 --- a/pkg/controller/discovery_controller.go +++ /dev/null @@ -1,563 +0,0 @@ -// Copyright 2026 BWI GmbH and Artefact Conduit contributors -// SPDX-License-Identifier: Apache-2.0 - -package controller - -import ( - "context" - "fmt" - "slices" - - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/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/intstr" - "k8s.io/client-go/tools/events" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" - "go.opendefense.cloud/solar/pkg/discovery" -) - -const ( - discoveryFinalizer = "solar.opendefense.cloud/discovery-finalizer" - workerRoleName = "solar-discovery-worker" -) - -// DiscoveryReconciler reconciles a Discovery object -type DiscoveryReconciler struct { - client.Client - Scheme *runtime.Scheme - Recorder events.EventRecorder - WorkerImage string - WorkerCommand string - WorkerArgs []string - // WatchNamespace restricts reconciliation to this namespace. - // Should be empty in production (watches all namespaces). - // Intended for use in integration tests only. - // See: https://book.kubebuilder.io/reference/envtest#testing-considerations - WatchNamespace string -} - -//nolint:lll -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=discoveries,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=discoveries/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=discoveries/finalizers,verbs=update -//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch -//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=components,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch;create;update;patch;delete - -// needed in order to be able to grant permissions -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=components,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch;create;update;patch;delete - -// Reconcile moves the current state of the cluster closer to the desired state -func (r *DiscoveryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := ctrl.LoggerFrom(ctx) - ctrlResult := ctrl.Result{} - - log.V(1).Info("Discovery is being reconciled", "req", req) - - if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace { - return ctrlResult, nil - } - - // Fetch the Order instance - res := &solarv1alpha1.Discovery{} - if err := r.Get(ctx, req.NamespacedName, res); err != nil { - if apierrors.IsNotFound(err) { - // Object not found, return. Created objects are automatically garbage collected. - return ctrlResult, nil - } - - return ctrlResult, errLogAndWrap(log, err, "failed to get object") - } - - // Handle deletion: cleanup artifact workflows, then remove finalizer - if !res.DeletionTimestamp.IsZero() { - log.V(1).Info("Discovery is being deleted") - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "Discovery is being deleted, cleaning up worker") - - // Cleanup worker resources, if exists - if err := r.deleteWorkerResources(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to clean up worker resources") - } - - // Remove finalizer - if slices.Contains(res.Finalizers, discoveryFinalizer) { - log.V(1).Info("Removing finalizer from resource") - res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool { - return f == discoveryFinalizer - }) - if err := r.Update(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer") - } - } - - return ctrlResult, nil - } - - // Add finalizer if not present and not deleting - if res.DeletionTimestamp.IsZero() { - if !slices.Contains(res.Finalizers, discoveryFinalizer) { - log.V(1).Info("Adding finalizer to resource") - res.Finalizers = append(res.Finalizers, discoveryFinalizer) - if err := r.Update(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer") - } - // Return without requeue; the Update event will trigger reconciliation again - return ctrlResult, nil - } - } - - pod := &corev1.Pod{} - err := r.Get(ctx, types.NamespacedName{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}, pod) - if err != nil && !apierrors.IsNotFound(err) { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "PodNotFound", "GetPod", "Failed to get pod", err) - return ctrlResult, errLogAndWrap(log, err, "failed to get pod information") - } - - // No pod yet, create it. - if apierrors.IsNotFound(err) { - if err := r.createWorkerResources(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to create pod") - } - - return ctrlResult, nil - } - - // Pod exists, check if it's up to date with our configuration and if it is healthy. - if res.Status.PodGeneration != res.GetGeneration() { - // Recreate pod, configuration mismatch - r.Recorder.Eventf(res, nil, corev1.EventTypeNormal, "ConfigurationChanged", "CompareConfiguration", "Configuration changed. Replacing pod.") - if err := r.deleteWorkerResources(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to clean up worker resources") - } - - if err := r.createWorkerResources(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to create pod") - } - - return ctrlResult, nil - } else { - log.V(1).Info("Configuration hasn't changed", "podGen", res.Status.PodGeneration, "gen", res.GetGeneration()) - } - - return ctrlResult, nil -} - -// deleteWorkerResources deletes the resources of the worker pod -func (r *DiscoveryReconciler) deleteWorkerResources(ctx context.Context, res *solarv1alpha1.Discovery) error { - log := ctrl.LoggerFrom(ctx) - - if err := r.Delete(ctx, &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "ServiceDeletionFailed", "DeleteService", "Failed to delete service", err) - return errLogAndWrap(log, err, "service deletion failed") - } - - if err := r.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "SecretDeletionFailed", "DeleteSecret", "Failed to delete secret", err) - return errLogAndWrap(log, err, "secret deletion failed") - } - - if err := r.Delete(ctx, &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "PodDeletionFailed", "DeletePod", "Failed to delete pod", err) - return errLogAndWrap(log, err, "pod deletion failed") - } - - if err := r.Delete(ctx, &rbacv1.Role{ObjectMeta: metav1.ObjectMeta{Name: workerRoleName, Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleDeletionFailed", "DeleteRole", "Failed to delete role", err) - return errLogAndWrap(log, err, "role deletion failed") - } - - if err := r.Delete(ctx, &rbacv1.RoleBinding{ObjectMeta: metav1.ObjectMeta{Name: workerRoleName, Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleBindingDeletionFailed", "DeleteRoleBinding", "Failed to delete rolebinding", err) - return errLogAndWrap(log, err, "rolebinding deletion failed") - } - - return nil -} - -// createWorkerResources creates the necessary resources for the worker pod -func (r *DiscoveryReconciler) createWorkerResources(ctx context.Context, res *solarv1alpha1.Discovery) error { - log := ctrl.LoggerFrom(ctx) - - // Create or get service account in the discovery's namespace - workerSA := &corev1.ServiceAccount{ - ObjectMeta: objectMeta(res), - } - - existingSA := &corev1.ServiceAccount{} - err := r.Get(ctx, types.NamespacedName{Name: workerSA.Name, Namespace: workerSA.Namespace}, existingSA) - if err != nil && !apierrors.IsNotFound(err) { - return errLogAndWrap(log, err, "failed to get service account") - } - - if apierrors.IsNotFound(err) { - if err := r.Create(ctx, workerSA); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "ServiceAccountCreationFailed", "CreateServiceAccount", "Failed to create service account", err) - return errLogAndWrap(log, err, "failed to create service account") - } - r.Recorder.Eventf(res, workerSA, corev1.EventTypeNormal, "ServiceAccountCreated", "CreateServiceAccount", "ServiceAccount created") - if err := controllerutil.SetControllerReference(res, workerSA, r.Scheme); err != nil { - return errLogAndWrap(log, err, "failed to set controller reference on service account") - } - } - - // Create Role to define RBAC permissions required for discovery worker - role := &rbacv1.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: workerRoleName, - Namespace: res.Namespace, - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "solar-discovery-controller", - }, - }, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{solarv1alpha1.SchemeGroupVersion.Group}, - Resources: []string{"componentversions", "components"}, - Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, - }, - }, - } - - existingRole := &rbacv1.Role{} - err = r.Get(ctx, types.NamespacedName{Name: role.Name, Namespace: role.Namespace}, existingRole) - if err != nil && !apierrors.IsNotFound(err) { - return errLogAndWrap(log, err, "failed to get role") - } - - if apierrors.IsNotFound(err) { - if err := r.Create(ctx, role); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleCreationFailed", "CreateRole", "Failed to create role", err) - return errLogAndWrap(log, err, "failed to create role") - } - r.Recorder.Eventf(res, role, corev1.EventTypeNormal, "RoleCreated", "CreateRole", "Role created") - if err := controllerutil.SetControllerReference(res, role, r.Scheme); err != nil { - return errLogAndWrap(log, err, "failed to set controller reference on role") - } - } else { - // check if out of sync - needsUpdate := false - if len(existingRole.Rules) != len(role.Rules) || - !slices.Equal(existingRole.Rules[0].Verbs, role.Rules[0].Verbs) || - !slices.Equal(existingRole.Rules[0].APIGroups, role.Rules[0].APIGroups) || - !slices.Equal(existingRole.Rules[0].Resources, role.Rules[0].Resources) { - existingRole.Rules = role.Rules - needsUpdate = true - } - if needsUpdate { - if err := r.Update(ctx, existingRole); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleUpdateFailed", "UpdateRole", "Failed to update role", err) - return errLogAndWrap(log, err, "failed to update role") - } - r.Recorder.Eventf(res, existingRole, corev1.EventTypeNormal, "RoleUpdated", "UpdateRole", "Role updated") - } - } - - // Create roleBinding to grant RBAC permissions to the worker service account - roleBinding := &rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: workerRoleName, - Namespace: res.Namespace, - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "solar-discovery-controller", - }, - }, - RoleRef: rbacv1.RoleRef{ - APIGroup: "rbac.authorization.k8s.io", - Kind: "Role", - Name: workerRoleName, - }, - Subjects: []rbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: workerSA.Name, - Namespace: res.Namespace, - }, - }, - } - - existingRB := &rbacv1.RoleBinding{} - err = r.Get(ctx, types.NamespacedName{Name: roleBinding.Name, Namespace: roleBinding.Namespace}, existingRB) - if err != nil && !apierrors.IsNotFound(err) { - return errLogAndWrap(log, err, "failed to get rolebinding") - } - - if apierrors.IsNotFound(err) { - if err := r.Create(ctx, roleBinding); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleBindingCreationFailed", "CreateRoleBinding", "Failed to create rolebinding", err) - return errLogAndWrap(log, err, "failed to create rolebinding") - } - r.Recorder.Eventf(res, roleBinding, corev1.EventTypeNormal, "RoleBindingCreated", "CreateRoleBinding", "RoleBinding created") - if err := controllerutil.SetControllerReference(res, roleBinding, r.Scheme); err != nil { - return errLogAndWrap(log, err, "failed to set controller reference on rolebinding") - } - } else { - needsUpdate := false - if existingRB.RoleRef.Name != workerRoleName { - existingRB.RoleRef.Name = workerRoleName - needsUpdate = true - } - if len(existingRB.Subjects) != 1 || - existingRB.Subjects[0].Kind != "ServiceAccount" || - existingRB.Subjects[0].Name != discoveryPrefixed(res.Name) || - existingRB.Subjects[0].Namespace != res.Namespace { - existingRB.Subjects = roleBinding.Subjects - needsUpdate = true - } - - if needsUpdate { - if err := r.Update(ctx, existingRB); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleBindingUpdateFailed", "UpdateRoleBinding", "Failed to update rolebinding", err) - return errLogAndWrap(log, err, "failed to update rolebinding") - } - r.Recorder.Eventf(res, existingRB, corev1.EventTypeNormal, "RoleBindingUpdated", "UpdateRoleBinding", "RoleBinding updated") - } - } - - // Create secret - secret := &corev1.Secret{ - ObjectMeta: objectMeta(res), - } - - rp := discovery.NewRegistryProvider() - reg := &discovery.Registry{ - Name: res.Name, - PlainHTTP: res.Spec.Registry.PlainHTTP, - Hostname: res.Spec.Registry.Endpoint, - } - if res.Spec.Webhook != nil { - reg.WebhookPath = res.Spec.Webhook.Path - reg.Flavor = res.Spec.Webhook.Flavor - } - if res.Spec.DiscoveryInterval != nil { - reg.ScanInterval = res.Spec.DiscoveryInterval.Duration - } - if err := rp.Register(reg); err != nil { - return errLogAndWrap(log, err, "failed to register registry") - } - - // Add credentials if specified - if res.Spec.Registry.SecretRef.Name != "" { - sec := &corev1.Secret{} - if err := r.Get(ctx, types.NamespacedName{Name: res.Spec.Registry.SecretRef.Name, Namespace: res.Namespace}, sec); err != nil { - return errLogAndWrap(log, err, "failed to get registry secret") - } else { - username, okUser := sec.Data["username"] - password, okPass := sec.Data["password"] - if okUser && okPass { - reg.Credentials = &discovery.RegistryCredentials{ - Username: string(username), - Password: string(password), - } - } else { - return fmt.Errorf("registry secret is missing username or password fields") - } - } - } - - confData, err := rp.Marshal() - if err != nil { - return errLogAndWrap(log, err, "failed to marshal registry configuration") - } - secret.StringData = map[string]string{ - "config.yaml": string(confData), - } - - existingSecret := &corev1.Secret{} - err = r.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, existingSecret) - if err != nil && !apierrors.IsNotFound(err) { - return errLogAndWrap(log, err, "failed to get secret") - } - - if apierrors.IsNotFound(err) { - if err := r.Create(ctx, secret); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "SecretCreationFailed", "CreateSecret", "Failed to create secret", err) - return errLogAndWrap(log, err, "failed to create secret") - } - r.Recorder.Eventf(res, secret, corev1.EventTypeNormal, "SecretCreated", "CreateSecret", "Secret created") - - if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil { - return errLogAndWrap(log, err, "failed to set controller reference") - } - } else { - existingSecret.StringData = secret.StringData - if err := r.Update(ctx, existingSecret); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "SecretUpdateFailed", "UpdateSecret", "Failed to update secret", err) - return errLogAndWrap(log, err, "failed to update secret") - } - r.Recorder.Eventf(res, existingSecret, corev1.EventTypeNormal, "SecretUpdated", "UpdateSecret", "Secret updated") - - if err := controllerutil.SetControllerReference(res, existingSecret, r.Scheme); err != nil { - return errLogAndWrap(log, err, "failed to set controller reference") - } - } - - // Create pod - var args = r.WorkerArgs - args = append(args, "--config", "/etc/worker/config.yaml", "--namespace", res.Namespace) - pod := &corev1.Pod{ - ObjectMeta: objectMeta(res), - Spec: corev1.PodSpec{ - ServiceAccountName: workerSA.Name, - Volumes: []corev1.Volume{ - { - Name: "config", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: discoveryPrefixed(res.Name), - }, - }, - }, - }, - }, - } - - container := corev1.Container{ - - Name: "worker", - Image: r.WorkerImage, - Command: []string{r.WorkerCommand}, - Args: args, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "config", - ReadOnly: true, - MountPath: "/etc/worker"}, - }, - Ports: []corev1.ContainerPort{ - { - Name: "webhook", - ContainerPort: 8080, - }, - }, - } - - if cmName := res.Spec.Registry.CAConfigMapRef.Name; cmName != "" { - pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ - Name: "ca-bundle", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: cmName, - }, - Items: []corev1.KeyToPath{ - { - Key: "trust-bundle.pem", - Path: "ca-bundle.pem", - }, - }, - }, - }, - }) - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: "ca-bundle", - MountPath: "/etc/ssl/certs", - ReadOnly: true, - }) - container.Env = append(container.Env, corev1.EnvVar{ - Name: "SSL_CERT_FILE", - Value: "/etc/ssl/certs/ca-bundle.pem", - }) - } - - pod.Spec.Containers = []corev1.Container{container} - - // Set owner references - if err := controllerutil.SetControllerReference(res, pod, r.Scheme); err != nil { - return errLogAndWrap(log, err, "failed to set controller reference") - } - - // Create pod in cluster - if err := r.Create(ctx, pod); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "PodCreationFailed", "CreatePod", "Failed to create pod", err) - return errLogAndWrap(log, err, "failed to create pod") - } - r.Recorder.Eventf(res, pod, corev1.EventTypeNormal, "PodCreated", "CreatePod", "Pod created") - log.V(1).Info("Pod created", "podGen", res.GetGeneration()) - - // Create or update service - svc := &corev1.Service{ - ObjectMeta: objectMeta(res), - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - Ports: []corev1.ServicePort{{Name: "webhook", Port: 8080, TargetPort: intstr.FromString("webhook")}}, - Selector: map[string]string{"app.kubernetes.io/name": discoveryPrefixed(res.Name)}, - }, - } - - existingSvc := &corev1.Service{} - err = r.Get(ctx, types.NamespacedName{Name: svc.Name, Namespace: svc.Namespace}, existingSvc) - if err != nil && !apierrors.IsNotFound(err) { - return errLogAndWrap(log, err, "failed to get service") - } - - if apierrors.IsNotFound(err) { - if err := r.Create(ctx, svc); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "ServiceCreationFailed", "CreateService", "Failed to create service", err) - return errLogAndWrap(log, err, "failed to create service") - } - r.Recorder.Eventf(res, svc, corev1.EventTypeNormal, "ServiceCreated", "CreateService", "Service created") - } else { - existingSvc.Spec = svc.Spec - if err := r.Update(ctx, existingSvc); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "ServiceUpdateFailed", "UpdateService", "Failed to update service", err) - return errLogAndWrap(log, err, "failed to update service") - } - r.Recorder.Eventf(res, existingSvc, corev1.EventTypeNormal, "ServiceUpdated", "UpdateService", "Service updated") - } - - // Update discovery version in status - res.Status.PodGeneration = res.GetGeneration() - if err := r.Status().Update(ctx, res); err != nil { - return errLogAndWrap(log, err, "failed to update status") - } - - return nil -} - -func objectMeta(res *solarv1alpha1.Discovery) metav1.ObjectMeta { - labels := res.Labels - if labels == nil { - labels = make(map[string]string) - } - labels["app.kubernetes.io/managed-by"] = "solar-discovery-controller" - labels["app.kubernetes.io/component"] = "discovery-worker" - labels["app.kubernetes.io/instance"] = res.Name - labels["app.kubernetes.io/name"] = discoveryPrefixed(res.Name) - - return metav1.ObjectMeta{ - Name: discoveryPrefixed(res.Name), - Namespace: res.Namespace, - Labels: labels, - Annotations: res.Annotations, - } -} - -// discoveryPrefixed returns the name of the discovery prefixed resource -func discoveryPrefixed(discoveryName string) string { - return fmt.Sprintf("discovery-%s", discoveryName) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *DiscoveryReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&solarv1alpha1.Discovery{}). - Owns(&corev1.Pod{}). - Owns(&corev1.Secret{}). - Complete(r) -} diff --git a/pkg/controller/discovery_controller_test.go b/pkg/controller/discovery_controller_test.go deleted file mode 100644 index 883d64e7..00000000 --- a/pkg/controller/discovery_controller_test.go +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright 2026 BWI GmbH and Solution Arsenal contributors -// SPDX-License-Identifier: Apache-2.0 - -package controller - -import ( - "context" - "fmt" - "io" - "log" - "net/http/httptest" - "net/url" - "os/exec" - "strings" - "time" - - "github.com/google/go-containerregistry/pkg/registry" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - apierrors "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/client" - - solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" - "go.opendefense.cloud/solar/test" - testregistry "go.opendefense.cloud/solar/test/registry" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("DiscoveryController", Ordered, func() { - var ( - testServer *httptest.Server - registryURL string - ) - - BeforeAll(func() { - reg := testregistry.New(registry.Logger(log.New(io.Discard, "", 0))) - testServer = httptest.NewServer(reg.HandleFunc()) - - testServerUrl, err := url.Parse(testServer.URL) - Expect(err).NotTo(HaveOccurred()) - - registryURL = testServerUrl.Host - - _, err = test.Run(exec.Command( - "./bin/ocm", - "transfer", - "ctf", - "./test/fixtures/ocm-demo-ctf", - fmt.Sprintf("http://%s/test", registryURL), - )) - - Expect(err).NotTo(HaveOccurred()) - }) - - AfterAll(func() { - testServer.Close() - }) - - Context("when reconciling Discoveries", func() { - It("should create required resources for a discovery resource", func() { - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "root-bundle", - }, - Data: map[string]string{ - "trust-bundle.pem": "certs-data", - }, - } - d := &solarv1alpha1.Discovery{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-discovery", - Namespace: ns.Name, - }, - Spec: solarv1alpha1.DiscoverySpec{ - Registry: solarv1alpha1.Registry{ - Endpoint: registryURL, - CAConfigMapRef: corev1.LocalObjectReference{ - Name: cm.Name, - }, - }, - }, - } - Expect(k8sClient.Create(ctx, d)).To(Succeed()) - - // Check for secret - secret := &corev1.Secret{} - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: discoveryPrefixed(d.Name), Namespace: ns.Name}, secret) - }).Should(Succeed()) - - Expect(secret).NotTo(BeNil()) - Expect(secret.Data).To(HaveKey("config.yaml")) - Expect(string(secret.Data["config.yaml"])).To(ContainSubstring(registryURL)) - - // Check for pod - pod := &corev1.Pod{} - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: discoveryPrefixed(d.Name), Namespace: ns.Name}, pod) - }).Should(Succeed()) - Expect(pod).NotTo(BeNil()) - Expect(pod.Labels).To(HaveKeyWithValue("app.kubernetes.io/name", discoveryPrefixed(d.Name))) - - Expect(pod.Spec.Volumes).To(HaveLen(2)) - Expect(pod.Spec.Volumes[0].Name).To(Equal("config")) - Expect(pod.Spec.Volumes[1].Name).To(Equal("ca-bundle")) - Expect(pod.Spec.Volumes[1].ConfigMap.Name).To(Equal("root-bundle")) - - container := pod.Spec.Containers[0] - Expect(strings.Join(container.Args, " ")).To(ContainSubstring("--namespace " + ns.Name)) - Expect(container.VolumeMounts).To(HaveLen(2)) - Expect(container.VolumeMounts[0].Name).To(Equal("config")) - Expect(container.VolumeMounts[1].Name).To(Equal("ca-bundle")) - - Expect(container.Env).To(ContainElement(corev1.EnvVar{ - Name: "SSL_CERT_FILE", - Value: "/etc/ssl/certs/ca-bundle.pem", - })) - - // Check for service - svc := &corev1.Service{} - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: discoveryPrefixed(d.Name), Namespace: ns.Name}, svc) - }).Should(Succeed()) - Expect(svc).NotTo(BeNil()) - - // Verify service selector - Expect(svc.Spec.Selector).To(HaveKeyWithValue("app.kubernetes.io/name", discoveryPrefixed(d.Name))) - - // Verify service account was created - sa := &corev1.ServiceAccount{} - saName := discoveryPrefixed(d.Name) - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: saName, Namespace: ns.Name}, sa) - }).Should(Succeed()) - Expect(sa).NotTo(BeNil()) - - // Verify role binding was created - rb := &rbacv1.RoleBinding{} - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: "solar-discovery-worker", Namespace: ns.Name}, rb) - }).Should(Succeed()) - Expect(rb).NotTo(BeNil()) - Expect(rb.RoleRef.Name).To(Equal("solar-discovery-worker")) - Expect(rb.Subjects).To(HaveLen(1)) - Expect(rb.Subjects[0].Name).To(Equal(saName)) - Expect(rb.Subjects[0].Namespace).To(Equal(ns.Name)) - - // Verify role was created - role := &rbacv1.Role{} - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: "solar-discovery-worker", Namespace: ns.Name}, role) - }).Should(Succeed()) - Expect(role).NotTo(BeNil()) - Expect(role.Rules).To(HaveLen(1)) - Expect(role.Rules[0].Verbs).To(ConsistOf("get", "list", "watch", "create", "update", "patch", "delete")) - Expect(role.Rules[0].APIGroups).To(ConsistOf(solarv1alpha1.GroupName)) - Expect(role.Rules[0].Resources).To(ConsistOf("components", "componentversions")) - }) - - It("should cleanup resources for a deleted discovery resource", func() { - // Create a Discovery - d := &solarv1alpha1.Discovery{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-discovery", - Namespace: ns.Name, - }, - Spec: solarv1alpha1.DiscoverySpec{ - Registry: solarv1alpha1.Registry{ - Endpoint: registryURL, - }, - DiscoveryInterval: &metav1.Duration{ - Duration: time.Hour * 12, - }, - }, - } - Expect(k8sClient.Create(ctx, d)).To(Succeed()) - - // Wait for all resources to be created - pod := &corev1.Pod{} - svc := &corev1.Service{} - rb := &rbacv1.RoleBinding{} - role := &rbacv1.Role{} - Eventually(func() error { - if err := k8sClient.Get(ctx, types.NamespacedName{Name: discoveryPrefixed(d.Name), Namespace: ns.Name}, pod); err != nil { - return err - } - if err := k8sClient.Get(ctx, types.NamespacedName{Name: discoveryPrefixed(d.Name), Namespace: ns.Name}, svc); err != nil { - return err - } - if err := k8sClient.Get(ctx, types.NamespacedName{Name: "solar-discovery-worker", Namespace: ns.Name}, rb); err != nil { - return err - } - if err := k8sClient.Get(ctx, types.NamespacedName{Name: "solar-discovery-worker", Namespace: ns.Name}, role); err != nil { - return err - } - - return nil - }).Should(Succeed()) - - // Delete Discovery - Expect(k8sClient.Delete(ctx, d)).To(Succeed()) - - checkGone := func(ctx context.Context, key client.ObjectKey, obj client.Object) error { - err := k8sClient.Get(ctx, key, obj) - if apierrors.IsNotFound(err) { - return nil - } - if err != nil { - return err - } - - return fmt.Errorf("Object `%s` was still there", obj.GetName()) - } - - // Validate resources were removed - Eventually(func() error { - if err := checkGone(ctx, types.NamespacedName{Name: discoveryPrefixed(d.Name), Namespace: ns.Name}, pod); err != nil { - return err - } - if err := checkGone(ctx, types.NamespacedName{Name: discoveryPrefixed(d.Name), Namespace: ns.Name}, svc); err != nil { - return err - } - if err := checkGone(ctx, types.NamespacedName{Name: "solar-discovery-worker", Namespace: ns.Name}, rb); err != nil { - return err - } - if err := checkGone(ctx, types.NamespacedName{Name: "solar-discovery-worker", Namespace: ns.Name}, role); err != nil { - return err - } - - return nil - }).Should(Succeed()) - }) - - It("should increase pod generation when spec changes", func() { - d := &solarv1alpha1.Discovery{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-discovery", - Namespace: ns.Name, - }, - Spec: solarv1alpha1.DiscoverySpec{ - Registry: solarv1alpha1.Registry{ - Endpoint: registryURL, - }, - DiscoveryInterval: &metav1.Duration{ - Duration: time.Hour * 12, - }, - }, - } - Expect(k8sClient.Create(ctx, d)).To(Succeed()) - // Verify status contains generation of discovery - initialGen := d.GetGeneration() - Eventually(func() int64 { - if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(d), d); err != nil { - return -1 - } - - return d.Status.PodGeneration - }).Should(Equal(d.GetGeneration())) - - d.Spec.DiscoveryInterval = &metav1.Duration{Duration: time.Hour * 24} - Expect(k8sClient.Update(ctx, d)).To(Succeed()) - - // Verify status contains new generation of discovery - Eventually(func() int64 { - if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(d), d); err != nil { - return -1 - } - - return d.Status.PodGeneration - }).Should(Not(Equal(initialGen))) - }) - - It("should handle existing resources (idempotency)", func() { - d := &solarv1alpha1.Discovery{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-discovery-idempotent", - Namespace: ns.Name, - }, - Spec: solarv1alpha1.DiscoverySpec{ - Registry: solarv1alpha1.Registry{ - Endpoint: registryURL, - }, - }, - } - Expect(k8sClient.Create(ctx, d)).To(Succeed()) - - // Wait for initial resources - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: discoveryPrefixed(d.Name), Namespace: ns.Name}, &corev1.Pod{}) - }).Should(Succeed()) - - // Verify Role exists with correct verbs - role := &rbacv1.Role{} - roleName := "solar-discovery-worker" - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: roleName, Namespace: ns.Name}, role) - }).Should(Succeed()) - Expect(role.Rules[0].Verbs).To(ConsistOf("get", "list", "watch", "create", "update", "patch", "delete")) - - // Verify CRB exists with correct roleRef - rb := &rbacv1.RoleBinding{} - rbName := roleName - Eventually(func() error { - return k8sClient.Get(ctx, types.NamespacedName{Name: rbName, Namespace: ns.Name}, rb) - }).Should(Succeed()) - Expect(rb.RoleRef.Name).To(Equal("solar-discovery-worker")) - - // Modify Role to test update - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: roleName, Namespace: ns.Name}, role)).To(Succeed()) - role.Rules[0].Verbs = []string{"get", "list", "watch"} - Expect(k8sClient.Update(ctx, rb)).To(Succeed()) - - // Modify CRB to test update - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: rbName, Namespace: ns.Name}, rb)).To(Succeed()) - rb.Subjects = append(rb.Subjects, rbacv1.Subject{ - Kind: "ServiceAccount", - Name: "foo", - Namespace: "bar", - }) - Expect(k8sClient.Update(ctx, rb)).To(Succeed()) - - // Trigger reconciliation again by updating discovery - Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(d), d)).To(Succeed()) - d.Spec.DiscoveryInterval = &metav1.Duration{Duration: time.Hour * 48} - Expect(k8sClient.Update(ctx, d)).To(Succeed()) - - // Verify Role was reconciled back to correct roleRef - Eventually(func() []string { - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: roleName, Namespace: ns.Name}, role)).To(Succeed()) - return role.Rules[0].Verbs - }).Should(ConsistOf("get", "list", "watch", "create", "update", "patch", "delete")) - - // Verify CRB was reconciled back to correct roleRef - Eventually(func() []rbacv1.Subject { - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: rbName, Namespace: ns.Name}, rb)).To(Succeed()) - return rb.Subjects - }).Should(ConsistOf(rbacv1.Subject{ - Kind: "ServiceAccount", - Name: discoveryPrefixed(d.Name), - Namespace: ns.Name, - })) - - // Verify pod still exists and was not duplicated - podList := &corev1.PodList{} - Eventually(func() int { - Expect(k8sClient.List(ctx, podList, client.InNamespace(ns.Name), client.MatchingLabels{"app.kubernetes.io/name": discoveryPrefixed(d.Name)})).To(Succeed()) - return len(podList.Items) - }).Should(Equal(1)) - }) - }) -}) diff --git a/pkg/controller/helpers.go b/pkg/controller/helpers.go index ab4501a1..d1446aee 100644 --- a/pkg/controller/helpers.go +++ b/pkg/controller/helpers.go @@ -9,7 +9,6 @@ import ( "encoding/hex" "fmt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -25,6 +24,10 @@ const ( indexOwnerName = "spec.ownerName" indexOwnerNamespace = "spec.ownerNamespace" + // Field index keys for looking up ReleaseBindings by target or release name. + indexReleaseBindingTargetName = "spec.targetRef.name" + indexReleaseBindingReleaseName = "spec.releaseRef.name" + maxK8sObjectNameLen = 253 maxK8sLabelValueLen = 63 ) @@ -44,12 +47,6 @@ func truncateName(name string, maxLen int) string { return name[:maxLen-9] + "-" + hashStr } -func renderTaskName(res metav1.Object) string { - base := fmt.Sprintf("%s-%s-%d", res.GetNamespace(), res.GetName(), res.GetGeneration()) - - return truncateName(base, maxK8sObjectNameLen) -} - // mapRenderTaskToOwner returns a handler.MapFunc that maps RenderTask events // to reconcile requests for the owning resource of the specified kind. func mapRenderTaskToOwner(kind string) handler.MapFunc { @@ -74,10 +71,61 @@ func mapRenderTaskToOwner(kind string) handler.MapFunc { } } -// IndexRenderTaskOwnerFields registers field indexers on the manager for -// looking up RenderTasks by owner kind, name, and namespace. +// releaseRenderTaskName returns a deterministic name for a per-release RenderTask +// scoped to a specific target. Each target creates its own release RenderTasks; +// the renderer job handles deduplication by skipping rendering if the chart +// already exists in the registry. +func releaseRenderTaskName(releaseName, targetName string, generation int64) string { + input := fmt.Sprintf("%s-%s-%d", releaseName, targetName, generation) + hash := sha256.Sum256([]byte(input)) + hashStr := hex.EncodeToString(hash[:])[:8] + + return truncateName(fmt.Sprintf("render-rel-%s-%s", releaseName, hashStr), maxK8sObjectNameLen) +} + +// targetRenderTaskName returns a deterministic name for a per-target bootstrap RenderTask. +// The bootstrapVersion is incremented each time the bootstrap needs re-rendering. +func targetRenderTaskName(targetName string, bootstrapVersion int64) string { + return truncateName(fmt.Sprintf("render-tgt-%s-%d", targetName, bootstrapVersion), maxK8sObjectNameLen) +} + +// IndexFields registers field indexers on the manager for efficient lookups. // Must be called once before any controller that uses these indexes is set up. -func IndexRenderTaskOwnerFields(ctx context.Context, mgr ctrl.Manager) error { +func IndexFields(ctx context.Context, mgr ctrl.Manager) error { + if err := indexReleaseBindingFields(ctx, mgr); err != nil { + return err + } + + return indexRenderTaskOwnerFields(ctx, mgr) +} + +func indexReleaseBindingFields(ctx context.Context, mgr ctrl.Manager) error { + indexer := mgr.GetFieldIndexer() + + if err := indexer.IndexField(ctx, &solarv1alpha1.ReleaseBinding{}, indexReleaseBindingTargetName, func(obj client.Object) []string { + rb := obj.(*solarv1alpha1.ReleaseBinding) + if rb.Spec.TargetRef.Name == "" { + return nil + } + + return []string{rb.Spec.TargetRef.Name} + }); err != nil { + return err + } + + return indexer.IndexField(ctx, &solarv1alpha1.ReleaseBinding{}, indexReleaseBindingReleaseName, func(obj client.Object) []string { + rb := obj.(*solarv1alpha1.ReleaseBinding) + if rb.Spec.ReleaseRef.Name == "" { + return nil + } + + return []string{rb.Spec.ReleaseRef.Name} + }) +} + +// indexRenderTaskOwnerFields registers field indexers on the manager for +// looking up RenderTasks by owner kind, name, and namespace. +func indexRenderTaskOwnerFields(ctx context.Context, mgr ctrl.Manager) error { indexer := mgr.GetFieldIndexer() if err := indexer.IndexField(ctx, &solarv1alpha1.RenderTask{}, indexOwnerKind, func(obj client.Object) []string { diff --git a/pkg/controller/predicates.go b/pkg/controller/predicates.go index dc65359f..56eb2b02 100644 --- a/pkg/controller/predicates.go +++ b/pkg/controller/predicates.go @@ -13,7 +13,7 @@ import ( // renderTaskStatusChangePredicate returns a predicate that filters RenderTask // events to only trigger reconciliation when the RenderTask's status changes. -// This avoids unnecessary reconciliation of the owning Release or Bootstrap +// This avoids unnecessary reconciliation of the owning Target // when only the RenderTask's metadata (e.g. finalizers) changes. func renderTaskStatusChangePredicate() predicate.Predicate { return predicate.Funcs{ diff --git a/pkg/controller/profile_controller.go b/pkg/controller/profile_controller.go new file mode 100644 index 00000000..e2ccdb1e --- /dev/null +++ b/pkg/controller/profile_controller.go @@ -0,0 +1,212 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "context" + "fmt" + + 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/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/events" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" +) + +// ProfileReconciler reconciles a Profile object. +// It evaluates the Profile's TargetSelector against all Targets in the namespace +// and creates/deletes ReleaseBindings accordingly. +type ProfileReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder events.EventRecorder + // WatchNamespace restricts reconciliation to this namespace. + WatchNamespace string +} + +//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=profiles,verbs=get;list;watch +//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=profiles/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releasebindings,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch + +// Reconcile evaluates the Profile's TargetSelector and ensures matching ReleaseBindings exist. +func (r *ProfileReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + + log.V(1).Info("Profile is being reconciled", "req", req) + + if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace { + return ctrl.Result{}, nil + } + + // Fetch Profile + profile := &solarv1alpha1.Profile{} + if err := r.Get(ctx, req.NamespacedName, profile); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + + return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Profile") + } + + // Evaluate TargetSelector against all Targets + selector, err := metav1.LabelSelectorAsSelector(&profile.Spec.TargetSelector) + if err != nil { + log.Error(err, "invalid targetSelector in Profile") + + return ctrl.Result{}, nil + } + + targetList := &solarv1alpha1.TargetList{} + if err := r.List(ctx, targetList, + client.InNamespace(profile.Namespace), + client.MatchingLabelsSelector{Selector: selector}, + ); err != nil { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to list Targets") + } + + // Build set of desired ReleaseBindings (one per matching target) + desiredTargets := map[string]bool{} + for _, target := range targetList.Items { + desiredTargets[target.Name] = true + } + + // List existing ReleaseBindings owned by this Profile + existingBindings := &solarv1alpha1.ReleaseBindingList{} + if err := r.List(ctx, existingBindings, + client.InNamespace(profile.Namespace), + client.MatchingFields{"metadata.ownerReferences.name": profile.Name}, + ); err != nil { + // Field index may not be available; fall back to listing all and filtering + allBindings := &solarv1alpha1.ReleaseBindingList{} + if err := r.List(ctx, allBindings, client.InNamespace(profile.Namespace)); err != nil { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings") + } + + existingBindings = &solarv1alpha1.ReleaseBindingList{} + + for i := range allBindings.Items { + if metav1.IsControlledBy(&allBindings.Items[i], profile) { + existingBindings.Items = append(existingBindings.Items, allBindings.Items[i]) + } + } + } + + // Delete ReleaseBindings for targets that no longer match + existingTargets := map[string]*solarv1alpha1.ReleaseBinding{} + for i := range existingBindings.Items { + rb := &existingBindings.Items[i] + existingTargets[rb.Spec.TargetRef.Name] = rb + + if !desiredTargets[rb.Spec.TargetRef.Name] { + log.V(1).Info("Deleting ReleaseBinding for unmatched target", "target", rb.Spec.TargetRef.Name) + if err := r.Delete(ctx, rb); err != nil && !apierrors.IsNotFound(err) { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete ReleaseBinding") + } + + r.Recorder.Eventf(profile, nil, corev1.EventTypeNormal, "Deleted", "Delete", + "Deleted ReleaseBinding for target %s", rb.Spec.TargetRef.Name) + } + } + + // Create ReleaseBindings for new matching targets + for _, target := range targetList.Items { + if _, exists := existingTargets[target.Name]; exists { + continue + } + + rb := &solarv1alpha1.ReleaseBinding{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-%s-", profile.Name, target.Name), + Namespace: profile.Namespace, + }, + Spec: solarv1alpha1.ReleaseBindingSpec{ + TargetRef: corev1.LocalObjectReference{Name: target.Name}, + ReleaseRef: profile.Spec.ReleaseRef, + }, + } + if err := ctrl.SetControllerReference(profile, rb, r.Scheme); err != nil { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to set controller reference on ReleaseBinding") + } + + if err := r.Create(ctx, rb); err != nil { + if apierrors.IsAlreadyExists(err) { + continue + } + + return ctrl.Result{}, errLogAndWrap(log, err, "failed to create ReleaseBinding") + } + + log.V(1).Info("Created ReleaseBinding for target", "target", target.Name) + r.Recorder.Eventf(profile, nil, corev1.EventTypeNormal, "Created", "Create", + "Created ReleaseBinding for target %s", target.Name) + } + + // Update status + original := profile.DeepCopy() + profile.Status.MatchedTargets = len(targetList.Items) + if profile.Status.MatchedTargets != original.Status.MatchedTargets { + if err := r.Status().Update(ctx, profile); err != nil { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Profile status") + } + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ProfileReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&solarv1alpha1.Profile{}). + Owns(&solarv1alpha1.ReleaseBinding{}). + Watches( + &solarv1alpha1.Target{}, + handler.EnqueueRequestsFromMapFunc(r.mapTargetToProfiles), + ). + Complete(r) +} + +// mapTargetToProfiles maps a Target to all Profiles in the same namespace that might match it. +func (r *ProfileReconciler) mapTargetToProfiles(ctx context.Context, obj client.Object) []reconcile.Request { + log := ctrl.LoggerFrom(ctx) + + target, ok := obj.(*solarv1alpha1.Target) + if !ok { + return nil + } + + profileList := &solarv1alpha1.ProfileList{} + if err := r.List(ctx, profileList, client.InNamespace(target.Namespace)); err != nil { + log.Error(err, "failed to list Profiles for Target mapping") + + return nil + } + + targetLabels := labels.Set(target.Labels) + var requests []reconcile.Request + + for _, profile := range profileList.Items { + selector, err := metav1.LabelSelectorAsSelector(&profile.Spec.TargetSelector) + if err != nil { + continue + } + + if selector.Matches(targetLabels) { + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(&profile), + }) + } + } + + return requests +} diff --git a/pkg/controller/profile_controller_test.go b/pkg/controller/profile_controller_test.go new file mode 100644 index 00000000..79e81f03 --- /dev/null +++ b/pkg/controller/profile_controller_test.go @@ -0,0 +1,166 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ProfileReconciler", Ordered, func() { + var ( + newProfile = func(name string, matchLabels map[string]string) *solarv1alpha1.Profile { + return &solarv1alpha1.Profile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns.Name, + }, + Spec: solarv1alpha1.ProfileSpec{ + TargetSelector: metav1.LabelSelector{ + MatchLabels: matchLabels, + }, + ReleaseRef: corev1.LocalObjectReference{Name: "test-release"}, + }, + } + } + + newTarget = func(name string, labels map[string]string) *solarv1alpha1.Target { + return &solarv1alpha1.Target{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns.Name, + Labels: labels, + }, + Spec: solarv1alpha1.TargetSpec{}, + } + } + + listOwnedBindings = func(profileName string) []solarv1alpha1.ReleaseBinding { + allBindings := &solarv1alpha1.ReleaseBindingList{} + ExpectWithOffset(1, k8sClient.List(ctx, allBindings, client.InNamespace(ns.Name))).To(Succeed()) + + var owned []solarv1alpha1.ReleaseBinding + for _, rb := range allBindings.Items { + for _, ref := range rb.OwnerReferences { + if ref.Name == profileName && ref.Kind == "Profile" { + owned = append(owned, rb) + } + } + } + + return owned + } + ) + + Context("when Profile matches Targets", func() { + It("should create ReleaseBindings for matching Targets", func() { + target1 := newTarget("target-env-prod", map[string]string{"env": "prod"}) + Expect(k8sClient.Create(ctx, target1)).To(Succeed()) + target2 := newTarget("target-env-test", map[string]string{"env": "test"}) + Expect(k8sClient.Create(ctx, target2)).To(Succeed()) + + profile := newProfile("profile-prod", map[string]string{"env": "prod"}) + Expect(k8sClient.Create(ctx, profile)).To(Succeed()) + + // Should create ReleaseBinding for target-env-prod only + Eventually(func() int { + return len(listOwnedBindings("profile-prod")) + }, eventuallyTimeout).Should(Equal(1)) + + bindings := listOwnedBindings("profile-prod") + Expect(bindings[0].Spec.TargetRef.Name).To(Equal("target-env-prod")) + Expect(bindings[0].Spec.ReleaseRef.Name).To(Equal("test-release")) + }) + + It("should create ReleaseBindings for all Targets when selector is empty", func() { + target1 := newTarget("target-all-1", map[string]string{"env": "prod"}) + Expect(k8sClient.Create(ctx, target1)).To(Succeed()) + target2 := newTarget("target-all-2", map[string]string{"env": "test"}) + Expect(k8sClient.Create(ctx, target2)).To(Succeed()) + + profile := newProfile("profile-all", map[string]string{}) + Expect(k8sClient.Create(ctx, profile)).To(Succeed()) + + Eventually(func() int { + return len(listOwnedBindings("profile-all")) + }, eventuallyTimeout).Should(Equal(2)) + }) + + It("should update MatchedTargets count in Profile status", func() { + target := newTarget("target-status", map[string]string{"role": "web"}) + Expect(k8sClient.Create(ctx, target)).To(Succeed()) + + profile := newProfile("profile-status", map[string]string{"role": "web"}) + Expect(k8sClient.Create(ctx, profile)).To(Succeed()) + + Eventually(func() int { + p := &solarv1alpha1.Profile{} + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(profile), p); err != nil { + return -1 + } + + return p.Status.MatchedTargets + }, eventuallyTimeout).Should(Equal(1)) + }) + }) + + Context("when Target labels change", func() { + It("should update ReleaseBindings when a Target no longer matches", func() { + target := newTarget("target-label-change", map[string]string{"env": "prod"}) + Expect(k8sClient.Create(ctx, target)).To(Succeed()) + + profile := newProfile("profile-label-change", map[string]string{"env": "prod"}) + Expect(k8sClient.Create(ctx, profile)).To(Succeed()) + + // Wait for binding to be created + Eventually(func() int { + return len(listOwnedBindings("profile-label-change")) + }, eventuallyTimeout).Should(Equal(1)) + + // Update target labels so it no longer matches + Eventually(func() error { + t := &solarv1alpha1.Target{} + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(target), t); err != nil { + return err + } + t.Labels = map[string]string{"env": "staging"} + + return k8sClient.Update(ctx, t) + }).Should(Succeed()) + + // ReleaseBinding should be deleted + Eventually(func() int { + return len(listOwnedBindings("profile-label-change")) + }, eventuallyTimeout).Should(Equal(0)) + }) + }) + + Context("when Profile is deleted", func() { + It("should have owner references on ReleaseBindings for garbage collection", func() { + target := newTarget("target-gc", map[string]string{"tier": "frontend"}) + Expect(k8sClient.Create(ctx, target)).To(Succeed()) + + profile := newProfile("profile-gc", map[string]string{"tier": "frontend"}) + Expect(k8sClient.Create(ctx, profile)).To(Succeed()) + + // Wait for binding to be created + Eventually(func() int { + return len(listOwnedBindings("profile-gc")) + }, eventuallyTimeout).Should(Equal(1)) + + // Verify the owner reference is set correctly for GC + bindings := listOwnedBindings("profile-gc") + Expect(bindings[0].OwnerReferences).To(HaveLen(1)) + Expect(bindings[0].OwnerReferences[0].Name).To(Equal("profile-gc")) + Expect(bindings[0].OwnerReferences[0].Kind).To(Equal("Profile")) + Expect(*bindings[0].OwnerReferences[0].Controller).To(BeTrue()) + }) + }) +}) diff --git a/pkg/controller/release_controller.go b/pkg/controller/release_controller.go index 32f6f7e1..e616c948 100644 --- a/pkg/controller/release_controller.go +++ b/pkg/controller/release_controller.go @@ -5,12 +5,7 @@ package controller import ( "context" - "fmt" - "net/url" - "slices" - "time" - corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -18,18 +13,18 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/events" 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/handler" solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" ) const ( - releaseFinalizer = "solar.opendefense.cloud/release-finalizer" + ConditionTypeComponentVersionResolved = "ComponentVersionResolved" ) -// ReleaseReconciler reconciles a Release object +// ReleaseReconciler reconciles a Release object. +// It validates that the referenced ComponentVersion exists and sets status conditions. +// Rendering is handled by the Target controller. type ReleaseReconciler struct { client.Client Scheme *runtime.Scheme @@ -43,26 +38,11 @@ type ReleaseReconciler struct { //+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases/finalizers,verbs=update //+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch //+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch -// Reconcile moves the current state of the cluster closer to the desired state -// -// Reconciliation Flow: -// -// Release created -// ↓ -// Add finalizer -// ↓ -// Check if already succeeded → YES → Return (no-op) -// ↓ NO -// Get or create RenderTask -// ↓ -// Update status from RenderTask - +// Reconcile validates the Release by resolving its ComponentVersion reference. func (r *ReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := ctrl.LoggerFrom(ctx) ctrlResult := ctrl.Result{} @@ -77,252 +57,59 @@ func (r *ReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct res := &solarv1alpha1.Release{} if err := r.Get(ctx, req.NamespacedName, res); err != nil { if apierrors.IsNotFound(err) { - // Object not found, return. Created objects are automatically garbage collected. return ctrlResult, nil } return ctrlResult, errLogAndWrap(log, err, "failed to get object") } - // Handle deletion: cleanup rendertask, then remove finalizer - if !res.DeletionTimestamp.IsZero() { - log.V(1).Info("Release is being deleted") - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "Release is being deleted, cleaning up resources") - - if err := r.deleteRenderTask(ctx, res); client.IgnoreNotFound(err) != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to delete render task") - } - - // Remove finalizer - if slices.Contains(res.Finalizers, releaseFinalizer) { - log.V(1).Info("Removing finalizer from resource") - res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool { - return f == releaseFinalizer + // Resolve ComponentVersion + cvRef := types.NamespacedName{ + Name: res.Spec.ComponentVersionRef.Name, + Namespace: res.Namespace, + } + cv := &solarv1alpha1.ComponentVersion{} + if err := r.Get(ctx, cvRef, cv); err != nil { + if apierrors.IsNotFound(err) { + changed := apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{ + Type: ConditionTypeComponentVersionResolved, + Status: metav1.ConditionFalse, + ObservedGeneration: res.Generation, + Reason: "NotFound", + Message: "ComponentVersion not found: " + res.Spec.ComponentVersionRef.Name, }) - if err := r.Update(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer") + if changed { + if err := r.Status().Update(ctx, res); err != nil { + return ctrlResult, errLogAndWrap(log, err, "failed to update status") + } } - } - - return ctrlResult, nil - } - // Add finalizer if not present and not deleting - if res.DeletionTimestamp.IsZero() { - if !slices.Contains(res.Finalizers, releaseFinalizer) { - log.V(1).Info("Adding finalizer to resource") - res.Finalizers = append(res.Finalizers, releaseFinalizer) - if err := r.Update(ctx, res); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer") - } - // Return without requeue; the Update event will trigger reconciliation again return ctrlResult, nil } - } - // Check if rendertask has already completed successfully - sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeTaskCompleted) - if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue { - log.V(1).Info("RenderTask has already completed successfully, no further action needed") - return ctrlResult, nil + return ctrlResult, errLogAndWrap(log, err, "failed to get ComponentVersion") } - // Check if rendertask has already failed - fc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeTaskFailed) - if fc != nil && fc.ObservedGeneration >= res.Generation && fc.Status == metav1.ConditionTrue { - log.V(1).Info("RenderTask has already failed, no further action needed") - return ctrlResult, nil - } - - // Reconcile RenderTask - rt := &solarv1alpha1.RenderTask{} - err := r.Get(ctx, client.ObjectKey{Name: renderTaskName(res)}, rt) - if client.IgnoreNotFound(err) != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to get RenderTask") - } - - if apierrors.IsNotFound(err) { - if err := r.createRenderTask(ctx, res); err != nil { - log.V(1).Error(err, "Failed to create RenderTask") - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", fmt.Sprintf("failed to create RenderTask: %q", err)) - - if apierrors.IsNotFound(err) { - return ctrl.Result{RequeueAfter: 30 * time.Second}, nil - } - - return ctrlResult, errLogAndWrap(log, err, "failed to create RenderTask") - } - log.V(1).Info("Created RenderTask", "res", res) - r.Recorder.Eventf(res, rt, corev1.EventTypeNormal, "Created", "Create", "RenderTask was created") - } - - if changed := r.updateStatusConditionsFromRenderTask(ctx, res, rt); changed { + // ComponentVersion found — set resolved condition + changed := apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{ + Type: ConditionTypeComponentVersionResolved, + Status: metav1.ConditionTrue, + ObservedGeneration: res.Generation, + Reason: "Resolved", + Message: "ComponentVersion resolved: " + cv.Name, + }) + if changed { if err := r.Status().Update(ctx, res); err != nil { return ctrlResult, errLogAndWrap(log, err, "failed to update status") } } - // RenderTask still running return ctrlResult, nil } -func (r *ReleaseReconciler) updateStatusConditionsFromRenderTask(ctx context.Context, res *solarv1alpha1.Release, rt *solarv1alpha1.RenderTask) (changed bool) { - if rt == nil || res == nil { - return false - } - - log := ctrl.LoggerFrom(ctx) - - if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) { - changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{ - Type: ConditionTypeTaskFailed, - Status: metav1.ConditionTrue, - ObservedGeneration: res.Generation, - Reason: "TaskFailed", - Message: "RenderTask failed", - }) - - log.V(1).Info("RenderTask failed", "name", rt.Name) - r.Recorder.Eventf(res, rt, corev1.EventTypeWarning, "TaskFailed", "RunTask", "RenderTask failed") - - return changed - } - - if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) { - changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{ - Type: ConditionTypeTaskCompleted, - Status: metav1.ConditionTrue, - ObservedGeneration: res.Generation, - Reason: "TaskCompleted", - Message: "RenderTask completed", - }) - - if res.Status.ChartURL != rt.Status.ChartURL { - res.Status.ChartURL = rt.Status.ChartURL - changed = true - } - - log.V(1).Info("RenderTask succeeded", "name", rt.Name) - r.Recorder.Eventf(res, rt, corev1.EventTypeWarning, "TaskCompleted", "RunTask", "RenderTask completed successfully") - - return changed - } - - log.V(1).Info("RenderTask has no final condtions yet", "name", rt.Name) - - return false -} - -func (r *ReleaseReconciler) createRenderTask(ctx context.Context, res *solarv1alpha1.Release) error { - log := ctrl.LoggerFrom(ctx) - - // Check if we need to cleanup an old task - if res.Status.RenderTaskRef != nil && res.Status.RenderTaskRef.Name != "" { - if err := r.deleteRenderTask(ctx, res); err != nil { - return errLogAndWrap(log, err, "failed to cleanup old task") - } - } - - spec, err := r.computeRenderTaskSpec(ctx, res) - if err != nil { - return err - } - rt := &solarv1alpha1.RenderTask{ - ObjectMeta: metav1.ObjectMeta{ - Name: renderTaskName(res), - }, - Spec: spec, - } - rt.Spec.OwnerName = res.Name - rt.Spec.OwnerNamespace = res.Namespace - rt.Spec.OwnerKind = "Release" - - if err := r.Create(ctx, rt); err != nil { - r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create RenderTask", err) - return errLogAndWrap(log, err, "failed to create RenderTask") - } - - // Set Reference in Status - res.Status.RenderTaskRef = &corev1.ObjectReference{ - APIVersion: solarv1alpha1.SchemeGroupVersion.String(), - Kind: "RenderTask", - Name: rt.Name, - } - - if err := r.Status().Update(ctx, res); err != nil { - return errLogAndWrap(log, err, "failed to update status") - } - - return nil -} - -func (r *ReleaseReconciler) deleteRenderTask(ctx context.Context, res *solarv1alpha1.Release) error { - if res.Status.RenderTaskRef == nil { - return nil - } - - rt := &solarv1alpha1.RenderTask{} - if err := r.Get(ctx, client.ObjectKey{Name: res.Status.RenderTaskRef.Name}, rt); client.IgnoreNotFound(err) != nil { - return err - } else if err == nil { - return r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)) - } - - return nil -} - -func (r *ReleaseReconciler) computeRenderTaskSpec(ctx context.Context, res *solarv1alpha1.Release) (solarv1alpha1.RenderTaskSpec, error) { - spec := solarv1alpha1.RenderTaskSpec{} - - cvRef := types.NamespacedName{ - Name: res.Spec.ComponentVersionRef.Name, - Namespace: res.Namespace, - } - - cv := &solarv1alpha1.ComponentVersion{} - if err := r.Get(ctx, cvRef, cv); err != nil { - return spec, err - } - - chartName := fmt.Sprintf("release-%s", res.Name) - repo, err := url.JoinPath(res.Namespace, chartName) - if err != nil { - return spec, err - } - - tag := fmt.Sprintf("v0.0.%d", res.GetGeneration()) - - spec.RendererConfig = solarv1alpha1.RendererConfig{ - Type: solarv1alpha1.RendererConfigTypeRelease, - ReleaseConfig: solarv1alpha1.ReleaseConfig{ - Chart: solarv1alpha1.ChartConfig{ - Name: chartName, - Description: fmt.Sprintf("Release of %s", res.Spec.ComponentVersionRef.Name), - Version: tag, - AppVersion: tag, - }, - Input: solarv1alpha1.ReleaseInput{ - Component: solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name}, - Resources: cv.Spec.Resources, - Entrypoint: cv.Spec.Entrypoint, - }, - Values: res.Spec.Values, - }, - } - spec.Repository = repo - spec.Tag = tag - spec.FailedJobTTL = res.Spec.FailedJobTTL - - return spec, nil -} - // SetupWithManager sets up the controller with the Manager. func (r *ReleaseReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&solarv1alpha1.Release{}). - Watches(&solarv1alpha1.RenderTask{}, - handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Release")), - builder.WithPredicates(renderTaskStatusChangePredicate()), - ). Complete(r) } diff --git a/pkg/controller/release_controller_test.go b/pkg/controller/release_controller_test.go index ccd60dfa..9be71c23 100644 --- a/pkg/controller/release_controller_test.go +++ b/pkg/controller/release_controller_test.go @@ -4,11 +4,7 @@ package controller import ( - "fmt" - "slices" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -63,244 +59,39 @@ var _ = Describe("ReleaseReconciler", Ordered, func() { } ) - BeforeEach(func() { - // Create the Componentversion - cv := validComponentVersion("my-component-v1", ns) - Expect(k8sClient.Create(ctx, cv)).To(Succeed()) - }) + Describe("ComponentVersion resolution", func() { + It("should set ComponentVersionResolved=True when ComponentVersion exists", func() { + cv := validComponentVersion("my-component-v1", ns) + Expect(k8sClient.Create(ctx, cv)).To(Succeed()) - Describe("Release creation and RenderTask scheduling", func() { - It("should create a Release and create a RenderTask", func() { - // Create a Release - release := validRelease("test-release", ns) + release := validRelease("test-release-resolved", ns) Expect(k8sClient.Create(ctx, release)).To(Succeed()) - // Verify the Release was created - createdRelease := &solarv1alpha1.Release{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: "test-release", Namespace: ns.Name}, createdRelease) - }).Should(Succeed()) - - // Verify finalizer was added after a reconciliation cycle - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-release", Namespace: ns.Name}, createdRelease) - if err != nil { - return false - } - - return len(createdRelease.Finalizers) > 0 && slices.Contains(createdRelease.Finalizers, releaseFinalizer) - }, eventuallyTimeout).Should(BeTrue(), "finalizer should be added by reconciler") - - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-release-0", ns.Name)}, task) - }, eventuallyTimeout).Should(Succeed()) - - Expect(task.Spec.RendererConfig.Type).To(Equal(solarv1alpha1.RendererConfigTypeRelease)) - Expect(task.Spec.RendererConfig.ReleaseConfig.Chart.Name).To(Equal("release-test-release")) - Expect(task.Spec.RendererConfig.ReleaseConfig.Chart.Version).To(Equal("v0.0.0")) - Expect(task.Spec.Repository).To(Equal(fmt.Sprintf("%s/release-test-release", ns.Name))) - Expect(task.Spec.Tag).To(Equal("v0.0.0")) - }) - - It("should propagate FailedJobTTL from Release to RenderTask", func() { - ttl := int32(3600) - release := validRelease("test-release-ttl", ns) - release.Spec.FailedJobTTL = &ttl - Expect(k8sClient.Create(ctx, release)).To(Succeed()) - - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-release-ttl-0", ns.Name)}, task) - }, eventuallyTimeout).Should(Succeed()) - - Expect(task.Spec.FailedJobTTL).ToNot(BeNil()) - Expect(*task.Spec.FailedJobTTL).To(Equal(int32(3600))) - }) - }) - - Describe("Release RenderTask completion", func() { - It("should represent completion when RenderTask completes successfully", func() { - // Create a Release - release := validRelease("test-release-success", ns) - Expect(k8sClient.Create(ctx, release)).To(Succeed()) - - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-release-success-0", ns.Name)}, task) - }, eventuallyTimeout).Should(Succeed()) - - // Manipulate Task to be Successful - Expect(apimeta.SetStatusCondition(&task.Status.Conditions, metav1.Condition{ - Type: ConditionTypeJobSucceeded, - Status: metav1.ConditionTrue, - ObservedGeneration: task.Generation, - Reason: ConditionTypeJobSucceeded, - })).To(BeTrue()) - Expect(k8sClient.Status().Update(ctx, task)).To(Succeed()) - updatedRelease := &solarv1alpha1.Release{} Eventually(func() bool { - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-success", Namespace: ns.Name}, updatedRelease); err != nil { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-resolved", Namespace: ns.Name}, updatedRelease); err != nil { return false } - return apimeta.IsStatusConditionTrue(updatedRelease.Status.Conditions, ConditionTypeTaskCompleted) + return apimeta.IsStatusConditionTrue(updatedRelease.Status.Conditions, ConditionTypeComponentVersionResolved) }, eventuallyTimeout).Should(BeTrue()) }) - It("should represent failure when RenderTask failed", func() { - // Create a Release - release := validRelease("test-release-failed", ns) + It("should set ComponentVersionResolved=False when ComponentVersion does not exist", func() { + release := validRelease("test-release-missing-cv", ns) + release.Spec.ComponentVersionRef.Name = "nonexistent-cv" Expect(k8sClient.Create(ctx, release)).To(Succeed()) - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-release-failed-0", ns.Name)}, task) - }, eventuallyTimeout).Should(Succeed()) - - // Manipulate Task to be Failed - Expect(apimeta.SetStatusCondition(&task.Status.Conditions, metav1.Condition{ - Type: ConditionTypeJobFailed, - Status: metav1.ConditionTrue, - ObservedGeneration: task.Generation, - Reason: ConditionTypeJobFailed, - })).To(BeTrue()) - Expect(k8sClient.Status().Update(ctx, task)).To(Succeed()) - updatedRelease := &solarv1alpha1.Release{} Eventually(func() bool { - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-failed", Namespace: ns.Name}, updatedRelease); err != nil { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-missing-cv", Namespace: ns.Name}, updatedRelease); err != nil { return false } - return apimeta.IsStatusConditionTrue(updatedRelease.Status.Conditions, ConditionTypeTaskFailed) - }, eventuallyTimeout).Should(BeTrue()) - }) - }) - - Describe("Release deletion", func() { - It("should cleanup RenderTask when Release is deleted", func() { - // Create a Release - release := validRelease("test-release-delete", ns) - Expect(k8sClient.Create(ctx, release)).To(Succeed()) - - // Wait for RenderTask to be created - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-release-delete-0", ns.Name)}, task) - }).Should(Succeed()) + cond := apimeta.FindStatusCondition(updatedRelease.Status.Conditions, ConditionTypeComponentVersionResolved) - // Delete the Release - createdRelease := &solarv1alpha1.Release{} - Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-delete", Namespace: ns.Name}, createdRelease)).To(Succeed()) - Expect(k8sClient.Delete(ctx, createdRelease)).To(Succeed()) - - // Verify RenderTask is deleted - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-release-delete-0", ns.Name)}, task) - }).Should(MatchError(ContainSubstring("not found"))) - - // Verify Release is deleted (finalizer removed) - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-delete", Namespace: ns.Name}, createdRelease) - }).Should(MatchError(ContainSubstring("not found"))) - }) - }) - - Describe("Release status references", func() { - It("should maintain references to created RenderTask in Release status", func() { - // Create a Release - release := validRelease("test-release-refs", ns) - Expect(k8sClient.Create(ctx, release)).To(Succeed()) - - // Wait for RenderTask to be created - task := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-release-refs-0", ns.Name)}, task) - }, eventuallyTimeout).Should(Succeed()) - - // Verify Release status has references - updatedRelease := &solarv1alpha1.Release{} - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-refs", Namespace: ns.Name}, updatedRelease) - if err != nil { - return false - } - - return updatedRelease.Status.RenderTaskRef != nil + return cond != nil && cond.Status == metav1.ConditionFalse && cond.Reason == "NotFound" }, eventuallyTimeout).Should(BeTrue()) - - // Verify RenderTaskRef details - Expect(updatedRelease.Status.RenderTaskRef.Name).To(Equal(fmt.Sprintf("%s-test-release-refs-0", ns.Name))) - Expect(updatedRelease.Status.RenderTaskRef.Kind).To(Equal("RenderTask")) - Expect(updatedRelease.Status.RenderTaskRef.APIVersion).To(Equal("solar.opendefense.cloud/v1alpha1")) - }) - }) - - Describe("Release updates", func() { - It("should increase the Generation when the Spec changes", func() { - // Create a Release - release := validRelease("test-release-gen", ns) - Expect(k8sClient.Create(ctx, release)).To(Succeed()) - - // Verify the Release was created - createdRelease := &solarv1alpha1.Release{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-gen", Namespace: ns.Name}, createdRelease) - }).Should(Succeed()) - - Expect(createdRelease.Generation).To(Equal(int64(0))) - - // Update the Release - Eventually(func() error { - latest := &solarv1alpha1.Release{} - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-gen", Namespace: ns.Name}, latest); err != nil { - return err - } - latest.Spec.Values.Raw = []byte(`{"new-shiny-value": true}`) - - return k8sClient.Update(ctx, latest) - }).Should(Succeed()) - - // Check Release after Update - updatedRelease := &solarv1alpha1.Release{} - Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-gen", Namespace: ns.Name}, updatedRelease)).To(Succeed()) - - Expect(updatedRelease.Generation).To(Equal(int64(1))) - }) - - It("should create a RenderTask for the latest Generation only", func() { - // Create a Release - release := validRelease("test-release-update", ns) - Expect(k8sClient.Create(ctx, release)).To(Succeed()) - - // Verify the RenderTask was created - initialTask := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-release-update-0", ns.Name)}, initialTask) - }).Should(Succeed()) - - // Update the Release - Eventually(func() error { - latest := &solarv1alpha1.Release{} - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-release-update", Namespace: ns.Name}, latest); err != nil { - return err - } - latest.Spec.Values.Raw = []byte(`{"new-shiny-value": true}`) - - return k8sClient.Update(ctx, latest) - }).Should(Succeed()) - - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-release-update-0", ns.Name)}, initialTask) - return apierrors.IsNotFound(err) - }).Should(BeTrue()) - - newTask := &solarv1alpha1.RenderTask{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-test-release-update-1", ns.Name)}, newTask) - }).Should(Succeed()) }) }) }) diff --git a/pkg/controller/rendertask_controller.go b/pkg/controller/rendertask_controller.go index 96d0282e..901925a6 100644 --- a/pkg/controller/rendertask_controller.go +++ b/pkg/controller/rendertask_controller.go @@ -37,7 +37,8 @@ const ( ConditionTypeTaskFailed = "TaskFailed" ) -// RenderTaskReconciler reconciles a RenderTask object +// RenderTaskReconciler reconciles a RenderTask object. +// Each RenderTask carries its own BaseURL and PushSecretRef for the target registry. type RenderTaskReconciler struct { client.Client Scheme *runtime.Scheme @@ -45,12 +46,7 @@ type RenderTaskReconciler struct { RendererImage string RendererCommand string RendererArgs []string - PushSecretRef *corev1.SecretReference - BaseURL string RendererCAConfigMap string - // Namespace is the namespace where Jobs and Secrets are created. - // Passed via --namespace flag from the controller-manager. - Namespace string } //+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete @@ -72,7 +68,6 @@ func (r *RenderTaskReconciler) Reconcile(ctx context.Context, req ctrl.Request) res := &solarv1alpha1.RenderTask{} if err := r.Get(ctx, req.NamespacedName, res); err != nil { if apierrors.IsNotFound(err) { - // Object not found, return. Created objects are automatically garbage collected. return ctrlResult, nil } @@ -91,39 +86,46 @@ func (r *RenderTaskReconciler) Reconcile(ctx context.Context, req ctrl.Request) sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobSucceeded) if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue { log.V(1).Info("RenderTask has already completed successfully, no further action needed") + return ctrlResult, nil } + // Determine the namespace for Jobs/Secrets — use the RenderTask's namespace + jobNS := r.taskNamespace(res) + // Reconcile Config Secret configSecret := &corev1.Secret{} - err := r.Get(ctx, r.configSecretKey(res), configSecret) + err := r.Get(ctx, r.configSecretKey(res, jobNS), configSecret) if err != nil && apierrors.IsNotFound(err) { - createdSecret, err := r.createConfigSecret(ctx, res) + createdSecret, err := r.createConfigSecret(ctx, res, jobNS) if err != nil { r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateConfigSecret", fmt.Sprintf("Failed to create config secret: %s", err)) + return ctrlResult, errLogAndWrap(log, err, "failed to create secret") } + configSecret = createdSecret } else if err != nil { return ctrlResult, errLogAndWrap(log, err, "could not get secret") } - // Resolve push secret (lives in controller namespace, referenced directly by Jobs) + // Resolve push secret from the RenderTask's PushSecretRef var pushSecret *corev1.Secret - if r.PushSecretRef != nil { + if res.Spec.PushSecretRef != nil { pushSecret = &corev1.Secret{} - if err := r.Get(ctx, client.ObjectKey{Name: r.PushSecretRef.Name, Namespace: r.PushSecretRef.Namespace}, pushSecret); err != nil { + if err := r.Get(ctx, client.ObjectKey{Name: res.Spec.PushSecretRef.Name, Namespace: jobNS}, pushSecret); err != nil { return ctrlResult, errLogAndWrap(log, err, "failed to get push secret") } } // Reconcile Job job := &batchv1.Job{} - err = r.Get(ctx, r.renderJobKey(res), job) + err = r.Get(ctx, r.renderJobKey(res, jobNS), job) if err != nil && apierrors.IsNotFound(err) { - err := r.createRenderJob(ctx, res, configSecret, pushSecret) + err := r.createRenderJob(ctx, res, configSecret, pushSecret, jobNS) if err != nil { r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateJobFailed", "CreateJob", fmt.Sprintf("Failed to create job: %s", err)) + return ctrlResult, errLogAndWrap(log, err, "failed to create job") } } else if err != nil { @@ -141,18 +143,19 @@ func (r *RenderTaskReconciler) Reconcile(ctx context.Context, req ctrl.Request) switch { case job.Status.Succeeded > 0: - cleanupRenderResources(ctx, r, res, job) + cleanupRenderResources(ctx, r, res, job, jobNS) log.V(1).Info("Cleaned up after successful job") return ctrlResult, nil case job.Status.Failed > 0: if shouldCleanupSecrets(res, ttlDuration) { - cleanupSecrets(ctx, r, res) + cleanupSecrets(ctx, r, res, jobNS) log.V(1).Info("Cleaned up secrets after failed job TTL") return ctrlResult, nil } + remaining := remainingTTL(res, ttlDuration) log.V(1).Info("Waiting for TTL to expire before cleaning up secrets", "remainingSeconds", remaining.Seconds()) @@ -162,6 +165,11 @@ func (r *RenderTaskReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrlResult, nil } +// taskNamespace returns the namespace to use for Jobs/Secrets. +func (r *RenderTaskReconciler) taskNamespace(res *solarv1alpha1.RenderTask) string { + return res.Namespace +} + // updateResourceStatusFromJob updates the resource status based on job status func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, res *solarv1alpha1.RenderTask, job *batchv1.Job) (changed bool) { log := ctrl.LoggerFrom(ctx) @@ -187,8 +195,9 @@ func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, Message: fmt.Sprintf("Renderer job completed successfully at %v", job.Status.CompletionTime), }) - if res.Status.ChartURL != r.reference(res.Spec.Repository, res.Spec.Tag) { - res.Status.ChartURL = r.reference(res.Spec.Repository, res.Spec.Tag) + chartURL := r.reference(res.Spec.BaseURL, res.Spec.Repository, res.Spec.Tag) + if res.Status.ChartURL != chartURL { + res.Status.ChartURL = chartURL changed = true } @@ -221,28 +230,29 @@ func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, }) } -func (r *RenderTaskReconciler) deleteRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask) error { +func (r *RenderTaskReconciler) deleteRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) error { job := &batchv1.Job{} - if err := r.Get(ctx, r.renderJobKey(res), job); err != nil { + if err := r.Get(ctx, r.renderJobKey(res, jobNS), job); err != nil { return err } return r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)) } -func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) error { +func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) error { secret := &corev1.Secret{} - if err := r.Get(ctx, r.configSecretKey(res), secret); err != nil { + if err := r.Get(ctx, r.configSecretKey(res, jobNS), secret); err != nil { return err } return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground)) } -func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, pushSecret *corev1.Secret) error { +func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, pushSecret *corev1.Secret, jobNS string) error { log := ctrl.LoggerFrom(ctx) - jobName := r.renderJobKey(res).Name + jobKey := r.renderJobKey(res, jobNS) + jobName := jobKey.Name backoffLimit := int32(3) ttlSecondsAfterFinished := int32(3600) if res.Spec.FailedJobTTL != nil { @@ -320,10 +330,12 @@ func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1 }) } + pushURL := r.reference(res.Spec.BaseURL, res.Spec.Repository, res.Spec.Tag) + job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: jobName, - Namespace: r.renderJobKey(res).Namespace, + Namespace: jobKey.Namespace, Annotations: map[string]string{ annotationJobName: jobName, }, @@ -341,7 +353,7 @@ func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1 Command: []string{r.RendererCommand}, Args: append(r.RendererArgs, "/etc/renderer/config.json", - fmt.Sprintf("--url=%s", r.reference(res.Spec.Repository, res.Spec.Tag)), + fmt.Sprintf("--url=%s", pushURL), ), Env: envVars, VolumeMounts: volumeMounts, @@ -419,6 +431,7 @@ func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1 if err := r.Create(ctx, job); err != nil { r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create job: %s", err) + return errLogAndWrap(log, err, "job creation failed") } @@ -436,7 +449,7 @@ func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1 return nil } -func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) (*corev1.Secret, error) { +func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) (*corev1.Secret, error) { log := ctrl.LoggerFrom(ctx) cfgJson, err := json.Marshal(res.Spec.RendererConfig) @@ -444,12 +457,13 @@ func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *sola return nil, err } + secretKey := r.configSecretKey(res, jobNS) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: r.configSecretKey(res).Name, - Namespace: r.configSecretKey(res).Namespace, + Name: secretKey.Name, + Namespace: secretKey.Namespace, Annotations: map[string]string{ - annotationSecretName: r.configSecretKey(res).Name, + annotationSecretName: secretKey.Name, }, }, Type: corev1.SecretTypeOpaque, @@ -465,6 +479,7 @@ func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *sola if err := r.Create(ctx, secret); err != nil { r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err) + return nil, errLogAndWrap(log, err, "secret creation failed") } @@ -482,29 +497,29 @@ func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *sola return secret, nil } -func (r *RenderTaskReconciler) configSecretKey(res *solarv1alpha1.RenderTask) client.ObjectKey { +func (r *RenderTaskReconciler) configSecretKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey { return client.ObjectKey{ Name: truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen), - Namespace: r.Namespace, + Namespace: jobNS, } } -func (r *RenderTaskReconciler) renderJobKey(res *solarv1alpha1.RenderTask) client.ObjectKey { +func (r *RenderTaskReconciler) renderJobKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey { return client.ObjectKey{ Name: truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen), - Namespace: r.Namespace, + Namespace: jobNS, } } -func (r *RenderTaskReconciler) reference(repo string, tag string) string { - base := r.BaseURL +func (r *RenderTaskReconciler) reference(baseURL, repo, tag string) string { + base := baseURL if !strings.HasPrefix(base, "oci://") { base = fmt.Sprintf("oci://%s", base) } + base = strings.TrimSuffix(base, "/") - url := fmt.Sprintf("%s/%s:%s", base, repo, tag) - return url + return fmt.Sprintf("%s/%s:%s", base, repo, tag) } func ttlSeconds(ttl *int32) int32 { @@ -517,6 +532,7 @@ func ttlSeconds(ttl *int32) int32 { func shouldCleanupSecrets(res *solarv1alpha1.RenderTask, ttl time.Duration) bool { cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed) + return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl } @@ -525,6 +541,7 @@ func remainingTTL(res *solarv1alpha1.RenderTask, ttl time.Duration) time.Duratio if cond == nil { return ttl } + remaining := ttl - time.Since(cond.LastTransitionTime.Time) if remaining < 0 { return 0 @@ -533,15 +550,15 @@ func remainingTTL(res *solarv1alpha1.RenderTask, ttl time.Duration) time.Duratio return remaining } -func cleanupSecrets(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask) { - if err := r.deleteConfigSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) { +func cleanupSecrets(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, jobNS string) { + if err := r.deleteConfigSecret(ctx, res, jobNS); err != nil && !apierrors.IsNotFound(err) { r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete config secret", err) } } -func cleanupRenderResources(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, job *batchv1.Job) { - cleanupSecrets(ctx, r, res) - if err := r.deleteRenderJob(ctx, res); err != nil && !apierrors.IsNotFound(err) { +func cleanupRenderResources(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, job *batchv1.Job, jobNS string) { + cleanupSecrets(ctx, r, res, jobNS) + if err := r.deleteRenderJob(ctx, res, jobNS); err != nil && !apierrors.IsNotFound(err) { r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete job", err) } } diff --git a/pkg/controller/rendertask_controller_test.go b/pkg/controller/rendertask_controller_test.go index 9deb894c..0778c530 100644 --- a/pkg/controller/rendertask_controller_test.go +++ b/pkg/controller/rendertask_controller_test.go @@ -25,10 +25,11 @@ import ( var _ = Describe("RenderTaskController", Ordered, func() { var ( - validRenderTask = func(name string, _ *corev1.Namespace) *solarv1alpha1.RenderTask { + validRenderTask = func(name string, testNS *corev1.Namespace) *solarv1alpha1.RenderTask { return &solarv1alpha1.RenderTask{ ObjectMeta: metav1.ObjectMeta{ - Name: name, + Name: name, + Namespace: testNS.Name, }, Spec: solarv1alpha1.RenderTaskSpec{ RendererConfig: solarv1alpha1.RendererConfig{ @@ -54,8 +55,10 @@ var _ = Describe("RenderTaskController", Ordered, func() { }, }, }, - Repository: "my-release", - Tag: "v1.0.0", + Repository: "my-release", + Tag: "v1.0.0", + BaseURL: "example.com", + PushSecretRef: &corev1.LocalObjectReference{Name: "rendertask-secret"}, }, } } @@ -123,7 +126,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { // Verify the RenderTask was created createdRenderTask := &solarv1alpha1.RenderTask{} Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: "test-config"}, createdRenderTask) + return k8sClient.Get(ctx, client.ObjectKey{Name: "test-config", Namespace: ns.Name}, createdRenderTask) }).Should(Succeed()) // Verify config secret was created @@ -207,7 +210,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { // Wait for ChartURL to be in Status createdTask := &solarv1alpha1.RenderTask{} Eventually(func() bool { - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-url"}, createdTask); err != nil { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-url", Namespace: ns.Name}, createdTask); err != nil { return false } @@ -230,7 +233,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { // Wait for JobScheduled condition updatedTask := &solarv1alpha1.RenderTask{} Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-running"}, updatedTask) + err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-running", Namespace: ns.Name}, updatedTask) if err != nil { return false } @@ -289,7 +292,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { // Wait for RenderTask to get JobSucceeded condition updatedTask := &solarv1alpha1.RenderTask{} Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-success"}, updatedTask) + err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-success", Namespace: ns.Name}, updatedTask) if err != nil { return false } @@ -333,7 +336,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { // Wait for resources to be cleaned up and RenderTask to show success Eventually(func() bool { updatedTask := &solarv1alpha1.RenderTask{} - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-stable"}, updatedTask); err != nil { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-stable", Namespace: ns.Name}, updatedTask); err != nil { return false } @@ -397,7 +400,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { // Wait for RenderTask to get JobFailed condition updatedTask := &solarv1alpha1.RenderTask{} Eventually(func() bool { - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-failed"}, updatedTask); err != nil { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-failed", Namespace: ns.Name}, updatedTask); err != nil { return false } @@ -457,7 +460,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { // Wait for JobFailed condition updatedTask := &solarv1alpha1.RenderTask{} Eventually(func() bool { - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-failed-ttl"}, updatedTask); err != nil { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-failed-ttl", Namespace: ns.Name}, updatedTask); err != nil { return false } @@ -528,7 +531,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { // Verify RenderTask status has references updatedTask := &solarv1alpha1.RenderTask{} Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-refs"}, updatedTask) + err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-refs", Namespace: ns.Name}, updatedTask) if err != nil { return false } @@ -557,7 +560,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { Consistently(func() error { latest := &solarv1alpha1.RenderTask{} - if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-update"}, latest); err != nil { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: "test-task-update", Namespace: ns.Name}, latest); err != nil { return err } latest.Spec.RendererConfig.Type = solarv1alpha1.RendererConfigTypeProfile @@ -572,7 +575,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "rendertask-secret", - Namespace: "default", + Namespace: ns.Name, }, } Expect(k8sClient.Delete(ctx, secret.DeepCopy())).To(Succeed()) @@ -624,7 +627,7 @@ var _ = Describe("RenderTaskController", Ordered, func() { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "rendertask-secret", - Namespace: "default", + Namespace: ns.Name, }, } Expect(k8sClient.Delete(ctx, secret.DeepCopy())).To(Succeed()) diff --git a/pkg/controller/suite_test.go b/pkg/controller/suite_test.go index 5ac8630e..6c6e225e 100644 --- a/pkg/controller/suite_test.go +++ b/pkg/controller/suite_test.go @@ -44,11 +44,10 @@ var ( ns *corev1.Namespace - discoveryReconciler *DiscoveryReconciler targetReconciler *TargetReconciler releaseReconciler *ReleaseReconciler - bootstrapReconciler *BootstrapReconciler renderTaskReconciler *RenderTaskReconciler + profileReconciler *ProfileReconciler ctx context.Context ) @@ -94,15 +93,6 @@ var _ = BeforeSuite(func() { ctx, cancel = context.WithCancel(context.Background()) DeferCleanup(cancel) - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "rendertask-secret", - Namespace: "default", - }, - Type: corev1.SecretTypeOpaque, - } - Expect(k8sClient.Create(ctx, secret)).To(Succeed()) - // log all events to GinkgoWriter fakeRecorder = events.NewFakeRecorder(1) go func() { @@ -121,18 +111,9 @@ var _ = BeforeSuite(func() { Expect(err).ToNot(HaveOccurred()) // Register field indexers (must be done before controller setup) - Expect(IndexRenderTaskOwnerFields(ctx, mgr)).To(Succeed()) + Expect(IndexFields(ctx, mgr)).To(Succeed()) // setup reconcilers - discoveryReconciler = &DiscoveryReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: fakeRecorder, - WorkerImage: "worker", - WorkerCommand: "start", - } - Expect(discoveryReconciler.SetupWithManager(mgr)).To(Succeed()) - targetReconciler = &TargetReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -147,32 +128,24 @@ var _ = BeforeSuite(func() { } Expect(releaseReconciler.SetupWithManager(mgr)).To(Succeed()) - bootstrapReconciler = &BootstrapReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: fakeRecorder, - } - Expect(bootstrapReconciler.SetupWithManager(mgr)).To(Succeed()) - renderTaskReconciler = &RenderTaskReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: fakeRecorder, - RendererImage: "image:tag", - RendererCommand: "solar-renderer", - RendererArgs: []string{ - "--plain-http", - }, - PushSecretRef: &corev1.SecretReference{ - Name: "rendertask-secret", - Namespace: "default", - }, - BaseURL: "example.com", + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: fakeRecorder, + RendererImage: "image:tag", + RendererCommand: "solar-renderer", + RendererArgs: []string{"--plain-http"}, RendererCAConfigMap: "root-bundle", - Namespace: "default", } Expect(renderTaskReconciler.SetupWithManager(mgr)).To(Succeed()) + profileReconciler = &ProfileReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: fakeRecorder, + } + Expect(profileReconciler.SetupWithManager(mgr)).To(Succeed()) + go func() { defer GinkgoRecover() Expect(mgr.Start(ctx)).To(Succeed(), "failed to start manager") @@ -187,25 +160,32 @@ var _ = BeforeEach(func() { } Expect(k8sClient.Create(ctx, ns)).To(Succeed(), "failed to create test namespace") + // Create push secret in test namespace for RenderTask tests + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rendertask-secret", + Namespace: ns.Name, + }, + Type: corev1.SecretTypeOpaque, + } + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + nsName := ns.Name - discoveryReconciler.WatchNamespace = nsName targetReconciler.WatchNamespace = nsName releaseReconciler.WatchNamespace = nsName - bootstrapReconciler.WatchNamespace = nsName - renderTaskReconciler.Namespace = nsName + profileReconciler.WatchNamespace = nsName }) var _ = AfterEach(func() { // Disable controllers from reconciling to prevent re-creation of RenderTasks during cleanup - discoveryReconciler.WatchNamespace = "cleanup-disabled" targetReconciler.WatchNamespace = "cleanup-disabled" releaseReconciler.WatchNamespace = "cleanup-disabled" - bootstrapReconciler.WatchNamespace = "cleanup-disabled" + profileReconciler.WatchNamespace = "cleanup-disabled" - // Clean up cluster-scoped RenderTasks (they are not deleted with the namespace). + // Clean up RenderTasks in the test namespace. // Delete first (sets DeletionTimestamp), then force-remove finalizers via patch. renderTasks := &solarv1alpha1.RenderTaskList{} - Expect(k8sClient.List(ctx, renderTasks)).To(Succeed()) + Expect(k8sClient.List(ctx, renderTasks, client.InNamespace(ns.Name))).To(Succeed()) for i := range renderTasks.Items { rt := &renderTasks.Items[i] Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, rt))).To(Succeed()) @@ -216,7 +196,31 @@ var _ = AfterEach(func() { // Poll until all RenderTasks are gone; re-patch any that reappear Eventually(func() int { list := &solarv1alpha1.RenderTaskList{} - if err := k8sClient.List(ctx, list); err != nil { + if err := k8sClient.List(ctx, list, client.InNamespace(ns.Name)); err != nil { + return -1 + } + for i := range list.Items { + _ = client.IgnoreNotFound(k8sClient.Delete(ctx, &list.Items[i])) + patch := client.RawPatch(types.JSONPatchType, []byte(`[{"op":"replace","path":"/metadata/finalizers","value":[]}]`)) + _ = client.IgnoreNotFound(k8sClient.Patch(ctx, &list.Items[i], patch)) + } + + return len(list.Items) + }, 30*time.Second).Should(Equal(0)) + + // Clean up Targets with finalizers + targets := &solarv1alpha1.TargetList{} + Expect(k8sClient.List(ctx, targets, client.InNamespace(ns.Name))).To(Succeed()) + for i := range targets.Items { + t := &targets.Items[i] + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, t))).To(Succeed()) + patch := client.RawPatch(types.JSONPatchType, []byte(`[{"op":"replace","path":"/metadata/finalizers","value":[]}]`)) + _ = client.IgnoreNotFound(k8sClient.Patch(ctx, t, patch)) + } + // Wait until all Targets are gone before deleting the namespace + Eventually(func() int { + list := &solarv1alpha1.TargetList{} + if err := k8sClient.List(ctx, list, client.InNamespace(ns.Name)); err != nil { return -1 } for i := range list.Items { @@ -230,9 +234,7 @@ var _ = AfterEach(func() { Expect(k8sClient.Delete(ctx, ns)).To(Succeed()) - discoveryReconciler.WatchNamespace = "" targetReconciler.WatchNamespace = "" releaseReconciler.WatchNamespace = "" - bootstrapReconciler.WatchNamespace = "" - renderTaskReconciler.Namespace = "default" + profileReconciler.WatchNamespace = "" }) diff --git a/pkg/controller/target_controller.go b/pkg/controller/target_controller.go index 0f739f8c..8674df79 100644 --- a/pkg/controller/target_controller.go +++ b/pkg/controller/target_controller.go @@ -5,23 +5,27 @@ package controller import ( "context" + "errors" "fmt" + "net/url" "slices" + "sort" + "strings" + "time" + ociname "github.com/google/go-containerregistry/pkg/name" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/events" 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/event" "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" @@ -29,8 +33,22 @@ import ( const ( targetFinalizer = "solar.opendefense.cloud/target-finalizer" + + ConditionTypeRegistryResolved = "RegistryResolved" + ConditionTypeReleasesRendered = "ReleasesRendered" + ConditionTypeBootstrapReady = "BootstrapReady" ) +var ErrReleaseNotRenderedYet = errors.New("release is not rendered yet") + +type releaseInfo struct { + name string + release *solarv1alpha1.Release + cv *solarv1alpha1.ComponentVersion + rtName string + chartURL string +} + type TargetReconciler struct { client.Client Scheme *runtime.Scheme @@ -38,22 +56,24 @@ type TargetReconciler struct { // WatchNamespace restricts reconciliation to this namespace. // Should be empty in production (watches all namespaces). // Intended for use in integration tests only. - // See: https://book.kubebuilder.io/reference/envtest#testing-considerations WatchNamespace string } +//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/status,verbs=get;update;patch //+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/finalizers,verbs=update -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=bootstraps,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=profiles,verbs=get;list;watch +//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registries,verbs=get;list;watch +//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releasebindings,verbs=get;list;watch +//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch +//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch +//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch //+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch -// Reconcile moves the current state of the cluster closer to the desired state +// Reconcile collects ReleaseBindings, resolves the render registry, creates per-release +// RenderTasks (with dedup), and creates a per-target bootstrap RenderTask. func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := ctrl.LoggerFrom(ctx) - ctrlResult := ctrl.Result{} log.V(1).Info("Target is being reconciled", "req", req) @@ -65,207 +85,603 @@ func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr target := &solarv1alpha1.Target{} if err := r.Get(ctx, req.NamespacedName, target); err != nil { if apierrors.IsNotFound(err) { - return ctrlResult, nil + return ctrl.Result{}, nil } - return ctrlResult, errLogAndWrap(log, err, "failed to get object") + return ctrl.Result{}, errLogAndWrap(log, err, "failed to get object") } // Handle deletion if !target.DeletionTimestamp.IsZero() { log.V(1).Info("Target is being deleted") - r.Recorder.Eventf(target, nil, corev1.EventTypeWarning, "Deleting", "Reconcile", "Target is being deleted, cleaning up Bootstrap") + r.Recorder.Eventf(target, nil, corev1.EventTypeWarning, "Deleting", "Reconcile", "Target is being deleted, cleaning up RenderTasks") - // Delete Bootstrap - if err := r.Delete(ctx, &solarv1alpha1.Bootstrap{ObjectMeta: metav1.ObjectMeta{Namespace: target.Namespace, Name: target.Name}}); err != nil && !apierrors.IsNotFound(err) { - return ctrlResult, errLogAndWrap(log, err, "failed to delete Bootstrap") + // Delete owned RenderTasks + if err := r.deleteOwnedRenderTasks(ctx, target); err != nil { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete owned RenderTasks") } // Remove finalizer if slices.Contains(target.Finalizers, targetFinalizer) { - // Re-fetch latest version to avoid conflicts latest := &solarv1alpha1.Target{} if err := r.Get(ctx, req.NamespacedName, latest); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal") + return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal") } - log.V(1).Info("Removing finalizer from Target") + original := latest.DeepCopy() latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool { return s == targetFinalizer }) - if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer from Target") + return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from Target") } } - return ctrlResult, nil + return ctrl.Result{}, nil } - // Set finalizer if not set already and not currently deleting - if target.DeletionTimestamp.IsZero() && !slices.Contains(target.Finalizers, targetFinalizer) { - log.V(1).Info("Target does not have finalizer set, adding finalizer") + // Set finalizer if not set + if !slices.Contains(target.Finalizers, targetFinalizer) { latest := &solarv1alpha1.Target{} if err := r.Get(ctx, req.NamespacedName, latest); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition") + return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition") } + original := latest.DeepCopy() latest.Finalizers = append(latest.Finalizers, targetFinalizer) if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer to Target") + return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to Target") } - return ctrlResult, nil + return ctrl.Result{}, nil } - // Get matching profiles - profileList := &solarv1alpha1.ProfileList{} - if err := r.List(ctx, profileList, client.InNamespace(target.Namespace)); err != nil { - return ctrl.Result{}, errLogAndWrap(log, err, "failed to list Profiles") + // Resolve render registry + registry := &solarv1alpha1.Registry{} + if err := r.Get(ctx, client.ObjectKey{ + Name: target.Spec.RenderRegistryRef.Name, + Namespace: target.Namespace, + }, registry); err != nil { + if apierrors.IsNotFound(err) { + if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound", + "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil { + return ctrl.Result{}, condErr + } + + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry") } - matchingProfiles := make(map[string]corev1.LocalObjectReference) - targetLabels := labels.Set(target.Labels) + if registry.Spec.SolarSecretRef == nil { + if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "MissingSolarSecretRef", + "Registry does not have SolarSecretRef set, required for rendering"); condErr != nil { + return ctrl.Result{}, condErr + } - for _, profile := range profileList.Items { - selector, err := metav1.LabelSelectorAsSelector(&profile.Spec.TargetSelector) - if err != nil { - log.Error(err, "invalid targetSelector in Profile; skipping") - continue + return ctrl.Result{}, nil + } + + if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionTrue, "Resolved", + "Registry resolved: "+registry.Name); condErr != nil { + return ctrl.Result{}, condErr + } + + // Collect ReleaseBindings for this target + bindingList := &solarv1alpha1.ReleaseBindingList{} + if err := r.List(ctx, bindingList, + client.InNamespace(target.Namespace), + client.MatchingFields{indexReleaseBindingTargetName: target.Name}, + ); err != nil { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings") + } + + if len(bindingList.Items) == 0 { + log.V(1).Info("No ReleaseBindings found for target") + if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoBindings", + "No ReleaseBindings found for this target"); condErr != nil { + return ctrl.Result{}, condErr + } + + return ctrl.Result{}, nil + } + + // For each bound release, ensure a per-release RenderTask exists + var releases []releaseInfo + + pendingDeps := false + + for _, binding := range bindingList.Items { + rel := &solarv1alpha1.Release{} + if err := r.Get(ctx, client.ObjectKey{ + Name: binding.Spec.ReleaseRef.Name, + Namespace: target.Namespace, + }, rel); err != nil { + if apierrors.IsNotFound(err) { + log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name) + pendingDeps = true + + continue + } + + return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release") + } + + cv := &solarv1alpha1.ComponentVersion{} + if err := r.Get(ctx, client.ObjectKey{ + Name: rel.Spec.ComponentVersionRef.Name, + Namespace: target.Namespace, + }, cv); err != nil { + if apierrors.IsNotFound(err) { + log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name) + pendingDeps = true + + continue + } + + return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion") + } + + rtName := releaseRenderTaskName(rel.Name, target.Name, rel.GetGeneration()) + releases = append(releases, releaseInfo{ + name: rel.Name, + release: rel, + cv: cv, + rtName: rtName, + }) + } + + // Create per-release RenderTasks (one per target+release pair). + // The renderer job handles dedup by skipping if the chart already exists in the registry. + allRendered := true + + for i, ri := range releases { + rt := &solarv1alpha1.RenderTask{} + err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt) + + if apierrors.IsNotFound(err) { + spec := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target) + rt = &solarv1alpha1.RenderTask{ + ObjectMeta: metav1.ObjectMeta{ + Name: ri.rtName, + Namespace: target.Namespace, + }, + Spec: spec, + } + + if err := r.Create(ctx, rt); err != nil { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask") + } + + log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName) + r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create", + "Created release RenderTask %s for release %s", ri.rtName, ri.name) + } else if err != nil { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask") + } + + // Check if release RenderTask is complete + if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) { + if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed", + fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil { + return ctrl.Result{}, condErr + } + + return ctrl.Result{}, nil } - if selector.Matches(targetLabels) { - matchingProfiles[profile.Name] = corev1.LocalObjectReference{Name: profile.Name} + if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" { + releases[i].chartURL = rt.Status.ChartURL + } else { + allRendered = false } } - // Check if bootstrap exists, if not create and make sure to SetControllerReference... - bootstrap := &solarv1alpha1.Bootstrap{} - err := r.Get(ctx, req.NamespacedName, bootstrap) + if pendingDeps { + if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies", + "One or more bound Releases or ComponentVersions not found"); condErr != nil { + return ctrl.Result{}, condErr + } + + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + if !allRendered { + if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending", + "Waiting for release RenderTasks to complete"); condErr != nil { + return ctrl.Result{}, condErr + } + + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered", + "All releases rendered successfully"); condErr != nil { + return ctrl.Result{}, condErr + } + + // Determine if a new bootstrap render is needed by checking whether the + // current bootstrapVersion's RenderTask still matches the desired release set. + bootstrapVersion := target.Status.BootstrapVersion + bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion) + bootstrapRT := &solarv1alpha1.RenderTask{} + err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT) + + needsNewBootstrap := false + + switch { + case apierrors.IsNotFound(err): + // No RenderTask for the current version yet — create one + needsNewBootstrap = true + case err != nil: + return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask") + default: + // RenderTask exists — check if the desired bootstrap input changed + // (release set, resolved refs/tags, or userdata) + desiredInput, inputErr := buildBootstrapInput(target, releases) + if inputErr != nil { + return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison") + } - if err != nil && !apierrors.IsNotFound(err) { - return ctrlResult, errLogAndWrap(log, err, "failed to get Bootstrap") + existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input + if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) { + bootstrapVersion++ + needsNewBootstrap = true + } } - // Create Bootstrap if not exists or update/override spec - if apierrors.IsNotFound(err) { - log.V(1).Info("Creating Bootstrap for Target", "target", req.NamespacedName) - bootstrap = &solarv1alpha1.Bootstrap{ + if needsNewBootstrap { + spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion) + if specErr != nil { + return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec") + } + + bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion) + bootstrapRT = &solarv1alpha1.RenderTask{ ObjectMeta: metav1.ObjectMeta{ - Name: target.Name, + Name: bootstrapRTName, Namespace: target.Namespace, }, - Spec: solarv1alpha1.BootstrapSpec{ - Releases: target.Spec.Releases, - Profiles: matchingProfiles, - Userdata: target.Spec.Userdata, - }, - } - if err := ctrl.SetControllerReference(target, bootstrap, r.Scheme); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to set controller reference on Bootstrap") + Spec: spec, } - if err := r.Create(ctx, bootstrap); err != nil { + + if err := r.Create(ctx, bootstrapRT); err != nil { if !apierrors.IsAlreadyExists(err) { - return ctrlResult, errLogAndWrap(log, err, "failed to create Bootstrap") + return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask") + } + + if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask") } - log.V(1).Info("Bootstrap already exists, will update", "bootstrap", req.NamespacedName) } else { - r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create", "Created Bootstrap %s/%s", bootstrap.Namespace, bootstrap.Name) - return ctrlResult, nil + log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion) + r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create", + "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion) + } + + // Persist the new bootstrapVersion in status + if bootstrapVersion != target.Status.BootstrapVersion { + target.Status.BootstrapVersion = bootstrapVersion + if err := r.Status().Update(ctx, target); err != nil { + return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion") + } } } - // Update if out of sync - // re-fetch target and bootstrap to avoid conflicts - bootstrap = &solarv1alpha1.Bootstrap{} - if err := r.Get(ctx, req.NamespacedName, bootstrap); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to re-fetch Bootstrap for update check") + // Update target status from bootstrap RenderTask + if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) { + if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed", + "Bootstrap rendering failed"); condErr != nil { + return ctrl.Result{}, condErr + } + + return ctrl.Result{}, nil } - target = &solarv1alpha1.Target{} - if err := r.Get(ctx, req.NamespacedName, target); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to re-fetch Target for update check") + + if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) { + if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready", + "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil { + return ctrl.Result{}, condErr + } + + // Clean up stale RenderTasks owned by this target (old versions) + currentRTNames := map[string]struct{}{bootstrapRTName: {}} + for _, ri := range releases { + currentRTNames[ri.rtName] = struct{}{} + } + if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil { + log.Error(err, "failed to clean up stale RenderTasks") + } + + return ctrl.Result{}, nil + } + + // Still running + return ctrl.Result{}, nil +} + +func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error { + changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{ + Type: condType, + Status: status, + ObservedGeneration: target.Generation, + Reason: reason, + Message: message, + }) + if changed { + if err := r.Status().Update(ctx, target); err != nil { + return fmt.Errorf("failed to update Target status condition %s: %w", condType, err) + } } - original := bootstrap.DeepCopy() + return nil +} + +// deleteStaleRenderTasks removes RenderTasks owned by this target that are no +// longer needed. Any owned RenderTask whose name is not in currentRTNames is +// deleted. This covers both old bootstrap versions and old release generations. +func (r *TargetReconciler) deleteStaleRenderTasks(ctx context.Context, target *solarv1alpha1.Target, currentRTNames map[string]struct{}) error { + log := ctrl.LoggerFrom(ctx) - bootstrap.Spec.Releases = target.Spec.Releases - bootstrap.Spec.Profiles = matchingProfiles - bootstrap.Spec.Userdata = target.Spec.Userdata + rtList := &solarv1alpha1.RenderTaskList{} + if err := r.List(ctx, rtList, + client.InNamespace(target.Namespace), + client.MatchingFields{indexOwnerKind: "Target"}, + ); err != nil { + return err + } - if !apiequality.Semantic.DeepEqual(original.Spec, bootstrap.Spec) { - log.V(1).Info("Updating Bootstrap for Target", "target", req.NamespacedName) - if err := r.Patch(ctx, bootstrap, client.MergeFrom(original)); err != nil { - return ctrlResult, errLogAndWrap(log, err, "failed to update Bootstrap") + for i := range rtList.Items { + rt := &rtList.Items[i] + if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace { + continue } - r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Updated", "Update", "Updated Bootstrap %s/%s", bootstrap.Namespace, bootstrap.Name) + + if _, current := currentRTNames[rt.Name]; current { + continue + } + + log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name) + if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { + return err + } + + r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete", + "Deleted stale RenderTask %s", rt.Name) } - return ctrl.Result{}, nil + return nil +} + +func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error { + rtList := &solarv1alpha1.RenderTaskList{} + if err := r.List(ctx, rtList, + client.InNamespace(target.Namespace), + client.MatchingFields{indexOwnerKind: "Target"}, + ); err != nil { + return err + } + + for i := range rtList.Items { + rt := &rtList.Items[i] + if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace { + if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { + return err + } + } + } + + return nil +} + +func (r *TargetReconciler) computeReleaseRenderTaskSpec(rel *solarv1alpha1.Release, cv *solarv1alpha1.ComponentVersion, registry *solarv1alpha1.Registry, target *solarv1alpha1.Target) solarv1alpha1.RenderTaskSpec { + chartName := fmt.Sprintf("release-%s", rel.Name) + repo := fmt.Sprintf("%s/%s", target.Namespace, chartName) + tag := fmt.Sprintf("v0.0.%d", rel.GetGeneration()) + + return solarv1alpha1.RenderTaskSpec{ + RendererConfig: solarv1alpha1.RendererConfig{ + Type: solarv1alpha1.RendererConfigTypeRelease, + ReleaseConfig: solarv1alpha1.ReleaseConfig{ + Chart: solarv1alpha1.ChartConfig{ + Name: chartName, + Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name), + Version: tag, + AppVersion: tag, + }, + Input: solarv1alpha1.ReleaseInput{ + Component: solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name}, + Resources: cv.Spec.Resources, + Entrypoint: cv.Spec.Entrypoint, + }, + Values: rel.Spec.Values, + }, + }, + Repository: repo, + Tag: tag, + BaseURL: registry.Spec.Hostname, + PushSecretRef: registry.Spec.SolarSecretRef, + FailedJobTTL: rel.Spec.FailedJobTTL, + OwnerName: target.Name, + OwnerNamespace: target.Namespace, + OwnerKind: "Target", + } +} + +// buildBootstrapInput constructs the desired BootstrapInput from the current +// target and resolved releases. Used for both comparison and spec construction. +func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo) (solarv1alpha1.BootstrapInput, error) { + resolvedReleases := map[string]solarv1alpha1.ResourceAccess{} + + for _, ri := range releases { + ref, err := ociname.ParseReference(ri.chartURL) + if err != nil { + return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err) + } + + repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr()) + if err != nil { + return solarv1alpha1.BootstrapInput{}, err + } + + resolvedReleases[ri.name] = solarv1alpha1.ResourceAccess{ + Repository: strings.TrimPrefix(repo, "oci://"), + Tag: ref.Identifier(), + } + } + + return solarv1alpha1.BootstrapInput{ + Releases: resolvedReleases, + Userdata: target.Spec.Userdata, + }, nil +} + +func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) { + input, err := buildBootstrapInput(target, releases) + if err != nil { + return solarv1alpha1.RenderTaskSpec{}, err + } + + releaseNames := make([]string, 0, len(releases)) + for _, ri := range releases { + releaseNames = append(releaseNames, ri.name) + } + + sort.Strings(releaseNames) + + chartName := fmt.Sprintf("bootstrap-%s", target.Name) + repo := fmt.Sprintf("%s/%s", target.Namespace, chartName) + tag := fmt.Sprintf("v0.0.%d", bootstrapVersion) + + return solarv1alpha1.RenderTaskSpec{ + RendererConfig: solarv1alpha1.RendererConfig{ + Type: solarv1alpha1.RendererConfigTypeBootstrap, + BootstrapConfig: solarv1alpha1.BootstrapConfig{ + Chart: solarv1alpha1.ChartConfig{ + Name: chartName, + Description: fmt.Sprintf("Bootstrap of %v", releaseNames), + Version: tag, + AppVersion: tag, + }, + Input: input, + }, + }, + Repository: repo, + Tag: tag, + BaseURL: registry.Spec.Hostname, + PushSecretRef: registry.Spec.SolarSecretRef, + OwnerName: target.Name, + OwnerNamespace: target.Namespace, + OwnerKind: "Target", + }, nil } // SetupWithManager sets up the controller with the Manager. func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&solarv1alpha1.Target{}). - Owns(&solarv1alpha1.Bootstrap{}). Watches( - &solarv1alpha1.Profile{}, - handler.EnqueueRequestsFromMapFunc(r.mapProfileToTargets), - builder.WithPredicates(profileSelectionPredicate()), + &solarv1alpha1.ReleaseBinding{}, + handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget), + ). + Watches( + &solarv1alpha1.RenderTask{}, + handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")), + builder.WithPredicates(renderTaskStatusChangePredicate()), + ). + Watches( + &solarv1alpha1.Registry{}, + handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets), + ). + Watches( + &solarv1alpha1.Release{}, + handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets), ). Complete(r) } -// profileSelectionPredicate filters events to only trigger reconciles when the target selector of a profile changes. -func profileSelectionPredicate() predicate.Predicate { - return predicate.Funcs{ - UpdateFunc: func(e event.UpdateEvent) bool { - oldObj, ok1 := e.ObjectOld.(*solarv1alpha1.Profile) - newObj, ok2 := e.ObjectNew.(*solarv1alpha1.Profile) - if !ok1 || !ok2 { - return false - } - - return !apiequality.Semantic.DeepEqual(oldObj.Spec.TargetSelector, newObj.Spec.TargetSelector) - }, +// mapRegistryToTargets maps a Registry event to reconcile requests for all +// Targets in the same namespace that reference it via renderRegistryRef. +func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request { + reg, ok := obj.(*solarv1alpha1.Registry) + if !ok { + return nil } -} -// mapProfileToTargets maps a Profile to a list of Target reconcile requests. -func (r *TargetReconciler) mapProfileToTargets(ctx context.Context, obj client.Object) []reconcile.Request { - log := ctrl.LoggerFrom(ctx) + targetList := &solarv1alpha1.TargetList{} + if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil { + ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name) - profile, ok := obj.(*solarv1alpha1.Profile) - if !ok { - log.Error(nil, "Object is not a Profile", "type", fmt.Sprintf("%T", obj)) return nil } - selector, err := metav1.LabelSelectorAsSelector(&profile.Spec.TargetSelector) - if err != nil { - log.Error(err, "Invalid targetSelector in Profile", "profile", profile.Name, "targetSelector", profile.Spec.TargetSelector.String()) + var requests []reconcile.Request + for _, t := range targetList.Items { + if t.Spec.RenderRegistryRef.Name == reg.Name { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: t.Name, + Namespace: t.Namespace, + }, + }) + } + } + + return requests +} + +// mapReleaseToTargets maps a Release event to reconcile requests for all +// Targets that are bound to the release via ReleaseBindings. +func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request { + rel, ok := obj.(*solarv1alpha1.Release) + if !ok { return nil } - targetList := &solarv1alpha1.TargetList{} - err = r.List(ctx, targetList, - client.InNamespace(profile.GetNamespace()), - client.MatchingLabelsSelector{Selector: selector}, - ) - if err != nil { - log.V(1).Error(err, "Failed to list Targets for Profile", "profile", profile.Name) + bindingList := &solarv1alpha1.ReleaseBindingList{} + if err := r.List(ctx, bindingList, + client.InNamespace(rel.Namespace), + client.MatchingFields{indexReleaseBindingReleaseName: rel.Name}, + ); err != nil { + ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name) + return nil } - requests := make([]reconcile.Request, 0, len(targetList.Items)) - for _, target := range targetList.Items { + seen := map[string]struct{}{} + var requests []reconcile.Request + + for _, rb := range bindingList.Items { + targetName := rb.Spec.TargetRef.Name + if _, ok := seen[targetName]; ok { + continue + } + + seen[targetName] = struct{}{} requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ - Name: target.Name, - Namespace: target.Namespace, + Name: targetName, + Namespace: rb.Namespace, }, }) } return requests } + +func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request { + rb, ok := obj.(*solarv1alpha1.ReleaseBinding) + if !ok || rb.Spec.TargetRef.Name == "" { + return nil + } + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: rb.Spec.TargetRef.Name, + Namespace: rb.Namespace, + }, + }, + } +} diff --git a/pkg/controller/target_controller_test.go b/pkg/controller/target_controller_test.go index 2bbfae85..f60b8cac 100644 --- a/pkg/controller/target_controller_test.go +++ b/pkg/controller/target_controller_test.go @@ -4,13 +4,13 @@ package controller import ( - "context" + "slices" corev1 "k8s.io/api/core/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1" @@ -19,282 +19,300 @@ import ( ) var _ = Describe("TargetController", Ordered, func() { - Context("when reconciling Target", Label("target"), func() { - It("should create Bootstrap for Target", func() { - target := newTargetWithEmptySpec("test-target", ns.Name, nil) - target.Spec = solarv1alpha1.TargetSpec{ - Userdata: runtime.RawExtension{Raw: []byte(`{"key":"value"}`)}, - Releases: map[string]corev1.LocalObjectReference{ - "example-release": {Name: "initial-release-name"}, + var ( + newTarget = func(name string) *solarv1alpha1.Target { + return &solarv1alpha1.Target{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns.Name, + }, + Spec: solarv1alpha1.TargetSpec{ + RenderRegistryRef: corev1.LocalObjectReference{Name: "test-registry"}, + Userdata: runtime.RawExtension{Raw: []byte(`{"key":"value"}`)}, }, } - Expect(k8sClient.Create(ctx, target)).To(Succeed()) - - bootstrap := &solarv1alpha1.Bootstrap{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKeyFromObject(target), bootstrap) - }).Should(Succeed()) - Expect(bootstrap).NotTo(BeNil()) - }) - }) + } - Context("when Target is deleted", Label("target"), func() { - It("should clean up Bootstrap", func() { - target := newTargetWithEmptySpec("test-target-to-delete", ns.Name, nil) - target.Spec = solarv1alpha1.TargetSpec{ - Userdata: runtime.RawExtension{Raw: []byte(`{"key":"value"}`)}, - Releases: map[string]corev1.LocalObjectReference{ - "example-release": {Name: "initial-release-name"}, + newRegistry = func(name string) *solarv1alpha1.Registry { + return &solarv1alpha1.Registry{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns.Name, + }, + Spec: solarv1alpha1.RegistrySpec{ + Hostname: "registry.example.com", + SolarSecretRef: &corev1.LocalObjectReference{ + Name: "registry-credentials", + }, }, } - Expect(k8sClient.Create(ctx, target)).To(Succeed()) - - bootstrap := &solarv1alpha1.Bootstrap{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKeyFromObject(target), bootstrap) - }).Should(Succeed()) - Expect(bootstrap).NotTo(BeNil()) + } - Expect(k8sClient.Delete(ctx, target)).To(Succeed()) + newReleaseBinding = func(name, targetName, releaseName string) *solarv1alpha1.ReleaseBinding { + return &solarv1alpha1.ReleaseBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns.Name, + }, + Spec: solarv1alpha1.ReleaseBindingSpec{ + TargetRef: corev1.LocalObjectReference{Name: targetName}, + ReleaseRef: corev1.LocalObjectReference{Name: releaseName}, + }, + } + } - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKeyFromObject(target), bootstrap) - }).ShouldNot(Succeed()) - }) - }) + newRelease = func(name string) *solarv1alpha1.Release { + return &solarv1alpha1.Release{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns.Name, + }, + Spec: solarv1alpha1.ReleaseSpec{ + ComponentVersionRef: corev1.LocalObjectReference{Name: "my-cv"}, + Values: runtime.RawExtension{Raw: []byte(`{"key":"value"}`)}, + }, + } + } - Context("when Target is updated", Label("target"), func() { - It("should update Bootstrap", func() { - target := newTargetWithEmptySpec("test-target-to-update", ns.Name, nil) - target.Spec = solarv1alpha1.TargetSpec{ - Userdata: runtime.RawExtension{Raw: []byte(`{"key":"value"}`)}, - Releases: map[string]corev1.LocalObjectReference{ - "example-release": {Name: "initial-release-name"}, + newComponentVersion = func(name string) *solarv1alpha1.ComponentVersion { + return &solarv1alpha1.ComponentVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns.Name, + }, + Spec: solarv1alpha1.ComponentVersionSpec{ + ComponentRef: corev1.LocalObjectReference{Name: "my-component"}, + Tag: "v1.0.0", + Resources: map[string]solarv1alpha1.ResourceAccess{ + "chart": {Repository: "example.com/resources/chart", Tag: "1.0.0"}, + }, + Entrypoint: solarv1alpha1.Entrypoint{ + ResourceName: "chart", + Type: solarv1alpha1.EntrypointTypeHelm, + }, }, } - Expect(k8sClient.Create(ctx, target)).To(Succeed()) + } + ) - bootstrap := &solarv1alpha1.Bootstrap{} - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKeyFromObject(target), bootstrap) - }).Should(Succeed()) - Expect(bootstrap.Spec.Releases).To(Equal(target.Spec.Releases)) + Context("when reconciling Target", Label("target"), func() { + It("should add a finalizer to a new Target", func() { + registry := newRegistry("test-registry") + Expect(k8sClient.Create(ctx, registry)).To(Succeed()) - // Get fresh version of Target and update example-release - latestTarget := &solarv1alpha1.Target{} - Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(target), latestTarget)).To(Succeed()) - latestTarget.Spec.Releases["example-release"] = corev1.LocalObjectReference{Name: "updated-release-name"} - Expect(k8sClient.Update(ctx, latestTarget)).To(Succeed()) + target := newTarget("test-finalizer") + Expect(k8sClient.Create(ctx, target)).To(Succeed()) - // Verify Bootstrap has been updated by the controller Eventually(func() bool { - bs := &solarv1alpha1.Bootstrap{} - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(target), bs) - if err != nil { + t := &solarv1alpha1.Target{} + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(target), t); err != nil { return false } - if release, exists := bs.Spec.Releases["example-release"]; exists { - return release.Name == "updated-release-name" - } - return false - }).Should(BeTrue(), "Bootstrap was not updated with new release name") + return slices.Contains(t.Finalizers, targetFinalizer) + }, eventuallyTimeout).Should(BeTrue()) }) - It("should update the Profiles of the Bootstrap", func() { - // Create a profile and two targets with labels so that target 1 does not match - // the profile and target 2 matches the profile - profile := newProfile("profile", ns.Name, map[string]string{"wave": "2"}) - Expect(k8sClient.Create(ctx, profile)).To(Succeed()) - - target1 := newTargetWithEmptySpec("target-1", ns.Name, map[string]string{"wave": "1"}) - Expect(k8sClient.Create(ctx, target1)).To(Succeed()) - target2 := newTargetWithEmptySpec("target-2", ns.Name, map[string]string{"wave": "2"}) - Expect(k8sClient.Create(ctx, target2)).To(Succeed()) - - expectProfilesInBootstrap(ctx, target1) - expectProfilesInBootstrap(ctx, target2, profile) - - // Update the labels of both targets so that target 1 now matches the profile - // and target 2 doesn't match the profile anymore - Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(target1), target1)).To(Succeed()) - target1.ObjectMeta.Labels = map[string]string{"wave": "2"} - Expect(k8sClient.Update(ctx, target1)).To(Succeed()) - Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(target2), target2)).To(Succeed()) - target2.ObjectMeta.Labels = map[string]string{"wave": "3"} - Expect(k8sClient.Update(ctx, target2)).To(Succeed()) - - expectProfilesInBootstrap(ctx, target1, profile) - expectProfilesInBootstrap(ctx, target2) + + It("should set RegistryResolved=False when Registry does not exist", func() { + target := newTarget("test-no-registry") + target.Spec.RenderRegistryRef.Name = "nonexistent-registry" + Expect(k8sClient.Create(ctx, target)).To(Succeed()) + + Eventually(func() bool { + t := &solarv1alpha1.Target{} + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(target), t); err != nil { + return false + } + cond := apimeta.FindStatusCondition(t.Status.Conditions, ConditionTypeRegistryResolved) + + return cond != nil && cond.Status == metav1.ConditionFalse && cond.Reason == "NotFound" + }, eventuallyTimeout).Should(BeTrue()) }) - }) - Context("when Profile is created", Label("target"), func() { - It("should update Bootstrap of Targets matching the Profile", func() { - target1 := newTargetWithEmptySpec("target-1", ns.Name, map[string]string{ - "env": "prod", - "region": "north", - }) - Expect(k8sClient.Create(ctx, target1)).To(Succeed()) - target2 := newTargetWithEmptySpec("target-2", ns.Name, map[string]string{"env": "prod"}) - Expect(k8sClient.Create(ctx, target2)).To(Succeed()) - target3 := newTargetWithEmptySpec("target-3", ns.Name, map[string]string{"env": "test"}) - Expect(k8sClient.Create(ctx, target3)).To(Succeed()) - - expectProfilesInBootstrap(ctx, target1) - expectProfilesInBootstrap(ctx, target2) - expectProfilesInBootstrap(ctx, target3) - - // Create two profiles so that target 1 matches two profiles, target 2 matches one profile, - // and target 3 doesn't match any profile - profile1 := newProfile("profile-1", ns.Name, map[string]string{"env": "prod"}) - Expect(k8sClient.Create(ctx, profile1)).To(Succeed()) - profile2 := newProfile("profile-2", ns.Name, map[string]string{"region": "north"}) - Expect(k8sClient.Create(ctx, profile2)).To(Succeed()) - - expectProfilesInBootstrap(ctx, target1, profile1, profile2) - expectProfilesInBootstrap(ctx, target2, profile1) - expectProfilesInBootstrap(ctx, target3) + It("should set RegistryResolved=False when Registry has no SolarSecretRef", func() { + registry := newRegistry("test-registry-nosecret") + registry.Spec.SolarSecretRef = nil + Expect(k8sClient.Create(ctx, registry)).To(Succeed()) + + target := newTarget("test-no-secret") + target.Spec.RenderRegistryRef.Name = "test-registry-nosecret" + Expect(k8sClient.Create(ctx, target)).To(Succeed()) + + Eventually(func() bool { + t := &solarv1alpha1.Target{} + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(target), t); err != nil { + return false + } + cond := apimeta.FindStatusCondition(t.Status.Conditions, ConditionTypeRegistryResolved) + + return cond != nil && cond.Status == metav1.ConditionFalse && cond.Reason == "MissingSolarSecretRef" + }, eventuallyTimeout).Should(BeTrue()) }) - It("should update Bootstrap for all Targets when the Profile doesn't define a target selector", func() { - target1 := newTargetWithEmptySpec("target-1", ns.Name, map[string]string{}) - Expect(k8sClient.Create(ctx, target1)).To(Succeed()) - target2 := newTargetWithEmptySpec("target-2", ns.Name, map[string]string{}) - Expect(k8sClient.Create(ctx, target2)).To(Succeed()) - - // Create a profile with no target selector so that it matches all targets - profile := newProfile("profile", ns.Name, map[string]string{}) - Expect(k8sClient.Create(ctx, profile)).To(Succeed()) - - expectProfilesInBootstrap(ctx, target1, profile) - expectProfilesInBootstrap(ctx, target2, profile) + + It("should set ReleasesRendered=NoBindings when no ReleaseBindings exist", func() { + registry := newRegistry("test-registry") + _ = k8sClient.Create(ctx, registry) // may already exist + + target := newTarget("test-no-bindings") + Expect(k8sClient.Create(ctx, target)).To(Succeed()) + + Eventually(func() bool { + t := &solarv1alpha1.Target{} + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(target), t); err != nil { + return false + } + cond := apimeta.FindStatusCondition(t.Status.Conditions, ConditionTypeReleasesRendered) + + return cond != nil && cond.Status == metav1.ConditionFalse && cond.Reason == "NoBindings" + }, eventuallyTimeout).Should(BeTrue()) }) - }) - Context("when Profile is updated", Label("target"), func() { - It("should update Bootstrap of matching Target", func() { - // Create a profile and a target so that they don't match - profile := newProfile("profile", ns.Name, map[string]string{"wave": "2"}) - Expect(k8sClient.Create(ctx, profile)).To(Succeed()) + It("should create a release RenderTask when ReleaseBinding exists", func() { + registry := newRegistry("test-registry") + _ = k8sClient.Create(ctx, registry) + + cv := newComponentVersion("my-cv") + Expect(k8sClient.Create(ctx, cv)).To(Succeed()) - target := newTargetWithEmptySpec("target", ns.Name, map[string]string{"wave": "1"}) + rel := newRelease("my-release") + Expect(k8sClient.Create(ctx, rel)).To(Succeed()) + + target := newTarget("test-release-rt") Expect(k8sClient.Create(ctx, target)).To(Succeed()) - expectProfilesInBootstrap(ctx, target) + binding := newReleaseBinding("binding-1", "test-release-rt", "my-release") + Expect(k8sClient.Create(ctx, binding)).To(Succeed()) - // Update the profile so that it matches the target + // Verify a release RenderTask was created + rtName := releaseRenderTaskName("my-release", "test-release-rt", 1) + rt := &solarv1alpha1.RenderTask{} Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKeyFromObject(profile), profile) - }).Should(Succeed()) - profile.Spec.TargetSelector = metav1.LabelSelector{ - MatchLabels: map[string]string{"wave": "1"}, - } - Expect(k8sClient.Update(ctx, profile)).To(Succeed()) - - expectProfilesInBootstrap(ctx, target, profile) + return k8sClient.Get(ctx, client.ObjectKey{Name: rtName, Namespace: ns.Name}, rt) + }, eventuallyTimeout).Should(Succeed()) + + Expect(rt.Spec.RendererConfig.Type).To(Equal(solarv1alpha1.RendererConfigTypeRelease)) + Expect(rt.Spec.BaseURL).To(Equal("registry.example.com")) + Expect(rt.Spec.PushSecretRef).NotTo(BeNil()) + Expect(rt.Spec.PushSecretRef.Name).To(Equal("registry-credentials")) + Expect(rt.Spec.OwnerKind).To(Equal("Target")) + Expect(rt.Spec.OwnerName).To(Equal("test-release-rt")) }) }) - Context("when Profile is deleted", Label("target"), func() { - It("should update Bootstrap of matching Target", func() { - // Create a profile a target so that they match - profile := newProfile("profile", ns.Name, map[string]string{"wave": "1"}) - Expect(k8sClient.Create(ctx, profile)).To(Succeed()) + Context("when bootstrap version changes", Label("target"), func() { + markRenderTaskSucceeded := func(name, chartURL string) { + rt := &solarv1alpha1.RenderTask{} + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: ns.Name}, rt) + }, eventuallyTimeout).Should(Succeed()) + + apimeta.SetStatusCondition(&rt.Status.Conditions, metav1.Condition{ + Type: ConditionTypeJobSucceeded, + Status: metav1.ConditionTrue, + Reason: "JobSucceeded", + }) + rt.Status.ChartURL = chartURL + ExpectWithOffset(1, k8sClient.Status().Update(ctx, rt)).To(Succeed()) + } + + It("should clean up stale bootstrap RenderTasks after a new one succeeds", func() { + registry := newRegistry("test-registry") + _ = k8sClient.Create(ctx, registry) + + cv := newComponentVersion("my-cv") + _ = k8sClient.Create(ctx, cv) - target := newTargetWithEmptySpec("target", ns.Name, map[string]string{"wave": "1"}) + rel1 := newRelease("rel-cleanup-1") + Expect(k8sClient.Create(ctx, rel1)).To(Succeed()) + + rel2 := newRelease("rel-cleanup-2") + rel2.Spec.ComponentVersionRef.Name = "my-cv" + Expect(k8sClient.Create(ctx, rel2)).To(Succeed()) + + target := newTarget("test-cleanup") Expect(k8sClient.Create(ctx, target)).To(Succeed()) - expectProfilesInBootstrap(ctx, target, profile) + binding1 := newReleaseBinding("binding-cleanup-1", "test-cleanup", "rel-cleanup-1") + Expect(k8sClient.Create(ctx, binding1)).To(Succeed()) - // Delete the profile - Eventually(func() error { - return k8sClient.Delete(ctx, profile) - }).Should(Succeed()) + // Wait for release RenderTask, then mark it succeeded + relRTName := releaseRenderTaskName("rel-cleanup-1", "test-cleanup", 1) + markRenderTaskSucceeded(relRTName, "oci://registry.example.com/"+ns.Name+"/release-rel-cleanup-1:v0.0.0") - expectProfilesInBootstrap(ctx, target) - }) - }) + // Wait for the first bootstrap RenderTask (version 0) + bootstrapV0 := targetRenderTaskName("test-cleanup", 0) + markRenderTaskSucceeded(bootstrapV0, "oci://registry.example.com/"+ns.Name+"/bootstrap-test-cleanup:v0.0.0") - Context("Profile Predicate", Label("target"), func() { - It("should trigger when a profile has been created", func() { - predicate := profileSelectionPredicate() + // Verify BootstrapReady=True + Eventually(func() bool { + t := &solarv1alpha1.Target{} + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(target), t); err != nil { + return false + } - ev := event.CreateEvent{Object: &solarv1alpha1.Profile{}} + return apimeta.IsStatusConditionTrue(t.Status.Conditions, ConditionTypeBootstrapReady) + }, eventuallyTimeout).Should(BeTrue()) - Expect(predicate.Create(ev)).To(BeTrue()) - }) - It("should trigger when a profile has been deleted", func() { - predicate := profileSelectionPredicate() + // Add a second release binding — triggers new bootstrap version + binding2 := newReleaseBinding("binding-cleanup-2", "test-cleanup", "rel-cleanup-2") + Expect(k8sClient.Create(ctx, binding2)).To(Succeed()) - ev := event.DeleteEvent{Object: &solarv1alpha1.Profile{}} + // Wait for second release RenderTask, then mark it succeeded + relRT2Name := releaseRenderTaskName("rel-cleanup-2", "test-cleanup", 1) + markRenderTaskSucceeded(relRT2Name, "oci://registry.example.com/"+ns.Name+"/release-rel-cleanup-2:v0.0.0") - Expect(predicate.Delete(ev)).To(BeTrue()) - }) - It("should trigger when the target selector of a profile has been updated", func() { - predicate := profileSelectionPredicate() - - oldProfile := newProfile("profile", ns.Name, map[string]string{"wave": "1"}) - newProfile := oldProfile.DeepCopy() - newProfile.Spec = solarv1alpha1.ProfileSpec{ - TargetSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{"wave": "2"}, - }, - } + // Wait for the new bootstrap RenderTask (version 1) + bootstrapV1 := targetRenderTaskName("test-cleanup", 1) + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: bootstrapV1, Namespace: ns.Name}, &solarv1alpha1.RenderTask{}) + }, eventuallyTimeout).Should(Succeed()) + + // Mark the new bootstrap RenderTask as succeeded + markRenderTaskSucceeded(bootstrapV1, "oci://registry.example.com/"+ns.Name+"/bootstrap-test-cleanup:v0.0.1") + + // Verify the old bootstrap RenderTask (v0) is cleaned up + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: bootstrapV0, Namespace: ns.Name}, &solarv1alpha1.RenderTask{}) + + return err != nil + }, eventuallyTimeout).Should(BeTrue(), "stale bootstrap RenderTask %s should be deleted", bootstrapV0) - ev := event.UpdateEvent{ObjectOld: oldProfile, ObjectNew: newProfile} + // Verify the new bootstrap RenderTask (v1) still exists + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: bootstrapV1, Namespace: ns.Name}, &solarv1alpha1.RenderTask{})).To(Succeed()) - Expect(predicate.Update(ev)).To(BeTrue()) + // Verify release RenderTasks are NOT cleaned up + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: relRTName, Namespace: ns.Name}, &solarv1alpha1.RenderTask{})).To(Succeed()) + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: relRT2Name, Namespace: ns.Name}, &solarv1alpha1.RenderTask{})).To(Succeed()) }) - It("should not trigger when other than the target selector of a profile has been updated", func() { - predicate := profileSelectionPredicate() + }) - oldProfile := newProfile("profile", ns.Name, map[string]string{"wave": "1"}) + Context("when Target is deleted", Label("target"), func() { + It("should remove the finalizer and allow deletion", func() { + registry := newRegistry("test-registry") + _ = k8sClient.Create(ctx, registry) + + target := newTarget("test-delete") + Expect(k8sClient.Create(ctx, target)).To(Succeed()) + + // Wait for finalizer to be added + Eventually(func() bool { + t := &solarv1alpha1.Target{} + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(target), t); err != nil { + return false + } + + return slices.Contains(t.Finalizers, targetFinalizer) + }, eventuallyTimeout).Should(BeTrue()) - newProfile := oldProfile.DeepCopy() - newProfile.ObjectMeta.Name = "profile-updated" + Expect(k8sClient.Delete(ctx, target)).To(Succeed()) - ev := event.UpdateEvent{ObjectOld: oldProfile, ObjectNew: newProfile} + // Verify Target is eventually deleted + Eventually(func() bool { + t := &solarv1alpha1.Target{} + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(target), t) - Expect(predicate.Update(ev)).To(BeFalse()) + return err != nil + }, eventuallyTimeout).Should(BeTrue()) }) }) }) - -func newTargetWithEmptySpec(name, namespace string, labels map[string]string) *solarv1alpha1.Target { - return &solarv1alpha1.Target{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - }, - Spec: solarv1alpha1.TargetSpec{}, - } -} - -func newProfile(name, namespace string, matchLabels map[string]string) *solarv1alpha1.Profile { - return &solarv1alpha1.Profile{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: solarv1alpha1.ProfileSpec{ - TargetSelector: metav1.LabelSelector{ - MatchLabels: matchLabels, - }, - }, - } -} - -func expectProfilesInBootstrap(ctx context.Context, target *solarv1alpha1.Target, expectedProfiles ...*solarv1alpha1.Profile) { - GinkgoHelper() - bs := &solarv1alpha1.Bootstrap{} - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(target), bs) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(bs.Spec.Profiles).To(HaveLen(len(expectedProfiles))) - for _, p := range expectedProfiles { - g.Expect(bs.Spec.Profiles).To(HaveKeyWithValue(p.Name, corev1.LocalObjectReference{ - Name: p.Name, - })) - } - }).Should(Succeed()) -} diff --git a/pkg/discovery/apiwriter/apiwriter_test.go b/pkg/discovery/apiwriter/apiwriter_test.go index f668629c..1cc73bac 100644 --- a/pkg/discovery/apiwriter/apiwriter_test.go +++ b/pkg/discovery/apiwriter/apiwriter_test.go @@ -47,7 +47,7 @@ func createEvent(eventType discovery.EventType) discovery.WriteAPIResourceEvent Source: discovery.RepositoryEvent{ Registry: "test-registry", Repository: "test/component-descriptors/opendefense.cloud/ocm-demo", - Version: "v26.4.0", + Version: "v26.4.1", Digest: "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", Type: eventType, Timestamp: time.Now(), @@ -71,7 +71,7 @@ func createEvent(eventType discovery.EventType) discovery.WriteAPIResourceEvent ev.ComponentSpec = compdesc.ComponentSpec{ ObjectMeta: compmetav1.ObjectMeta{ Name: "opendefense.cloud/ocm-demo", - Version: "v26.4.0", + Version: "v26.4.1", }, Resources: compdesc.Resources{ { @@ -195,7 +195,7 @@ var _ = Describe("APIWriter", Ordered, func() { Expect(errEvent.Error).NotTo(HaveOccurred()) default: } - mcv, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-0", metav1.GetOptions{}) + mcv, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-1", metav1.GetOptions{}) cv = mcv return err @@ -250,7 +250,7 @@ var _ = Describe("APIWriter", Ordered, func() { Expect(errEvent.Error).NotTo(HaveOccurred()) default: } - _, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-0", metav1.GetOptions{}) + _, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-1", metav1.GetOptions{}) return err }).ShouldNot(HaveOccurred()) @@ -280,7 +280,7 @@ var _ = Describe("APIWriter", Ordered, func() { Expect(errEvent.Error).NotTo(HaveOccurred()) default: } - cv, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-0", metav1.GetOptions{}) + cv, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-1", metav1.GetOptions{}) if err != nil { return false } @@ -303,7 +303,7 @@ var _ = Describe("APIWriter", Ordered, func() { Expect(errEvent.Error).NotTo(HaveOccurred()) default: } - _, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-0", metav1.GetOptions{}) + _, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-1", metav1.GetOptions{}) if err != nil { return err } @@ -320,7 +320,7 @@ var _ = Describe("APIWriter", Ordered, func() { Expect(errEvent.Error).NotTo(HaveOccurred()) default: } - _, err = solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-0", metav1.GetOptions{}) + _, err = solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-1", metav1.GetOptions{}) return err }).Should(HaveOccurred()) @@ -356,7 +356,7 @@ var _ = Describe("APIWriter", Ordered, func() { Expect(errEvent.Error).NotTo(HaveOccurred()) default: } - _, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-0", metav1.GetOptions{}) + _, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-1", metav1.GetOptions{}) if err != nil { return err } @@ -377,7 +377,7 @@ var _ = Describe("APIWriter", Ordered, func() { Expect(errEvent.Error).NotTo(HaveOccurred()) default: } - _, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-0", metav1.GetOptions{}) + _, err := solarClient.ComponentVersions("default").Get(ctx, "opendefense-cloud-ocm-demo-v26-4-1", metav1.GetOptions{}) return apierrors.IsNotFound(err) }).To(BeTrue()) diff --git a/pkg/discovery/handler/filter_test.go b/pkg/discovery/handler/filter_test.go index 0589e37c..50882936 100644 --- a/pkg/discovery/handler/filter_test.go +++ b/pkg/discovery/handler/filter_test.go @@ -36,7 +36,7 @@ var _ = Describe("Filter", Ordered, func() { outputChan = make(chan discovery.ComponentVersionEvent, 100) errChan = make(chan discovery.ErrorEvent, 100) solarClient = fake.NewClientset(&v1alpha1.ComponentVersion{ - ObjectMeta: metav1.ObjectMeta{Name: discovery.SanitizeWithHash("opendefense-cloud-ocm-demo-v26-4-0"), Namespace: "default"}, + ObjectMeta: metav1.ObjectMeta{Name: discovery.SanitizeWithHash("opendefense-cloud-ocm-demo-v26-4-1"), Namespace: "default"}, }).SolarV1alpha1() ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) @@ -61,7 +61,7 @@ var _ = Describe("Filter", Ordered, func() { Source: discovery.RepositoryEvent{ Registry: "default", Repository: "test/component-descriptors/opendefense.cloud/ocm-demo", - Version: "v26.4.0", + Version: "v26.4.1", Type: discovery.EventCreated, }, Namespace: "test", @@ -98,7 +98,7 @@ var _ = Describe("Filter", Ordered, func() { Source: discovery.RepositoryEvent{ Registry: "default", Repository: "test/component-descriptors/opendefense.cloud/ocm-demo", - Version: "v26.4.0", + Version: "v26.4.1", Type: discovery.EventUpdated, }, Namespace: "test", @@ -108,7 +108,7 @@ var _ = Describe("Filter", Ordered, func() { var ev discovery.ComponentVersionEvent Eventually(outputChan).Should(Receive(&ev)) Expect(ev.Component).To(Equal("opendefense.cloud/ocm-demo")) - Expect(ev.Source.Version).To(Equal("v26.4.0")) + Expect(ev.Source.Version).To(Equal("v26.4.1")) Expect(ev.Source.Type).To(Equal(discovery.EventUpdated)) Consistently(errChan).ShouldNot(Receive()) }) @@ -119,7 +119,7 @@ var _ = Describe("Filter", Ordered, func() { Source: discovery.RepositoryEvent{ Registry: "default", Repository: "test/component-descriptors/opendefense.cloud/ocm-demo", - Version: "v26.4.0", + Version: "v26.4.1", Type: discovery.EventDeleted, }, Namespace: "test", diff --git a/pkg/discovery/handler/handler_test.go b/pkg/discovery/handler/handler_test.go index bb9604fd..27a64069 100644 --- a/pkg/discovery/handler/handler_test.go +++ b/pkg/discovery/handler/handler_test.go @@ -115,7 +115,7 @@ var _ = Describe("Handler", Ordered, func() { Source: discovery.RepositoryEvent{ Registry: testRegistry.Name, Repository: "test/component-descriptors/opendefense.cloud/ocm-demo", - Version: "v26.4.0", + Version: "v26.4.1", Type: discovery.EventCreated, }, Namespace: "test", @@ -160,7 +160,7 @@ var _ = Describe("Handler", Ordered, func() { Source: discovery.RepositoryEvent{ Registry: testRegistry.Name, Repository: "test/component-descriptors/opendefense.cloud/ocm-demo", - Version: "v26.4.0", + Version: "v26.4.1", Type: discovery.EventCreated, }, Namespace: "test", diff --git a/pkg/discovery/qualifier/qualifier_test.go b/pkg/discovery/qualifier/qualifier_test.go index 70a35f97..de18e2df 100644 --- a/pkg/discovery/qualifier/qualifier_test.go +++ b/pkg/discovery/qualifier/qualifier_test.go @@ -122,13 +122,13 @@ var _ = Describe("Qualifier", Ordered, func() { inputEventsChan <- discovery.RepositoryEvent{ Registry: testRegistry.Name, Repository: "test/component-descriptors/opendefense.cloud/ocm-demo", - Version: "v26.4.0", + Version: "v26.4.1", } expected := SatisfyAll( HaveField("Component", "opendefense.cloud/ocm-demo"), HaveField("Source", - HaveField("Version", "v26.4.0"), + HaveField("Version", "v26.4.1"), ), ) @@ -170,7 +170,7 @@ var _ = Describe("Qualifier", Ordered, func() { expected := SatisfyAll( HaveField("Component", "opendefense.cloud/ocm-demo"), HaveField("Source", - HaveField("Version", "v26.4.0"), + HaveField("Version", "v26.4.1"), ), ) Eventually(outputEventsChan).Should(Receive(expected)) diff --git a/pkg/discovery/registry.go b/pkg/discovery/registry.go index dfd67ee3..5777ac3b 100644 --- a/pkg/discovery/registry.go +++ b/pkg/discovery/registry.go @@ -14,14 +14,11 @@ import ( type RegistryCredentials struct { // Username is the username used to authenticate with the registry Username string `yaml:"username"` - // Password is the password used to authenticate with the registry - // If the discovery worker is run as a standalone binary users would have to - // write the registry password into the configuration file. This use-case is - // not supported for now since the configuration gets built by the - // discovery-controller and both source and destination are kubernetes - // secrets. Therefore we can ignore linting the security antipattern. - // nolint:gosec - Password string `yaml:"password"` + // Password is the password used to authenticate with the registry. + // In standalone mode, use environment variable substitution in the + // config file (e.g. ${REGISTRY_PASSWORD}) to avoid storing passwords + // in plaintext. + Password string `yaml:"password"` //nolint:gosec // credential value injected via env var substitution at load time } // Registry is a struct representing an OCI registry. diff --git a/pkg/discovery/registry_provider.go b/pkg/discovery/registry_provider.go index ebef4bae..7b355f06 100644 --- a/pkg/discovery/registry_provider.go +++ b/pkg/discovery/registry_provider.go @@ -6,6 +6,7 @@ package discovery import ( "fmt" "os" + "strings" "sync" "gopkg.in/yaml.v3" @@ -29,15 +30,23 @@ func NewRegistryProvider() *RegistryProvider { } // Unmarshal loads registries from a YAML file located at the given path. +// Environment variables referenced in the file via $VAR or ${VAR} syntax +// are expanded before parsing, allowing credentials and other sensitive +// values to be injected from the environment rather than stored in the +// config file directly. func (p *RegistryProvider) Unmarshal(path string) error { - file, err := os.Open(path) + data, err := os.ReadFile(path) if err != nil { - return fmt.Errorf("failed to open registry file: %w", err) + return fmt.Errorf("failed to read registry file: %w", err) + } + + expanded, err := expandEnvStrict(string(data)) + if err != nil { + return fmt.Errorf("failed to expand environment variables in registry file: %w", err) } - defer file.Close() var ymlConfig RegistryProviderConfig - if err := yaml.NewDecoder(file).Decode(&ymlConfig); err != nil { + if err := yaml.Unmarshal([]byte(expanded), &ymlConfig); err != nil { return fmt.Errorf("failed to parse registry file: %w", err) } @@ -108,3 +117,27 @@ func (p *RegistryProvider) GetAll() []*Registry { return out } + +// expandEnvStrict expands $VAR and ${VAR} references in s using os.LookupEnv. +// Unlike os.ExpandEnv, it returns an error listing all undefined variables +// instead of silently replacing them with empty strings. +func expandEnvStrict(s string) (string, error) { + var missing []string + + expanded := os.Expand(s, func(key string) string { + val, ok := os.LookupEnv(key) + if !ok { + missing = append(missing, key) + + return "" + } + + return val + }) + + if len(missing) > 0 { + return "", fmt.Errorf("undefined environment variables: %s", strings.Join(missing, ", ")) + } + + return expanded, nil +} diff --git a/pkg/discovery/webhook/zot/zot_test.go b/pkg/discovery/webhook/zot/zot_test.go index 4867bf6a..c02e4a4b 100644 --- a/pkg/discovery/webhook/zot/zot_test.go +++ b/pkg/discovery/webhook/zot/zot_test.go @@ -435,7 +435,7 @@ var _ = Describe("Zot Webhook Handler", Ordered, func() { eventData := ZotEventData{ Name: "test/component-descriptors/opendefense.cloud/ocm-demo", - Reference: "v26.4.0", + Reference: "v26.4.1", Digest: "sha256:40bac3123555936fd4aa8260a853669283fa8d64be8f665ba9d60fd9f7d7df3b", } @@ -465,7 +465,7 @@ var _ = Describe("Zot Webhook Handler", Ordered, func() { var repositoryEvent discovery.RepositoryEvent Expect(eventsChan).Should(Receive(&repositoryEvent)) - Expect(repositoryEvent.Version).To(Equal("v26.4.0")) + Expect(repositoryEvent.Version).To(Equal("v26.4.1")) Expect(repositoryEvent.Type).To(Equal(discovery.EventUpdated)) }) }) @@ -477,7 +477,7 @@ var _ = Describe("Zot Webhook Handler", Ordered, func() { }, Entry("sha256 digest", "sha256:40bac3123555936fd4aa8260a853669283fa8d64be8f665ba9d60fd9f7d7df3b", true), Entry("sha512 digest", "sha512:abcdef1234567890", true), - Entry("semver version", "v26.4.0", false), + Entry("semver version", "v26.4.1", false), Entry("semver with v prefix", "v1.0.0", false), Entry("simple tag", "latest", false), Entry("numeric tag", "123", false), diff --git a/pkg/renderer/push_chart.go b/pkg/renderer/push_chart.go index 7b2874f8..5c4d76f8 100644 --- a/pkg/renderer/push_chart.go +++ b/pkg/renderer/push_chart.go @@ -7,6 +7,8 @@ import ( "fmt" "os" "path/filepath" + "slices" + "strings" "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/registry" @@ -98,6 +100,36 @@ func packageChart(chartDir string, outputDir string, version string) (string, er return packagedPath, nil } +// ChartExists checks whether the chart reference in opts already exists in the +// OCI registry by listing tags. Returns true if the tag is already present. +func ChartExists(opts PushOptions) (bool, error) { + if opts.Reference == "" { + return false, fmt.Errorf("registry reference is required") + } + + // Parse "oci://host/repo:tag" into repo ref and tag + parts := strings.Split(opts.Reference, ":") + if len(parts) < 2 { + return false, fmt.Errorf("invalid reference, no tag found: %s", opts.Reference) + } + + tag := parts[len(parts)-1] + repoRef := strings.TrimSuffix(opts.Reference, ":"+tag) + + client, err := registry.NewClient(opts.ClientOptions...) + if err != nil { + return false, fmt.Errorf("failed to create registry client: %w", err) + } + + tags, err := client.Tags(repoRef) + if err != nil { + // Repository may not exist yet — chart doesn't exist + return false, nil //nolint:nilerr // missing repo means chart doesn't exist + } + + return slices.Contains(tags, tag), nil +} + // pushChartToRegistry pushes a packaged helm chart to an OCI registry. // It handles authentication and registry configuration based on PushOptions. func pushChartToRegistry(packagePath string, opts PushOptions) (string, error) { diff --git a/pkg/ui/api/handler.go b/pkg/ui/api/handler.go new file mode 100644 index 00000000..a3c46097 --- /dev/null +++ b/pkg/ui/api/handler.go @@ -0,0 +1,499 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "slices" + + "github.com/go-logr/logr" + authorizationv1 "k8s.io/api/authorization/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "go.opendefense.cloud/solar/pkg/ui/auth" + "go.opendefense.cloud/solar/pkg/ui/session" +) + +// resourceMap maps resource names to their GVR. +var resourceMap = map[string]schema.GroupVersionResource{ + "targets": {Group: "solar.opendefense.cloud", Version: "v1alpha1", Resource: "targets"}, + "releases": {Group: "solar.opendefense.cloud", Version: "v1alpha1", Resource: "releases"}, + "releasebindings": {Group: "solar.opendefense.cloud", Version: "v1alpha1", Resource: "releasebindings"}, + "components": {Group: "solar.opendefense.cloud", Version: "v1alpha1", Resource: "components"}, + "componentversions": {Group: "solar.opendefense.cloud", Version: "v1alpha1", Resource: "componentversions"}, + "registries": {Group: "solar.opendefense.cloud", Version: "v1alpha1", Resource: "registries"}, + "registrybindings": {Group: "solar.opendefense.cloud", Version: "v1alpha1", Resource: "registrybindings"}, + "profiles": {Group: "solar.opendefense.cloud", Version: "v1alpha1", Resource: "profiles"}, + "rendertasks": {Group: "solar.opendefense.cloud", Version: "v1alpha1", Resource: "rendertasks"}, +} + +// Handler serves the K8s API proxy routes. +type Handler struct { + baseConfig *rest.Config + clientset kubernetes.Interface + sessionStore *session.Store + authProvider auth.Provider + log logr.Logger +} + +// NewHandler creates a new API handler. +func NewHandler(kubeconfig string, store *session.Store, provider auth.Provider, log logr.Logger) (*Handler, error) { + var cfg *rest.Config + var err error + + if kubeconfig != "" { + cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + } else { + cfg, err = rest.InClusterConfig() + } + if err != nil { + return nil, fmt.Errorf("failed to create kubernetes config: %w", err) + } + + clientset, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create kubernetes clientset: %w", err) + } + + return &Handler{ + baseConfig: cfg, + clientset: clientset, + sessionStore: store, + authProvider: provider, + log: log.WithName("api"), + }, nil +} + +// clientFor returns a dynamic client for the given session. +func (h *Handler) clientFor(r *http.Request) (dynamic.Interface, error) { + sess := h.sessionStore.Get(r) + cfg := h.authProvider.WrapConfig(h.baseConfig, sess) + + return dynamic.NewForConfig(cfg) +} + +// isAdminUser returns true when the session user is a subject of any +// ClusterRoleBinding labeled solar.opendefense.cloud/admin=true. +// Uses the BFF's own service-account credentials so the check is immune to +// any active session-level impersonation override. +func (h *Handler) isAdminUser(ctx context.Context, sess *session.Data) bool { + bindingList, err := h.clientset.RbacV1().ClusterRoleBindings().List(ctx, metav1.ListOptions{ + LabelSelector: adminLabel + "=true", + }) + if err != nil { + h.log.Error(err, "failed to list admin ClusterRoleBindings") + + return false + } + + for _, binding := range bindingList.Items { + for _, subject := range binding.Subjects { + switch subject.Kind { + case "User": + if subject.Name == sess.Username { + return true + } + case "Group": + if slices.Contains(sess.Groups, subject.Name) { + return true + } + } + } + } + + return false +} + +// RequireAdmin returns a middleware that rejects requests from users who are not +// listed as subjects in a ClusterRoleBinding labeled solar.opendefense.cloud/admin=true. +func (h *Handler) RequireAdmin(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + sess := h.sessionStore.Get(r) + if sess == nil || !h.isAdminUser(r.Context(), sess) { + http.Error(w, "forbidden", http.StatusForbidden) + + return + } + + next(w, r) + } +} + +// HandleMe returns the current user info, including an isAdmin flag derived +// from membership in a ClusterRoleBinding labeled solar.opendefense.cloud/admin=true +func (h *Handler) HandleMe() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + data := h.sessionStore.Get(r) + if data == nil { + writeJSON(w, map[string]any{"authenticated": false}) + + return + } + + resp := map[string]any{ + "authenticated": true, + "username": data.Username, + "groups": data.Groups, + "isAdmin": h.isAdminUser(r.Context(), data), + } + + if data.ImpersonatingAs != "" { + resp["impersonating"] = map[string]any{ + "username": data.ImpersonatingAs, + "groups": data.ImpersonatingGroups, + } + } + + writeJSON(w, resp) + } +} + +// HandleList returns a handler that lists resources of the given type. +func (h *Handler) HandleList(resource string) http.HandlerFunc { + gvr, ok := resourceMap[resource] + if !ok { + return func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, fmt.Sprintf("unknown resource: %s", resource), http.StatusNotFound) + } + } + + return func(w http.ResponseWriter, r *http.Request) { + namespace := r.PathValue("namespace") + + client, err := h.clientFor(r) + if err != nil { + h.log.Error(err, "failed to create client") + http.Error(w, "internal error", http.StatusInternalServerError) + + return + } + + list, err := client.Resource(gvr).Namespace(namespace).List(r.Context(), listOptions()) + if err != nil { + h.log.Error(err, "failed to list resources", "resource", resource, "namespace", namespace) + writeK8sError(w, err) + + return + } + + writeJSON(w, list) + } +} + +// HandleGet returns a handler that gets a single resource. +func (h *Handler) HandleGet(resource string) http.HandlerFunc { + gvr, ok := resourceMap[resource] + if !ok { + return func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, fmt.Sprintf("unknown resource: %s", resource), http.StatusNotFound) + } + } + + return func(w http.ResponseWriter, r *http.Request) { + namespace := r.PathValue("namespace") + name := r.PathValue("name") + + client, err := h.clientFor(r) + if err != nil { + h.log.Error(err, "failed to create client") + http.Error(w, "internal error", http.StatusInternalServerError) + + return + } + + obj, err := client.Resource(gvr).Namespace(namespace).Get(r.Context(), name, getOptions()) + if err != nil { + h.log.Error(err, "failed to get resource", "resource", resource, "namespace", namespace, "name", name) + writeK8sError(w, err) + + return + } + + writeJSON(w, obj) + } +} + +// HandleSSE returns a handler that streams resource watch events as SSE. +func (h *Handler) HandleSSE() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + namespace := r.PathValue("namespace") + + client, err := h.clientFor(r) + if err != nil { + h.log.Error(err, "failed to create client") + http.Error(w, "internal error", http.StatusInternalServerError) + + return + } + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "streaming not supported", http.StatusInternalServerError) + + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + flusher.Flush() + + // Watch all solar resources and multiplex into SSE via a channel + type sseEvent struct { + Type string `json:"type"` + Resource string `json:"resource"` + Namespace string `json:"namespace"` + } + events := make(chan sseEvent, 64) + + for resourceName, gvr := range resourceMap { + go func(ctx context.Context) { + watcher, err := client.Resource(gvr).Namespace(namespace).Watch(ctx, watchOptions()) + if err != nil { + h.log.Error(err, "failed to watch", "resource", resourceName) + + return + } + defer watcher.Stop() + + for event := range watcher.ResultChan() { + select { + case events <- sseEvent{ + Type: string(event.Type), + Resource: resourceName, + Namespace: namespace, + }: + case <-ctx.Done(): + return + } + } + }(r.Context()) + } + + // Single writer goroutine — serializes all writes and respects client disconnect + for { + select { + case evt := <-events: + b, _ := json.Marshal(evt) + fmt.Fprintf(w, "data: %s\n\n", b) + flusher.Flush() + case <-r.Context().Done(): + return + } + } + } +} + +// permissionRule is the JSON representation of a Kubernetes ResourceRule. +type permissionRule struct { + Verbs []string `json:"verbs"` + APIGroups []string `json:"apiGroups"` + Resources []string `json:"resources"` +} + +// permissionsResponse is the response body for HandlePermissions. +type permissionsResponse struct { + Incomplete bool `json:"incomplete"` + Rules []permissionRule `json:"rules"` +} + +// HandlePermissions calls SelfSubjectRulesReview using the caller's own +// credentials and returns the resulting resource rules for the namespace. +// The frontend uses this to show/hide pages based on RBAC permissions. +func (h *Handler) HandlePermissions() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + namespace := r.PathValue("namespace") + + sess := h.sessionStore.Get(r) + cfg := h.authProvider.WrapConfig(h.baseConfig, sess) + + clientset, err := kubernetes.NewForConfig(cfg) + if err != nil { + h.log.Error(err, "failed to create kubernetes clientset") + http.Error(w, "internal error", http.StatusInternalServerError) + + return + } + + review := &authorizationv1.SelfSubjectRulesReview{ + Spec: authorizationv1.SelfSubjectRulesReviewSpec{ + Namespace: namespace, + }, + } + + result, err := clientset.AuthorizationV1().SelfSubjectRulesReviews().Create( + r.Context(), review, metav1.CreateOptions{}, + ) + if err != nil { + h.log.Error(err, "failed to evaluate self-subject rules", "namespace", namespace) + writeK8sError(w, err) + + return + } + + rules := make([]permissionRule, 0, len(result.Status.ResourceRules)) + for _, rr := range result.Status.ResourceRules { + rules = append(rules, permissionRule{ + Verbs: rr.Verbs, + APIGroups: rr.APIGroups, + Resources: rr.Resources, + }) + } + + writeJSON(w, permissionsResponse{ + Incomplete: result.Status.Incomplete, + Rules: rules, + }) + } +} + +// impersonatableLabel is the well-known label that marks a ClusterRole as +// defining an impersonatable user persona. +const impersonatableLabel = "solar.opendefense.cloud/impersonatable" + +// adminLabel is the well-known label that marks a ClusterRoleBinding as +// granting solar-ui admin access. +const adminLabel = "solar.opendefense.cloud/admin" + +// ImpersonationTarget describes a user persona that an admin can preview as. +type ImpersonationTarget struct { + Username string `json:"username"` + Groups []string `json:"groups"` +} + +// listImpersonationTargets reads ClusterRoles labeled +// solar.opendefense.cloud/impersonatable=true using the BFF's own service +// account credentials (not the logged-in user's). Each ClusterRole is expected +// to grant the impersonate verb on both the "users" and "groups" resources; the +// resourceNames in those rules define the username and group membership of the +// persona respectively. +func (h *Handler) listImpersonationTargets(ctx context.Context) ([]ImpersonationTarget, error) { + roleList, err := h.clientset.RbacV1().ClusterRoles().List(ctx, metav1.ListOptions{ + LabelSelector: impersonatableLabel + "=true", + }) + if err != nil { + return nil, fmt.Errorf("failed to list impersonation ClusterRoles: %w", err) + } + + targets := make([]ImpersonationTarget, 0, len(roleList.Items)) + + for _, role := range roleList.Items { + var username string + var groups []string + + for _, rule := range role.Rules { + if !slices.Contains(rule.Verbs, "impersonate") && !slices.Contains(rule.Verbs, "*") { + continue + } + + for _, resource := range rule.Resources { + switch resource { + case "users": + // FIXME: currently only the first resourceName is used if multiple are present. + // we could support multiple users per ClusterRole if needed + if len(rule.ResourceNames) > 0 { + username = rule.ResourceNames[0] + } + case "groups": + groups = append(groups, rule.ResourceNames...) + } + } + } + + if username != "" { + targets = append(targets, ImpersonationTarget{ + Username: username, + Groups: groups, + }) + } + } + + return targets, nil +} + +// HandleListImpersonationTargets returns the available impersonation personas +// for the admin "preview as" feature. Requires admin privileges (enforced by +// the server-level middleware); the lookup itself uses BFF credentials. +func (h *Handler) HandleListImpersonationTargets() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + targets, err := h.listImpersonationTargets(r.Context()) + if err != nil { + h.log.Error(err, "failed to list impersonation targets") + http.Error(w, "internal error", http.StatusInternalServerError) + + return + } + + writeJSON(w, targets) + } +} + +// HandleImpersonate validates the requested username against the ClusterRole- +// defined impersonation personas and activates the override on the session. +func (h *Handler) HandleImpersonate() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req struct { + Username string `json:"username"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Username == "" { + http.Error(w, "invalid request body: username is required", http.StatusBadRequest) + + return + } + + targets, err := h.listImpersonationTargets(r.Context()) + if err != nil { + h.log.Error(err, "failed to list impersonation targets") + http.Error(w, "internal error", http.StatusInternalServerError) + + return + } + + var target *ImpersonationTarget + + for i := range targets { + if targets[i].Username == req.Username { + target = &targets[i] + + break + } + } + + if target == nil { + http.Error(w, "unknown impersonation target", http.StatusBadRequest) + + return + } + + if !h.sessionStore.SetImpersonation(r, target.Username, target.Groups) { + http.Error(w, "no session found", http.StatusUnauthorized) + + return + } + + w.WriteHeader(http.StatusNoContent) + } +} + +// HandleClearImpersonation removes the impersonation override from the session. +func (h *Handler) HandleClearImpersonation() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !h.sessionStore.ClearImpersonation(r) { + http.Error(w, "no session found", http.StatusUnauthorized) + + return + } + + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/pkg/ui/api/helpers.go b/pkg/ui/api/helpers.go new file mode 100644 index 00000000..83ece3aa --- /dev/null +++ b/pkg/ui/api/helpers.go @@ -0,0 +1,48 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "encoding/json" + "log" + "net/http" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func writeJSON(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(v); err != nil { + log.Printf("failed to encode JSON response: %v", err) + } +} + +func writeK8sError(w http.ResponseWriter, err error) { + if statusErr, ok := err.(*apierrors.StatusError); ok { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(int(statusErr.ErrStatus.Code)) + if encErr := json.NewEncoder(w).Encode(statusErr.ErrStatus); encErr != nil { + log.Printf("failed to encode error response: %v", encErr) + } + + return + } + + http.Error(w, err.Error(), http.StatusInternalServerError) +} + +func listOptions() metav1.ListOptions { + return metav1.ListOptions{} +} + +func getOptions() metav1.GetOptions { + return metav1.GetOptions{} +} + +func watchOptions() metav1.ListOptions { + return metav1.ListOptions{ + Watch: true, + } +} diff --git a/pkg/ui/auth/noop.go b/pkg/ui/auth/noop.go new file mode 100644 index 00000000..0c05fe1c --- /dev/null +++ b/pkg/ui/auth/noop.go @@ -0,0 +1,41 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package auth + +import ( + "net/http" + + "k8s.io/client-go/rest" + + "go.opendefense.cloud/solar/pkg/ui/session" +) + +// NoopProvider is an auth provider that does not perform any authentication. +// It is used when no OIDC issuer is configured — requests use the server's +// own service account credentials (useful for development and testing). +type NoopProvider struct{} + +// NewNoopProvider creates a no-op auth provider. +func NewNoopProvider() *NoopProvider { + return &NoopProvider{} +} + +// HandleLogin returns 501 — no login flow in noop mode. +func (p *NoopProvider) HandleLogin(_ *session.Store) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "authentication not configured", http.StatusNotImplemented) + } +} + +// HandleCallback returns 501 — no callback in noop mode. +func (p *NoopProvider) HandleCallback(_ *session.Store) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "authentication not configured", http.StatusNotImplemented) + } +} + +// WrapConfig returns the base config unmodified (uses the server's own identity). +func (p *NoopProvider) WrapConfig(base *rest.Config, _ *session.Data) *rest.Config { + return base +} diff --git a/pkg/ui/auth/oidc.go b/pkg/ui/auth/oidc.go new file mode 100644 index 00000000..984a8706 --- /dev/null +++ b/pkg/ui/auth/oidc.go @@ -0,0 +1,209 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package auth + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" + "k8s.io/client-go/rest" + + "go.opendefense.cloud/solar/pkg/ui/session" +) + +// AuthMode determines how the OIDC user identity is conveyed to the K8s API. +type AuthMode string + +const ( + // AuthModeToken forwards the OIDC id_token as a bearer token. + // Requires the K8s API server to be configured with OIDC flags. + AuthModeToken AuthMode = "token" + + // AuthModeImpersonate uses K8s user impersonation. The backend's own + // credentials (SA or kubeconfig) must have impersonation privileges. + AuthModeImpersonate AuthMode = "impersonate" +) + +// OIDCConfig holds the configuration for the OIDC provider. +type OIDCConfig struct { + Issuer string + ClientID string + ClientSecret string //nolint:gosec // config field, not a hardcoded credential + RedirectURL string + AuthMode AuthMode +} + +// OIDCProvider implements the Provider interface using OpenID Connect. +type OIDCProvider struct { + provider *oidc.Provider + oauth oauth2.Config + verifier *oidc.IDTokenVerifier + authMode AuthMode +} + +// NewOIDCProvider creates a new OIDC provider. +func NewOIDCProvider(cfg OIDCConfig) (*OIDCProvider, error) { + provider, err := oidc.NewProvider(context.Background(), cfg.Issuer) + if err != nil { + return nil, fmt.Errorf("failed to create OIDC provider for issuer %q: %w", cfg.Issuer, err) + } + + oauthCfg := oauth2.Config{ + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + RedirectURL: cfg.RedirectURL, + Endpoint: provider.Endpoint(), + Scopes: []string{oidc.ScopeOpenID, "profile", "email", "groups"}, + } + + verifier := provider.Verifier(&oidc.Config{ClientID: cfg.ClientID}) + + authMode := cfg.AuthMode + if authMode == "" { + authMode = AuthModeToken + } + + return &OIDCProvider{ + provider: provider, + oauth: oauthCfg, + verifier: verifier, + authMode: authMode, + }, nil +} + +// HandleLogin redirects the user to the OIDC provider. +func (p *OIDCProvider) HandleLogin(store *session.Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + state := generateState() + store.SetState(w, state) + http.Redirect(w, r, p.oauth.AuthCodeURL(state), http.StatusFound) + } +} + +// HandleCallback processes the OIDC callback. +func (p *OIDCProvider) HandleCallback(store *session.Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + if code == "" { + http.Error(w, "missing code parameter", http.StatusBadRequest) + + return + } + + expectedState := store.GetState(r) + actualState := r.URL.Query().Get("state") + if expectedState == "" || actualState != expectedState { + http.Error(w, "invalid state parameter", http.StatusBadRequest) + + return + } + store.ClearState(w) + + token, err := p.oauth.Exchange(r.Context(), code) + if err != nil { + http.Error(w, fmt.Sprintf("token exchange failed: %v", err), http.StatusInternalServerError) + + return + } + + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + http.Error(w, "no id_token in response", http.StatusInternalServerError) + + return + } + + idToken, err := p.verifier.Verify(r.Context(), rawIDToken) + if err != nil { + http.Error(w, fmt.Sprintf("id_token verification failed: %v", err), http.StatusInternalServerError) + + return + } + + var claims struct { + Email string `json:"email"` + Name string `json:"name"` + Groups []string `json:"groups"` + } + if err := idToken.Claims(&claims); err != nil { + http.Error(w, fmt.Sprintf("failed to parse claims: %v", err), http.StatusInternalServerError) + + return + } + + username := claims.Email + if username == "" { + username = claims.Name + } + + store.Set(w, &session.Data{ + Username: username, + Groups: claims.Groups, + IDToken: rawIDToken, + AccessToken: token.AccessToken, + }) + + http.Redirect(w, r, "/", http.StatusFound) + } +} + +// WrapConfig returns a rest.Config that authenticates as the session's user. +// In token mode, the OIDC id_token is forwarded as a bearer token. +// In impersonate mode, K8s user impersonation is used. +// When the session has an active impersonation override (admin previewing as +// another user), impersonation headers are always used regardless of authMode. +func (p *OIDCProvider) WrapConfig(base *rest.Config, sess *session.Data) *rest.Config { + cfg := rest.CopyConfig(base) + + // Session-level impersonation (admin "preview as" feature) takes precedence over the global authMode + if sess.ImpersonatingAs != "" { + cfg.Impersonate = rest.ImpersonationConfig{ + UserName: sess.ImpersonatingAs, + Groups: sess.ImpersonatingGroups, + } + + return cfg + } + + switch p.authMode { + case AuthModeImpersonate: + cfg.Impersonate = rest.ImpersonationConfig{ + UserName: sess.Username, + Groups: sess.Groups, + } + default: // token + cfg.BearerToken = sess.IDToken + cfg.BearerTokenFile = "" + // Clear client certificate credentials so the K8s API server + // authenticates via the OIDC bearer token only. Without this, + // kubeconfigs that use client cert auth (e.g. Kind) would have + // the cert take precedence and bypass per-user RBAC enforcement. + cfg.CertData = nil + cfg.CertFile = "" + cfg.KeyData = nil + cfg.KeyFile = "" + } + + return cfg +} + +// MarshalJSON is used only for /auth/me responses. +func (p *OIDCProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]string{"type": "oidc"}) +} + +func generateState() string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + panic(err) + } + + return hex.EncodeToString(b) +} diff --git a/pkg/ui/auth/provider.go b/pkg/ui/auth/provider.go new file mode 100644 index 00000000..356ac5a7 --- /dev/null +++ b/pkg/ui/auth/provider.go @@ -0,0 +1,23 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package auth + +import ( + "net/http" + + "k8s.io/client-go/rest" + + "go.opendefense.cloud/solar/pkg/ui/session" +) + +// Provider abstracts authentication mechanisms for the UI backend. +type Provider interface { + // HandleLogin initiates the authentication flow. + HandleLogin(store *session.Store) http.HandlerFunc + // HandleCallback handles the authentication callback (e.g. OIDC redirect). + HandleCallback(store *session.Store) http.HandlerFunc + // WrapConfig returns a rest.Config that authenticates as the session's user, + // applying any active impersonation override. + WrapConfig(base *rest.Config, sess *session.Data) *rest.Config +} diff --git a/pkg/ui/config.go b/pkg/ui/config.go new file mode 100644 index 00000000..82fed04e --- /dev/null +++ b/pkg/ui/config.go @@ -0,0 +1,21 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package ui + +// Config holds the configuration for the solar-ui server. +type Config struct { + ListenAddr string + OIDCIssuer string + OIDCClientID string + OIDCClientSecret string //nolint:gosec // config field, not a hardcoded credential + OIDCRedirectURL string + SessionKey string //nolint:gosec // config field, not a hardcoded credential + Kubeconfig string + // AuthMode controls how OIDC identity is conveyed to K8s: "token" (default) + // forwards the id_token as a bearer token; "impersonate" uses K8s impersonation. + AuthMode string + // DevViteURL, when set, proxies non-API requests to the Vite dev server + // instead of serving the embedded static files. Example: "http://localhost:5173" + DevViteURL string +} diff --git a/pkg/ui/server.go b/pkg/ui/server.go new file mode 100644 index 00000000..867414c2 --- /dev/null +++ b/pkg/ui/server.go @@ -0,0 +1,187 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package ui + +import ( + "context" + "embed" + "errors" + "fmt" + "io/fs" + "net/http" + "net/http/httputil" + "net/url" + "time" + + "github.com/go-logr/logr" + + "go.opendefense.cloud/solar/pkg/ui/api" + "go.opendefense.cloud/solar/pkg/ui/auth" + "go.opendefense.cloud/solar/pkg/ui/session" +) + +//go:embed all:static +var staticFS embed.FS + +// Server is the solar-ui HTTP server. +type Server struct { + cfg Config + log logr.Logger + server *http.Server +} + +// NewServer creates a new solar-ui server. +func NewServer(cfg Config, log logr.Logger) (*Server, error) { + sessionStore, err := session.NewStore(cfg.SessionKey) + if err != nil { + return nil, fmt.Errorf("failed to create session store: %w", err) + } + + var authProvider auth.Provider + if cfg.OIDCIssuer != "" { + authProvider, err = auth.NewOIDCProvider(auth.OIDCConfig{ + Issuer: cfg.OIDCIssuer, + ClientID: cfg.OIDCClientID, + ClientSecret: cfg.OIDCClientSecret, + RedirectURL: cfg.OIDCRedirectURL, + AuthMode: auth.AuthMode(cfg.AuthMode), + }) + if err != nil { + return nil, fmt.Errorf("failed to create OIDC provider: %w", err) + } + } else { + authProvider = auth.NewNoopProvider() + } + + k8sHandler, err := api.NewHandler(cfg.Kubeconfig, sessionStore, authProvider, log) + if err != nil { + return nil, fmt.Errorf("failed to create API handler: %w", err) + } + + mux := http.NewServeMux() + + // Auth routes — always accessible (no auth required) + mux.HandleFunc("POST /api/auth/login", authProvider.HandleLogin(sessionStore)) + mux.HandleFunc("GET /api/auth/login", authProvider.HandleLogin(sessionStore)) + mux.HandleFunc("GET /api/auth/callback", authProvider.HandleCallback(sessionStore)) + mux.HandleFunc("GET /api/auth/me", k8sHandler.HandleMe()) + mux.HandleFunc("DELETE /api/auth/session", func(w http.ResponseWriter, r *http.Request) { + sessionStore.Clear(w, r) + w.WriteHeader(http.StatusNoContent) + }) + mux.HandleFunc("GET /api/auth/logout", func(w http.ResponseWriter, r *http.Request) { + sessionStore.Clear(w, r) + http.Redirect(w, r, "/", http.StatusFound) + }) + + // K8s resource routes — require authentication + requireAuth := authMiddleware(sessionStore) + mux.Handle("GET /api/namespaces/{namespace}/targets", requireAuth(k8sHandler.HandleList("targets"))) + mux.Handle("GET /api/namespaces/{namespace}/targets/{name}", requireAuth(k8sHandler.HandleGet("targets"))) + mux.Handle("GET /api/namespaces/{namespace}/releases", requireAuth(k8sHandler.HandleList("releases"))) + mux.Handle("GET /api/namespaces/{namespace}/releases/{name}", requireAuth(k8sHandler.HandleGet("releases"))) + mux.Handle("GET /api/namespaces/{namespace}/releasebindings", requireAuth(k8sHandler.HandleList("releasebindings"))) + mux.Handle("GET /api/namespaces/{namespace}/components", requireAuth(k8sHandler.HandleList("components"))) + mux.Handle("GET /api/namespaces/{namespace}/components/{name}", requireAuth(k8sHandler.HandleGet("components"))) + mux.Handle("GET /api/namespaces/{namespace}/componentversions", requireAuth(k8sHandler.HandleList("componentversions"))) + mux.Handle("GET /api/namespaces/{namespace}/registries", requireAuth(k8sHandler.HandleList("registries"))) + mux.Handle("GET /api/namespaces/{namespace}/profiles", requireAuth(k8sHandler.HandleList("profiles"))) + mux.Handle("GET /api/namespaces/{namespace}/rendertasks", requireAuth(k8sHandler.HandleList("rendertasks"))) + + // SSE events + mux.Handle("GET /api/namespaces/{namespace}/events", requireAuth(k8sHandler.HandleSSE())) + + // Permissions — uses SelfSubjectRulesReview with caller's own credentials + mux.Handle("GET /api/namespaces/{namespace}/permissions", requireAuth(k8sHandler.HandlePermissions())) + + // Impersonation + // Admin check is based on presence of ClusterRoleBinding with solar.opendefense.cloud/admin=true label + // Available personas are discovered from ClusterRoles labeled + // solar.opendefense.cloud/impersonatable=true (see Helm chart values.ui.impersonationTargets). + mux.Handle("GET /api/auth/impersonation-targets", requireAuth(k8sHandler.RequireAdmin(k8sHandler.HandleListImpersonationTargets()))) + mux.Handle("PUT /api/auth/impersonate", requireAuth(k8sHandler.RequireAdmin(k8sHandler.HandleImpersonate()))) + mux.Handle("DELETE /api/auth/impersonate", requireAuth(k8sHandler.RequireAdmin(k8sHandler.HandleClearImpersonation()))) + + // SPA — either proxy to Vite dev server or serve embedded static files + if cfg.DevViteURL != "" { + viteURL, err := url.Parse(cfg.DevViteURL) + if err != nil { + return nil, fmt.Errorf("invalid dev-vite-url: %w", err) + } + + proxy := httputil.NewSingleHostReverseProxy(viteURL) + log.Info("proxying non-API requests to Vite dev server", "url", cfg.DevViteURL) + mux.Handle("/", proxy) + } else { + staticContent, err := fs.Sub(staticFS, "static") + if err != nil { + return nil, fmt.Errorf("failed to create sub filesystem: %w", err) + } + + mux.Handle("/", spaFileServer(http.FS(staticContent))) + } + + srv := &http.Server{ + Addr: cfg.ListenAddr, + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, + } + + return &Server{cfg: cfg, log: log, server: srv}, nil +} + +// Run starts the server and blocks until the context is cancelled. +func (s *Server) Run(ctx context.Context) error { + errCh := make(chan error, 1) + + go func() { + s.log.Info("starting solar-ui", "addr", s.cfg.ListenAddr) + if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + errCh <- err + } + }() + + select { + case err := <-errCh: + return err + case <-ctx.Done(): + s.log.Info("shutting down solar-ui") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + return s.server.Shutdown(shutdownCtx) //nolint:contextcheck // ctx is cancelled; need a fresh context with deadline for graceful shutdown + } +} + +// authMiddleware returns 401 for unauthenticated requests. +func authMiddleware(store *session.Store) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if sess := store.Get(r); sess == nil { + http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) + + return + } + + next.ServeHTTP(w, r) + }) + } +} + +// spaFileServer serves static files and falls back to index.html for unknown paths. +func spaFileServer(fsys http.FileSystem) http.Handler { + fileServer := http.FileServer(fsys) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Try to serve the file directly + f, err := fsys.Open(r.URL.Path) + if err != nil { + // File not found — serve index.html for SPA routing + r.URL.Path = "/" + } else { + f.Close() + } + fileServer.ServeHTTP(w, r) + }) +} diff --git a/pkg/ui/session/store.go b/pkg/ui/session/store.go new file mode 100644 index 00000000..10f266ed --- /dev/null +++ b/pkg/ui/session/store.go @@ -0,0 +1,213 @@ +// Copyright 2026 BWI GmbH and Solution Arsenal contributors +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "net/http" + "sync" + "time" +) + +const ( + cookieName = "solar-session" + stateCookieName = "solar-oidc-state" //nolint:gosec // not a credential +) + +// Data holds session data. +type Data struct { + Username string `json:"username"` + Groups []string `json:"groups"` + IDToken string `json:"id_token,omitempty"` //nolint:gosec // not a hardcoded credential + AccessToken string `json:"access_token,omitempty"` //nolint:gosec // not a hardcoded credential + + // ImpersonatingAs is set when an admin is previewing as another user. + // The BE will forward K8s requests with Impersonate-User headers. + ImpersonatingAs string `json:"impersonating_as,omitempty"` + ImpersonatingGroups []string `json:"impersonating_groups,omitempty"` +} + +// Store manages encrypted cookie-based sessions. +// For the MVP / spike, this uses a simple in-memory map keyed by session ID. +type Store struct { + mu sync.RWMutex + sessions map[string]*Data + key []byte // unused for now, reserved for cookie encryption +} + +// NewStore creates a new session store. +func NewStore(hexKey string) (*Store, error) { + var key []byte + if hexKey != "" { + var err error + key, err = hex.DecodeString(hexKey) + if err != nil { + return nil, fmt.Errorf("invalid session key: %w", err) + } + if len(key) != 32 { + return nil, fmt.Errorf("session key must be 32 bytes (64 hex chars), got %d", len(key)) + } + } else { + key = make([]byte, 32) + if _, err := rand.Read(key); err != nil { + return nil, fmt.Errorf("failed to generate session key: %w", err) + } + } + + return &Store{ + sessions: make(map[string]*Data), + key: key, + }, nil +} + +// Get retrieves a copy of the session data from the request. +// A copy is returned so that callers can read fields +// without holding the store lock. This prevents data races with +// SetImpersonation / ClearImpersonation, which mutate the stored object +// under the write lock while concurrent requests may be reading it. +func (s *Store) Get(r *http.Request) *Data { + cookie, err := r.Cookie(cookieName) + if err != nil { + return nil + } + + s.mu.RLock() + defer s.mu.RUnlock() + + sess, ok := s.sessions[cookie.Value] + if !ok { + return nil + } + + // Return a shallow copy of the struct with independently-allocated slices + // so the caller owns the data and no lock is needed after this point. + cp := *sess + cp.Groups = append([]string(nil), sess.Groups...) + cp.ImpersonatingGroups = append([]string(nil), sess.ImpersonatingGroups...) + + return &cp +} + +// Set stores session data and sets the cookie. +func (s *Store) Set(w http.ResponseWriter, data *Data) { + id := generateSessionID() + + s.mu.Lock() + s.sessions[id] = data + s.mu.Unlock() + + http.SetCookie(w, &http.Cookie{ + Name: cookieName, + Value: id, + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, + MaxAge: int(24 * time.Hour / time.Second), + }) +} + +// Clear deletes the session from the server-side store and removes the cookie. +func (s *Store) Clear(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie(cookieName); err == nil { + s.mu.Lock() + delete(s.sessions, cookie.Value) + s.mu.Unlock() + } + + http.SetCookie(w, &http.Cookie{ + Name: cookieName, + Value: "", + Path: "/", + HttpOnly: true, + Secure: true, + MaxAge: -1, + }) +} + +// SetImpersonation updates ImpersonatingAs/Groups in the existing session in-place. +func (s *Store) SetImpersonation(r *http.Request, username string, groups []string) bool { + cookie, err := r.Cookie(cookieName) + if err != nil { + return false + } + + s.mu.Lock() + defer s.mu.Unlock() + + if sess, ok := s.sessions[cookie.Value]; ok { + sess.ImpersonatingAs = username + sess.ImpersonatingGroups = groups + + return true + } + + return false +} + +// ClearImpersonation removes the impersonation override from the existing session. +func (s *Store) ClearImpersonation(r *http.Request) bool { + cookie, err := r.Cookie(cookieName) + if err != nil { + return false + } + + s.mu.Lock() + defer s.mu.Unlock() + + if sess, ok := s.sessions[cookie.Value]; ok { + sess.ImpersonatingAs = "" + sess.ImpersonatingGroups = nil + + return true + } + + return false +} + +// SetState stores the OIDC state parameter in a short-lived cookie. +func (s *Store) SetState(w http.ResponseWriter, state string) { + http.SetCookie(w, &http.Cookie{ + Name: stateCookieName, + Value: state, + Path: "/api/auth/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, + MaxAge: 300, // 5 minutes + }) +} + +// GetState retrieves the OIDC state parameter from the cookie. +func (s *Store) GetState(r *http.Request) string { + cookie, err := r.Cookie(stateCookieName) + if err != nil { + return "" + } + + return cookie.Value +} + +// ClearState removes the OIDC state cookie. +func (s *Store) ClearState(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{ + Name: stateCookieName, + Value: "", + Path: "/api/auth/", + HttpOnly: true, + Secure: true, + MaxAge: -1, + }) +} + +func generateSessionID() string { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + panic(err) + } + + return hex.EncodeToString(b) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 37fb8221..052a4459 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -14,8 +14,6 @@ import ( "strings" "time" - "oras.land/oras-go/v2/registry" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -64,18 +62,30 @@ var _ = Describe("solar", Ordered, func() { "--values", filepath.Join(dir, "test", "fixtures", "solar.values.yaml"), "--set", "apiserver.image.tag=e2e", "--set", "controller.image.tag=e2e", - "--set", "renderer.image.tag=e2e", - "--set", "discovery.image.tag=e2e") + "--set", "renderer.image.tag=e2e") _, err = run(cmd) Expect(err).NotTo(HaveOccurred()) testns = setupTestNS() - // update discovery webhook pointer service to point to the actual discovery webhook address which has been - // determined once the name of the test namespace has been defined + By("deploying registry credentials to test namespace for per-task push auth") + applyResource(testns, filepath.Join(dir, "test", "fixtures", "e2e", "zot-deploy-auth.yaml")) + + By("deploying discovery credentials secret") + applyResource(testns, filepath.Join(dir, "test", "fixtures", "e2e", "zot-discovery-auth.yaml")) + + By("deploying solar-discovery (webhook mode)") + cmd = exec.Command(helmBinary, "upgrade", "--install", + "--namespace", testns, "solar-discovery", filepath.Join(dir, "charts", "solar-discovery"), + "--values", filepath.Join(dir, "test", "fixtures", "solar-discovery-webhook.values.yaml"), + "--set", "namespace="+testns) + _, err = run(cmd) + Expect(err).NotTo(HaveOccurred()) + + // update discovery webhook pointer service to point to the Helm-deployed discovery service svc := patchYAMLFile( filepath.Join(dir, "test", "fixtures", "discovery-webhook-ptr-svc.yaml"), - fmt.Sprintf(`[{"op": "replace", "path": "/spec/externalName", "value":"discovery-zot-webhook.%s.svc.cluster.local"}]`, testns), + fmt.Sprintf(`[{"op": "replace", "path": "/spec/externalName", "value":"solar-discovery.%s.svc.cluster.local"}]`, testns), ) defer func() { _ = os.Remove(svc) }() applyResource("zot", svc) @@ -84,8 +94,14 @@ var _ = Describe("solar", Ordered, func() { // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs, // and deleting the namespaces. AfterAll(func() { + By("undeploying solar-discovery") + cmd := exec.Command(helmBinary, "uninstall", "-n", testns, "solar-discovery") + _, _ = run(cmd) + cmd = exec.Command(helmBinary, "uninstall", "-n", testns, "solar-discovery-scan") + _, _ = run(cmd) + By("undeploying the apiserver and controller-manager") - cmd := exec.Command(helmBinary, "uninstall", "-n", controllerNamespace, "solar") + cmd = exec.Command(helmBinary, "uninstall", "-n", controllerNamespace, "solar") _, _ = run(cmd) By("removing manager namespace") @@ -172,18 +188,17 @@ var _ = Describe("solar", Ordered, func() { Expect(err).NotTo(HaveOccurred()) }) - It("should create a component version", func() { - applyResource(testns, filepath.Join(dir, "test", "fixtures", "e2e", "discovery-webhook.yaml")) + It("should discover components via webhook", func() { + By("waiting for discovery deployment to be ready") + Eventually(func() error { + cmd := exec.Command(kubectlBinary, "wait", "deployment/solar-discovery", + "-n", testns, "--for=condition=Available", "--timeout=0") + _, err := run(cmd) - // wait for discovery webhook to be ready to handle requests - Eventually(func(g Gomega) { - cmd := exec.Command(kubectlBinary, "get", "endpointslice", "-l", "kubernetes.io/service-name=discovery-zot-webhook", "-n", testns, "-o", "jsonpath='{.items[0].endpoints[0].conditions.ready}'") - output, err := run(cmd) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("true")) + return err }).Should(Succeed()) - // set up port forwarding for Zot registry to upload OCM package + By("pushing OCM package to zot-discovery") localport := getFreePort() stop := portForward("service/zot-discovery", localport, 443, "-n", "zot") defer stop() @@ -203,12 +218,13 @@ var _ = Describe("solar", Ordered, func() { } verifyCompVers := func(g Gomega) { - cmd := exec.Command(kubectlBinary, "get", "cv", "-n", testns, "opendefense-cloud-ocm-demo-v26-4-0", "-o", "jsonpath='{.spec.componentRef.name}'") + cmd := exec.Command(kubectlBinary, "get", "cv", "-n", testns, "opendefense-cloud-ocm-demo-v26-4-1", "-o", "jsonpath='{.spec.componentRef.name}'") output, err := run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(ContainSubstring("opendefense-cloud-ocm-demo")) } + By("verifying Component was created via webhook discovery") Eventually(func(g Gomega) { verifyComp(g) }).Should(Succeed()) @@ -217,9 +233,6 @@ var _ = Describe("solar", Ordered, func() { }).Should(Succeed()) // --- Delete test: remove OCI tag while webhook discovery is active --- - // The webhook-based discovery receives Zot events on tag deletion, - // so this must run before the webhook discovery is torn down. - By("starting port-forward to zot-discovery for tag deletion") deletePort := getFreePort() stopDelete := portForward("service/zot-discovery", deletePort, 443, "-n", "zot") @@ -231,13 +244,13 @@ var _ = Describe("solar", Ordered, func() { deleteCtx := context.Background() deleteRepo, repoErr := zotDiscovery.Repository(deleteCtx, ociRepoPath) Expect(repoErr).NotTo(HaveOccurred()) - desc, resolveErr := deleteRepo.Resolve(deleteCtx, "v26.4.0") + desc, resolveErr := deleteRepo.Resolve(deleteCtx, "v26.4.1") Expect(resolveErr).NotTo(HaveOccurred()) Expect(deleteRepo.Delete(deleteCtx, desc)).To(Succeed()) By("verifying the ComponentVersion was deleted") Eventually(func(g Gomega) { - cmd := exec.Command(kubectlBinary, "wait", "--for=delete", "cv/opendefense-cloud-ocm-demo-v26-4-0", "-n", testns, "--timeout=0") + cmd := exec.Command(kubectlBinary, "wait", "--for=delete", "cv/opendefense-cloud-ocm-demo-v26-4-1", "-n", testns, "--timeout=0") output, err := run(cmd) g.Expect(err).NotTo(HaveOccurred(), "ComponentVersion should be NotFound, got: %s", output) }).Should(Succeed()) @@ -249,27 +262,36 @@ var _ = Describe("solar", Ordered, func() { g.Expect(err).NotTo(HaveOccurred(), "Component should be NotFound when last CV is removed, got: %s", output) }).Should(Succeed()) - // Clean up webhook discovery - cmd = exec.Command(kubectlBinary, "delete", "discovery", "zot-webhook", "-n", testns) - output, err := run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to delete discovery resource, got: %s", output) + // --- Scan mode test: uninstall webhook, deploy scan, re-push, verify --- + By("uninstalling webhook discovery") + cmd = exec.Command(helmBinary, "uninstall", "-n", testns, "solar-discovery") + _, err = run(cmd) + Expect(err).NotTo(HaveOccurred()) - By("confirming the webhook discovery resource was deleted") - Eventually(func(g Gomega) { - cmd := exec.Command(kubectlBinary, "wait", "--for=delete", "discovery/zot-webhook", "-n", testns, "--timeout=0") - output, err := run(cmd) - g.Expect(err).NotTo(HaveOccurred(), "Discovery resource should be NotFound, got: %s", output) + By("deploying solar-discovery (scan mode)") + cmd = exec.Command(helmBinary, "upgrade", "--install", + "--namespace", testns, "solar-discovery-scan", filepath.Join(dir, "charts", "solar-discovery"), + "--values", filepath.Join(dir, "test", "fixtures", "solar-discovery-scan.values.yaml"), + "--set", "namespace="+testns) + _, err = run(cmd) + Expect(err).NotTo(HaveOccurred()) + + By("waiting for scan discovery deployment to be ready") + Eventually(func() error { + cmd := exec.Command(kubectlBinary, "wait", "deployment/solar-discovery-scan", + "-n", testns, "--for=condition=Available", "--timeout=0") + _, err := run(cmd) + + return err }).Should(Succeed()) - // re-push OCM package, re-create via scan for subsequent tests - By("re-pushing the OCM package after tag deletion") + By("re-pushing the OCM package for scan discovery") cmd = exec.Command(ocmBinary, "--config", ocmconfig, "transfer", "ctf", ocmDemoCtf, fmt.Sprintf("localhost:%d/test", localport)) cmd.Env = append(cmd.Env, "SSL_CERT_FILE="+caCrt) _, err = run(cmd) Expect(err).NotTo(HaveOccurred()) - applyResource(testns, filepath.Join(dir, "test", "fixtures", "e2e", "discovery-scan.yaml")) - + By("verifying Component was created via scan discovery") Eventually(func(g Gomega) { verifyComp(g) }).Should(Succeed()) @@ -282,63 +304,42 @@ var _ = Describe("solar", Ordered, func() { By("creating a Release for the ComponentVersion") applyResource(testns, filepath.Join(dir, "test", "fixtures", "e2e", "release.yaml")) - By("waiting for the rendered chart URL to be set") + By("waiting for ComponentVersionResolved condition to be set") Eventually(func(g Gomega) { cmd := exec.Command(kubectlBinary, "get", "release", "-n", testns, - "test-opendefense-cloud-ocm-demo-v26-4-0-release", - "-o", `jsonpath={.status.chartURL}`) + "test-opendefense-cloud-ocm-demo-v26-4-1-release", + "-o", `jsonpath={.status.conditions[?(@.type=="ComponentVersionResolved")].status}`) output, err := run(cmd) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).NotTo(BeEmpty(), "chartURL should be set after rendering") + g.Expect(output).To(Equal("True")) }).Should(Succeed()) - - By("verifying the rendered Helm chart exists in the OCI registry") - localport := getFreePort() - stop := portForward("service/zot-deploy", localport, 443, "-n", "zot") - defer stop() - - zotDeploy := newZotClient(localport) - - ctx := context.Background() - var repo registry.Repository - Eventually(func() error { - var err error - repo, err = zotDeploy.Repository(ctx, - fmt.Sprintf("%s/release-test-opendefense-cloud-ocm-demo-v26-4-0-release", testns)) - return err - }).Should(Succeed()) - - _, _, err := repo.FetchReference(ctx, "v0.0.0") - Expect(err).NotTo(HaveOccurred()) }) It("should render a target when a target gets registered", func() { - By("creating a target") + By("creating registry and target") + applyResource(testns, filepath.Join(dir, "test", "fixtures", "e2e", "registry.yaml")) applyResource(testns, filepath.Join(dir, "test", "fixtures", "e2e", "target.yaml")) // Verify Target creation Eventually(func(g Gomega) { - cmd := exec.Command(kubectlBinary, "get", "targets", "-n", testns, "cluster-1", "-o", "jsonpath=\"{.spec.releases}\"") + cmd := exec.Command(kubectlBinary, "get", "targets", "-n", testns, "cluster-1", "-o", "jsonpath={.spec.renderRegistryRef.name}") output, err := run(cmd) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("test-release")) + g.Expect(output).To(Equal("deploy-registry")) }).Should(Succeed()) - By("verifying Bootstrap gets created") - Eventually(func(g Gomega) { - cmd := exec.Command(kubectlBinary, "get", "bootstraps", "-n", testns, "cluster-1") - _, err := run(cmd) - g.Expect(err).NotTo(HaveOccurred()) - }).Should(Succeed()) + By("creating a ReleaseBinding to bind the release to the target") + applyResource(testns, filepath.Join(dir, "test", "fixtures", "e2e", "releasebinding.yaml")) - By("verifying RenderTask gets created") + By("verifying release RenderTask gets created") Eventually(func(g Gomega) { - cmd := exec.Command(kubectlBinary, "get", "rendertasks", testns+"-test-opendefense-cloud-ocm-demo-v26-4-0-release-0") - _, err := run(cmd) + cmd := exec.Command(kubectlBinary, "get", "rendertasks", "-n", testns) + output, err := run(cmd) g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("render-rel-")) }).Should(Succeed()) - By("verifying the rendered Helm chart exists in the OCI registry") + By("verifying the rendered bootstrap Helm chart exists in the OCI registry") localport := getFreePort() stop := portForward("service/zot-deploy", localport, 443, "-n", "zot") defer stop() @@ -346,27 +347,40 @@ var _ = Describe("solar", Ordered, func() { zotDeploy := newZotClient(localport) ctx := context.Background() - var repo registry.Repository Eventually(func() error { - var err error - repo, err = zotDeploy.Repository(ctx, fmt.Sprintf("%s/bootstrap-cluster-1", testns)) + repo, err := zotDeploy.Repository(ctx, fmt.Sprintf("%s/bootstrap-cluster-1", testns)) + if err != nil { + return err + } + _, _, err = repo.FetchReference(ctx, "v0.0.0") return err }).Should(Succeed()) - - _, _, err = repo.FetchReference(ctx, "v0.0.0") - Expect(err).NotTo(HaveOccurred()) }) - It("should add matching profiles to a bootstrap", func() { + It("should create ReleaseBindings when a matching profile exists", func() { + By("creating a second release for the profile to reference") + applyResource(testns, filepath.Join(dir, "test", "fixtures", "e2e", "profile-release.yaml")) + + By("creating the profile that matches the target") applyResource(testns, filepath.Join(dir, "test", "fixtures", "e2e", "profile.yaml")) - // Verify that the profile has been added to the bootstrap + By("verifying the profile controller created a ReleaseBinding for the matching target") + Eventually(func(g Gomega) { + cmd := exec.Command(kubectlBinary, "get", "releasebindings", "-n", testns, + "-o", "jsonpath={.items[*].spec.targetRef.name}") + output, err := run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("cluster-1")) + }).Should(Succeed()) + + By("verifying the ReleaseBinding references the profile's release") Eventually(func(g Gomega) { - cmd := exec.Command(kubectlBinary, "get", "-n", testns, "bootstrap", "cluster-1", "-o", "jsonpath='{.spec.profiles.*}'") + cmd := exec.Command(kubectlBinary, "get", "releasebindings", "-n", testns, + "-o", "jsonpath={.items[*].spec.releaseRef.name}") output, err := run(cmd) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("production")) + g.Expect(output).To(ContainSubstring("profile-ocm-demo-release")) }).Should(Succeed()) }) @@ -388,6 +402,25 @@ var _ = Describe("solar", Ordered, func() { "ocirepositories.source.toolkit.fluxcd.io/solar-bootstrap", "Ready") }).Should(BeTrue()) + + By("waiting for the OCI repository to pick up the latest bootstrap chart version") + // The profile test created a second ReleaseBinding which caused + // bootstrapVersion to increment and a v0.0.1 chart to be pushed. + // Force FluxCD to reconcile so it picks up v0.0.1 instead of v0.0.0. + Eventually(func(g Gomega) { + cmd := exec.Command(kubectlBinary, "annotate", "ocirepository", "solar-bootstrap", + "-n", testns, "reconcile.fluxcd.io/requestedAt="+time.Now().Format(time.RFC3339Nano), + "--overwrite") + _, err := run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + + cmd = exec.Command(kubectlBinary, "get", "ocirepository", "solar-bootstrap", + "-n", testns, "-o", "jsonpath={.status.artifact.revision}") + out, err := run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(ContainSubstring("v0.0.1"), "OCI repository has not picked up v0.0.1 yet: %s", out) + }).Should(Succeed()) + Eventually(func() bool { return getStatusCondition( testns, @@ -395,10 +428,10 @@ var _ = Describe("solar", Ordered, func() { "Ready") }).Should(BeTrue()) - By("verifying inner release was rolled out") - // The inner HelmRelease has a hash-suffixed name, so look it up - // via the FluxCD owner label set on resources templated by the - // bootstrap HelmRelease. + By("verifying inner releases were rolled out") + // The bootstrap chart creates one inner HelmRelease per bound release. + // We expect two: one from the directly assigned ReleaseBinding and one + // from the Profile-created ReleaseBinding. innerSelector := "helm.toolkit.fluxcd.io/name=solar-bootstrap" Eventually(func(g Gomega) { cmd := exec.Command(kubectlBinary, "get", "-n", testns, @@ -407,10 +440,11 @@ var _ = Describe("solar", Ordered, func() { "-o", "jsonpath={.items[*].metadata.name}") out, err := run(cmd) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(strings.TrimSpace(out)).NotTo(BeEmpty(), "no inner HelmRelease found via label %s", innerSelector) + names := strings.Fields(out) + g.Expect(names).To(HaveLen(2), "expected 2 inner HelmReleases (direct + profile), got: %v", names) }).Should(Succeed()) - By("verifying inner release reaches ready") + By("verifying inner releases reach ready") Eventually(func(g Gomega) { cmd := exec.Command(kubectlBinary, "get", "-n", testns, "helmreleases.helm.toolkit.fluxcd.io", @@ -418,14 +452,17 @@ var _ = Describe("solar", Ordered, func() { "-o", "jsonpath={.items[*].status.conditions[?(@.type=='Ready')].status}") out, err := run(cmd) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(strings.TrimSpace(out)).To(Equal("True")) + statuses := strings.Fields(out) + g.Expect(statuses).To(HaveLen(2), "expected 2 ready statuses, got: %v", statuses) + for _, status := range statuses { + g.Expect(status).To(Equal("True")) + } }).Should(Succeed()) - By("verifying workload deployment becomes available") - // Deployments managed by Helm via FluxCD carry the - // helm.toolkit.fluxcd.io/namespace label matching the release - // namespace, which uniquely identifies workloads owned by this - // bootstrap chain. + By("verifying workload deployments from both releases become available") + // Each inner HelmRelease deploys its own workload. We expect two + // deployments: one from the directly assigned release and one from + // the profile-assigned release. deploySelector := "helm.toolkit.fluxcd.io/namespace=" + testns Eventually(func(g Gomega) { cmd := exec.Command(kubectlBinary, "get", "deployments", "-n", testns, @@ -433,14 +470,16 @@ var _ = Describe("solar", Ordered, func() { "-o", "jsonpath={.items[*].metadata.name}") out, err := run(cmd) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(strings.TrimSpace(out)).NotTo(BeEmpty(), "no workload deployment found via label %s", deploySelector) + deployments := strings.Fields(out) + g.Expect(deployments).To(HaveLen(2), + "expected 2 workload deployments (direct + profile release), got: %v", deployments) }).Should(Succeed()) cmd := exec.Command(kubectlBinary, "wait", "-n", testns, "deployments", "-l", deploySelector, "--for=condition=Available", "--timeout=5m") _, err := run(cmd) - Expect(err).NotTo(HaveOccurred(), "workload deployment did not become Available") + Expect(err).NotTo(HaveOccurred(), "workload deployments did not become Available") }) }) }) diff --git a/test/fixtures/discovery-webhook-ptr-svc.yaml b/test/fixtures/discovery-webhook-ptr-svc.yaml index 29856f80..b5e5eb91 100644 --- a/test/fixtures/discovery-webhook-ptr-svc.yaml +++ b/test/fixtures/discovery-webhook-ptr-svc.yaml @@ -5,4 +5,4 @@ metadata: namespace: zot spec: type: ExternalName - externalName: discovery-zot-webhook.test.svc.cluster.local + externalName: solar-discovery.solar-system.svc.cluster.local diff --git a/test/fixtures/e2e/bootstrap-ocirepository.yaml b/test/fixtures/e2e/bootstrap-ocirepository.yaml index 75f5f08f..56c69428 100644 --- a/test/fixtures/e2e/bootstrap-ocirepository.yaml +++ b/test/fixtures/e2e/bootstrap-ocirepository.yaml @@ -10,6 +10,6 @@ spec: mediaType: "application/vnd.cncf.helm.chart.content.v1.tar+gzip" operation: copy ref: - semver: "^0.0.0" + semver: ">=0.0.0" secretRef: name: regcred diff --git a/test/fixtures/e2e/componentversion.yaml b/test/fixtures/e2e/componentversion.yaml index 62ea2010..5d056f61 100644 --- a/test/fixtures/e2e/componentversion.yaml +++ b/test/fixtures/e2e/componentversion.yaml @@ -10,13 +10,13 @@ spec: apiVersion: solar.opendefense.cloud/v1alpha1 kind: ComponentVersion metadata: - name: test-opendefense-cloud-ocm-demo-v26-4-0 + name: test-opendefense-cloud-ocm-demo-v26-4-1 labels: solar.bwi.io/component: test-opendefense-cloud-ocm-demo spec: componentRef: name: test-opendefense-cloud-ocm-demo - tag: "v26.4.0" + tag: "v26.4.1" resources: {} entrypoint: resourceName: "" diff --git a/test/fixtures/e2e/dex/dev-solar-rbac.yaml b/test/fixtures/e2e/dex/dev-solar-rbac.yaml new file mode 100644 index 00000000..4c685415 --- /dev/null +++ b/test/fixtures/e2e/dex/dev-solar-rbac.yaml @@ -0,0 +1,141 @@ +# Dev-only RBAC fixtures for solar-ui impersonation testing. +# +# Two personas are defined: +# maintainer@solar.local — solution maintainer: read-only access to components/versions +# coordinator@solar.local — deployment coordinator: read-only access to targets, releases, profiles +# +# These users do NOT need Dex entries; they exist only as K8s impersonation targets. +# The admin user (admin@solar.local) is granted solar-ui admin access via the +# solar-ui:admin ClusterRoleBinding below (labeled solar.opendefense.cloud/admin=true). +# The solar-ui BFF lists ClusterRoleBindings with this label using its own kubeconfig +# credentials to derive isAdmin — it does NOT use a SelfSubjectAccessReview. +# +# The solar-ui process runs with the Kind admin kubeconfig so it can always list +# these ClusterRoles and perform the impersonation on behalf of the admin user. + +--- +# solar-ui:admin — grants admin@solar.local access to impersonation management endpoints. +# The solar-ui BFF discovers this via the solar.opendefense.cloud/admin=true label. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: solar-ui:admin + labels: + solar.opendefense.cloud/admin: "true" +rules: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: solar-ui:admin + labels: + solar.opendefense.cloud/admin: "true" +subjects: + - kind: User + name: admin@solar.local + apiGroup: rbac.authorization.k8s.io +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: solar-ui:admin + +--- +# ClusterRole: solution maintainer +# Read access to catalog resources only. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: solar:maintainer +rules: + - apiGroups: ["solar.opendefense.cloud"] + resources: + - components + - componentversions + - registries + verbs: ["get", "list", "watch"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: solar:maintainer +subjects: + - kind: User + name: maintainer@solar.local + apiGroup: rbac.authorization.k8s.io +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: solar:maintainer + +--- +# ClusterRole: deployment coordinator +# Read access to deployment resources only. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: solar:coordinator +rules: + - apiGroups: ["solar.opendefense.cloud"] + resources: + - targets + - releases + - releasebindings + - profiles + verbs: ["get", "list", "watch"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: solar:coordinator +subjects: + - kind: User + name: coordinator@solar.local + apiGroup: rbac.authorization.k8s.io +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: solar:coordinator + +--- +# Impersonation ClusterRole for maintainer@solar.local. +# Labeled solar.opendefense.cloud/impersonatable=true so the solar-ui BFF +# discovers it via label selector and surfaces it in the "Preview as" dropdown. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: solar-ui:impersonate:maintainer-solar-local + labels: + solar.opendefense.cloud/impersonatable: "true" +rules: + - apiGroups: [""] + resources: ["users"] + verbs: ["impersonate"] + resourceNames: + - maintainer@solar.local + - apiGroups: [""] + resources: ["groups"] + verbs: ["impersonate"] + resourceNames: + - maintainer + +--- +# Impersonation ClusterRole for coordinator@solar.local. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: solar-ui:impersonate:coordinator-solar-local + labels: + solar.opendefense.cloud/impersonatable: "true" +rules: + - apiGroups: [""] + resources: ["users"] + verbs: ["impersonate"] + resourceNames: + - coordinator@solar.local + - apiGroups: [""] + resources: ["groups"] + verbs: ["impersonate"] + resourceNames: + - coordinator diff --git a/test/fixtures/e2e/dex/dex-config.yaml b/test/fixtures/e2e/dex/dex-config.yaml new file mode 100644 index 00000000..c8c302f7 --- /dev/null +++ b/test/fixtures/e2e/dex/dex-config.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: dex-config + namespace: dex +data: + config.yaml: | + issuer: https://localhost:5556 + storage: + type: memory + web: + https: 0.0.0.0:5556 + tlsCert: /etc/dex/tls/tls.crt + tlsKey: /etc/dex/tls/tls.key + oauth2: + skipApprovalScreen: true + staticClients: + - id: solar-ui + name: SolAr UI + secret: solar-ui-secret + redirectURIs: + - http://localhost:8090/api/auth/callback + enablePasswordDB: true + staticPasswords: + - email: admin@solar.local + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" # password + username: admin + userID: "1" + connectors: [] diff --git a/test/fixtures/e2e/dex/dex-deployment.yaml b/test/fixtures/e2e/dex/dex-deployment.yaml new file mode 100644 index 00000000..8f0c7b4d --- /dev/null +++ b/test/fixtures/e2e/dex/dex-deployment.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dex + namespace: dex + labels: + app: dex +spec: + replicas: 1 + selector: + matchLabels: + app: dex + template: + metadata: + labels: + app: dex + spec: + containers: + - name: dex + image: ghcr.io/dexidp/dex:v2.43.1 + command: + - dex + - serve + - /etc/dex/config.yaml + ports: + - containerPort: 5556 + name: https + # hostPort makes Dex reachable at localhost:5556 from inside the + # Kind node, so the K8s API server can validate OIDC tokens. + hostPort: 5556 + volumeMounts: + - name: config + mountPath: /etc/dex + readOnly: true + - name: tls + mountPath: /etc/dex/tls + readOnly: true + volumes: + - name: config + configMap: + name: dex-config + - name: tls + secret: + secretName: dex-tls +--- +apiVersion: v1 +kind: Service +metadata: + name: dex + namespace: dex +spec: + selector: + app: dex + ports: + - port: 5556 + targetPort: 5556 + name: https diff --git a/test/fixtures/e2e/dex/dex-rbac.yaml b/test/fixtures/e2e/dex/dex-rbac.yaml new file mode 100644 index 00000000..456e93f7 --- /dev/null +++ b/test/fixtures/e2e/dex/dex-rbac.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: solar-ui-oidc-admin +subjects: + - kind: User + name: admin@solar.local + apiGroup: rbac.authorization.k8s.io +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin diff --git a/test/fixtures/e2e/discovery-scan.yaml b/test/fixtures/e2e/discovery-scan.yaml deleted file mode 100644 index 11b208a0..00000000 --- a/test/fixtures/e2e/discovery-scan.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: zot-discovery-auth -type: Opaque -stringData: - username: admin - password: admin ---- -apiVersion: solar.opendefense.cloud/v1alpha1 -kind: Discovery -metadata: - name: zot-scan -spec: - registry: - endpoint: zot-discovery.zot.svc.cluster.local:443 - secretRef: - name: zot-discovery-auth - caConfigMapRef: - name: root-bundle - discoveryInterval: 24h diff --git a/test/fixtures/e2e/discovery-webhook.yaml b/test/fixtures/e2e/discovery-webhook.yaml deleted file mode 100644 index ee2e37ce..00000000 --- a/test/fixtures/e2e/discovery-webhook.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: zot-discovery-auth -type: Opaque -stringData: - username: admin - password: admin ---- -apiVersion: solar.opendefense.cloud/v1alpha1 -kind: Discovery -metadata: - name: zot-webhook -spec: - registry: - endpoint: zot-discovery.zot.svc.cluster.local:443 - secretRef: - name: zot-discovery-auth - caConfigMapRef: - name: root-bundle - webhook: - flavor: zot - path: events diff --git a/test/fixtures/e2e/kind-config-oidc.yaml b/test/fixtures/e2e/kind-config-oidc.yaml new file mode 100644 index 00000000..60659014 --- /dev/null +++ b/test/fixtures/e2e/kind-config-oidc.yaml @@ -0,0 +1,13 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane + extraMounts: + - hostPath: /tmp/solar-dex-auth-config.yaml + containerPath: /etc/kubernetes/pki/oidc-auth-config.yaml + kubeadmConfigPatches: + - | + kind: ClusterConfiguration + apiServer: + extraArgs: + authentication-config: /etc/kubernetes/pki/oidc-auth-config.yaml diff --git a/test/fixtures/e2e/profile-release.yaml b/test/fixtures/e2e/profile-release.yaml new file mode 100644 index 00000000..80b10e5d --- /dev/null +++ b/test/fixtures/e2e/profile-release.yaml @@ -0,0 +1,7 @@ +apiVersion: solar.opendefense.cloud/v1alpha1 +kind: Release +metadata: + name: profile-ocm-demo-release +spec: + componentVersionRef: + name: opendefense-cloud-ocm-demo-v26-4-1 diff --git a/test/fixtures/e2e/profile.yaml b/test/fixtures/e2e/profile.yaml index 6d655a2a..e4b5a31a 100644 --- a/test/fixtures/e2e/profile.yaml +++ b/test/fixtures/e2e/profile.yaml @@ -3,6 +3,8 @@ kind: Profile metadata: name: production spec: + releaseRef: + name: profile-ocm-demo-release targetSelector: matchLabels: env: prod diff --git a/test/fixtures/e2e/registry.yaml b/test/fixtures/e2e/registry.yaml new file mode 100644 index 00000000..d9155f0f --- /dev/null +++ b/test/fixtures/e2e/registry.yaml @@ -0,0 +1,9 @@ +apiVersion: solar.opendefense.cloud/v1alpha1 +kind: Registry +metadata: + name: deploy-registry +spec: + hostname: zot-deploy.zot.svc.cluster.local + plainHTTP: false + solarSecretRef: + name: zot-deploy-auth diff --git a/test/fixtures/e2e/release.yaml b/test/fixtures/e2e/release.yaml index 9b3e4c3f..0621b006 100644 --- a/test/fixtures/e2e/release.yaml +++ b/test/fixtures/e2e/release.yaml @@ -1,7 +1,7 @@ apiVersion: solar.opendefense.cloud/v1alpha1 kind: Release metadata: - name: test-opendefense-cloud-ocm-demo-v26-4-0-release + name: test-opendefense-cloud-ocm-demo-v26-4-1-release spec: componentVersionRef: - name: opendefense-cloud-ocm-demo-v26-4-0 + name: opendefense-cloud-ocm-demo-v26-4-1 diff --git a/test/fixtures/e2e/releasebinding.yaml b/test/fixtures/e2e/releasebinding.yaml new file mode 100644 index 00000000..f389d976 --- /dev/null +++ b/test/fixtures/e2e/releasebinding.yaml @@ -0,0 +1,9 @@ +apiVersion: solar.opendefense.cloud/v1alpha1 +kind: ReleaseBinding +metadata: + name: cluster-1-test-release +spec: + targetRef: + name: cluster-1 + releaseRef: + name: test-opendefense-cloud-ocm-demo-v26-4-1-release diff --git a/test/fixtures/e2e/solar-ui-deployment.yaml b/test/fixtures/e2e/solar-ui-deployment.yaml new file mode 100644 index 00000000..e813e220 --- /dev/null +++ b/test/fixtures/e2e/solar-ui-deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: solar-ui +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: solar-ui +rules: + # Allow reading all SolAr resources + - apiGroups: ["solar.opendefense.cloud"] + resources: ["*"] + verbs: ["get", "list", "watch"] + # Impersonation fallback — only needed if --auth-mode=impersonate + - apiGroups: [""] + resources: ["users", "groups"] + verbs: ["impersonate"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: solar-ui +subjects: + - kind: ServiceAccount + name: solar-ui +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: solar-ui +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: solar-ui + labels: + app: solar-ui +spec: + replicas: 1 + selector: + matchLabels: + app: solar-ui + template: + metadata: + labels: + app: solar-ui + spec: + serviceAccountName: solar-ui + containers: + - name: solar-ui + image: localhost/local/solar-ui:e2e + args: + - --listen=0.0.0.0:8090 + - --oidc-issuer=https://localhost:5556 + - --oidc-client-id=solar-ui + - --oidc-client-secret=solar-ui-secret + - --oidc-redirect-url=http://localhost:8090/api/auth/callback + - --auth-mode=token + ports: + - containerPort: 8090 + name: http + env: + - name: SSL_CERT_DIR + value: /etc/ssl/certs:/etc/dex-ca + volumeMounts: + - name: dex-ca + mountPath: /etc/dex-ca + readOnly: true + volumes: + - name: dex-ca + secret: + secretName: dex-ca +--- +apiVersion: v1 +kind: Service +metadata: + name: solar-ui +spec: + selector: + app: solar-ui + ports: + - port: 8090 + targetPort: 8090 + name: http diff --git a/test/fixtures/e2e/target.yaml b/test/fixtures/e2e/target.yaml index b4d995ec..4f6bfd1c 100644 --- a/test/fixtures/e2e/target.yaml +++ b/test/fixtures/e2e/target.yaml @@ -5,9 +5,8 @@ metadata: labels: env: prod spec: - releases: - test-release: - name: test-opendefense-cloud-ocm-demo-v26-4-0-release + renderRegistryRef: + name: deploy-registry userdata: foo: bar environment: dev diff --git a/test/fixtures/e2e/zot-discovery-auth.yaml b/test/fixtures/e2e/zot-discovery-auth.yaml new file mode 100644 index 00000000..167b339c --- /dev/null +++ b/test/fixtures/e2e/zot-discovery-auth.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: zot-discovery-auth +type: Opaque +stringData: + username: admin + password: admin diff --git a/test/fixtures/solar-discovery-scan.values.yaml b/test/fixtures/solar-discovery-scan.values.yaml new file mode 100644 index 00000000..1beff312 --- /dev/null +++ b/test/fixtures/solar-discovery-scan.values.yaml @@ -0,0 +1,36 @@ +# E2E test values for solar-discovery (scan mode) +image: + repository: localhost/local/solar-discovery + tag: e2e + pullPolicy: IfNotPresent + +registries: + - name: zot-scan + hostname: zot-discovery.zot.svc.cluster.local:443 + scanInterval: 10s + credentials: + username: ${username} + password: ${password} + +# namespace is overridden per-install via --set +namespace: default + +service: + enabled: false + +envFrom: + - secretRef: + name: zot-discovery-auth + +caBundle: + enabled: true + configMapName: root-bundle + key: trust-bundle.pem + +resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi diff --git a/test/fixtures/solar-discovery-webhook.values.yaml b/test/fixtures/solar-discovery-webhook.values.yaml new file mode 100644 index 00000000..010c03d7 --- /dev/null +++ b/test/fixtures/solar-discovery-webhook.values.yaml @@ -0,0 +1,38 @@ +# E2E test values for solar-discovery (webhook mode) +image: + repository: localhost/local/solar-discovery + tag: e2e + pullPolicy: IfNotPresent + +registries: + - name: zot-webhook + hostname: zot-discovery.zot.svc.cluster.local:443 + webhookPath: events + flavor: zot + credentials: + username: ${username} + password: ${password} + +# namespace is overridden per-install via --set +namespace: default + +service: + enabled: true + port: 8080 + +envFrom: + - secretRef: + name: zot-discovery-auth + +caBundle: + enabled: true + configMapName: root-bundle + key: trust-bundle.pem + +resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi diff --git a/test/fixtures/solar.values.yaml b/test/fixtures/solar.values.yaml index bea2f771..ff24472b 100644 --- a/test/fixtures/solar.values.yaml +++ b/test/fixtures/solar.values.yaml @@ -14,10 +14,23 @@ controller: renderer: image: repository: localhost/local/solar-renderer - baseURL: zot-deploy.zot.svc.cluster.local - pushSecretName: zot-deploy-auth caConfigMap: root-bundle discovery: image: repository: localhost/local/solar-discovery-worker + +ui: + admin: + subjects: + - kind: User + name: admin@solar.local + apiGroup: rbac.authorization.k8s.io + impersonation: + targets: + - username: maintainer@solar.local + groups: + - maintainer + - username: coordinator@solar.local + groups: + - coordinator diff --git a/web/e2e/api-proxy.spec.ts b/web/e2e/api-proxy.spec.ts new file mode 100644 index 00000000..47e27408 --- /dev/null +++ b/web/e2e/api-proxy.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from "@playwright/test"; + +test.describe("API proxy", () => { + // These tests verify that API routes require authentication. + + test("should return 401 for unauthenticated target list", async ({ + request, + }) => { + const response = await request.get("/api/namespaces/default/targets"); + expect(response.status()).toBe(401); + }); + + test("should return 401 for unauthenticated release list", async ({ + request, + }) => { + const response = await request.get("/api/namespaces/default/releases"); + expect(response.status()).toBe(401); + }); + + test("should return 401 for unauthenticated SSE endpoint", async ({ + request, + }) => { + const response = await request + .get("/api/namespaces/default/events", { + timeout: 3000, + }) + .catch(() => null); + + if (response) { + expect(response.status()).toBe(401); + } + }); + + test("should allow unauthenticated access to /api/auth/me", async ({ + request, + }) => { + const response = await request.get("/api/auth/me"); + expect(response.status()).toBe(200); + + const body = await response.json(); + expect(body.authenticated).toBe(false); + }); +}); diff --git a/web/e2e/auth.setup.ts b/web/e2e/auth.setup.ts new file mode 100644 index 00000000..3ad66935 --- /dev/null +++ b/web/e2e/auth.setup.ts @@ -0,0 +1,28 @@ +import { test as setup, expect } from "@playwright/test"; + +setup("authenticate via Dex", async ({ browser }) => { + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + const page = await context.newPage(); + + // Initiate OIDC login + await page.goto("/api/auth/login"); + + // Wait for Dex login page + await page.waitForURL(/localhost.*5556/, { timeout: 10_000 }); + + // Fill in credentials + await page.fill('input[name="login"]', "admin@solar.local"); + await page.fill('input[name="password"]', "password"); + await page.click('button[type="submit"]'); + + // Wait for redirect back to the app + await page.waitForURL("http://localhost:8090/", { timeout: 15_000 }); + + // Verify authentication + const me = await page.request.get("/api/auth/me"); + expect((await me.json()).authenticated).toBe(true); + + // Save session state (cookies) for authenticated tests + await context.storageState({ path: "e2e/.auth/session.json" }); + await context.close(); +}); diff --git a/web/e2e/auth.spec.ts b/web/e2e/auth.spec.ts new file mode 100644 index 00000000..65cfbfad --- /dev/null +++ b/web/e2e/auth.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Authentication", () => { + test("should show unauthenticated state initially", async ({ page }) => { + await page.goto("/"); + + // The user info section should not show a username + // (no session cookie, so /api/auth/me returns authenticated: false) + const response = await page.request.get("/api/auth/me"); + const body = await response.json(); + expect(body.authenticated).toBe(false); + }); + + test("should redirect to Dex on login", async ({ request }) => { + const response = await request.post("/api/auth/login", { + maxRedirects: 0, + }); + + expect(response.status()).toBe(302); + const location = response.headers()["location"]; + expect(location).toContain("localhost"); + expect(location).toContain("client_id=solar-ui"); + }); + + test("should clear session on logout", async ({ page }) => { + await page.goto("/"); + + // Hit logout endpoint + const response = await page.request.get("/api/auth/logout", { + maxRedirects: 0, + }); + + expect(response.status()).toBe(302); + expect(response.headers()["location"]).toBe("/"); + }); +}); diff --git a/web/e2e/impersonation.spec.ts b/web/e2e/impersonation.spec.ts new file mode 100644 index 00000000..37c5f467 --- /dev/null +++ b/web/e2e/impersonation.spec.ts @@ -0,0 +1,124 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Impersonation — unauthenticated access", () => { + // storageState: undefined overrides the project-level stored session so + // these contexts have no cookies and are treated as unauthenticated. + + test("GET /api/auth/impersonation-targets returns 401 when not logged in", async ({ + browser, + }) => { + const ctx = await browser.newContext({ storageState: undefined }); + const response = await ctx.request.get("/api/auth/impersonation-targets"); + expect(response.status()).toBe(401); + await ctx.close(); + }); + + test("PUT /api/auth/impersonate returns 401 when not logged in", async ({ + browser, + }) => { + const ctx = await browser.newContext({ storageState: undefined }); + const response = await ctx.request.put("/api/auth/impersonate", { + data: { username: "maintainer@solar.local" }, + }); + expect(response.status()).toBe(401); + await ctx.close(); + }); +}); + +test.describe("Impersonation — admin user", () => { + // Session from auth.setup.ts: logged in as admin@solar.local (isAdmin=true). + + test.afterEach(async ({ request }) => { + // Best-effort: ensure no test leaves the shared session impersonating. + await request.delete("/api/auth/impersonate").catch(() => {}); + }); + + test("GET /api/auth/me reports isAdmin=true for admin user", async ({ + request, + }) => { + const response = await request.get("/api/auth/me"); + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.authenticated).toBe(true); + expect(body.username).toBe("admin@solar.local"); + expect(body.isAdmin).toBe(true); + }); + + test("GET /api/auth/impersonation-targets returns available personas", async ({ + request, + }) => { + const response = await request.get("/api/auth/impersonation-targets"); + expect(response.status()).toBe(200); + const targets: { username: string; groups: string[] }[] = + await response.json(); + expect(Array.isArray(targets)).toBe(true); + expect(targets.length).toBeGreaterThan(0); + + const usernames = targets.map((t) => t.username); + expect(usernames).toContain("maintainer@solar.local"); + expect(usernames).toContain("coordinator@solar.local"); + }); + + test("can activate impersonation, sees persona permissions, then clear it", async ({ + request, + }) => { + // Activate impersonation as coordinator + const impersonate = await request.put("/api/auth/impersonate", { + data: { username: "coordinator@solar.local" }, + }); + expect(impersonate.status()).toBe(204); + + // /auth/me should reflect impersonating state + const meImpersonating = await request.get("/api/auth/me"); + expect(meImpersonating.status()).toBe(200); + const meBody = await meImpersonating.json(); + expect(meBody.impersonating?.username).toBe("coordinator@solar.local"); + expect(meBody.impersonating?.groups).toContain("coordinator"); + // isAdmin must still reflect the real admin identity, not the persona's + expect(meBody.isAdmin).toBe(true); + + // Permissions endpoint should return the coordinator's rules only + const perms = await request.get("/api/namespaces/default/permissions"); + expect(perms.status()).toBe(200); + const { rules } = await perms.json(); + const solarRules = ( + rules as { apiGroups: string[]; resources: string[]; verbs: string[] }[] + ).filter((r) => r.apiGroups.includes("solar.opendefense.cloud")); + // coordinator has get/list/watch on targets, releases, releasebindings, profiles + expect( + solarRules.some((r) => r.resources.includes("targets")), + ).toBe(true); + // coordinator does NOT have write verbs on any solar resource + const hasWrite = solarRules.some((r) => + r.verbs.some((v) => ["create", "update", "patch", "delete"].includes(v)), + ); + expect(hasWrite).toBe(false); + + // Clear impersonation + const clear = await request.delete("/api/auth/impersonate"); + expect(clear.status()).toBe(204); + + // /auth/me should no longer show impersonating + const meCleared = await request.get("/api/auth/me"); + const meClearedBody = await meCleared.json(); + expect(meClearedBody.impersonating).toBeUndefined(); + }); + + test("PUT /api/auth/impersonate rejects unknown username", async ({ + request, + }) => { + const response = await request.put("/api/auth/impersonate", { + data: { username: "nobody@solar.local" }, + }); + expect(response.status()).toBe(400); + }); + + test("PUT /api/auth/impersonate rejects missing username", async ({ + request, + }) => { + const response = await request.put("/api/auth/impersonate", { + data: {}, + }); + expect(response.status()).toBe(400); + }); +}); diff --git a/web/e2e/oidc-login.spec.ts b/web/e2e/oidc-login.spec.ts new file mode 100644 index 00000000..1cf69c95 --- /dev/null +++ b/web/e2e/oidc-login.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from "@playwright/test"; + +// Full OIDC BFF flow via Dex (plain HTTP): +// 1. POST /api/auth/login → Go backend redirects to Dex (localhost:5556) +// 2. User enters credentials on Dex login page +// 3. Dex redirects back to /api/auth/callback +// 4. Go backend exchanges code for tokens, creates session +// 5. Browser ends up at / with a valid session cookie +// +// Prerequisites (handled by `make test-e2e-ui`): +// - Dex port-forwarded to localhost:5556 +// - solar-ui backend running on :8090 with OIDC configured +// - DEX_LOCAL_PORT env var set to 5556 +// - Static user: admin@solar.local / password + +test.describe("OIDC login flow", () => { + // Skip if DEX_LOCAL_PORT is not set (Dex is not port-forwarded) + const dexPort = process.env.DEX_LOCAL_PORT; + + test("should redirect to Dex on login", async ({ page }) => { + const loginResponse = await page.request.post("/api/auth/login", { + maxRedirects: 0, + }); + expect(loginResponse.status()).toBe(302); + + const location = loginResponse.headers()["location"]; + expect(location).toContain("localhost"); + expect(location).toContain("client_id=solar-ui"); + expect(location).toContain("response_type=code"); + }); + + test("should complete full login via Dex", async ({ page }) => { + if (!dexPort) { + test.skip(); + + return; + } + + // Verify we start unauthenticated + await page.goto("/"); + const meBefore = await page.request.get("/api/auth/me"); + expect((await meBefore.json()).authenticated).toBe(false); + + // Initiate OIDC login — this redirects to Dex + await page.goto("/api/auth/login"); + + // We should now be on the Dex login page + // Dex redirects through /auth → /auth/local/login for the password connector + await page.waitForURL(/localhost.*5556/, { timeout: 10_000 }); + + // Fill in the login form + await page.fill('input[name="login"]', "admin@solar.local"); + await page.fill('input[name="password"]', "password"); + await page.click('button[type="submit"]'); + + // After successful auth, Dex redirects back to /api/auth/callback, + // which sets the session and redirects to / + await page.waitForURL("http://localhost:8090/", { timeout: 15_000 }); + + // Verify we are authenticated + const meAfter = await page.request.get("/api/auth/me"); + const meBody = await meAfter.json(); + expect(meBody.authenticated).toBe(true); + expect(meBody.username).toBe("admin@solar.local"); + + // Verify the UI shows the username + await expect(page.locator("text=admin@solar.local")).toBeVisible(); + + // Verify API calls work with the session + const targets = await page.request.get( + "/api/namespaces/default/targets", + ); + expect(targets.status()).toBe(200); + expect(await targets.json()).toHaveProperty("items"); + + // Logout + await page.goto("/api/auth/logout"); + const meLogout = await page.request.get("/api/auth/me"); + expect((await meLogout.json()).authenticated).toBe(false); + }); +}); diff --git a/web/e2e/spa.spec.ts b/web/e2e/spa.spec.ts new file mode 100644 index 00000000..5bbddb24 --- /dev/null +++ b/web/e2e/spa.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from "@playwright/test"; + +test.describe("SPA serving", () => { + test("should load the dashboard page", async ({ page }) => { + await page.goto("/"); + await expect( + page.getByRole("heading", { name: "Dashboard" }), + ).toBeVisible(); + }); + + test("should handle client-side routing to /targets", async ({ page }) => { + await page.goto("/targets"); + await expect( + page.getByRole("heading", { name: "Targets" }), + ).toBeVisible(); + }); + + test("should handle client-side routing to /releases", async ({ page }) => { + await page.goto("/releases"); + await expect( + page.getByRole("heading", { name: "Releases" }), + ).toBeVisible(); + }); + + test("should handle client-side routing to /components", async ({ + page, + }) => { + await page.goto("/components"); + await expect( + page.getByRole("heading", { name: "Components" }), + ).toBeVisible(); + }); + + test("should handle client-side routing to /profiles", async ({ page }) => { + await page.goto("/profiles"); + await expect( + page.getByRole("heading", { name: "Profiles" }), + ).toBeVisible(); + }); + + test("should navigate between pages via sidebar", async ({ page }) => { + await page.goto("/"); + + // Click on Targets in the sidebar + await page.click('a[href="/targets"]'); + await expect(page).toHaveURL(/\/targets/); + await expect( + page.getByRole("heading", { name: "Targets" }), + ).toBeVisible(); + + // Click on Releases + await page.click('a[href="/releases"]'); + await expect(page).toHaveURL(/\/releases/); + await expect( + page.getByRole("heading", { name: "Releases" }), + ).toBeVisible(); + + // Click on Dashboard + await page.click('a[href="/"]'); + await expect(page).toHaveURL(/\/$/); + await expect( + page.getByRole("heading", { name: "Dashboard" }), + ).toBeVisible(); + }); +}); diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 00000000..e0816855 --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,28 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist", "node_modules", "e2e"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2022, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + }, +); diff --git a/web/index.html b/web/index.html new file mode 100644 index 00000000..0db83b0c --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + SolAr UI + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 00000000..131d764a --- /dev/null +++ b/web/package.json @@ -0,0 +1,52 @@ +{ + "name": "solar-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint .", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.80.7", + "@tanstack/react-router": "^1.121.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.515.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwind-merge": "^3.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.28.0", + "@playwright/test": "^1.52.0", + "@tailwindcss/vite": "^4.1.8", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.5.2", + "concurrently": "^9.2.1", + "eslint": "^9.28.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.2.0", + "jsdom": "^26.1.0", + "tailwindcss": "^4.1.8", + "typescript": "~5.8.3", + "typescript-eslint": "^8.33.1", + "vite": "^6.3.5", + "vitest": "^3.2.1" + } +} diff --git a/web/playwright.config.ts b/web/playwright.config.ts new file mode 100644 index 00000000..315e52ad --- /dev/null +++ b/web/playwright.config.ts @@ -0,0 +1,64 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + timeout: 60_000, + expect: { + timeout: 10_000, + }, + fullyParallel: false, + retries: 1, + reporter: "html", + use: { + baseURL: "http://localhost:8090", + trace: "on-first-retry", + screenshot: "only-on-failure", + // Dex uses a self-signed TLS certificate in dev/test + ignoreHTTPSErrors: true, + launchOptions: { + executablePath: + process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || undefined, + }, + }, + projects: [ + { + name: "setup", + testMatch: /auth\.setup\.ts/, + use: { + ...devices["Desktop Chrome"], + }, + }, + { + name: "unauthenticated", + testMatch: /api-proxy\.spec\.ts|auth\.spec\.ts/, + use: { + ...devices["Desktop Chrome"], + }, + }, + { + name: "authenticated", + testMatch: /spa\.spec\.ts/, + dependencies: ["setup"], + use: { + ...devices["Desktop Chrome"], + storageState: "e2e/.auth/session.json", + }, + }, + { + name: "oidc", + testMatch: /oidc-login\.spec\.ts/, + use: { + ...devices["Desktop Chrome"], + }, + }, + { + name: "impersonation", + testMatch: /impersonation\.spec\.ts/, + dependencies: ["setup"], + use: { + ...devices["Desktop Chrome"], + storageState: "e2e/.auth/session.json", + }, + }, + ], +}); diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 00000000..d50bfe47 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,4417 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@radix-ui/react-avatar': + specifier: ^1.1.10 + version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-dialog': + specifier: ^1.1.14 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.15 + version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-navigation-menu': + specifier: ^1.2.13 + version: 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.4(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-tabs': + specifier: ^1.1.12 + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-tooltip': + specifier: ^1.2.7 + version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-query': + specifier: ^5.80.7 + version: 5.97.0(react@19.2.5) + '@tanstack/react-router': + specifier: ^1.121.3 + version: 1.168.15(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^0.515.0 + version: 0.515.0(react@19.2.5) + react: + specifier: ^19.1.0 + version: 19.2.5 + react-dom: + specifier: ^19.1.0 + version: 19.2.5(react@19.2.5) + tailwind-merge: + specifier: ^3.3.1 + version: 3.5.0 + devDependencies: + '@eslint/js': + specifier: ^9.28.0 + version: 9.39.4 + '@playwright/test': + specifier: ^1.52.0 + version: 1.59.1 + '@tailwindcss/vite': + specifier: ^4.1.8 + version: 4.2.2(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0)) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@types/react': + specifier: ^19.1.6 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^4.5.2 + version: 4.7.0(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0)) + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + eslint: + specifier: ^9.28.0 + version: 9.39.4(jiti@2.6.1) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: ^0.4.20 + version: 0.4.26(eslint@9.39.4(jiti@2.6.1)) + globals: + specifier: ^16.2.0 + version: 16.5.0 + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + tailwindcss: + specifier: ^4.1.8 + version: 4.2.2 + typescript: + specifier: ~5.8.3 + version: 5.8.3 + typescript-eslint: + specifier: ^8.33.1 + version: 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) + vite: + specifier: ^6.3.5 + version: 6.4.2(jiti@2.6.1)(lightningcss@1.32.0) + vitest: + specifier: ^3.2.1 + version: 3.2.4(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0) + +packages: + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.11': + resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.3': + resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.1': + resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.1': + resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.2': + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/history@1.161.6': + resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} + engines: {node: '>=20.19'} + + '@tanstack/query-core@5.97.0': + resolution: {integrity: sha512-QdpLP5VzVMgo4VtaPppRA2W04UFjIqX+bxke/ZJhE5cfd5UPkRzqIAJQt9uXkQJjqE8LBOMbKv7f8HCsZltXlg==} + + '@tanstack/react-query@5.97.0': + resolution: {integrity: sha512-y4So4eGcQoK2WVMAcDNZE9ofB/p5v1OlKvtc1F3uqHwrtifobT7q+ZnXk2mRkc8E84HKYSlAE9z6HXl2V0+ySQ==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-router@1.168.15': + resolution: {integrity: sha512-V6evK+2d8r4VtaxNFT7I3ZUSPJegaXWoocCcIkAsFTBq66ymlD6FrNTOdl7qRB0I+ApFT1CGV3OMmzq5HgX74Q==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.168.11': + resolution: {integrity: sha512-5dOSXyQnLHnw7aBoH+8cQtQyc66rT2/MnhFjQgWH7YZKedSYsmneQ1ZhB6I0HJuYbTtfvZIcGMoruXMSuEHYNg==} + engines: {node: '>=20.19'} + hasBin: true + + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@typescript-eslint/eslint-plugin@8.58.1': + resolution: {integrity: sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.58.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.58.1': + resolution: {integrity: sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.58.1': + resolution: {integrity: sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.58.1': + resolution: {integrity: sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.58.1': + resolution: {integrity: sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.58.1': + resolution: {integrity: sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.58.1': + resolution: {integrity: sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.58.1': + resolution: {integrity: sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.58.1': + resolution: {integrity: sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.58.1': + resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.17: + resolution: {integrity: sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001787: + resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@2.0.1: + resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + electron-to-chromium@1.5.335: + resolution: {integrity: sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.26: + resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isbot@5.1.37: + resolution: {integrity: sha512-5bcicX81xf6NlTEV8rWdg7Pk01LFizDetuYGHx6d/f6y3lR2/oo8IfxjzJqn1UdDEyCcwT9e7NRloj8DwCYujQ==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.515.0: + resolution: {integrity: sha512-Sy7bY0MeicRm2pzrnoHm2h6C1iVoeHyBU2fjdQDsXGP51fhkhau1/ZV/dzrcxEmAKsxYb6bGaIsMnGHuQ5s0dw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rollup@4.60.1: + resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + seroval-plugins@1.5.2: + resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.2: + resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} + engines: {node: '>=10'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.58.1: + resolution: {integrity: sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@adobe/css-tools@4.4.4': {} + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + dependencies: + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@floating-ui/utils@0.2.11': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context@1.1.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/rect': 1.1.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.1': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.60.1': + optional: true + + '@rollup/rollup-android-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-x64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.1': + optional: true + + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/vite@4.2.2(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 6.4.2(jiti@2.6.1)(lightningcss@1.32.0) + + '@tanstack/history@1.161.6': {} + + '@tanstack/query-core@5.97.0': {} + + '@tanstack/react-query@5.97.0(react@19.2.5)': + dependencies: + '@tanstack/query-core': 5.97.0 + react: 19.2.5 + + '@tanstack/react-router@1.168.15(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/history': 1.161.6 + '@tanstack/react-store': 0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/router-core': 1.168.11 + isbot: 5.1.37 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@tanstack/react-store@0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/store': 0.9.3 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) + + '@tanstack/router-core@1.168.11': + dependencies: + '@tanstack/history': 1.161.6 + cookie-es: 2.0.1 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + + '@tanstack/store@0.9.3': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 10.4.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.58.1 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.58.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.8.3) + '@typescript-eslint/types': 8.58.1 + debug: 4.4.3 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.58.1': + dependencies: + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + + '@typescript-eslint/tsconfig-utils@8.58.1(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.58.1': {} + + '@typescript-eslint/typescript-estree@8.58.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.58.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.8.3) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.8.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.58.1': + dependencies: + '@typescript-eslint/types': 8.58.1 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@4.7.0(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.2(jiti@2.6.1)(lightningcss@1.32.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.2(jiti@2.6.1)(lightningcss@1.32.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.17: {} + + brace-expansion@1.1.13: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.17 + caniuse-lite: 1.0.30001787 + electron-to-chromium: 1.5.335 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + cac@6.7.14: {} + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001787: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + convert-source-map@2.0.0: {} + + cookie-es@2.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + electron-to-chromium@1.5.335: {} + + emoji-regex@8.0.0: {} + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.2 + + entities@6.0.1: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + + eslint-plugin-react-refresh@0.4.26(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-nonce@1.0.1: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.5.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-potential-custom-element-name@1.0.1: {} + + isbot@5.1.37: {} + + isexe@2.0.0: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.20.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.515.0(react@19.2.5): + dependencies: + react: 19.2.5 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + min-indent@1.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.13 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.37: {} + + nwsapi@2.2.23: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss@8.5.9: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + punycode@2.3.1: {} + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.5) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.5) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + get-nonce: 1.0.1 + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react@19.2.5: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + require-directory@2.1.1: {} + + resolve-from@4.0.0: {} + + rollup@4.60.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.1 + '@rollup/rollup-android-arm64': 4.60.1 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-freebsd-arm64': 4.60.1 + '@rollup/rollup-freebsd-x64': 4.60.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 + '@rollup/rollup-linux-arm-musleabihf': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-arm64-musl': 4.60.1 + '@rollup/rollup-linux-loong64-gnu': 4.60.1 + '@rollup/rollup-linux-loong64-musl': 4.60.1 + '@rollup/rollup-linux-ppc64-gnu': 4.60.1 + '@rollup/rollup-linux-ppc64-musl': 4.60.1 + '@rollup/rollup-linux-riscv64-gnu': 4.60.1 + '@rollup/rollup-linux-riscv64-musl': 4.60.1 + '@rollup/rollup-linux-s390x-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-musl': 4.60.1 + '@rollup/rollup-openbsd-x64': 4.60.1 + '@rollup/rollup-openharmony-arm64': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-ia32-msvc': 4.60.1 + '@rollup/rollup-win32-x64-gnu': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + seroval-plugins@1.5.2(seroval@1.5.2): + dependencies: + seroval: 1.5.2 + + seroval@1.5.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + + tailwind-merge@3.5.0: {} + + tailwindcss@4.2.2: {} + + tapable@2.3.2: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-api-utils@2.5.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + typescript@5.8.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sync-external-store@1.6.0(react@19.2.5): + dependencies: + react: 19.2.5 + + vite-node@3.2.4(jiti@2.6.1)(lightningcss@1.32.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.2(jiti@2.6.1)(lightningcss@1.32.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.9 + rollup: 4.60.1 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.32.0 + + vitest@3.2.4(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.16 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.4.2(jiti@2.6.1)(lightningcss@1.32.0) + vite-node: 3.2.4(jiti@2.6.1)(lightningcss@1.32.0) + why-is-node-running: 2.3.0 + optionalDependencies: + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + ws@8.20.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} diff --git a/web/public/solar.svg b/web/public/solar.svg new file mode 100644 index 00000000..27f3f3b1 --- /dev/null +++ b/web/public/solar.svg @@ -0,0 +1,326 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/api/client.ts b/web/src/api/client.ts new file mode 100644 index 00000000..0825c81c --- /dev/null +++ b/web/src/api/client.ts @@ -0,0 +1,43 @@ +// API client for solar-ui backend + +const API_BASE = "/api"; + +export interface ApiError { + status: number; + message: string; +} + +async function request(path: string, init?: RequestInit): Promise { + const res = await fetch(`${API_BASE}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...init?.headers, + }, + credentials: "same-origin", + }); + + if (res.status === 401) { + // Redirect to OIDC login flow + window.location.href = "/api/auth/login"; + + // Return a never-resolving promise to prevent further rendering + return new Promise(() => {}); + } + + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw { status: res.status, message: body || res.statusText } as ApiError; + } + + return res.json(); +} + +export const api = { + get: (path: string) => request(path), + post: (path: string, body?: unknown) => + request(path, { method: "POST", body: JSON.stringify(body) }), + put: (path: string, body?: unknown) => + request(path, { method: "PUT", body: JSON.stringify(body) }), + delete: (path: string) => request(path, { method: "DELETE" }), +}; diff --git a/web/src/api/queries.ts b/web/src/api/queries.ts new file mode 100644 index 00000000..1d5e5c8f --- /dev/null +++ b/web/src/api/queries.ts @@ -0,0 +1,149 @@ +import { queryOptions } from "@tanstack/react-query"; +import { api } from "./client"; +import type { + Target, + Release, + ReleaseBinding, + Component, + ComponentVersion, + Registry, + Profile, + RenderTask, + ResourceList, + UserInfo, + PermissionsResponse, + ImpersonationTarget, +} from "./types"; + +export const authQueries = { + me: () => + queryOptions({ + queryKey: ["auth", "me"], + queryFn: () => api.get("/auth/me"), + retry: false, + }), +}; + +export const permissionQueries = { + rules: (namespace: string) => + queryOptions({ + queryKey: ["permissions", namespace], + queryFn: () => + api.get(`/namespaces/${namespace}/permissions`), + staleTime: 5 * 60 * 1000, + retry: false, + }), +}; + +export const impersonationQueries = { + targets: () => + queryOptions({ + queryKey: ["impersonation", "targets"], + queryFn: () => api.get("/auth/impersonation-targets"), + staleTime: 5 * 60 * 1000, + retry: false, + }), +}; + +export const impersonationMutations = { + impersonate: (username: string) => + api.put("/auth/impersonate", { username }), + clear: () => api.delete("/auth/impersonate"), +}; + +export const targetQueries = { + list: (namespace: string) => + queryOptions({ + queryKey: ["targets", namespace], + queryFn: () => + api.get>(`/namespaces/${namespace}/targets`), + staleTime: 60 * 1000, + }), + detail: (namespace: string, name: string) => + queryOptions({ + queryKey: ["targets", namespace, name], + queryFn: () => + api.get(`/namespaces/${namespace}/targets/${name}`), + }), +}; + +export const releaseQueries = { + list: (namespace: string) => + queryOptions({ + queryKey: ["releases", namespace], + queryFn: () => + api.get>(`/namespaces/${namespace}/releases`), + }), + detail: (namespace: string, name: string) => + queryOptions({ + queryKey: ["releases", namespace, name], + queryFn: () => + api.get(`/namespaces/${namespace}/releases/${name}`), + }), +}; + +export const releaseBindingQueries = { + list: (namespace: string) => + queryOptions({ + queryKey: ["releasebindings", namespace], + queryFn: () => + api.get>( + `/namespaces/${namespace}/releasebindings`, + ), + }), +}; + +export const componentQueries = { + list: (namespace: string) => + queryOptions({ + queryKey: ["components", namespace], + queryFn: () => + api.get>(`/namespaces/${namespace}/components`), + }), + detail: (namespace: string, name: string) => + queryOptions({ + queryKey: ["components", namespace, name], + queryFn: () => + api.get(`/namespaces/${namespace}/components/${name}`), + }), +}; + +export const componentVersionQueries = { + list: (namespace: string) => + queryOptions({ + queryKey: ["componentversions", namespace], + queryFn: () => + api.get>( + `/namespaces/${namespace}/componentversions`, + ), + }), +}; + +export const registryQueries = { + list: (namespace: string) => + queryOptions({ + queryKey: ["registries", namespace], + queryFn: () => + api.get>(`/namespaces/${namespace}/registries`), + }), +}; + +export const profileQueries = { + list: (namespace: string) => + queryOptions({ + queryKey: ["profiles", namespace], + queryFn: () => + api.get>(`/namespaces/${namespace}/profiles`), + }), +}; + +export const renderTaskQueries = { + list: (namespace: string) => + queryOptions({ + queryKey: ["rendertasks", namespace], + queryFn: () => + api.get>( + `/namespaces/${namespace}/rendertasks`, + ), + }), +}; diff --git a/web/src/api/types.ts b/web/src/api/types.ts new file mode 100644 index 00000000..3d52db8a --- /dev/null +++ b/web/src/api/types.ts @@ -0,0 +1,169 @@ +// Kubernetes-style metadata +export interface ObjectMeta { + name: string; + namespace: string; + creationTimestamp: string; + labels?: Record; + annotations?: Record; + generation?: number; +} + +// Condition from K8s status +export interface Condition { + type: string; + status: "True" | "False" | "Unknown"; + lastTransitionTime: string; + reason: string; + message: string; +} + +// Target +export interface Target { + metadata: ObjectMeta; + spec: { + renderRegistryRef: { name: string }; + userdata?: unknown; + }; + status?: { + conditions?: Condition[]; + }; +} + +// Release +export interface Release { + metadata: ObjectMeta; + spec: { + componentVersionRef: { name: string }; + }; + status?: { + conditions?: Condition[]; + }; +} + +// ReleaseBinding +export interface ReleaseBinding { + metadata: ObjectMeta; + spec: { + targetRef: { name: string }; + releaseRef: { name: string }; + }; + status?: { + conditions?: Condition[]; + }; +} + +// Component +export interface Component { + metadata: ObjectMeta; + spec: { + scheme: string; + repository: string; + registry: string; + }; +} + +// ComponentVersion +export interface ComponentVersion { + metadata: ObjectMeta; + spec: { + componentRef: { name: string }; + tag: string; + resources?: Record< + string, + { + repository: string; + tag: string; + insecure?: boolean; + } + >; + entrypoint?: { + type: string; + resourceName: string; + }; + }; +} + +// Registry +export interface Registry { + metadata: ObjectMeta; + spec: { + hostname: string; + plainHTTP?: boolean; + solarSecretRef?: { name: string }; + targetSecretRef?: { name: string; namespace: string }; + }; + status?: { + conditions?: Condition[]; + }; +} + +// Profile +export interface Profile { + metadata: ObjectMeta; + spec: { + releaseRef: { name: string }; + targetSelector: { + matchLabels?: Record; + }; + }; + status?: { + conditions?: Condition[]; + matchedTargets?: number; + }; +} + +// RenderTask +export interface RenderTask { + metadata: ObjectMeta; + spec: { + type: string; + baseURL: string; + }; + status?: { + conditions?: Condition[]; + }; +} + +// List wrapper +export interface ResourceList { + items: T[]; +} + +// Auth +export interface UserInfo { + username: string; + groups: string[]; + authenticated: boolean; + /** True when the user has the K8s impersonate verb on users (computed by BFF). */ + isAdmin?: boolean; + impersonating?: { + username: string; + groups: string[]; + }; +} + +// Admin impersonation persona +export interface ImpersonationTarget { + username: string; + groups: string[]; +} + +// Permissions — derived from SelfSubjectRulesReview +export interface PolicyRule { + verbs: string[]; + apiGroups: string[]; + resources: string[]; +} + +export interface PermissionsResponse { + incomplete: boolean; + rules: PolicyRule[]; +} + +// SSE event +export interface ResourceEvent { + type: "ADDED" | "MODIFIED" | "DELETED"; + resource: string; + namespace: string; + name: string; +} diff --git a/web/src/components/layout.tsx b/web/src/components/layout.tsx new file mode 100644 index 00000000..b1542f9e --- /dev/null +++ b/web/src/components/layout.tsx @@ -0,0 +1,183 @@ +import { Link, useRouterState } from "@tanstack/react-router"; +import { useQuery } from "@tanstack/react-query"; +import { authQueries } from "@/api/queries"; +import { + LayoutDashboard, + Server, + Package, + Boxes, + Users, + LogOut, + Eye, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { ThemeToggle } from "@/components/ui/theme-toggle"; +import { usePermissions } from "@/hooks/usePermissions"; +import { useImpersonation } from "@/hooks/useImpersonation"; + +const SOLAR_GROUP = "solar.opendefense.cloud"; +const DEFAULT_NAMESPACE = "default"; + +// Each guarded nav item declares which verb+resource the user must have. +type NavItem = { + to: string; + label: string; + icon: React.ElementType; + guard: { verb: string; resource: string } | null; +}; + +const navItems: NavItem[] = [ + { to: "/", label: "Dashboard", icon: LayoutDashboard, guard: null }, + { to: "/targets", label: "Targets", icon: Server, guard: { verb: "list", resource: "targets" } }, + { to: "/releases", label: "Releases", icon: Package, guard: { verb: "list", resource: "releases" } }, + { to: "/components", label: "Components", icon: Boxes, guard: { verb: "list", resource: "components" } }, + { to: "/profiles", label: "Profiles", icon: Users, guard: { verb: "list", resource: "profiles" } }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + const { data: user } = useQuery(authQueries.me()); + const router = useRouterState(); + const currentPath = router.location.pathname; + // potentially check permissions in specific namespaces in the future, but for now just check at the cluster level + const permissions = usePermissions(DEFAULT_NAMESPACE); + const { targets, impersonatedUsername, isImpersonating, impersonate, clearImpersonation } = + useImpersonation(); + + const visibleNavItems = navItems.filter(({ guard }) => { + if (!guard) return true; + return permissions.can(guard.verb, guard.resource, SOLAR_GROUP); + }); + + return ( +
+ {/* Sidebar */} + + + {/* Main content */} +
+
{children}
+
+
+ ); +} diff --git a/web/src/components/ui/badge.tsx b/web/src/components/ui/badge.tsx new file mode 100644 index 00000000..c4fe6861 --- /dev/null +++ b/web/src/components/ui/badge.tsx @@ -0,0 +1,34 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ring-1 ring-inset", + { + variants: { + variant: { + default: "bg-primary/10 text-primary ring-primary/20", + success: + "bg-emerald-50 text-emerald-700 ring-emerald-600/20 dark:bg-emerald-500/10 dark:text-emerald-400 dark:ring-emerald-500/20", + warning: + "bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-500/10 dark:text-amber-400 dark:ring-amber-500/20", + destructive: + "bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-500/10 dark:text-red-400 dark:ring-red-500/20", + secondary: + "bg-secondary text-secondary-foreground ring-border", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +export function Badge({ className, variant, ...props }: BadgeProps) { + return ( + + ); +} diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx new file mode 100644 index 00000000..69e78807 --- /dev/null +++ b/web/src/components/ui/card.tsx @@ -0,0 +1,54 @@ +import { cn } from "@/lib/utils"; + +export function Card({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export function CardHeader({ + className, + ...props +}: React.HTMLAttributes) { + return
; +} + +export function CardTitle({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} + +export function CardDescription({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} + +export function CardContent({ + className, + ...props +}: React.HTMLAttributes) { + return

; +} diff --git a/web/src/components/ui/status-badge.tsx b/web/src/components/ui/status-badge.tsx new file mode 100644 index 00000000..9db9caf7 --- /dev/null +++ b/web/src/components/ui/status-badge.tsx @@ -0,0 +1,30 @@ +import type { Condition } from "@/api/types"; +import { Badge } from "./badge"; +import { getCondition } from "@/lib/utils"; + +interface StatusBadgeProps { + conditions?: Condition[]; + type?: string; +} + +export function StatusBadge({ + conditions, + type = "Ready", +}: StatusBadgeProps) { + const condition = getCondition(conditions, type); + + if (!condition) { + return Unknown; + } + + switch (condition.status) { + case "True": + return {condition.reason || "Ready"}; + case "False": + return ( + {condition.reason || "Not Ready"} + ); + default: + return {condition.reason || "Pending"}; + } +} diff --git a/web/src/components/ui/theme-toggle.tsx b/web/src/components/ui/theme-toggle.tsx new file mode 100644 index 00000000..f6ad6037 --- /dev/null +++ b/web/src/components/ui/theme-toggle.tsx @@ -0,0 +1,33 @@ +import { Sun, Moon, Monitor } from "lucide-react"; +import { useTheme } from "@/hooks/useTheme"; +import { cn } from "@/lib/utils"; + +const options = [ + { value: "light" as const, icon: Sun, label: "Light" }, + { value: "dark" as const, icon: Moon, label: "Dark" }, + { value: "system" as const, icon: Monitor, label: "System" }, +]; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + + return ( +
+ {options.map(({ value, icon: Icon, label }) => ( + + ))} +
+ ); +} diff --git a/web/src/hooks/useImpersonation.ts b/web/src/hooks/useImpersonation.ts new file mode 100644 index 00000000..d70bd41a --- /dev/null +++ b/web/src/hooks/useImpersonation.ts @@ -0,0 +1,47 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { authQueries, impersonationQueries, impersonationMutations } from "@/api/queries"; + +export interface UseImpersonationResult { + /** Available personas fetched from the BFF (empty until loaded). */ + targets: { username: string; groups: string[] }[]; + /** Username currently being impersonated, or null when acting as self. */ + impersonatedUsername: string | null; + isImpersonating: boolean; + /** Activate impersonation for the given username (must be a known target). */ + impersonate: (username: string) => void; + /** Revert to acting as the real admin user. */ + clearImpersonation: () => void; +} + +export function useImpersonation(): UseImpersonationResult { + const queryClient = useQueryClient(); + const { data: user } = useQuery(authQueries.me()); + const { data: targets = [] } = useQuery(impersonationQueries.targets()); + + const impersonatedUsername = user?.impersonating?.username ?? null; + + // After either mutation resolves, invalidate /auth/me and permissions so the + // UI immediately reflects the new effective identity. + const onSuccess = () => { + queryClient.invalidateQueries({ queryKey: ["auth", "me"] }); + queryClient.invalidateQueries({ queryKey: ["permissions"] }); + }; + + const impersonateMutation = useMutation({ + mutationFn: (username: string) => impersonationMutations.impersonate(username), + onSuccess, + }); + + const clearMutation = useMutation({ + mutationFn: () => impersonationMutations.clear(), + onSuccess, + }); + + return { + targets, + impersonatedUsername, + isImpersonating: impersonatedUsername !== null, + impersonate: (username) => impersonateMutation.mutate(username), + clearImpersonation: () => clearMutation.mutate(), + }; +} diff --git a/web/src/hooks/usePermissions.ts b/web/src/hooks/usePermissions.ts new file mode 100644 index 00000000..369c9010 --- /dev/null +++ b/web/src/hooks/usePermissions.ts @@ -0,0 +1,76 @@ +import { useQuery } from "@tanstack/react-query"; +import { authQueries, permissionQueries } from "@/api/queries"; +import type { PermissionsResponse, PolicyRule } from "@/api/types"; + +/** + * Returns true if any rule in the list grants `verb` on `resource` in `apiGroup`. + * Wildcards ("*") in verbs, resources, or apiGroups are honoured. + * + * When `apiGroup` is omitted the check is group-agnostic (matches any rule that + * grants the verb+resource regardless of group). + */ +export function canDo( + rules: PolicyRule[], + verb: string, + resource: string, + apiGroup?: string, +): boolean { + for (const rule of rules) { + const verbMatch = + rule.verbs.includes("*") || rule.verbs.includes(verb); + const resourceMatch = + rule.resources.includes("*") || rule.resources.includes(resource); + const groupMatch = + apiGroup === undefined || + rule.apiGroups.includes("*") || + rule.apiGroups.includes(apiGroup); + + if (verbMatch && resourceMatch && groupMatch) { + return true; + } + } + return false; +} + +export type UsePermissionsResult = + | { ready: false; isAdmin: false; can: () => false } + | { + ready: true; + incomplete: boolean; + isAdmin: boolean; + /** Check whether the current user may perform `verb` on `resource`. */ + can: (verb: string, resource: string, apiGroup?: string) => boolean; + }; + +/** + * Fetches and exposes the current user's RBAC permissions for `namespace`. + */ +export function usePermissions(namespace: string): UsePermissionsResult { + const { data } = useQuery(permissionQueries.rules(namespace)); + const { data: user, isPending } = useQuery(authQueries.me()); + // isAdmin is determined by the BFF + const isAdmin = user?.isAdmin ?? false; + + if (!data || isPending) { + return { ready: false, isAdmin: false, can: () => false }; + } + + return { + ready: true, + isAdmin, + incomplete: data.incomplete, + can: (verb, resource, apiGroup) => + canDo(data.rules, verb, resource, apiGroup), + }; +} + +/** + * Checks permissions from an already-fetched PermissionsResponse. + * Used in TanStack Router `beforeLoad` where we have the raw data. + */ +export function permissionsFromResponse( + data: PermissionsResponse, +): (verb: string, resource: string, apiGroup?: string) => boolean { + return (verb, resource, apiGroup) => + canDo(data.rules, verb, resource, apiGroup); +} diff --git a/web/src/hooks/useSSE.ts b/web/src/hooks/useSSE.ts new file mode 100644 index 00000000..08bd18c3 --- /dev/null +++ b/web/src/hooks/useSSE.ts @@ -0,0 +1,29 @@ +import { useEffect } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import type { ResourceEvent } from "@/api/types"; + +export function useSSE(namespace: string) { + const queryClient = useQueryClient(); + + useEffect(() => { + const source = new EventSource(`/api/namespaces/${namespace}/events`); + + source.onmessage = (event) => { + try { + const data: ResourceEvent = JSON.parse(event.data); + // Invalidate the relevant query when a resource changes + queryClient.invalidateQueries({ + queryKey: [data.resource, data.namespace], + }); + } catch { + // ignore parse errors + } + }; + + source.onerror = () => { + // EventSource auto-reconnects + }; + + return () => source.close(); + }, [namespace, queryClient]); +} diff --git a/web/src/hooks/useTheme.ts b/web/src/hooks/useTheme.ts new file mode 100644 index 00000000..f2b63c86 --- /dev/null +++ b/web/src/hooks/useTheme.ts @@ -0,0 +1,42 @@ +import { useState, useEffect, useCallback } from "react"; + +type Theme = "light" | "dark" | "system"; + +function getSystemTheme(): "light" | "dark" { + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; +} + +function applyTheme(theme: Theme) { + const resolved = theme === "system" ? getSystemTheme() : theme; + document.documentElement.classList.toggle("dark", resolved === "dark"); +} + +export function useTheme() { + const [theme, setThemeState] = useState(() => { + const stored = localStorage.getItem("solar-theme") as Theme | null; + + return stored ?? "system"; + }); + + const setTheme = useCallback((t: Theme) => { + setThemeState(t); + localStorage.setItem("solar-theme", t); + applyTheme(t); + }, []); + + useEffect(() => { + applyTheme(theme); + + if (theme === "system") { + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + const handler = () => applyTheme("system"); + mq.addEventListener("change", handler); + + return () => mq.removeEventListener("change", handler); + } + }, [theme]); + + return { theme, setTheme } as const; +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 00000000..284481f7 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,74 @@ +@import "tailwindcss"; + +@custom-variant dark (&:where(.dark, .dark *)); + +@theme { + /* Light mode (default) — warm amber palette inspired by the SolAr logo */ + --color-background: oklch(0.985 0.002 80); + --color-foreground: oklch(0.18 0.04 55); + --color-card: oklch(0.995 0.001 80); + --color-card-foreground: oklch(0.18 0.04 55); + --color-popover: oklch(0.995 0.001 80); + --color-popover-foreground: oklch(0.18 0.04 55); + --color-primary: oklch(0.65 0.17 55); + --color-primary-foreground: oklch(0.995 0.001 80); + --color-secondary: oklch(0.955 0.015 75); + --color-secondary-foreground: oklch(0.30 0.04 55); + --color-muted: oklch(0.955 0.015 75); + --color-muted-foreground: oklch(0.50 0.03 55); + --color-accent: oklch(0.94 0.025 70); + --color-accent-foreground: oklch(0.30 0.04 55); + --color-destructive: oklch(0.577 0.245 27.325); + --color-destructive-foreground: oklch(0.985 0 0); + --color-border: oklch(0.91 0.02 75); + --color-input: oklch(0.91 0.02 75); + --color-ring: oklch(0.65 0.17 55); + --color-success: oklch(0.627 0.194 149.214); + --color-warning: oklch(0.769 0.188 70.08); + --color-sidebar: oklch(0.97 0.012 75); + --color-sidebar-foreground: oklch(0.30 0.04 55); + --color-sidebar-border: oklch(0.91 0.02 75); + --color-sidebar-active: oklch(0.65 0.17 55); + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; +} + +/* Dark mode colors */ +.dark { + --color-background: oklch(0.16 0.02 55); + --color-foreground: oklch(0.93 0.01 75); + --color-card: oklch(0.20 0.02 55); + --color-card-foreground: oklch(0.93 0.01 75); + --color-popover: oklch(0.20 0.02 55); + --color-popover-foreground: oklch(0.93 0.01 75); + --color-primary: oklch(0.75 0.16 65); + --color-primary-foreground: oklch(0.16 0.02 55); + --color-secondary: oklch(0.24 0.02 55); + --color-secondary-foreground: oklch(0.85 0.02 75); + --color-muted: oklch(0.24 0.02 55); + --color-muted-foreground: oklch(0.60 0.03 70); + --color-accent: oklch(0.26 0.03 55); + --color-accent-foreground: oklch(0.90 0.01 75); + --color-destructive: oklch(0.60 0.22 25); + --color-destructive-foreground: oklch(0.93 0.01 75); + --color-border: oklch(0.28 0.02 55); + --color-input: oklch(0.28 0.02 55); + --color-ring: oklch(0.75 0.16 65); + --color-sidebar: oklch(0.18 0.02 55); + --color-sidebar-foreground: oklch(0.80 0.02 75); + --color-sidebar-border: oklch(0.26 0.02 55); + --color-sidebar-active: oklch(0.75 0.16 65); +} + +@layer base { + body { + font-family: + "Inter", + system-ui, + -apple-system, + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } +} diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts new file mode 100644 index 00000000..bcc60c29 --- /dev/null +++ b/web/src/lib/utils.ts @@ -0,0 +1,31 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; +import type { Condition } from "@/api/types"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function getCondition( + conditions: Condition[] | undefined, + type: string, +): Condition | undefined { + return conditions?.find((c) => c.type === type); +} + +export function isReady(conditions: Condition[] | undefined): boolean { + const ready = getCondition(conditions, "Ready"); + return ready?.status === "True"; +} + +export function formatAge(timestamp: string): string { + const diff = Date.now() - new Date(timestamp).getTime(); + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h`; + const days = Math.floor(hours / 24); + return `${days}d`; +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 00000000..4f4cb642 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,34 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { RouterProvider, createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree"; +import "./index.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + refetchOnWindowFocus: true, + }, + }, +}); + +const router = createRouter({ + routeTree, + context: { queryClient }, +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/web/src/pages/components/index.tsx b/web/src/pages/components/index.tsx new file mode 100644 index 00000000..f654076d --- /dev/null +++ b/web/src/pages/components/index.tsx @@ -0,0 +1,89 @@ +import { useQuery } from "@tanstack/react-query"; +import { componentQueries, componentVersionQueries } from "@/api/queries"; +import { Card, CardTitle, CardContent } from "@/components/ui/card"; +import { useSSE } from "@/hooks/useSSE"; +import { formatAge } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Boxes } from "lucide-react"; + +const namespace = "default"; + +export function ComponentsPage() { + useSSE(namespace); + + const { data, isLoading } = useQuery(componentQueries.list(namespace)); + const { data: versionsData } = useQuery( + componentVersionQueries.list(namespace), + ); + + if (isLoading) { + return ( +
+ + Loading components... +
+ ); + } + + const components = data?.items ?? []; + const versions = versionsData?.items ?? []; + + return ( +
+
+

Components

+ + {components.length} component{components.length !== 1 ? "s" : ""} + +
+ + {components.length === 0 ? ( + + +
+ +

+ No components discovered yet +

+
+
+
+ ) : ( +
+ {components.map((comp) => { + const compVersions = versions.filter( + (cv) => cv.spec.componentRef.name === comp.metadata.name, + ); + + return ( + +
+ + {comp.spec.repository} + +

+ Registry:{" "} + {comp.spec.registry} + {" | "} + Scheme: {comp.spec.scheme} + {" | "} + Age: {formatAge(comp.metadata.creationTimestamp)} +

+ {compVersions.length > 0 && ( +
+ {compVersions.map((cv) => ( + + {cv.spec.tag} + + ))} +
+ )} +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/web/src/pages/dashboard/index.tsx b/web/src/pages/dashboard/index.tsx new file mode 100644 index 00000000..abe3f9fd --- /dev/null +++ b/web/src/pages/dashboard/index.tsx @@ -0,0 +1,110 @@ +import { useQuery } from "@tanstack/react-query"; +import { + targetQueries, + releaseQueries, + componentQueries, + renderTaskQueries, +} from "@/api/queries"; +import { Card, CardTitle, CardContent } from "@/components/ui/card"; +import { useSSE } from "@/hooks/useSSE"; +import { Server, Package, Boxes, Loader } from "lucide-react"; + +const namespace = "default"; // TODO: namespace selector + +export function DashboardPage() { + useSSE(namespace); + + const targets = useQuery(targetQueries.list(namespace)); + const releases = useQuery(releaseQueries.list(namespace)); + const components = useQuery(componentQueries.list(namespace)); + const renderTasks = useQuery(renderTaskQueries.list(namespace)); + + const stats = [ + { + label: "Targets", + value: targets.data?.items.length ?? 0, + icon: Server, + loading: targets.isLoading, + color: "text-blue-600 dark:text-blue-400", + bg: "bg-blue-50 dark:bg-blue-500/10", + }, + { + label: "Releases", + value: releases.data?.items.length ?? 0, + icon: Package, + loading: releases.isLoading, + color: "text-primary", + bg: "bg-primary/10", + }, + { + label: "Components", + value: components.data?.items.length ?? 0, + icon: Boxes, + loading: components.isLoading, + color: "text-emerald-600 dark:text-emerald-400", + bg: "bg-emerald-50 dark:bg-emerald-500/10", + }, + { + label: "Active Renders", + value: renderTasks.data?.items.length ?? 0, + icon: Loader, + loading: renderTasks.isLoading, + color: "text-violet-600 dark:text-violet-400", + bg: "bg-violet-50 dark:bg-violet-500/10", + }, + ]; + + return ( +
+

Dashboard

+ +
+ {stats.map(({ label, value, icon: Icon, loading, color, bg }) => ( + +
+
+

+ {label} +

+ +

+ {loading ? "-" : value} +

+
+
+
+ +
+
+
+ ))} +
+ + {/* Recent render tasks */} + {renderTasks.data && renderTasks.data.items.length > 0 && ( + + Recent Render Tasks + +
+ {renderTasks.data.items.slice(0, 10).map((rt) => ( +
+
+

+ {rt.metadata.name} +

+

+ {rt.spec.type} +

+
+
+ ))} +
+
+
+ )} +
+ ); +} diff --git a/web/src/pages/profiles/index.tsx b/web/src/pages/profiles/index.tsx new file mode 100644 index 00000000..f00d215f --- /dev/null +++ b/web/src/pages/profiles/index.tsx @@ -0,0 +1,91 @@ +import { useQuery } from "@tanstack/react-query"; +import { profileQueries } from "@/api/queries"; +import { Card, CardTitle, CardContent } from "@/components/ui/card"; +import { StatusBadge } from "@/components/ui/status-badge"; +import { useSSE } from "@/hooks/useSSE"; +import { formatAge } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Users } from "lucide-react"; + +const namespace = "default"; + +export function ProfilesPage() { + useSSE(namespace); + + const { data, isLoading } = useQuery(profileQueries.list(namespace)); + + if (isLoading) { + return ( +
+ + Loading profiles... +
+ ); + } + + const profiles = data?.items ?? []; + + return ( +
+
+

Profiles

+ + {profiles.length} profile{profiles.length !== 1 ? "s" : ""} + +
+ + {profiles.length === 0 ? ( + + +
+ +

No profiles found

+
+
+
+ ) : ( +
+ {profiles.map((profile) => ( + +
+
+ + {profile.metadata.name} + +

+ Release:{" "} + + {profile.spec.releaseRef.name} + + {" | "} + Age: {formatAge(profile.metadata.creationTimestamp)} + {profile.status?.matchedTargets !== undefined && ( + <> + {" | "} + {profile.status.matchedTargets} target + {profile.status.matchedTargets !== 1 ? "s" : ""}{" "} + matched + + )} +

+ {profile.spec.targetSelector.matchLabels && ( +
+ {Object.entries( + profile.spec.targetSelector.matchLabels, + ).map(([k, v]) => ( + + {k}={v} + + ))} +
+ )} +
+ +
+
+ ))} +
+ )} +
+ ); +} diff --git a/web/src/pages/releases/index.tsx b/web/src/pages/releases/index.tsx new file mode 100644 index 00000000..7a7b935e --- /dev/null +++ b/web/src/pages/releases/index.tsx @@ -0,0 +1,74 @@ +import { useQuery } from "@tanstack/react-query"; +import { releaseQueries } from "@/api/queries"; +import { Card, CardTitle, CardContent } from "@/components/ui/card"; +import { StatusBadge } from "@/components/ui/status-badge"; +import { useSSE } from "@/hooks/useSSE"; +import { formatAge } from "@/lib/utils"; +import { Package } from "lucide-react"; + +const namespace = "default"; + +export function ReleasesPage() { + useSSE(namespace); + + const { data, isLoading } = useQuery(releaseQueries.list(namespace)); + + if (isLoading) { + return ( +
+ + Loading releases... +
+ ); + } + + const releases = data?.items ?? []; + + return ( +
+
+

Releases

+ + {releases.length} release{releases.length !== 1 ? "s" : ""} + +
+ + {releases.length === 0 ? ( + + +
+ +

No releases found

+
+
+
+ ) : ( +
+ {releases.map((release) => ( + +
+
+ + {release.metadata.name} + +

+ ComponentVersion:{" "} + + {release.spec.componentVersionRef.name} + + {" | "} + Age: {formatAge(release.metadata.creationTimestamp)} +

+
+ +
+
+ ))} +
+ )} +
+ ); +} diff --git a/web/src/pages/targets/index.tsx b/web/src/pages/targets/index.tsx new file mode 100644 index 00000000..d6464c79 --- /dev/null +++ b/web/src/pages/targets/index.tsx @@ -0,0 +1,90 @@ +import { useQuery } from "@tanstack/react-query"; +import { targetQueries, releaseBindingQueries } from "@/api/queries"; +import { Card, CardTitle, CardContent } from "@/components/ui/card"; +import { StatusBadge } from "@/components/ui/status-badge"; +import { useSSE } from "@/hooks/useSSE"; +import { formatAge } from "@/lib/utils"; +import { Server } from "lucide-react"; + +const namespace = "default"; // TODO: namespace selector + +export function TargetsPage() { + useSSE(namespace); + + const { data, isLoading } = useQuery(targetQueries.list(namespace)); + const { data: bindings } = useQuery( + releaseBindingQueries.list(namespace), + ); + + if (isLoading) { + return ( +
+ + Loading targets... +
+ ); + } + + const targets = data?.items ?? []; + + return ( +
+
+

Targets

+ + {targets.length} target{targets.length !== 1 ? "s" : ""} + +
+ + {targets.length === 0 ? ( + + +
+ +

+ No targets found in namespace “{namespace}” +

+
+
+
+ ) : ( +
+ {targets.map((target) => { + const targetBindings = + bindings?.items.filter( + (b) => b.spec.targetRef.name === target.metadata.name, + ) ?? []; + + return ( + +
+
+ + {target.metadata.name} + +

+ Registry:{" "} + + {target.spec.renderRegistryRef.name} + + {" | "} + Age: {formatAge(target.metadata.creationTimestamp)} + {targetBindings.length > 0 && ( + <> + {" | "} + {targetBindings.length} release + {targetBindings.length !== 1 ? "s" : ""} bound + + )} +

+
+ +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/web/src/routeTree.tsx b/web/src/routeTree.tsx new file mode 100644 index 00000000..57e3deb6 --- /dev/null +++ b/web/src/routeTree.tsx @@ -0,0 +1,107 @@ +import { + createRootRouteWithContext, + createRoute, + Outlet, + redirect, +} from "@tanstack/react-router"; +import type { QueryClient } from "@tanstack/react-query"; +import { Layout } from "@/components/layout"; +import { DashboardPage } from "@/pages/dashboard"; +import { TargetsPage } from "@/pages/targets"; +import { ReleasesPage } from "@/pages/releases"; +import { ComponentsPage } from "@/pages/components"; +import { ProfilesPage } from "@/pages/profiles"; +import { permissionQueries } from "@/api/queries"; +import { permissionsFromResponse } from "@/hooks/usePermissions"; + +interface RouterContext { + queryClient: QueryClient; +} + +// All guarded routes operate on this namespace. +// When a namespace selector is added this should come from router context. +const DEFAULT_NAMESPACE = "default"; + +const SOLAR_GROUP = "solar.opendefense.cloud"; + +const rootRoute = createRootRouteWithContext()({ + component: () => ( + + + + ), +}); + +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/", + component: DashboardPage, +}); + +const targetsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/targets", + beforeLoad: async ({ context }) => { + const data = await context.queryClient.ensureQueryData( + permissionQueries.rules(DEFAULT_NAMESPACE), + ); + const can = permissionsFromResponse(data); + if (!can("list", "targets", SOLAR_GROUP)) { + throw redirect({ to: "/" }); + } + }, + component: TargetsPage, +}); + +const releasesRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/releases", + beforeLoad: async ({ context }) => { + const data = await context.queryClient.ensureQueryData( + permissionQueries.rules(DEFAULT_NAMESPACE), + ); + const can = permissionsFromResponse(data); + if (!can("list", "releases", SOLAR_GROUP)) { + throw redirect({ to: "/" }); + } + }, + component: ReleasesPage, +}); + +const componentsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/components", + beforeLoad: async ({ context }) => { + const data = await context.queryClient.ensureQueryData( + permissionQueries.rules(DEFAULT_NAMESPACE), + ); + const can = permissionsFromResponse(data); + if (!can("list", "components", SOLAR_GROUP)) { + throw redirect({ to: "/" }); + } + }, + component: ComponentsPage, +}); + +const profilesRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/profiles", + beforeLoad: async ({ context }) => { + const data = await context.queryClient.ensureQueryData( + permissionQueries.rules(DEFAULT_NAMESPACE), + ); + const can = permissionsFromResponse(data); + if (!can("list", "profiles", SOLAR_GROUP)) { + throw redirect({ to: "/" }); + } + }, + component: ProfilesPage, +}); + +export const routeTree = rootRoute.addChildren([ + dashboardRoute, + targetsRoute, + releasesRoute, + componentsRoute, + profilesRoute, +]); diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 00000000..fb2cb37b --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 00000000..4afb0352 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import path from "path"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + build: { + outDir: "dist", + }, +});