From 506f2af1fdc1da3674b36566099e46f7141eaf65 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 12:06:07 +0200 Subject: [PATCH 01/22] feat: remove Helm charts and keep only operator-based deployment Remove all Helm-related code, configuration, CI steps, and documentation from the repository. The project has transitioned to OLM operator-based deployment, making Helm artifacts dead code. Changes: - Delete controller/deploy/helm/ directory (43 files) - Delete controller/hack/deploy_with_helm.sh and install_helm.sh - Update controller Makefile to generate CRDs/RBAC directly to operator config paths and remove Helm targets - Update Go test CRD paths to use operator config/crd/bases - Remove jumpstarter_kubernetes install.py, cluster/helm.py and their test files - Remove jumpstarter_cli_admin install.py and its test file - Update all __init__.py exports to remove Helm symbols - Update cluster/operations.py to always use operator install - Update cluster/kubectl.py to use CRD-only detection - Update CLI create.py and get.py to remove Helm options - Remove publish-helm-charts job from build-images.yaml - Remove lint-helm job from lint.yaml - Remove Helm from controller-kind.yaml CI matrix - Update e2e scripts to use operator-only deployment - Update documentation and rule files Closes #445 Generated-By: Forge/20260409_114748_239149_5e586097 Co-Authored-By: Claude Opus 4.6 --- .cursor/rules/working-with-operator.mdc | 1 - .github/workflows/build-images.yaml | 39 - .github/workflows/controller-kind.yaml | 9 +- .github/workflows/lint.yaml | 18 - controller/Makefile | 35 +- .../deploy/helm/jumpstarter/.helmignore | 23 - controller/deploy/helm/jumpstarter/Chart.yaml | 9 - .../charts/jumpstarter-controller/Chart.yaml | 7 - .../charts/jumpstarter-controller/model.py | 385 ----- .../templates/_endpoints.tpl | 2 - .../additional-router-deployment.yaml | 111 -- .../templates/additional-router-ingress.yaml | 47 - .../templates/additional-router-route.yaml | 42 - .../templates/additional-router-service.yaml | 27 - .../templates/cms/controller-cm.yaml | 42 - .../templates/controller-deployment.yaml | 144 -- .../templates/controller-ingress.yaml | 44 - .../templates/controller-route.yaml | 34 - .../templates/controller-service.yaml | 24 - .../crds/jumpstarter.dev_clients.yaml | 69 - ...umpstarter.dev_exporteraccesspolicies.yaml | 166 -- .../crds/jumpstarter.dev_exporters.yaml | 185 --- .../crds/jumpstarter.dev_leases.yaml | 256 --- .../templates/login-ingress.yaml | 36 - .../templates/login-route.yaml | 38 - .../templates/login-service.yaml | 26 - .../templates/metrics/metrics_service.yaml | 16 - .../templates/metrics/monitor.yaml | 20 - .../templates/rbac/client_editor_role.yaml | 26 - .../templates/rbac/client_viewer_role.yaml | 22 - .../templates/rbac/exporter_editor_role.yaml | 26 - .../templates/rbac/exporter_viewer_role.yaml | 22 - .../templates/rbac/leader_election_role.yaml | 40 - .../rbac/leader_election_role_binding.yaml | 15 - .../templates/rbac/role.yaml | 67 - .../templates/rbac/role_binding.yaml | 16 - .../templates/rbac/service_account.yaml | 9 - .../templates/router-deployment.yaml | 109 -- .../templates/router-ingress.yaml | 44 - .../templates/router-route.yaml | 39 - .../templates/router-service.yaml | 24 - .../templates/secrets-job.yaml | 36 - .../jumpstarter-controller/values.schema.json | 1458 ----------------- .../charts/jumpstarter-controller/values.yaml | 76 - controller/deploy/helm/jumpstarter/model.py | 68 - .../deploy/helm/jumpstarter/values.kind.yaml | 10 - .../helm/jumpstarter/values.schema.json | 233 --- .../deploy/helm/jumpstarter/values.yaml | 125 -- controller/deploy/operator/README.md | 18 - .../jumpstarter/jumpstarter_controller.go | 4 +- controller/hack/deploy_with_helm.sh | 74 - controller/hack/install_helm.sh | 17 - controller/hack/utils | 2 +- controller/internal/controller/suite_test.go | 2 +- controller/internal/service/suite_test.go | 2 +- e2e/compat/setup.sh | 41 +- e2e/setup-e2e.sh | 42 +- .../jumpstarter_cli_admin/__init__.py | 5 - .../jumpstarter_cli_admin/create.py | 37 - .../jumpstarter_cli_admin/create_test.py | 35 - .../jumpstarter_cli_admin/get.py | 12 +- .../jumpstarter_cli_admin/install.py | 255 --- .../jumpstarter_cli_admin/install_test.py | 137 -- .../jumpstarter_kubernetes/__init__.py | 8 - .../cluster/__init__.py | 8 - .../jumpstarter_kubernetes/cluster/common.py | 1 - .../jumpstarter_kubernetes/cluster/helm.py | 53 - .../cluster/helm_test.py | 243 --- .../jumpstarter_kubernetes/cluster/kubectl.py | 95 +- .../cluster/kubectl_test.py | 68 +- .../cluster/operations.py | 70 +- .../cluster/operations_test.py | 30 +- .../jumpstarter_kubernetes/exceptions.py | 2 +- .../jumpstarter_kubernetes/install.py | 91 - .../jumpstarter_kubernetes/install_test.py | 258 --- 75 files changed, 85 insertions(+), 5845 deletions(-) delete mode 100644 controller/deploy/helm/jumpstarter/.helmignore delete mode 100644 controller/deploy/helm/jumpstarter/Chart.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/Chart.yaml delete mode 100755 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/model.py delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/_endpoints.tpl delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-ingress.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-route.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-service.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/cms/controller-cm.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-deployment.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-ingress.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-route.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-service.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_clients.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporteraccesspolicies.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporters.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_leases.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-ingress.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-route.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-service.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/metrics/metrics_service.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/metrics/monitor.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/client_editor_role.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/client_viewer_role.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/exporter_editor_role.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/exporter_viewer_role.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-ingress.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-route.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-service.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/secrets-job.yaml delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/values.schema.json delete mode 100644 controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/values.yaml delete mode 100755 controller/deploy/helm/jumpstarter/model.py delete mode 100644 controller/deploy/helm/jumpstarter/values.kind.yaml delete mode 100644 controller/deploy/helm/jumpstarter/values.schema.json delete mode 100644 controller/deploy/helm/jumpstarter/values.yaml delete mode 100755 controller/hack/deploy_with_helm.sh delete mode 100755 controller/hack/install_helm.sh delete mode 100644 python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/install.py delete mode 100644 python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/install_test.py delete mode 100644 python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/helm.py delete mode 100644 python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/helm_test.py delete mode 100644 python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/install.py delete mode 100644 python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/install_test.py diff --git a/.cursor/rules/working-with-operator.mdc b/.cursor/rules/working-with-operator.mdc index 878c66c4f..b070569fb 100644 --- a/.cursor/rules/working-with-operator.mdc +++ b/.cursor/rules/working-with-operator.mdc @@ -25,7 +25,6 @@ controller/ │ │ ├── bundle/ # OLM bundle manifests │ │ ├── test/e2e/ # E2E tests │ │ └── Makefile # Operator-specific make targets -│ └── helm/ # Helm charts (alternative deployment) ├── api/ # Jumpstarter core CRDs (Client, Exporter, Lease, etc.) ├── internal/ # Controller business logic │ └── config/ # Config structs used by controller diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml index 3f123d183..9d786c2e3 100644 --- a/.github/workflows/build-images.yaml +++ b/.github/workflows/build-images.yaml @@ -273,42 +273,3 @@ jobs: }); } - publish-helm-charts: - needs: build-and-push-image - if: ${{ github.repository_owner == 'jumpstarter-dev' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/release-')) }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get version - run: | - VERSION=$(git describe --tags) - VERSION=${VERSION#v} # remove the leading v prefix for version - echo "VERSION=${VERSION}" >> $GITHUB_ENV - echo "VERSION=${VERSION}" - - - name: Build helm charts - run: | - echo packaging ${VERSION} - # patch the sub-chart app-version, because helm package won't do it - sed -i "s/^appVersion:.*/appVersion: $VERSION/" controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/Chart.yaml - helm package ./controller/deploy/helm/jumpstarter --version "${VERSION}" --app-version "${VERSION}" - - - name: Login helm - env: - PASSWORD: ${{ secrets.QUAY_TOKEN }} - USER: jumpstarter-dev+jumpstarter_ci - run: - helm registry login quay.io -u ${USER} -p ${PASSWORD} - - - name: Push helm charts - run: | - helm push jumpstarter-*.tgz oci://${{ env.QUAY_ORG }}/helm - - if [[ "${{ github.ref }}" == "refs/heads/release-*" ]]; then - RELEASE_BRANCH_NAME=$(basename "${{ github.ref }}") - helm chart save jumpstarter-*.tgz ${{ env.QUAY_ORG }}/helm:${RELEASE_BRANCH_NAME} - helm chart push ${{ env.QUAY_ORG }}/helm:${RELEASE_BRANCH_NAME} - fi diff --git a/.github/workflows/controller-kind.yaml b/.github/workflows/controller-kind.yaml index 59dee0e52..246f3d225 100644 --- a/.github/workflows/controller-kind.yaml +++ b/.github/workflows/controller-kind.yaml @@ -8,11 +8,6 @@ on: jobs: deploy-kind: - strategy: - matrix: - method: - - helm - - operator runs-on: ubuntu-latest steps: - name: Checkout repository @@ -20,11 +15,9 @@ jobs: with: fetch-depth: 0 - - name: Run make deploy (${{ matrix.method }}) + - name: Run make deploy working-directory: controller run: make deploy - env: - METHOD: ${{ matrix.method }} e2e-test-operator: runs-on: ubuntu-latest diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 56c7227cd..0df223888 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -19,7 +19,6 @@ jobs: runs-on: ubuntu-latest outputs: controller: ${{ steps.filter.outputs.controller }} - helm: ${{ steps.filter.outputs.helm }} protocol: ${{ steps.filter.outputs.protocol }} python: ${{ steps.filter.outputs.python }} steps: @@ -31,9 +30,6 @@ jobs: controller: - 'controller/**' - '.github/workflows/lint.yaml' - helm: - - 'controller/deploy/helm/**' - - '.github/workflow/lint.yaml' protocol: - 'protocol/**' - '.github/workflows/lint.yaml' @@ -60,20 +56,6 @@ jobs: working-directory: controller run: make lint - lint-helm: - needs: changes - if: needs.changes.outputs.helm == 'true' - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Run helm linter - working-directory: controller - run: make lint-helm - lint-protobuf: needs: changes if: needs.changes.outputs.protocol == 'true' diff --git a/controller/Makefile b/controller/Makefile index 08c9aa00e..b7da7c872 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -30,7 +30,7 @@ endif # tools. (i.e. podman) CONTAINER_TOOL ?= podman -# Deployment method: operator (default) or helm +# Deployment method METHOD ?= operator # Cluster type: kind (default) or k3s @@ -67,17 +67,8 @@ help: ## Display this help. .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=jumpstarter-manager-role crd webhook paths="./api/..." paths="./internal/..." \ - output:crd:artifacts:config=deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/ \ - output:rbac:artifacts:config=deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/ - - # Add ArgoCD sync-wave annotation to RBAC role for proper deployment ordering (PR #207) - @awk '/^ name: jumpstarter-manager-role$$/{print; print " annotations:"; print " argocd.argoproj.io/sync-wave: \"-1\""; next}1' \ - deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role.yaml > \ - deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role.yaml.tmp && \ - mv deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role.yaml.tmp \ - deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role.yaml - - cp deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/* deploy/operator/config/crd/bases/ + output:crd:artifacts:config=deploy/operator/config/crd/bases/ \ + output:rbac:artifacts:config=deploy/operator/config/rbac/ # Regenerate operator install.yaml to include updated CRDs $(MAKE) -C deploy/operator build-installer @@ -188,25 +179,12 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - .PHONY: deploy -deploy: cluster grpcurl ## Deploy controller using METHOD (operator or helm). Set SKIP_BUILD=1 to skip image builds. +deploy: cluster grpcurl ## Deploy controller using the operator. Set SKIP_BUILD=1 to skip image builds. Set CLUSTER_TYPE=k3s for k3s. ifeq ($(SKIP_BUILD),) $(MAKE) docker-build -endif -ifeq ($(METHOD),operator) -ifeq ($(SKIP_BUILD),) $(MAKE) build-operator endif ./hack/deploy_with_operator.sh -else ifeq ($(METHOD),helm) - ./hack/deploy_with_helm.sh -else - $(error Unknown METHOD=$(METHOD). Use 'operator' or 'helm') -endif - -# Backward compatibility alias -.PHONY: deploy-with-operator -deploy-with-operator: - $(MAKE) deploy METHOD=operator .PHONY: deploy-operator deploy-operator: docker-build build-operator cluster grpcurl ## Deploy only the operator (without Jumpstarter CR) @@ -227,11 +205,6 @@ deploy-with-operator-parallel: deploy-exporters: ./hack/demoenv/prepare_exporters.sh -.PHONY: lint-helm -lint-helm: - helm lint deploy/helm/jumpstarter - - .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - diff --git a/controller/deploy/helm/jumpstarter/.helmignore b/controller/deploy/helm/jumpstarter/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/controller/deploy/helm/jumpstarter/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/controller/deploy/helm/jumpstarter/Chart.yaml b/controller/deploy/helm/jumpstarter/Chart.yaml deleted file mode 100644 index c6cbd9782..000000000 --- a/controller/deploy/helm/jumpstarter/Chart.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v2 -name: jumpstarter -description: A helm chart for the jumpstarter project -type: application -version: 0.1.0 -appVersion: "0.1.0" -dependencies: - - name: jumpstarter-controller - condition: jumpstarter-controller.enabled \ No newline at end of file diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/Chart.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/Chart.yaml deleted file mode 100644 index 3c297a894..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v2 -name: jumpstarter-controller -description: A helm chart for jumpstarter-controller -type: application -version: 0.0.1 -appVersion: 0.0.1 - diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/model.py b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/model.py deleted file mode 100755 index e9d789d7d..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/model.py +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.12" -# dependencies = ["pydantic"] -# /// - -from __future__ import annotations - -import json - -from enum import Enum -from typing import List, Optional, Union - -from pydantic import BaseModel, ConfigDict, Field, RootModel, conint - - -class Provisioning(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = None - - -class Internal(BaseModel): - model_config = ConfigDict(extra="forbid") - - prefix: Optional[str] = None - - -class Keepalive(BaseModel): - model_config = ConfigDict(extra="forbid") - - minTime: Optional[str] = Field( - None, - description="The minimum amount of time a client should wait before sending a keepalive ping", - ) - permitWithoutStream: Optional[bool] = Field( - None, - description="Whether to allow keepalive pings even when there are no active streams(RPCs)", - ) - - -class Grpc(BaseModel): - model_config = ConfigDict(extra="forbid") - - keepalive: Optional[Keepalive] = None - - -class Metrics(BaseModel): - enabled: Optional[bool] = None - - -class Global(BaseModel): - baseDomain: Optional[str] = Field( - None, description="Base domain to construct the FQDN for the service endpoints" - ) - metrics: Optional[Metrics] = None - - -class Mode(Enum): - ingress = "ingress" - route = "route" - nodeport = "nodeport" - external = "external" # Disable ingress and route generation - - -class Mode1(Enum): - passthrough = "passthrough" - reencrypt = "reencrypt" - - -class Port(RootModel): - root: conint(ge=0, le=65535) - - -class Ingress(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = Field( - None, description="Whether to enable Ingress for the gRPC endpoint" - ) - class_: Optional[str] = Field( - None, alias="class", description="IngressClass to use for the gRPC endpoint" - ) - - -class Route(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = Field( - None, description="Whether to enable OpenShift Router for the gRPC endpoint" - ) - - -class LoginRoute(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = Field( - None, description="Whether to enable OpenShift Route for the login endpoint" - ) - annotations: Optional[dict[str, str]] = Field( - None, description="Annotations for the login route" - ) - labels: Optional[dict[str, str]] = Field( - None, description="Labels for the login route" - ) - - -class LoginIngressTls(BaseModel): - model_config = ConfigDict(extra="forbid") - - secretName: Optional[str] = Field( - None, description="Secret name for the TLS certificate" - ) - - -class LoginIngress(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = Field( - None, description="Whether to enable Ingress for the login endpoint" - ) - class_: Optional[str] = Field( - None, alias="class", description="IngressClass to use for the login endpoint" - ) - annotations: Optional[dict[str, str]] = Field( - None, description="Annotations for the login ingress" - ) - labels: Optional[dict[str, str]] = Field( - None, description="Labels for the login ingress" - ) - tls: Optional[LoginIngressTls] = None - - -class LoginNodeport(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = Field( - None, description="Whether to enable NodePort for the login endpoint" - ) - port: Optional[Port] = Field( - None, description="NodePort port number for the login service" - ) - - -class LoginTls(BaseModel): - model_config = ConfigDict(extra="forbid") - - secretName: Optional[str] = Field( - None, - description="Name of the Kubernetes secret containing tls.crt and tls.key for edge TLS termination", - ) - - -class Login(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = Field( - None, description="Whether to enable the login endpoint for simplified CLI login" - ) - hostname: Optional[str] = Field( - None, description="Hostname for the login endpoint" - ) - endpoint: Optional[str] = Field( - None, description="The endpoint URL to display in the login landing page" - ) - tls: Optional[LoginTls] = Field( - None, - description="TLS configuration for edge termination (used by both route and ingress)", - ) - route: Optional[LoginRoute] = None - ingress: Optional[LoginIngress] = None - nodeport: Optional[LoginNodeport] = None - - -class PrefixedClaimOrExpression1(BaseModel): - model_config = ConfigDict(extra="forbid") - - claim: str - prefix: str - - -class PrefixedClaimOrExpression2(BaseModel): - model_config = ConfigDict(extra="forbid") - - expression: str - - -class PrefixedClaimOrExpression(RootModel): - root: Union[PrefixedClaimOrExpression1, PrefixedClaimOrExpression2] - - -class ClaimOrExpression1(BaseModel): - model_config = ConfigDict(extra="forbid") - - claim: str - expression: Optional[str] = None - - -class ClaimOrExpression2(BaseModel): - model_config = ConfigDict(extra="forbid") - - claim: Optional[str] = None - expression: str - - -class ClaimOrExpression(RootModel): - root: Union[ClaimOrExpression1, ClaimOrExpression2] - - -class AudienceMatchPolicy(Enum): - MatchAny = "MatchAny" - - -class Issuer(BaseModel): - model_config = ConfigDict(extra="forbid") - - url: Optional[str] = None - discoveryURL: Optional[str] = None - certificateAuthority: Optional[str] = None - audiences: Optional[List[str]] = None - audienceMatchPolicy: Optional[AudienceMatchPolicy] = None - - -class ClaimValidationRule(BaseModel): - model_config = ConfigDict(extra="forbid") - - claim: Optional[str] = None - requiredValue: Optional[str] = None - expression: Optional[str] = None - message: Optional[str] = None - - -class ExtraItem(BaseModel): - model_config = ConfigDict(extra="forbid") - - key: Optional[str] = None - valueExpression: Optional[str] = None - - -class ClaimMappings(BaseModel): - model_config = ConfigDict(extra="forbid") - - username: Optional[PrefixedClaimOrExpression] = None - groups: Optional[PrefixedClaimOrExpression] = None - uid: Optional[ClaimOrExpression] = None - extra: Optional[List[ExtraItem]] = None - - -class UserValidationRule(BaseModel): - model_config = ConfigDict(extra="forbid") - - expression: Optional[str] = None - message: Optional[str] = None - - -class JWTAuthenticator(BaseModel): - model_config = ConfigDict(extra="forbid") - - issuer: Optional[Issuer] = None - claimValidationRules: Optional[List[ClaimValidationRule]] = None - claimMappings: Optional[ClaimMappings] = None - userValidationRules: Optional[List[UserValidationRule]] = None - - -class Authentication(BaseModel): - model_config = ConfigDict(extra="forbid") - - internal: Optional[Internal] = None - jwt: Optional[List[JWTAuthenticator]] = Field( - None, - description="External OIDC authentication, see https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-authentication-configuration for documentation", - ) - - -class JumpstarterConfig(BaseModel): - model_config = ConfigDict(extra="forbid") - - provisioning: Optional[Provisioning] = None - authentication: Optional[Authentication] = None - grpc: Optional[Grpc] = None - - -class Nodeport(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = None - port: Optional[Port] = None - routerPort: Optional[Port] = None - - -class Tls(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = None - secret: Optional[str] = None - controllerCertSecret: Optional[str] = Field( - None, - description="Secret containing the TLS certificate/key for the gRPC controller endpoint", - ) - routerCertSecret: Optional[str] = Field( - None, - description="Secret containing the TLS certificate/key for the gRPC router endpoints", - ) - port: Optional[Port] = Field( - None, - description="Port to use for the gRPC endpoints Ingress or Route, this can be useful for ingress routers on non-standard ports", - ) - mode: Optional[Mode1] = Field(None, description="TLS mode for gRPC endpoints") - - -class Grpc1(BaseModel): - model_config = ConfigDict(extra="forbid") - - hostname: Optional[str] = Field( - None, description="Hostname for the controller to use for the controller gRPC" - ) - routerHostname: Optional[str] = Field( - None, description="Hostname for the router to use for the router gRPC" - ) - endpoint: Optional[str] = Field( - None, - description="The endpoints are passed down to the services to know where to announce the endpoints to the clients", - ) - routerEndpoint: Optional[str] = Field( - None, - description="The endpoints are passed down to the services to know where to announce the endpoints to the clients", - ) - additionalRouters: dict[str, Router] | None = Field( - None, description="Additional routers to deploy" - ) - ingress: Optional[Ingress] = None - route: Optional[Route] = None - nodeport: Optional[Nodeport] = None - mode: Optional[Mode] = None - tls: Optional[Tls] = None - - -class Router(BaseModel): - model_config = ConfigDict(extra="forbid") - hostname: str | None = None - endpoint: str | None = None - labels: dict[str, str] | None = None - nodeSelector: dict[str, str] | None = None - - -class CertManager(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = Field( - None, - description="Enable cert-manager integration. When enabled, jumpstarter-service-ca-cert configmap is required.", - ) - - -class Model(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = Field( - None, description="Whether to enable jumpstarter controller" - ) - authenticationConfig: Optional[str] = None - config: Optional[JumpstarterConfig] = None - namespace: Optional[str] = Field( - None, - description="Namespace where the controller will be deployed, defaults to global.namespace", - ) - image: str = Field(..., description="Image for the controller") - tag: Optional[str] = Field(None, description="Image tag for the controller") - imagePullPolicy: str = Field( - ..., description="Image pull policy for the controller" - ) - global_: Optional[Global] = Field( - None, alias="global", description="Global parameters" - ) - certManager: Optional[CertManager] = Field( - None, - description="cert-manager integration for automatic TLS certificate management", - ) - grpc: Optional[Grpc1] = None - login: Optional[Login] = Field( - None, description="Login endpoint configuration for simplified CLI login" - ) - - -print(json.dumps(Model.model_json_schema(), indent=2)) diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/_endpoints.tpl b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/_endpoints.tpl deleted file mode 100644 index a925e71cb..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/_endpoints.tpl +++ /dev/null @@ -1,2 +0,0 @@ -{{- define "router.endpoint" }}{{ if .Values.grpc.routerHostname }}{{ .Values.grpc.routerHostname }}{{ else }}router.{{ .Values.global.baseDomain | required "grpc.routerHostname or global.baseDomain must be set"}}{{ end }}{{- end }} -{{- define "controller.endpoint" }}{{ if .Values.grpc.hostname }}{{ .Values.grpc.hostname }}{{ else }}grpc.{{ .Values.global.baseDomain | required "grpc.hostname or global.baseDomain must be set"}}{{ end }}{{- end }} \ No newline at end of file diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml deleted file mode 100644 index 49cc02726..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml +++ /dev/null @@ -1,111 +0,0 @@ -{{ range $k, $v := .Values.grpc.additionalRouters }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: jumpstarter-router-{{ $k }} - namespace: {{ default $.Release.Namespace $.Values.namespace }} - labels: - control-plane: controller-router-{{ $k }} - app.kubernetes.io/name: jumpstarter-controller - {{ if $.Values.global.timestamp }} - deployment.timestamp: {{ $.Values.global.timestamp | quote }} - {{ end }} - annotations: - argocd.argoproj.io/sync-wave: "1" -spec: - selector: - matchLabels: - control-plane: controller-router-{{ $k }} - replicas: 1 - template: - metadata: - annotations: - kubectl.kubernetes.io/default-container: router - configmap-sha256: {{ include (print $.Template.BasePath "/cms/controller-cm.yaml") $ | sha256sum }} - labels: - control-plane: controller-router-{{ $k }} - {{ if $.Values.global.timestamp }} - deployment.timestamp: {{ $.Values.global.timestamp | quote }} - {{ end }} - spec: - # TODO(user): Uncomment the following code to configure the nodeAffinity expression - # according to the platforms which are supported by your solution. - # It is considered best practice to support multiple architectures. You can - # build your manager image using the makefile target docker-buildx. - # affinity: - # nodeAffinity: - # requiredDuringSchedulingIgnoredDuringExecution: - # nodeSelectorTerms: - # - matchExpressions: - # - key: kubernetes.io/arch - # operator: In - # values: - # - amd64 - # - arm64 - # - ppc64le - # - s390x - # - key: kubernetes.io/os - # operator: In - # values: - # - linux - {{ if $v.nodeSelector }} - nodeSelector: - {{ $v.nodeSelector | toYaml | indent 1 }} - {{ end }} - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - command: - - /router - env: - - name: GRPC_ROUTER_ENDPOINT - {{ if $v.endpoint }} - value: {{ $v.endpoint }} - {{ else if $v.hostname }} - value: {{ $v.hostname }}:{{ default 443 $.Values.grpc.tls.port }} - {{ else }} - value: router-{{ $k }}.{{ $.Values.global.baseDomain | required "set .global.baseDomain, or provide grpc.additionalRouters[...].endpoint/hostname" }}:{{ default 443 $.Values.grpc.tls.port }} - {{ end }} - - name: ROUTER_KEY - valueFrom: - secretKeyRef: - name: jumpstarter-router-secret - key: key - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - image: {{ $.Values.image }}:{{ default $.Chart.AppVersion $.Values.tag }} - imagePullPolicy: {{ $.Values.imagePullPolicy }} - name: router - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - "ALL" - # livenessProbe: - # httpGet: - # path: /healthz - # port: 8081 - # initialDelaySeconds: 15 - # periodSeconds: 20 - # readinessProbe: - # httpGet: - # path: /readyz - # port: 8081 - # initialDelaySeconds: 5 - # periodSeconds: 10 - resources: - limits: - cpu: 2000m - memory: 1024Mi - requests: - cpu: 1000m - memory: 256Mi - serviceAccountName: controller-manager - terminationGracePeriodSeconds: 10 -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-ingress.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-ingress.yaml deleted file mode 100644 index 92522341e..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-ingress.yaml +++ /dev/null @@ -1,47 +0,0 @@ -{{ if eq .Values.grpc.mode "ingress" }} -{{ range $k, $v := .Values.grpc.additionalRouters }} ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx.ingress.kubernetes.io/backend-protocol: "GRPC" - nginx.ingress.kubernetes.io/proxy-read-timeout: "300" - nginx.ingress.kubernetes.io/proxy-send-timeout: "300" - {{ if eq $.Values.grpc.tls.mode "passthrough" }} - nginx.ingress.kubernetes.io/ssl-passthrough: "true" - {{ end }} - name: jumpstarter-router-ingress-{{ $k }} - namespace: {{ default $.Release.Namespace $.Values.namespace }} -spec: - {{ if $.Values.grpc.ingress.class }} - ingressClassName: {{ $.Values.grpc.ingress.class }} - {{ end }} - rules: - {{ if $v.hostname }} - - host: {{ $v.hostname }} - {{ else }} - - host: router-{{ $k }}.{{ $.Values.global.baseDomain | required "a global.baseDomain or a grpc.routerHostname must be provided"}} - {{ end }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: jumpstarter-router-grpc-{{ $k }} - port: - number: 8083 - tls: - - hosts: - {{ if $v.hostname }} - - {{ $v.hostname }} - {{ else }} - - router-{{ $k }}.{{ $.Values.global.baseDomain | required "a global.baseDomain or a grpc.routerHostname must be provided"}} - {{ end }} - {{ if $.Values.grpc.tls.routerCertSecret }} - secretName: {{ $.Values.grpc.tls.routerCertSecret }} - {{ end }} -{{ end }} -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-route.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-route.yaml deleted file mode 100644 index b001804a8..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-route.yaml +++ /dev/null @@ -1,42 +0,0 @@ -{{ if eq .Values.grpc.mode "route" }} -{{ range $k, $v := .Values.grpc.additionalRouters }} ---- -apiVersion: route.openshift.io/v1 -kind: Route -metadata: - labels: - external-exposed: "true" - shard: external - annotations: - haproxy.router.openshift.io/timeout: 2d - haproxy.router.openshift.io/timeout-tunnel: 2d - name: jumpstarter-router-route-{{ $k }} - namespace: {{ default $.Release.Namespace $.Values.namespace }} -spec: - {{ if $v.hostname }} - host: {{ $v.hostname }} - {{ else }} - host: router-{{ $k }}.{{ $.Values.global.baseDomain | required "a global.baseDomain or a grpc.routerHostname must be provided"}} - {{ end }} - port: - targetPort: 8083 - tls: - {{ if eq $.Values.grpc.tls.mode "passthrough" }} - termination: passthrough - {{ end }} - {{ if eq $.Values.grpc.tls.mode "reencrypt" }} - termination: reencrypt - {{ end }} - insecureEdgeTerminationPolicy: None - {{ if $.Values.grpc.tls.routerCertSecret }} - externalCertificate: - name: {{ $.Values.grpc.tls.routerCertSecret }} - {{ end }} - - to: - kind: Service - name: jumpstarter-router-grpc-{{ $k }} - weight: 100 - wildcardPolicy: None -{{ end }} -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-service.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-service.yaml deleted file mode 100644 index 0b0de609b..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-service.yaml +++ /dev/null @@ -1,27 +0,0 @@ -{{ range $k, $v := .Values.grpc.additionalRouters }} ---- -apiVersion: v1 -kind: Service -metadata: - labels: - control-plane: controller-router-{{ $k }} - app.kubernetes.io/name: jumpstarter-controller - name: jumpstarter-router-grpc-{{ $k }} - namespace: {{ default $.Release.Namespace $.Values.namespace }} -spec: - {{ if .Values.grpc.nodeport.enabled }} - type: NodePort - {{ end }} - - ports: - - name: grpc - port: 8083 - protocol: TCP - targetPort: 8083 - appProtocol: h2c # HTTP/2 over cleartext for gRPC (fixes edge termination in ingress/router) - {{ if .Values.grpc.nodeport.enabled }} - nodePort: {{ .Values.grpc.nodeport.routerPort }} - {{ end }} - selector: - control-plane: controller-router-{{ $k }} -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/cms/controller-cm.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/cms/controller-cm.yaml deleted file mode 100644 index 7f75608d9..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/cms/controller-cm.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: jumpstarter-controller - namespace: {{ default .Release.Namespace .Values.namespace }} - labels: - control-plane: controller-manager - app.kubernetes.io/name: jumpstarter-controller - {{ if .Values.global.timestamp }} - deployment.timestamp: {{ .Values.global.timestamp | quote }} - {{ end }} -data: - # backwards compatibility - # TODO: remove in 0.7.0 - {{ if .Values.authenticationConfig }} - authentication: {{- .Values.authenticationConfig | toYaml | indent 1 }} - {{ end }} - config: | -{{ .Values.config | toYaml | indent 4 }} - router: | - default: - {{ if .Values.grpc.routerEndpoint }} - endpoint: {{ .Values.grpc.routerEndpoint }} - {{ else if .Values.routerHostname }} - endpoint: {{ .Values.routerHostname }}:{{ .Values.grpc.tls.port }} - {{ else }} - endpoint: router.{{ .Values.global.baseDomain }}:{{ .Values.grpc.tls.port }} - {{ end }} - {{ range $k, $v := .Values.grpc.additionalRouters }} - {{ $k }}: - {{ if $v.endpoint }} - endpoint: {{ $v.endpoint }} - {{ else if $v.hostname }} - endpoint: {{ $v.hostname }}:{{ $.Values.grpc.tls.port }} - {{ else }} - endpoint: router-{{ $k }}.{{ $.Values.global.baseDomain }}:{{ $.Values.grpc.tls.port }} - {{ end }} - {{ if $v.labels }} - labels: - {{ $v.labels | toYaml | indent 1 }} - {{ end }} - {{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-deployment.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-deployment.yaml deleted file mode 100644 index f3b96a56d..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-deployment.yaml +++ /dev/null @@ -1,144 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: jumpstarter-controller - namespace: {{ default .Release.Namespace .Values.namespace }} - labels: - control-plane: controller-manager - app.kubernetes.io/name: jumpstarter-controller - {{ if .Values.global.timestamp }} - deployment.timestamp: {{ .Values.global.timestamp | quote }} - {{ end }} - annotations: - argocd.argoproj.io/sync-wave: "1" -spec: - selector: - matchLabels: - control-plane: controller-manager - replicas: 1 - template: - metadata: - annotations: - kubectl.kubernetes.io/default-container: manager - configmap-sha256: {{ include (print $.Template.BasePath "/cms/controller-cm.yaml") . | sha256sum }} - labels: - control-plane: controller-manager - {{ if .Values.global.timestamp }} - deployment.timestamp: {{ .Values.global.timestamp | quote }} - {{ end }} - spec: - # TODO(user): Uncomment the following code to configure the nodeAffinity expression - # according to the platforms which are supported by your solution. - # It is considered best practice to support multiple architectures. You can - # build your manager image using the makefile target docker-buildx. - # affinity: - # nodeAffinity: - # requiredDuringSchedulingIgnoredDuringExecution: - # nodeSelectorTerms: - # - matchExpressions: - # - key: kubernetes.io/arch - # operator: In - # values: - # - amd64 - # - arm64 - # - ppc64le - # - s390x - # - key: kubernetes.io/os - # operator: In - # values: - # - linux - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - args: - - --leader-elect - - --health-probe-bind-address=:8081 - - -metrics-bind-address=:8080 - env: - - name: GRPC_ENDPOINT - {{ if .Values.grpc.endpoint }} - value : {{ .Values.grpc.endpoint }} - {{ else if .Values.hostname }} - value: {{ .Values.hostname }}:{{ .Values.grpc.tls.port }} - {{ else }} - value: grpc.{{ .Values.global.baseDomain }}:{{ .Values.grpc.tls.port }} - {{ end }} - - name: LOGIN_ENDPOINT - {{ if .Values.login.endpoint }} - value: {{ .Values.login.endpoint }} - {{ else }} - value: login.{{ .Values.global.baseDomain }} - {{ end }} - - name: CONTROLLER_KEY - valueFrom: - secretKeyRef: - name: jumpstarter-controller-secret - key: key - - name: ROUTER_KEY - valueFrom: - secretKeyRef: - name: jumpstarter-router-secret - key: key - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - {{- if .Values.grpc.tls.controllerCertSecret }} - - name: EXTERNAL_CERT_PEM - value: /secrets/tls.crt - - name: EXTERNAL_KEY_PEM - value: /secrets/tls.key - {{- end }} - - name: CA_BUNDLE_PEM - valueFrom: - configMapKeyRef: - name: jumpstarter-service-ca-cert - key: ca.crt - # When cert-manager is enabled, require the configmap (fail fast on missing cert) - # When disabled, make it optional (users may not have TLS configured) - optional: {{ not .Values.certManager.enabled }} - - name: GIN_MODE - value: release - image: {{ .Values.image }}:{{ default .Chart.AppVersion .Values.tag }} - imagePullPolicy: {{ .Values.imagePullPolicy }} - name: manager - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - "ALL" - livenessProbe: - httpGet: - path: /healthz - port: 8081 - initialDelaySeconds: 15 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /readyz - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - limits: - cpu: 2000m - memory: 1024Mi - requests: - cpu: 1000m - memory: 256Mi - {{- if .Values.grpc.tls.controllerCertSecret }} - volumeMounts: - - name: external-cert - mountPath: /secrets - readOnly: true - {{- end }} - {{- if .Values.grpc.tls.controllerCertSecret }} - volumes: - - name: external-cert - secret: - secretName: {{ .Values.grpc.tls.controllerCertSecret }} - {{- end }} - serviceAccountName: controller-manager - terminationGracePeriodSeconds: 10 diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-ingress.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-ingress.yaml deleted file mode 100644 index aba515c4b..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-ingress.yaml +++ /dev/null @@ -1,44 +0,0 @@ -{{ if eq .Values.grpc.mode "ingress" }} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx.ingress.kubernetes.io/backend-protocol: "GRPC" - nginx.ingress.kubernetes.io/proxy-read-timeout: "300" - nginx.ingress.kubernetes.io/proxy-send-timeout: "300" - {{ if eq .Values.grpc.tls.mode "passthrough" }} - nginx.ingress.kubernetes.io/ssl-passthrough: "true" - {{ end }} - name: jumpstarter-controller-ingress - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - {{ if .Values.grpc.ingress.class }} - ingressClassName: {{ .Values.grpc.ingress.class }} - {{ end }} - rules: - {{ if .Values.grpc.hostname }} - - host: {{ .Values.grpc.hostname }} - {{ else }} - - host: grpc.{{ .Values.global.baseDomain | required "a global.baseDomain or a grpc.hostname must be provided"}} - {{ end }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: jumpstarter-grpc - port: - number: 8082 - tls: - - hosts: - {{ if .Values.grpc.hostname }} - - {{ .Values.grpc.hostname }} - {{ else }} - - grpc.{{ .Values.global.baseDomain | required "a global.baseDomain or a grpc.hostname must be provided"}} - {{ end }} - {{ if .Values.grpc.tls.controllerCertSecret }} - secretName: {{ .Values.grpc.tls.controllerCertSecret }} - {{ end }} -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-route.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-route.yaml deleted file mode 100644 index f7a781f0d..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-route.yaml +++ /dev/null @@ -1,34 +0,0 @@ -{{ if eq .Values.grpc.mode "route" }} -apiVersion: route.openshift.io/v1 -kind: Route -metadata: - labels: - external-exposed: "true" - shard: external - annotations: - haproxy.router.openshift.io/timeout: 2d - haproxy.router.openshift.io/timeout-tunnel: 2d - name: jumpstarter-controller-route - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - {{ if .Values.grpc.hostname }} - host: {{ .Values.grpc.hostname }} - {{ else }} - host: grpc.{{ .Values.global.baseDomain | required "a global.baseDomain or a grpc.hostname must be provided"}} - {{ end }} - port: - targetPort: 8082 - tls: - termination: {{ .Values.grpc.tls.mode }} - insecureEdgeTerminationPolicy: None - {{ if .Values.grpc.tls.controllerCertSecret }} - externalCertificate: - name: {{ .Values.grpc.tls.controllerCertSecret }} - {{ end }} - - to: - kind: Service - name: jumpstarter-grpc - weight: 100 - wildcardPolicy: None -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-service.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-service.yaml deleted file mode 100644 index c75803241..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/controller-service.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - control-plane: controller-manager - app.kubernetes.io/name: jumpstarter-controller - name: jumpstarter-grpc - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - {{ if .Values.grpc.nodeport.enabled }} - type: NodePort - {{ end }} - - ports: - - name: grpc - port: 8082 - protocol: TCP - targetPort: 8082 - appProtocol: h2c # HTTP/2 over cleartext for gRPC (fixes edge termination in ingress/router) - {{ if .Values.grpc.nodeport.enabled }} - nodePort: {{ .Values.grpc.nodeport.port }} - {{ end }} - selector: - control-plane: controller-manager diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_clients.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_clients.yaml deleted file mode 100644 index d9dd6d0cb..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_clients.yaml +++ /dev/null @@ -1,69 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.3 - name: clients.jumpstarter.dev -spec: - group: jumpstarter.dev - names: - kind: Client - listKind: ClientList - plural: clients - singular: client - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: Client is the Schema for the identities API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ClientSpec defines the desired state of Identity - properties: - username: - type: string - type: object - status: - description: ClientStatus defines the observed state of Identity - properties: - credential: - description: Status field for the clients - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - endpoint: - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporteraccesspolicies.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporteraccesspolicies.yaml deleted file mode 100644 index ec1b7878c..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporteraccesspolicies.yaml +++ /dev/null @@ -1,166 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.3 - name: exporteraccesspolicies.jumpstarter.dev -spec: - group: jumpstarter.dev - names: - kind: ExporterAccessPolicy - listKind: ExporterAccessPolicyList - plural: exporteraccesspolicies - singular: exporteraccesspolicy - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: ExporterAccessPolicy is the Schema for the exporteraccesspolicies - API. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ExporterAccessPolicySpec defines the desired state of ExporterAccessPolicy. - properties: - exporterSelector: - description: |- - A label selector is a label query over a set of resources. The result of matchLabels and - matchExpressions are ANDed. An empty label selector matches all objects. A null - label selector matches no objects. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - policies: - items: - properties: - from: - items: - properties: - clientSelector: - description: |- - A label selector is a label query over a set of resources. The result of matchLabels and - matchExpressions are ANDed. An empty label selector matches all objects. A null - label selector matches no objects. - properties: - matchExpressions: - description: matchExpressions is a list of label selector - requirements. The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector - applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - type: object - type: array - maximumDuration: - type: string - priority: - type: integer - spotAccess: - type: boolean - type: object - type: array - type: object - status: - description: ExporterAccessPolicyStatus defines the observed state of - ExporterAccessPolicy. - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporters.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporters.yaml deleted file mode 100644 index 9e4d57bb8..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_exporters.yaml +++ /dev/null @@ -1,185 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.3 - name: exporters.jumpstarter.dev -spec: - group: jumpstarter.dev - names: - kind: Exporter - listKind: ExporterList - plural: exporters - singular: exporter - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .status.exporterStatus - name: Status - type: string - - jsonPath: .status.statusMessage - name: Message - priority: 1 - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: Exporter is the Schema for the exporters API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: ExporterSpec defines the desired state of Exporter - properties: - username: - type: string - type: object - status: - description: ExporterStatus defines the observed state of Exporter - properties: - conditions: - description: Exporter status fields - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - credential: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - devices: - items: - properties: - labels: - additionalProperties: - type: string - type: object - parent_uuid: - type: string - uuid: - type: string - type: object - type: array - endpoint: - type: string - exporterStatus: - description: ExporterStatusValue is the current operational status - reported by the exporter - enum: - - Unspecified - - Offline - - Available - - BeforeLeaseHook - - LeaseReady - - AfterLeaseHook - - BeforeLeaseHookFailed - - AfterLeaseHookFailed - type: string - lastSeen: - format: date-time - type: string - leaseRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - statusMessage: - description: StatusMessage is an optional human-readable message describing - the current state - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_leases.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_leases.yaml deleted file mode 100644 index 2df4db845..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/crds/jumpstarter.dev_leases.yaml +++ /dev/null @@ -1,256 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.3 - name: leases.jumpstarter.dev -spec: - group: jumpstarter.dev - names: - kind: Lease - listKind: LeaseList - plural: leases - singular: lease - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .status.ended - name: Ended - type: boolean - - jsonPath: .spec.clientRef.name - name: Client - type: string - - jsonPath: .status.exporterRef.name - name: Exporter - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: Lease is the Schema for the exporters API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: LeaseSpec defines the desired state of Lease - properties: - beginTime: - description: |- - Requested start time. If omitted, lease starts when exporter is acquired. - Immutable after lease starts (cannot change the past). - format: date-time - type: string - clientRef: - description: The client that is requesting the lease - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - duration: - description: |- - Duration of the lease. Must be positive when provided. - Can be omitted (nil) when both BeginTime and EndTime are provided, - in which case it's calculated as EndTime - BeginTime. - type: string - endTime: - description: |- - Requested end time. If specified with BeginTime, Duration is calculated. - Can be updated to extend or shorten active leases. - format: date-time - type: string - exporterRef: - description: Optionally pin this lease to a specific exporter name. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - release: - description: The release flag requests the controller to end the lease - now - type: boolean - selector: - default: {} - description: The selector for the exporter to be used - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - required: - - clientRef - - selector - type: object - x-kubernetes-validations: - - message: one of selector or exporterRef.name is required - rule: ((has(self.selector.matchLabels) && size(self.selector.matchLabels) - > 0) || (has(self.selector.matchExpressions) && size(self.selector.matchExpressions) - > 0)) || (has(self.exporterRef) && has(self.exporterRef.name) && size(self.exporterRef.name) - > 0) - status: - description: LeaseStatus defines the observed state of Lease - properties: - beginTime: - description: |- - If the lease has been acquired an exporter name is assigned - and then it can be used, it will be empty while still pending - format: date-time - type: string - conditions: - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - endTime: - format: date-time - type: string - ended: - type: boolean - exporterRef: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - priority: - type: integer - spotAccess: - type: boolean - required: - - ended - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-ingress.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-ingress.yaml deleted file mode 100644 index cd0382c3d..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-ingress.yaml +++ /dev/null @@ -1,36 +0,0 @@ -{{ if and .Values.login.enabled .Values.login.ingress.enabled }} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - labels: - component: login - {{- if .Values.login.ingress.labels }} - {{- toYaml .Values.login.ingress.labels | nindent 4 }} - {{- end }} - {{- if .Values.login.ingress.annotations }} - annotations: - {{- toYaml .Values.login.ingress.annotations | nindent 4 }} - {{- end }} - name: jumpstarter-login-ing - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - {{- if .Values.login.ingress.class }} - ingressClassName: {{ .Values.login.ingress.class }} - {{- end }} - rules: - - host: {{ .Values.login.hostname | default (printf "login.%s" .Values.global.baseDomain) | required "a global.baseDomain or a login.hostname must be provided" }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: jumpstarter-login - port: - number: 8086 - tls: - - hosts: - - {{ .Values.login.hostname | default (printf "login.%s" .Values.global.baseDomain) }} - {{- /* Use login.tls.secretName (new), fallback to login.ingress.tls.secretName (legacy), then default */}} - secretName: {{ .Values.login.tls.secretName | default .Values.login.ingress.tls.secretName | default "jumpstarter-login-tls" }} -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-route.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-route.yaml deleted file mode 100644 index beab12d87..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-route.yaml +++ /dev/null @@ -1,38 +0,0 @@ -{{ if and .Values.login.enabled .Values.login.route.enabled }} -apiVersion: route.openshift.io/v1 -kind: Route -metadata: - labels: - component: login - {{- if .Values.login.route.labels }} - {{- toYaml .Values.login.route.labels | nindent 4 }} - {{- end }} - {{- if .Values.login.route.annotations }} - annotations: - {{- toYaml .Values.login.route.annotations | nindent 4 }} - {{- end }} - name: jumpstarter-login-route - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - {{ if .Values.login.hostname }} - host: {{ .Values.login.hostname }} - {{ else }} - host: login.{{ .Values.global.baseDomain | required "a global.baseDomain or a login.hostname must be provided"}} - {{ end }} - port: - targetPort: 8086 - tls: - # Edge termination - TLS terminated at the router, plain HTTP to backend - termination: edge - insecureEdgeTerminationPolicy: Redirect - {{- if .Values.login.tls.secretName }} - # Reference TLS certificate from secret (OpenShift 4.14+) - externalCertificate: - name: {{ .Values.login.tls.secretName }} - {{- end }} - to: - kind: Service - name: jumpstarter-login - weight: 100 - wildcardPolicy: None -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-service.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-service.yaml deleted file mode 100644 index 5e7dc71f1..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/login-service.yaml +++ /dev/null @@ -1,26 +0,0 @@ -{{ if .Values.login.enabled }} -apiVersion: v1 -kind: Service -metadata: - labels: - control-plane: controller-manager - app.kubernetes.io/name: jumpstarter-controller - component: login - name: jumpstarter-login - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - {{ if .Values.login.nodeport.enabled }} - type: NodePort - {{ end }} - - ports: - - name: login - port: 8086 - protocol: TCP - targetPort: 8086 - {{ if .Values.login.nodeport.enabled }} - nodePort: {{ .Values.login.nodeport.port }} - {{ end }} - selector: - control-plane: controller-manager -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/metrics/metrics_service.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/metrics/metrics_service.yaml deleted file mode 100644 index b16a19ffc..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/metrics/metrics_service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - control-plane: controller-manager - app.kubernetes.io/name: jumpstarter-controller - name: controller-manager-metrics-service - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - ports: - - name: http - port: 8080 - protocol: TCP - targetPort: 8080 - selector: - control-plane: controller-manager diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/metrics/monitor.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/metrics/monitor.yaml deleted file mode 100644 index cb8098a7c..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/metrics/monitor.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# enable monitoring only if monitoring is enabled -{{- if .Values.global.metrics.enabled }} -# Prometheus Monitor Service (Metrics) -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - labels: - control-plane: controller-manager - app.kubernetes.io/name: jumpstarter-controller - name: controller-manager-metrics-monitor - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - endpoints: - - path: /metrics - port: http # Ensure this is the name of the port that exposes HTTP metrics - scheme: http - selector: - matchLabels: - control-plane: controller-manager -{{- end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/client_editor_role.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/client_editor_role.yaml deleted file mode 100644 index 94773c031..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/client_editor_role.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# permissions for end users to edit identities. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: jumpstarter-router - name: client-editor-role -rules: -- apiGroups: - - jumpstarter.dev - resources: - - clients - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - jumpstarter.dev - resources: - - identities/status - verbs: - - get diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/client_viewer_role.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/client_viewer_role.yaml deleted file mode 100644 index 039ea624d..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/client_viewer_role.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# permissions for end users to view identities. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: jumpstarter-router - name: client-viewer-role -rules: -- apiGroups: - - jumpstarter.dev - resources: - - clients - verbs: - - get - - list - - watch -- apiGroups: - - jumpstarter.dev - resources: - - identities/status - verbs: - - get diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/exporter_editor_role.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/exporter_editor_role.yaml deleted file mode 100644 index c52b8c246..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/exporter_editor_role.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# permissions for end users to edit exporters. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: jumpstarter-router - name: exporter-editor-role -rules: -- apiGroups: - - jumpstarter.dev - resources: - - exporters - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - jumpstarter.dev - resources: - - exporters/status - verbs: - - get diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/exporter_viewer_role.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/exporter_viewer_role.yaml deleted file mode 100644 index 5ba5614c5..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/exporter_viewer_role.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# permissions for end users to view exporters. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/name: jumpstarter-router - name: exporter-viewer-role -rules: -- apiGroups: - - jumpstarter.dev - resources: - - exporters - verbs: - - get - - list - - watch -- apiGroups: - - jumpstarter.dev - resources: - - exporters/status - verbs: - - get diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml deleted file mode 100644 index b0390bd15..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# permissions to do leader election. -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - labels: - app.kubernetes.io/name: jumpstarter-router - name: leader-election-role - namespace: {{ default .Release.Namespace .Values.namespace }} -rules: -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete -- apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete -- apiGroups: - - "" - resources: - - events - verbs: - - create - - patch diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml deleted file mode 100644 index d60dc3c9f..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - app.kubernetes.io/name: jumpstarter-router - namespace: {{ default .Release.Namespace .Values.namespace }} - name: leader-election-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: leader-election-role -subjects: -- kind: ServiceAccount - name: controller-manager - namespace: {{ default .Release.Namespace .Values.namespace }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role.yaml deleted file mode 100644 index 659678fe9..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role.yaml +++ /dev/null @@ -1,67 +0,0 @@ ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: jumpstarter-manager-role - annotations: - argocd.argoproj.io/sync-wave: "-1" -rules: -- apiGroups: - - "" - resources: - - events - verbs: - - create - - patch -- apiGroups: - - "" - resources: - - secrets - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - jumpstarter.dev - resources: - - clients - - exporters - - leases - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - jumpstarter.dev - resources: - - clients/finalizers - - exporters/finalizers - - leases/finalizers - verbs: - - update -- apiGroups: - - jumpstarter.dev - resources: - - clients/status - - exporters/status - - leases/status - verbs: - - get - - patch - - update -- apiGroups: - - jumpstarter.dev - resources: - - exporteraccesspolicies - verbs: - - get - - list - - watch diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml deleted file mode 100644 index 71d864b05..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - app.kubernetes.io/name: jumpstarter-router - annotations: - argocd.argoproj.io/sync-wave: "-1" - name: jumpstarter-manager-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: jumpstarter-manager-role -subjects: -- kind: ServiceAccount - name: controller-manager - namespace: {{ default .Release.Namespace .Values.namespace }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml deleted file mode 100644 index 5359726af..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - labels: - app.kubernetes.io/name: jumpstarter-router - annotations: - argocd.argoproj.io/sync-wave: "-1" - name: controller-manager - namespace: {{ default .Release.Namespace .Values.namespace }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml deleted file mode 100644 index fa9978bf3..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml +++ /dev/null @@ -1,109 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: jumpstarter-router - namespace: {{ default .Release.Namespace .Values.namespace }} - labels: - control-plane: controller-router - app.kubernetes.io/name: jumpstarter-controller - {{ if .Values.global.timestamp }} - deployment.timestamp: {{ .Values.global.timestamp | quote }} - {{ end }} - annotations: - argocd.argoproj.io/sync-wave: "1" -spec: - selector: - matchLabels: - control-plane: controller-router - replicas: 1 - template: - metadata: - annotations: - kubectl.kubernetes.io/default-container: router - configmap-sha256: {{ include (print $.Template.BasePath "/cms/controller-cm.yaml") . | sha256sum }} - labels: - control-plane: controller-router - {{ if .Values.global.timestamp }} - deployment.timestamp: {{ .Values.global.timestamp | quote }} - {{ end }} - spec: - # TODO(user): Uncomment the following code to configure the nodeAffinity expression - # according to the platforms which are supported by your solution. - # It is considered best practice to support multiple architectures. You can - # build your manager image using the makefile target docker-buildx. - # affinity: - # nodeAffinity: - # requiredDuringSchedulingIgnoredDuringExecution: - # nodeSelectorTerms: - # - matchExpressions: - # - key: kubernetes.io/arch - # operator: In - # values: - # - amd64 - # - arm64 - # - ppc64le - # - s390x - # - key: kubernetes.io/os - # operator: In - # values: - # - linux - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - command: - - /router - env: - - name: GRPC_ROUTER_ENDPOINT - {{ if .Values.grpc.routerEndpoint }} - value: {{ .Values.grpc.routerEndpoint }} - {{ else if .Values.routerHostname }} - value: {{ .Values.routerHostname }}:{{ .Values.grpc.tls.port }} - {{ else }} - value: router.{{ .Values.global.baseDomain }}:{{ .Values.grpc.tls.port }} - {{ end }} - - name: ROUTER_KEY - valueFrom: - secretKeyRef: - name: jumpstarter-router-secret - key: key - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - {{- if .Values.grpc.tls.routerCertSecret }} - - name: EXTERNAL_CERT_PEM - value: /secrets/tls.crt - - name: EXTERNAL_KEY_PEM - value: /secrets/tls.key - {{- end }} - image: {{ .Values.image }}:{{ default .Chart.AppVersion .Values.tag }} - imagePullPolicy: {{ .Values.imagePullPolicy }} - name: router - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - "ALL" - resources: - limits: - cpu: 2000m - memory: 1024Mi - requests: - cpu: 1000m - memory: 256Mi - {{- if .Values.grpc.tls.routerCertSecret }} - volumeMounts: - - name: external-cert - mountPath: /secrets - readOnly: true - {{- end }} - {{- if .Values.grpc.tls.routerCertSecret }} - volumes: - - name: external-cert - secret: - secretName: {{ .Values.grpc.tls.routerCertSecret }} - {{- end }} - serviceAccountName: controller-manager - terminationGracePeriodSeconds: 10 diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-ingress.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-ingress.yaml deleted file mode 100644 index 57e2235e5..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-ingress.yaml +++ /dev/null @@ -1,44 +0,0 @@ -{{ if eq .Values.grpc.mode "ingress" }} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx.ingress.kubernetes.io/backend-protocol: "GRPC" - nginx.ingress.kubernetes.io/proxy-read-timeout: "300" - nginx.ingress.kubernetes.io/proxy-send-timeout: "300" - {{ if eq .Values.grpc.tls.mode "passthrough" }} - nginx.ingress.kubernetes.io/ssl-passthrough: "true" - {{ end }} - name: jumpstarter-router-ingress - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - {{ if .Values.grpc.ingress.class }} - ingressClassName: {{ .Values.grpc.ingress.class }} - {{ end }} - rules: - {{ if .Values.grpc.routerHostname }} - - host: {{ .Values.grpc.routerHostname }} - {{ else }} - - host: router.{{ .Values.global.baseDomain | required "a global.baseDomain or a grpc.routerHostname must be provided"}} - {{ end }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: jumpstarter-router-grpc - port: - number: 8083 - tls: - - hosts: - {{ if .Values.grpc.routerHostname }} - - {{ .Values.grpc.routerHostname }} - {{ else }} - - router.{{ .Values.global.baseDomain | required "a global.baseDomain or a grpc.routerHostname must be provided" }} - {{ end }} - {{ if .Values.grpc.tls.routerCertSecret }} - secretName: {{ .Values.grpc.tls.routerCertSecret }} - {{ end }} -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-route.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-route.yaml deleted file mode 100644 index e0659fbe8..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-route.yaml +++ /dev/null @@ -1,39 +0,0 @@ -{{ if eq .Values.grpc.mode "route" }} -apiVersion: route.openshift.io/v1 -kind: Route -metadata: - labels: - external-exposed: "true" - shard: external - annotations: - haproxy.router.openshift.io/timeout: 2d - haproxy.router.openshift.io/timeout-tunnel: 2d - name: jumpstarter-router-route - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - {{ if .Values.grpc.routerHostname }} - host: {{ .Values.grpc.routerHostname }} - {{ else }} - host: router.{{ .Values.global.baseDomain | required "a global.baseDomain or a grpc.routerHostname must be provided"}} - {{ end }} - port: - targetPort: 8083 - tls: - {{ if eq .Values.grpc.tls.mode "passthrough" }} - termination: passthrough - {{ end }} - {{ if eq .Values.grpc.tls.mode "reencrypt" }} - termination: reencrypt - {{ end }} - insecureEdgeTerminationPolicy: None - {{ if .Values.grpc.tls.routerCertSecret }} - externalCertificate: - name: {{ .Values.grpc.tls.routerCertSecret }} - {{ end }} - - to: - kind: Service - name: jumpstarter-router-grpc - weight: 100 - wildcardPolicy: None -{{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-service.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-service.yaml deleted file mode 100644 index ad4a2b1b3..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-service.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - control-plane: controller-manager - app.kubernetes.io/name: jumpstarter-controller - name: jumpstarter-router-grpc - namespace: {{ default .Release.Namespace .Values.namespace }} -spec: - {{ if .Values.grpc.nodeport.enabled }} - type: NodePort - {{ end }} - - ports: - - name: grpc - port: 8083 - protocol: TCP - targetPort: 8083 - appProtocol: h2c # HTTP/2 over cleartext for gRPC (fixes edge termination in ingress/router) - {{ if .Values.grpc.nodeport.enabled }} - nodePort: {{ .Values.grpc.nodeport.routerPort }} - {{ end }} - selector: - control-plane: controller-router diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/secrets-job.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/secrets-job.yaml deleted file mode 100644 index 309d16fc9..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/secrets-job.yaml +++ /dev/null @@ -1,36 +0,0 @@ -{{- $namespace := default .Release.Namespace .Values.namespace }} -apiVersion: batch/v1 -kind: Job -metadata: - labels: - control-plane: controller-manager - app.kubernetes.io/name: jumpstarter-controller - annotations: - # https://argo-cd.readthedocs.io/en/stable/user-guide/resource_hooks/#hook-deletion-policies - argocd.argoproj.io/hook: Sync - argocd.argoproj.io/hook-delete-policy: HookSucceeded - argocd.argoproj.io/sync-wave: "-1" - name: jumpstarter-secrets - namespace: {{ $namespace }} -spec: - ttlSecondsAfterFinished: 30 - template: - metadata: - name: jumpstarter-secrets - spec: - serviceAccountName: controller-manager - containers: - - name: jumpstarter-secrets - image: quay.io/jumpstarter-dev/jumpstarter-utils:latest - command: - - /bin/sh - - -c - - | - set -e - {{- range $name := tuple "jumpstarter-router-secret" "jumpstarter-controller-secret" }} - if ! kubectl get secret {{ $name }} -n {{ $namespace }} >/dev/null 2>&1; then - kubectl create secret generic {{ $name }} -n={{ $namespace }} \ - --from-literal=key="$(openssl rand -hex 32)" - fi - {{- end }} - restartPolicy: Never diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/values.schema.json b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/values.schema.json deleted file mode 100644 index 021e046dc..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/values.schema.json +++ /dev/null @@ -1,1458 +0,0 @@ -{ - "$defs": { - "AudienceMatchPolicy": { - "enum": [ - "MatchAny" - ], - "title": "AudienceMatchPolicy", - "type": "string" - }, - "Authentication": { - "additionalProperties": false, - "properties": { - "internal": { - "anyOf": [ - { - "$ref": "#/$defs/Internal" - }, - { - "type": "null" - } - ], - "default": null - }, - "jwt": { - "anyOf": [ - { - "items": { - "$ref": "#/$defs/JWTAuthenticator" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "description": "External OIDC authentication, see https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-authentication-configuration for documentation", - "title": "Jwt" - } - }, - "title": "Authentication", - "type": "object" - }, - "CertManager": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Enable cert-manager integration. When enabled, jumpstarter-service-ca-cert configmap is required.", - "title": "Enabled" - } - }, - "title": "CertManager", - "type": "object" - }, - "ClaimMappings": { - "additionalProperties": false, - "properties": { - "username": { - "anyOf": [ - { - "$ref": "#/$defs/PrefixedClaimOrExpression" - }, - { - "type": "null" - } - ], - "default": null - }, - "groups": { - "anyOf": [ - { - "$ref": "#/$defs/PrefixedClaimOrExpression" - }, - { - "type": "null" - } - ], - "default": null - }, - "uid": { - "anyOf": [ - { - "$ref": "#/$defs/ClaimOrExpression" - }, - { - "type": "null" - } - ], - "default": null - }, - "extra": { - "anyOf": [ - { - "items": { - "$ref": "#/$defs/ExtraItem" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Extra" - } - }, - "title": "ClaimMappings", - "type": "object" - }, - "ClaimOrExpression": { - "anyOf": [ - { - "$ref": "#/$defs/ClaimOrExpression1" - }, - { - "$ref": "#/$defs/ClaimOrExpression2" - } - ], - "title": "ClaimOrExpression" - }, - "ClaimOrExpression1": { - "additionalProperties": false, - "properties": { - "claim": { - "title": "Claim", - "type": "string" - }, - "expression": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Expression" - } - }, - "required": [ - "claim" - ], - "title": "ClaimOrExpression1", - "type": "object" - }, - "ClaimOrExpression2": { - "additionalProperties": false, - "properties": { - "claim": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Claim" - }, - "expression": { - "title": "Expression", - "type": "string" - } - }, - "required": [ - "expression" - ], - "title": "ClaimOrExpression2", - "type": "object" - }, - "ClaimValidationRule": { - "additionalProperties": false, - "properties": { - "claim": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Claim" - }, - "requiredValue": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Requiredvalue" - }, - "expression": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Expression" - }, - "message": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Message" - } - }, - "title": "ClaimValidationRule", - "type": "object" - }, - "ExtraItem": { - "additionalProperties": false, - "properties": { - "key": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Key" - }, - "valueExpression": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Valueexpression" - } - }, - "title": "ExtraItem", - "type": "object" - }, - "Global": { - "properties": { - "baseDomain": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Base domain to construct the FQDN for the service endpoints", - "title": "Basedomain" - }, - "metrics": { - "anyOf": [ - { - "$ref": "#/$defs/Metrics" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "title": "Global", - "type": "object" - }, - "Grpc": { - "additionalProperties": false, - "properties": { - "keepalive": { - "anyOf": [ - { - "$ref": "#/$defs/Keepalive" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "title": "Grpc", - "type": "object" - }, - "Grpc1": { - "additionalProperties": false, - "properties": { - "hostname": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Hostname for the controller to use for the controller gRPC", - "title": "Hostname" - }, - "routerHostname": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Hostname for the router to use for the router gRPC", - "title": "Routerhostname" - }, - "endpoint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "The endpoints are passed down to the services to know where to announce the endpoints to the clients", - "title": "Endpoint" - }, - "routerEndpoint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "The endpoints are passed down to the services to know where to announce the endpoints to the clients", - "title": "Routerendpoint" - }, - "additionalRouters": { - "anyOf": [ - { - "additionalProperties": { - "$ref": "#/$defs/Router" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Additional routers to deploy", - "title": "Additionalrouters" - }, - "ingress": { - "anyOf": [ - { - "$ref": "#/$defs/Ingress" - }, - { - "type": "null" - } - ], - "default": null - }, - "route": { - "anyOf": [ - { - "$ref": "#/$defs/Route" - }, - { - "type": "null" - } - ], - "default": null - }, - "nodeport": { - "anyOf": [ - { - "$ref": "#/$defs/Nodeport" - }, - { - "type": "null" - } - ], - "default": null - }, - "mode": { - "anyOf": [ - { - "$ref": "#/$defs/Mode" - }, - { - "type": "null" - } - ], - "default": null - }, - "tls": { - "anyOf": [ - { - "$ref": "#/$defs/Tls" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "title": "Grpc1", - "type": "object" - }, - "Ingress": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Whether to enable Ingress for the gRPC endpoint", - "title": "Enabled" - }, - "class": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "IngressClass to use for the gRPC endpoint", - "title": "Class" - } - }, - "title": "Ingress", - "type": "object" - }, - "Internal": { - "additionalProperties": false, - "properties": { - "prefix": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Prefix" - } - }, - "title": "Internal", - "type": "object" - }, - "Issuer": { - "additionalProperties": false, - "properties": { - "url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Url" - }, - "discoveryURL": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Discoveryurl" - }, - "certificateAuthority": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Certificateauthority" - }, - "audiences": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Audiences" - }, - "audienceMatchPolicy": { - "anyOf": [ - { - "$ref": "#/$defs/AudienceMatchPolicy" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "title": "Issuer", - "type": "object" - }, - "JWTAuthenticator": { - "additionalProperties": false, - "properties": { - "issuer": { - "anyOf": [ - { - "$ref": "#/$defs/Issuer" - }, - { - "type": "null" - } - ], - "default": null - }, - "claimValidationRules": { - "anyOf": [ - { - "items": { - "$ref": "#/$defs/ClaimValidationRule" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Claimvalidationrules" - }, - "claimMappings": { - "anyOf": [ - { - "$ref": "#/$defs/ClaimMappings" - }, - { - "type": "null" - } - ], - "default": null - }, - "userValidationRules": { - "anyOf": [ - { - "items": { - "$ref": "#/$defs/UserValidationRule" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Uservalidationrules" - } - }, - "title": "JWTAuthenticator", - "type": "object" - }, - "JumpstarterConfig": { - "additionalProperties": false, - "properties": { - "provisioning": { - "anyOf": [ - { - "$ref": "#/$defs/Provisioning" - }, - { - "type": "null" - } - ], - "default": null - }, - "authentication": { - "anyOf": [ - { - "$ref": "#/$defs/Authentication" - }, - { - "type": "null" - } - ], - "default": null - }, - "grpc": { - "anyOf": [ - { - "$ref": "#/$defs/Grpc" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "title": "JumpstarterConfig", - "type": "object" - }, - "Keepalive": { - "additionalProperties": false, - "properties": { - "minTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "The minimum amount of time a client should wait before sending a keepalive ping", - "title": "Mintime" - }, - "permitWithoutStream": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Whether to allow keepalive pings even when there are no active streams(RPCs)", - "title": "Permitwithoutstream" - } - }, - "title": "Keepalive", - "type": "object" - }, - "Login": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Whether to enable the login endpoint for simplified CLI login", - "title": "Enabled" - }, - "hostname": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Hostname for the login endpoint", - "title": "Hostname" - }, - "endpoint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "The endpoint URL to display in the login landing page", - "title": "Endpoint" - }, - "tls": { - "anyOf": [ - { - "$ref": "#/$defs/LoginTls" - }, - { - "type": "null" - } - ], - "default": null, - "description": "TLS configuration for edge termination (used by both route and ingress)" - }, - "route": { - "anyOf": [ - { - "$ref": "#/$defs/LoginRoute" - }, - { - "type": "null" - } - ], - "default": null - }, - "ingress": { - "anyOf": [ - { - "$ref": "#/$defs/LoginIngress" - }, - { - "type": "null" - } - ], - "default": null - }, - "nodeport": { - "anyOf": [ - { - "$ref": "#/$defs/LoginNodeport" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "title": "Login", - "type": "object" - }, - "LoginIngress": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Whether to enable Ingress for the login endpoint", - "title": "Enabled" - }, - "class": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "IngressClass to use for the login endpoint", - "title": "Class" - }, - "annotations": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Annotations for the login ingress", - "title": "Annotations" - }, - "labels": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Labels for the login ingress", - "title": "Labels" - }, - "tls": { - "anyOf": [ - { - "$ref": "#/$defs/LoginIngressTls" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "title": "LoginIngress", - "type": "object" - }, - "LoginIngressTls": { - "additionalProperties": false, - "properties": { - "secretName": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Secret name for the TLS certificate", - "title": "Secretname" - } - }, - "title": "LoginIngressTls", - "type": "object" - }, - "LoginNodeport": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Whether to enable NodePort for the login endpoint", - "title": "Enabled" - }, - "port": { - "anyOf": [ - { - "$ref": "#/$defs/Port" - }, - { - "type": "null" - } - ], - "default": null, - "description": "NodePort port number for the login service" - } - }, - "title": "LoginNodeport", - "type": "object" - }, - "LoginRoute": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Whether to enable OpenShift Route for the login endpoint", - "title": "Enabled" - }, - "annotations": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Annotations for the login route", - "title": "Annotations" - }, - "labels": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Labels for the login route", - "title": "Labels" - } - }, - "title": "LoginRoute", - "type": "object" - }, - "LoginTls": { - "additionalProperties": false, - "properties": { - "secretName": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Name of the Kubernetes secret containing tls.crt and tls.key for edge TLS termination", - "title": "Secretname" - } - }, - "title": "LoginTls", - "type": "object" - }, - "Metrics": { - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Enabled" - } - }, - "title": "Metrics", - "type": "object" - }, - "Mode": { - "enum": [ - "ingress", - "route", - "nodeport", - "external" - ], - "title": "Mode", - "type": "string" - }, - "Mode1": { - "enum": [ - "passthrough", - "reencrypt" - ], - "title": "Mode1", - "type": "string" - }, - "Nodeport": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Enabled" - }, - "port": { - "anyOf": [ - { - "$ref": "#/$defs/Port" - }, - { - "type": "null" - } - ], - "default": null - }, - "routerPort": { - "anyOf": [ - { - "$ref": "#/$defs/Port" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "title": "Nodeport", - "type": "object" - }, - "Port": { - "maximum": 65535, - "minimum": 0, - "title": "Port", - "type": "integer" - }, - "PrefixedClaimOrExpression": { - "anyOf": [ - { - "$ref": "#/$defs/PrefixedClaimOrExpression1" - }, - { - "$ref": "#/$defs/PrefixedClaimOrExpression2" - } - ], - "title": "PrefixedClaimOrExpression" - }, - "PrefixedClaimOrExpression1": { - "additionalProperties": false, - "properties": { - "claim": { - "title": "Claim", - "type": "string" - }, - "prefix": { - "title": "Prefix", - "type": "string" - } - }, - "required": [ - "claim", - "prefix" - ], - "title": "PrefixedClaimOrExpression1", - "type": "object" - }, - "PrefixedClaimOrExpression2": { - "additionalProperties": false, - "properties": { - "expression": { - "title": "Expression", - "type": "string" - } - }, - "required": [ - "expression" - ], - "title": "PrefixedClaimOrExpression2", - "type": "object" - }, - "Provisioning": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Enabled" - } - }, - "title": "Provisioning", - "type": "object" - }, - "Route": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Whether to enable OpenShift Router for the gRPC endpoint", - "title": "Enabled" - } - }, - "title": "Route", - "type": "object" - }, - "Router": { - "additionalProperties": false, - "properties": { - "hostname": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Hostname" - }, - "endpoint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Endpoint" - }, - "labels": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Labels" - }, - "nodeSelector": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Nodeselector" - } - }, - "title": "Router", - "type": "object" - }, - "Tls": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Enabled" - }, - "secret": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Secret" - }, - "controllerCertSecret": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Secret containing the TLS certificate/key for the gRPC controller endpoint", - "title": "Controllercertsecret" - }, - "routerCertSecret": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Secret containing the TLS certificate/key for the gRPC router endpoints", - "title": "Routercertsecret" - }, - "port": { - "anyOf": [ - { - "$ref": "#/$defs/Port" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Port to use for the gRPC endpoints Ingress or Route, this can be useful for ingress routers on non-standard ports" - }, - "mode": { - "anyOf": [ - { - "$ref": "#/$defs/Mode1" - }, - { - "type": "null" - } - ], - "default": null, - "description": "TLS mode for gRPC endpoints" - } - }, - "title": "Tls", - "type": "object" - }, - "UserValidationRule": { - "additionalProperties": false, - "properties": { - "expression": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Expression" - }, - "message": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Message" - } - }, - "title": "UserValidationRule", - "type": "object" - } - }, - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Whether to enable jumpstarter controller", - "title": "Enabled" - }, - "authenticationConfig": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Authenticationconfig" - }, - "config": { - "anyOf": [ - { - "$ref": "#/$defs/JumpstarterConfig" - }, - { - "type": "null" - } - ], - "default": null - }, - "namespace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Namespace where the controller will be deployed, defaults to global.namespace", - "title": "Namespace" - }, - "image": { - "description": "Image for the controller", - "title": "Image", - "type": "string" - }, - "tag": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Image tag for the controller", - "title": "Tag" - }, - "imagePullPolicy": { - "description": "Image pull policy for the controller", - "title": "Imagepullpolicy", - "type": "string" - }, - "global": { - "anyOf": [ - { - "$ref": "#/$defs/Global" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Global parameters" - }, - "certManager": { - "anyOf": [ - { - "$ref": "#/$defs/CertManager" - }, - { - "type": "null" - } - ], - "default": null, - "description": "cert-manager integration for automatic TLS certificate management" - }, - "grpc": { - "anyOf": [ - { - "$ref": "#/$defs/Grpc1" - }, - { - "type": "null" - } - ], - "default": null - }, - "login": { - "anyOf": [ - { - "$ref": "#/$defs/Login" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Login endpoint configuration for simplified CLI login" - } - }, - "required": [ - "image", - "imagePullPolicy" - ], - "title": "Model", - "type": "object" -} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/values.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/values.yaml deleted file mode 100644 index 068d2b29b..000000000 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/values.yaml +++ /dev/null @@ -1,76 +0,0 @@ - -namespace: "" - -# cert-manager integration for automatic TLS certificate management -# When enabled, the jumpstarter-service-ca-cert configmap is expected to be -# created by cert-manager and is required (not optional). -# When disabled, the configmap is optional - users must pre-create it manually -# if they want TLS, otherwise TLS will be disabled. -certManager: - enabled: false - -grpc: - hostname: "" - routerHostname: "" - - # the endpoints are passed down to the service to know where - # to announce the endpoints to the clients - endpoint: "" - routerEndpoint: "" - - additionalRouters: {} - - tls: - enabled: false - secret: "" - - # enabling ingress route - ingress: - enabled: false - class: "" - - # enabling openshift route - route: - enabled: false - - # NodePort service for grpc, useful for local development - nodeport: - enabled: false - port: 30010 - routerPort: 30011 - -# Login endpoint configuration for simplified CLI login -# The login service runs on HTTP with TLS terminated at Route/Ingress level -login: - enabled: false - hostname: "" - # the endpoint is passed down to the service to know where to announce the login endpoint - endpoint: "" - # TLS configuration for edge termination (used by both route and ingress) - tls: - # Name of the Kubernetes secret containing tls.crt and tls.key - # If not specified, defaults to "jumpstarter-login-tls" for ingress - # For routes, uses externalCertificate to reference the secret (OpenShift 4.14+) - secretName: "" - # OpenShift Route configuration (uses edge TLS termination) - route: - enabled: false - annotations: {} - labels: {} - # Kubernetes Ingress configuration (uses edge TLS termination) - ingress: - enabled: false - class: "" - annotations: {} - labels: {} - tls: - # Deprecated: use login.tls.secretName instead (kept for backward compatibility) - secretName: "" - # NodePort service for login, useful for local development - nodeport: - enabled: false - port: 30014 - -image: quay.io/jumpstarter-dev/jumpstarter-controller -tag: "" -imagePullPolicy: IfNotPresent diff --git a/controller/deploy/helm/jumpstarter/model.py b/controller/deploy/helm/jumpstarter/model.py deleted file mode 100755 index 2a57634a3..000000000 --- a/controller/deploy/helm/jumpstarter/model.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.12" -# dependencies = ["pydantic"] -# /// - -from __future__ import annotations - -import json - -from typing import Any, Dict, Optional, Union - -from pydantic import BaseModel, ConfigDict, Field - - -class Metrics(BaseModel): - model_config = ConfigDict(extra="forbid") - - enabled: Optional[bool] = Field( - None, description="Whether to enable metrics exporting and service" - ) - - -class Global(BaseModel): - model_config = ConfigDict(extra="forbid") - - namespace: Optional[str] = Field( - None, description="Namespace where the components will be deployed" - ) - timestamp: Optional[Union[int, str]] = Field( - None, - description="Timestamp to be used to trigger a new deployment, i.e. if you want pods to be restarted and pickup the latest tag", - ) - baseDomain: Optional[str] = Field( - None, description="Base domain to construct the FQDN for the service endpoints" - ) - storageClassName: Optional[str] = Field( - None, description="Storage class name for multiple reader/writer PVC" - ) - storageClassNameRWM: Optional[str] = Field( - None, description="Storage class name for the PVCs" - ) - metrics: Optional[Metrics] = None - - -class Login(BaseModel): - """Login endpoint configuration for simplified CLI login""" - enabled: Optional[bool] = None - hostname: Optional[str] = None - endpoint: Optional[str] = None - route: Optional[Dict[str, Any]] = None - ingress: Optional[Dict[str, Any]] = None - nodeport: Optional[Dict[str, Any]] = None - - -class Model(BaseModel): - model_config = ConfigDict(extra="forbid") - - jumpstarter_controller: Optional[Dict[str, Any]] = Field( - None, alias="jumpstarter-controller" - ) - global_: Optional[Global] = Field(None, alias="global") - login: Optional[Login] = Field( - None, description="Login endpoint configuration (passed to subchart)" - ) - - -print(json.dumps(Model.model_json_schema(), indent=2)) diff --git a/controller/deploy/helm/jumpstarter/values.kind.yaml b/controller/deploy/helm/jumpstarter/values.kind.yaml deleted file mode 100644 index d95f0283e..000000000 --- a/controller/deploy/helm/jumpstarter/values.kind.yaml +++ /dev/null @@ -1,10 +0,0 @@ -global: - baseDomain: jumpstarter.127.0.0.1.nip.io - metrics: - enabled: false - -jumpstarter-controller: - # image: quay.io/jumpstarter-dev/jumpstarter-controller - # tag: latest - grpc: - mode: "ingress" diff --git a/controller/deploy/helm/jumpstarter/values.schema.json b/controller/deploy/helm/jumpstarter/values.schema.json deleted file mode 100644 index f015467e6..000000000 --- a/controller/deploy/helm/jumpstarter/values.schema.json +++ /dev/null @@ -1,233 +0,0 @@ -{ - "$defs": { - "Global": { - "additionalProperties": false, - "properties": { - "namespace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Namespace where the components will be deployed", - "title": "Namespace" - }, - "timestamp": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Timestamp to be used to trigger a new deployment, i.e. if you want pods to be restarted and pickup the latest tag", - "title": "Timestamp" - }, - "baseDomain": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Base domain to construct the FQDN for the service endpoints", - "title": "Basedomain" - }, - "storageClassName": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Storage class name for multiple reader/writer PVC", - "title": "Storageclassname" - }, - "storageClassNameRWM": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Storage class name for the PVCs", - "title": "Storageclassnamerwm" - }, - "metrics": { - "anyOf": [ - { - "$ref": "#/$defs/Metrics" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "title": "Global", - "type": "object" - }, - "Login": { - "description": "Login endpoint configuration for simplified CLI login", - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Enabled" - }, - "hostname": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Hostname" - }, - "endpoint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Endpoint" - }, - "route": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Route" - }, - "ingress": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Ingress" - }, - "nodeport": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Nodeport" - } - }, - "title": "Login", - "type": "object" - }, - "Metrics": { - "additionalProperties": false, - "properties": { - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Whether to enable metrics exporting and service", - "title": "Enabled" - } - }, - "title": "Metrics", - "type": "object" - } - }, - "additionalProperties": false, - "properties": { - "jumpstarter-controller": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Jumpstarter-Controller" - }, - "global": { - "anyOf": [ - { - "$ref": "#/$defs/Global" - }, - { - "type": "null" - } - ], - "default": null - }, - "login": { - "anyOf": [ - { - "$ref": "#/$defs/Login" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Login endpoint configuration (passed to subchart)" - } - }, - "title": "Model", - "type": "object" -} diff --git a/controller/deploy/helm/jumpstarter/values.yaml b/controller/deploy/helm/jumpstarter/values.yaml deleted file mode 100644 index 98247a4e8..000000000 --- a/controller/deploy/helm/jumpstarter/values.yaml +++ /dev/null @@ -1,125 +0,0 @@ -## @section Global parameters -## @descriptionStart This section contains parameters common to all the -## components in the deployment. -## @descriptionEnd -## -## @param global.baseDomain Base domain to construct the FQDN for the service endpoints. -## @param global.namespace Namespace where the components will be deployed. -## @param global.storageClassName Storage class name for the PVCs. -## @param global.storageClassNameRWM Storage class name for multiple reader/writer PVCs. -## @param global.metrics.enabled Enable metrics exporting and service -## @param global.timestamp Timestamp to be used to trigger a new deployment, i.e. if you want pods to be restarted and pickup ":latest" - -global: - baseDomain: jumpstarter.my.domain.com - namespace: jumpstarter-lab - storageClassName: standard - storageClassNameRWM: standard - metrics: - enabled: false - timestamp: "" # can be used to timestamp deployments and make them reload - -## @section Jumpstarter Controller parameters -## @descriptionStart This section contains parameters for the Jumpstarter Controller. -## @descriptionEnd -## -## @param jumpstarter-controller.enabled Enable the Jumpstarter Controller. - -## @param jumpstarter-controller.image Image for the controller. -## @param jumpstarter-controller.tag Tag for the controller image. -## @param jumpstarter-controller.imagePullPolicy Image pull policy for the controller. - -## @param jumpstarter-controller.namespace Namespace where the controller will be deployed, defaults to global.namespace. - -## @param jumpstarter-controller.config.grpc.keepalive.minTime. The minimum amount of time a client should wait before sending a keepalive ping. -## @param jumpstarter-controller.config.grpc.keepalive.permitWithoutStream. Whether to allow keepalive pings even when there are no active streams(RPCs). - -## @param jumpstarter-controller.config.authentication.internal.prefix. Prefix to add to the subject claim of the tokens issued by the builtin authenticator. -## @param jumpstarter-controller.config.authentication.jwt. External OIDC authentication, see https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-authentication-configuration for documentation - -## @section Ingress And Route parameters -## @descriptionStart This section contains parameters for the Ingress and Route configurations. -## You can enable either the gRPC ingress or the OpenShift route but not both. -## @descriptionEnd -## -## @param jumpstarter-controller.grpc.hostname Hostname for the controller to use for the controller gRPC. -## @param jumpstarter-controller.grpc.routerHostname Hostname for the controller to use for the router gRPC. -## -## @param jumpstarter-controller.grpc.tls.mode Setup the TLS mode for endpoints, either "passthrough" or "reencrypt". -## @param jumpstarter-controller.grpc.tls.port Port to use for the gRPC endpoints ingress or route, this can be useful for ingress routers on non-standard ports. -## @param jumpstarter-controller.grpc.tls.controllerCertSecret Secret containing the TLS certificate/key for the gRPC endpoint. -## @param jumpstarter-controller.grpc.tls.routerCertSecret Secret containing the TLS certificate/key for the gRPC router endpoints. -## -## @param jumpstarter-controller.grpc.endpoint The endpoints are passed down to the services to -## know where to announce the endpoints to the clients. -## -## @param jumpstarter-controller.grpc.routerEndpoint The endpoints are passed down to the services to -## know where to announce the endpoints to the clients. -## -## @param jumpstarter-controller.grpc.ingress.enabled Enable the gRPC ingress configuration. -## -## @param jumpstarter-controller.grpc.mode Mode to use for the gRPC endpoints, either route or ingress. -## -## @param jumpstarter-controller.certManager.enabled Enable cert-manager integration for TLS certificates. -## When enabled, the jumpstarter-service-ca-cert configmap is required (deployment fails if missing). -## When disabled, the configmap is optional - pre-create it manually if TLS is needed. - - - -jumpstarter-controller: - enabled: true - - image: quay.io/jumpstarter-dev/jumpstarter-controller - tag: "" - imagePullPolicy: IfNotPresent - - namespace: "" - - # cert-manager integration for automatic TLS certificate management - # When enabled, the jumpstarter-service-ca-cert configmap is required - # When disabled (default), the configmap is optional - certManager: - enabled: false - - config: - grpc: - keepalive: - # Safety: potentially makes server vulnerable to DDoS - # https://grpc.io/docs/guides/keepalive/#how-configuring-keepalive-affects-a-call - minTime: 3s - permitWithoutStream: true - authentication: - internal: - prefix: "internal:" - # To trust service account tokens, first execute: - # kubectl create clusterrolebinding oidc-reviewer \ - # --clusterrole=system:service-account-issuer-discovery \ - # --group=system:unauthenticated - # Then uncomment: - # - # jwt: - # - issuer: - # url: https://kubernetes.default.svc.cluster.local - # audiences: - # - https://kubernetes.default.svc.cluster.local - # audienceMatchPolicy: MatchAny - # certificateAuthority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - # claimMappings: - # username: - # claim: "sub" - # prefix: "kubernetes:" - - grpc: - hostname: "" - routerHostname: "" - - endpoint: "" - routerEndpoint: "" - - tls: - mode: "passthrough" - port: 443 - routerCertSecret: "" - controllerCertSecret: "" - - mode: "route" # route, ingress or external(user created) diff --git a/controller/deploy/operator/README.md b/controller/deploy/operator/README.md index 5e9d8d49c..f03cb1359 100644 --- a/controller/deploy/operator/README.md +++ b/controller/deploy/operator/README.md @@ -92,24 +92,6 @@ the project, i.e.: kubectl apply -f https://raw.githubusercontent.com//jumpstarter-operator//dist/install.yaml ``` -### By providing a Helm Chart - -1. Build the chart using the optional helm plugin - -```sh -operator-sdk edit --plugins=helm/v1-alpha -``` - -2. See that a chart was generated under 'dist/chart', and users -can obtain this solution from there. - -**NOTE:** If you change the project, you need to update the Helm Chart -using the same command above to sync the latest changes. Furthermore, -if you create webhooks, you need to use the above command with -the '--force' flag and manually ensure that any custom configuration -previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' -is manually re-applied afterwards. - ## Contributing // TODO(user): Add detailed information on how you would like others to contribute to this project diff --git a/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go b/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go index 03e1d3123..85feb1a8e 100644 --- a/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go +++ b/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go @@ -543,7 +543,7 @@ func (r *JumpstarterReconciler) reconcileSecrets(ctx context.Context, jumpstarte log := logf.FromContext(ctx) // Create controller secret if it doesn't exist - // Use fixed name to match Helm chart for migration compatibility + // Use fixed name for stable secret references across CR updates controllerSecretName := "jumpstarter-controller-secret" if err := r.ensureSecretExists(ctx, jumpstarter, controllerSecretName); err != nil { log.Error(err, "Failed to ensure controller secret exists", "secret", controllerSecretName) @@ -551,7 +551,7 @@ func (r *JumpstarterReconciler) reconcileSecrets(ctx context.Context, jumpstarte } // Create router secret if it doesn't exist - // Use fixed name to match Helm chart for migration compatibility + // Use fixed name for stable secret references across CR updates routerSecretName := "jumpstarter-router-secret" if err := r.ensureSecretExists(ctx, jumpstarter, routerSecretName); err != nil { log.Error(err, "Failed to ensure router secret exists", "secret", routerSecretName) diff --git a/controller/hack/deploy_with_helm.sh b/controller/hack/deploy_with_helm.sh deleted file mode 100755 index 9c598ec76..000000000 --- a/controller/hack/deploy_with_helm.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" - -# Source common utilities -source "${SCRIPT_DIR}/utils" - -# Source common deployment variables -source "${SCRIPT_DIR}/deploy_vars" - -METHOD=install - -kubectl config use-context kind-jumpstarter - -# Install nginx ingress if in ingress mode -if [ "${NETWORKING_MODE}" == "ingress" ]; then - install_nginx_ingress -else - echo -e "${GREEN}Deploying with nodeport ...${NC}" -fi - -# Build Helm sets based on configuration -HELM_SETS="" -HELM_SETS="${HELM_SETS} --set global.baseDomain=${BASEDOMAIN}" -HELM_SETS="${HELM_SETS} --set jumpstarter-controller.grpc.endpoint=${GRPC_ENDPOINT}" -HELM_SETS="${HELM_SETS} --set jumpstarter-controller.grpc.routerEndpoint=${GRPC_ROUTER_ENDPOINT}" -HELM_SETS="${HELM_SETS} --set jumpstarter-controller.image=${IMAGE_REPO}" -HELM_SETS="${HELM_SETS} --set jumpstarter-controller.tag=${IMAGE_TAG}" - -# Enable appropriate networking mode -if [ "${NETWORKING_MODE}" == "ingress" ]; then - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.grpc.ingress.enabled=true" - # Login endpoint via ingress - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.login.enabled=true" - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.login.ingress.enabled=true" - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.login.ingress.class=nginx" - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.login.hostname=login.${BASEDOMAIN}" - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.login.endpoint=${LOGIN_ENDPOINT}" -else - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.grpc.nodeport.enabled=true" - # Login endpoint via nodeport (30014 -> 8086 on host) - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.login.enabled=true" - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.login.nodeport.enabled=true" - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.login.nodeport.port=30014" - HELM_SETS="${HELM_SETS} --set jumpstarter-controller.login.endpoint=${LOGIN_ENDPOINT}" -fi - -echo -e "${GREEN}Loading the ${IMG} in kind ...${NC}" -# load the docker image into the kind cluster -kind_load_image ${IMG} - - -# if we have an existing deployment, try to upgrade it instead -if helm list -A | grep jumpstarter > /dev/null; then - METHOD=upgrade -fi - -echo -e "${GREEN}Performing helm ${METHOD} ...${NC}" - -# install/update with helm -helm ${METHOD} --namespace jumpstarter-lab \ - --create-namespace \ - ${HELM_SETS} \ - --set global.timestamp=$(date +%s) \ - --values ./deploy/helm/jumpstarter/values.kind.yaml ${EXTRA_VALUES} jumpstarter \ - ./deploy/helm/jumpstarter/ - -kubectl config set-context --current --namespace=jumpstarter-lab - -# Check gRPC endpoints are ready -check_grpc_endpoints - -# Print success banner -print_deployment_success "Helm" diff --git a/controller/hack/install_helm.sh b/controller/hack/install_helm.sh deleted file mode 100755 index c2ea718a0..000000000 --- a/controller/hack/install_helm.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -x -euo pipefail - -which helm 2>/dev/null 1>/dev/null -if [ $? -eq 0 ]; then - echo "Helm already installed" - exit 0 -fi - -# Get the remote shell script and make sure it's the one we expect, inside the script there is also -# verification of the downloaded binaries -curl -fsSL -o /tmp/get_helm.sh https://raw.githubusercontent.com/helm/helm/0d0f91d1ce277b2c8766cdc4c7aa04dbafbf2503/scripts/get-helm-3 -echo "6701e269a95eec0a5f67067f504f43ad94e9b4a52ec1205d26b3973d6f5cb3dc /tmp/get_helm.sh" | sha256sum --check || exit 1 -chmod a+x /tmp/get_helm.sh -/tmp/get_helm.sh - -rm /tmp/get_helm.sh diff --git a/controller/hack/utils b/controller/hack/utils index e9437873c..a0c55ff4e 100755 --- a/controller/hack/utils +++ b/controller/hack/utils @@ -252,7 +252,7 @@ check_grpc_endpoints() { # Print deployment success banner # Args: -# $1: deployment method (e.g., "Helm", "operator") - optional +# $1: deployment method (e.g., "operator") - optional print_deployment_success() { local method=${1:-""} local method_text="" diff --git a/controller/internal/controller/suite_test.go b/controller/internal/controller/suite_test.go index ce79acd67..972f9fc0c 100644 --- a/controller/internal/controller/suite_test.go +++ b/controller/internal/controller/suite_test.go @@ -63,7 +63,7 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "deploy", "helm", "jumpstarter", "charts", "jumpstarter-controller", "templates", "crds")}, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "deploy", "operator", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, // The BinaryAssetsDirectory is only required if you want to run the tests directly diff --git a/controller/internal/service/suite_test.go b/controller/internal/service/suite_test.go index 607698e22..33117fd3d 100644 --- a/controller/internal/service/suite_test.go +++ b/controller/internal/service/suite_test.go @@ -54,7 +54,7 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "deploy", "helm", "jumpstarter", "charts", "jumpstarter-controller", "templates", "crds")}, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "deploy", "operator", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, // The BinaryAssetsDirectory is only required if you want to run the tests directly diff --git a/e2e/compat/setup.sh b/e2e/compat/setup.sh index 43150f44d..953edd0b8 100755 --- a/e2e/compat/setup.sh +++ b/e2e/compat/setup.sh @@ -2,8 +2,8 @@ # Jumpstarter Compatibility E2E Testing Setup Script # This script sets up the environment for cross-version compatibility tests. # -# No OIDC/dex is needed — tests use legacy auth (--unsafe --save). -# Uses helm directly (no operator) for simplicity. +# No OIDC/dex is needed -- tests use legacy auth (--unsafe --save). +# Uses operator-based deployment. # # Environment variables: # COMPAT_SCENARIO - "old-controller" or "old-client" (required) @@ -24,8 +24,7 @@ REPO_ROOT="$(cd "$E2E_DIR/.." && pwd)" # Default namespace for tests export JS_NAMESPACE="${JS_NAMESPACE:-jumpstarter-lab}" -# Always use helm for compat tests (simpler, direct control) -export METHOD="helm" +export METHOD="operator" # Scenario configuration COMPAT_SCENARIO="${COMPAT_SCENARIO:-old-controller}" @@ -92,15 +91,11 @@ create_cluster() { log_info "Kind cluster created" } -# Deploy old controller using the OCI helm chart from quay.io deploy_old_controller() { log_info "Deploying old controller (version: $COMPAT_CONTROLLER_TAG)..." cd "$REPO_ROOT" - # Strip leading 'v' for helm version (v0.7.1 -> 0.7.1) - local HELM_VERSION="${COMPAT_CONTROLLER_TAG#v}" - # Compute networking variables local IP IP=$(get_external_ip) @@ -110,18 +105,10 @@ deploy_old_controller() { kubectl config use-context kind-jumpstarter - # Install old controller from OCI helm chart - log_info "Installing old controller via helm (version: ${HELM_VERSION})..." - helm install --namespace jumpstarter-lab \ - --create-namespace \ - --set global.baseDomain="${BASEDOMAIN}" \ - --set jumpstarter-controller.grpc.endpoint="${GRPC_ENDPOINT}" \ - --set jumpstarter-controller.grpc.routerEndpoint="${GRPC_ROUTER_ENDPOINT}" \ - --set jumpstarter-controller.grpc.nodeport.enabled=true \ - --set jumpstarter-controller.grpc.mode=nodeport \ - --set global.metrics.enabled=false \ - --version="${HELM_VERSION}" \ - jumpstarter oci://quay.io/jumpstarter-dev/helm/jumpstarter + # Install old controller using operator installer from the release tag + local INSTALLER_URL="https://raw.githubusercontent.com/jumpstarter-dev/jumpstarter/${COMPAT_CONTROLLER_TAG}/controller/deploy/operator/dist/install.yaml" + log_info "Installing old controller via operator (version: ${COMPAT_CONTROLLER_TAG})..." + kubectl apply -f "${INSTALLER_URL}" || log_warn "Operator installer may not be available for ${COMPAT_CONTROLLER_TAG}, skipping" kubectl config set-context --current --namespace=jumpstarter-lab @@ -144,20 +131,12 @@ deploy_old_controller() { log_info "Old controller deployed" } -# Deploy new controller from HEAD deploy_new_controller() { log_info "Deploying new controller from HEAD..." cd "$REPO_ROOT" - if [ -z "${SKIP_BUILD:-}" ]; then - make -C controller docker-build - else - log_info "Skipping controller image build (SKIP_BUILD is set)" - fi - cd controller - ./hack/deploy_with_helm.sh - cd "$REPO_ROOT" + make -C controller deploy log_info "New controller deployed" } @@ -199,9 +178,9 @@ setup_test_environment() { cd "$REPO_ROOT" - # Get the controller endpoint from helm values + # Get the controller endpoint from Jumpstarter CR export ENDPOINT - ENDPOINT=$(helm get values jumpstarter --output json | jq -r '."jumpstarter-controller".grpc.endpoint') + ENDPOINT="grpc.$(kubectl get jumpstarter -n "${JS_NAMESPACE}" jumpstarter -o jsonpath='{.spec.baseDomain}' 2>/dev/null || echo 'jumpstarter.127.0.0.1.nip.io'):8082" log_info "Controller endpoint: $ENDPOINT" diff --git a/e2e/setup-e2e.sh b/e2e/setup-e2e.sh index fc8bce67f..0579e56da 100755 --- a/e2e/setup-e2e.sh +++ b/e2e/setup-e2e.sh @@ -13,7 +13,7 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Default namespace for tests export JS_NAMESPACE="${JS_NAMESPACE:-jumpstarter-lab}" -# Deployment method: operator (default) or helm +# Deployment method export METHOD="${METHOD:-operator}" # Color output @@ -266,26 +266,8 @@ deploy_controller() { cd "$REPO_ROOT" - # Validate METHOD - if [ "$METHOD" != "operator" ] && [ "$METHOD" != "helm" ]; then - log_error "Unknown deployment method: $METHOD (expected 'operator' or 'helm')" - exit 1 - fi - - # Use parallel make on Linux (GNU Make 4+); macOS ships an old make without --output-sync - local make_parallel="" - if [ "$(uname -s)" = "Linux" ]; then - make_parallel="-j5 --output-sync=target" - fi - - log_info "Deploying controller with CA certificate using $METHOD..." - if [ "$METHOD" = "operator" ]; then - OPERATOR_USE_DEX=true DEX_CA_FILE="$REPO_ROOT/ca.pem" METHOD=$METHOD \ - make -C controller deploy $make_parallel - else - EXTRA_VALUES="--values $REPO_ROOT/.e2e/values.kind.yaml" METHOD=$METHOD \ - make -C controller deploy $make_parallel - fi + log_info "Deploying controller with CA certificate using operator..." + OPERATOR_USE_DEX=true DEX_CA_FILE="$REPO_ROOT/ca.pem" make -C controller deploy log_info "✓ Controller deployed" } @@ -300,23 +282,13 @@ setup_test_environment() { cd "$REPO_ROOT" - # Get the controller endpoint based on deployment method + # Get the controller endpoint from the Jumpstarter CR # Note: We declare BASEDOMAIN separately from assignment so that command # failures propagate under set -e (local VAR=$(...) masks exit codes). local BASEDOMAIN - if [ "$METHOD" = "operator" ]; then - # For operator deployment, construct the endpoint from the Jumpstarter CR - # The operator uses nodeport mode by default with port 8082 - BASEDOMAIN=$(kubectl get jumpstarter -n "${JS_NAMESPACE}" jumpstarter -o jsonpath='{.spec.baseDomain}') - export ENDPOINT="grpc.${BASEDOMAIN}:8082" - export LOGIN_ENDPOINT="login.${BASEDOMAIN}:8086" - else - # For helm deployment, get the endpoint from helm values - export ENDPOINT=$(helm get values jumpstarter --output json | jq -r '."jumpstarter-controller".grpc.endpoint') - # Login endpoint is on nodeport 30014 mapped to host port 8086 - BASEDOMAIN=$(helm get values jumpstarter --output json | jq -r '.global.baseDomain') - export LOGIN_ENDPOINT="login.${BASEDOMAIN}:8086" - fi + BASEDOMAIN=$(kubectl get jumpstarter -n "${JS_NAMESPACE}" jumpstarter -o jsonpath='{.spec.baseDomain}') + export ENDPOINT="grpc.${BASEDOMAIN}:8082" + export LOGIN_ENDPOINT="login.${BASEDOMAIN}:8086" log_info "Controller endpoint: $ENDPOINT" log_info "Login endpoint: $LOGIN_ENDPOINT" diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/__init__.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/__init__.py index 41eb3f9f3..81a739798 100644 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/__init__.py +++ b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/__init__.py @@ -7,7 +7,6 @@ from .delete import delete from .get import get from .import_res import import_res -from .install import install, ip, uninstall @click.group(cls=AliasedGroup) @@ -19,12 +18,8 @@ def admin(): admin.add_command(get) admin.add_command(create) admin.add_command(delete) -admin.add_command(install) -admin.add_command(uninstall) -admin.add_command(ip) admin.add_command(import_res) admin.add_command(version) -admin.add_command(ip) if __name__ == "__main__": admin() diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py index 2b32c83c8..5b5134dc7 100644 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py +++ b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py @@ -223,31 +223,12 @@ async def create_exporter( is_flag=True, help="Skip installing Jumpstarter after creating the cluster", ) -@click.option( - "--install-method", - type=click.Choice(["operator", "helm"]), - default=None, - help="Method to install Jumpstarter (default: helm for kind/minikube, operator for k3s)", -) @click.option( "--operator-installer", type=str, default=None, help="Path or URL to the operator installer YAML (auto-detected from version if not specified)", ) -@click.option( - "--helm", - type=str, - help="Path or name of a helm executable (only used with --install-method helm)", - default="helm", -) -@click.option( - "--chart", - type=str, - help="The URL of a Jumpstarter helm chart to install", - default="oci://quay.io/jumpstarter-dev/helm/jumpstarter", -) -@click.option("--chart-name", type=str, help="The name of the chart installation", default="jumpstarter") @click.option( "-n", "--namespace", type=str, help="Namespace to install Jumpstarter components in", default="jumpstarter-lab" ) @@ -256,14 +237,6 @@ async def create_exporter( @click.option("-g", "--grpc-endpoint", type=str, help="The gRPC endpoint to use for the Jumpstarter API", default=None) @click.option("-r", "--router-endpoint", type=str, help="The gRPC endpoint to use for the router", default=None) @click.option("-v", "--version", help="The version of the service to install", default=None) -@click.option( - "-f", - "--values-file", - "values_files", - type=click.Path(exists=True, readable=True, dir_okay=False, resolve_path=True), - multiple=True, - help="Path to custom helm values file (can be specified multiple times)", -) @opt_kubeconfig @opt_context @opt_nointeractive @@ -279,18 +252,13 @@ async def create_cluster( minikube_extra_args: str, extra_certs: Optional[str], skip_install: bool, - install_method: str, operator_installer: Optional[str], - helm: str, - chart: str, - chart_name: str, namespace: str, ip: Optional[str], basedomain: Optional[str], grpc_endpoint: Optional[str], router_endpoint: Optional[str], version: Optional[str], - values_files: tuple[str, ...], kubeconfig: Optional[str], context: Optional[str], nointeractive: bool, @@ -329,9 +297,6 @@ async def create_cluster( minikube or "minikube", extra_certs, install_jumpstarter=not skip_install, - helm=helm, - chart=chart, - chart_name=chart_name, namespace=namespace, version=version, kubeconfig=kubeconfig, @@ -341,9 +306,7 @@ async def create_cluster( grpc_endpoint=grpc_endpoint, router_endpoint=router_endpoint, callback=callback, - values_files=list(values_files) if values_files else None, k3s_ssh_host=k3s, - install_method=install_method, operator_installer=operator_installer, ) except JumpstarterKubernetesError as e: diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py index e6b60685e..d5c76acfa 100644 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py +++ b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py @@ -489,29 +489,6 @@ def test_create_cluster_skip_install(self, mock_validate, mock_create): kwargs = mock_create.call_args[1] assert kwargs.get("install_jumpstarter", True) is False - @patch("jumpstarter_cli_admin.create.create_cluster_and_install") - @patch("jumpstarter_cli_admin.create.validate_cluster_type_selection") - def test_create_cluster_with_custom_chart(self, mock_validate, mock_create): - """Test with custom chart and version options""" - mock_validate.return_value = "kind" - mock_create.return_value = None - - result = self.runner.invoke(create, [ - "cluster", "test-cluster", "--kind", "kind", - "--chart", "oci://custom.registry/helm/chart", - "--chart-name", "custom-jumpstarter", - "--version", "v1.2.3" - ]) - - assert result.exit_code == 0 - mock_create.assert_called_once() - - # Verify custom chart options - kwargs = mock_create.call_args[1] - assert kwargs.get("chart") == "oci://custom.registry/helm/chart" - assert kwargs.get("chart_name") == "custom-jumpstarter" - assert kwargs.get("version") == "v1.2.3" - @patch("jumpstarter_cli_admin.create.create_cluster_and_install") @patch("jumpstarter_cli_admin.create.validate_cluster_type_selection") def test_create_cluster_with_custom_endpoints(self, mock_validate, mock_create): @@ -622,18 +599,6 @@ def test_create_cluster_with_extra_certs(self, mock_validate, mock_create): import os os.unlink(cert_path) - @patch("jumpstarter_cli_admin.create.create_cluster_and_install") - @patch("jumpstarter_cli_admin.create.validate_cluster_type_selection") - def test_create_cluster_helm_not_installed(self, mock_validate, mock_create): - """Test error when helm is not installed (for installation)""" - mock_validate.return_value = "kind" - mock_create.side_effect = click.ClickException("helm is not installed") - - result = self.runner.invoke(create, ["cluster", "test-cluster", "--kind", "kind"]) - - assert result.exit_code != 0 - assert "helm is not installed" in result.output - @patch("jumpstarter_cli_admin.create.create_cluster_and_install") @patch("jumpstarter_cli_admin.create.validate_cluster_type_selection") def test_create_cluster_kind_not_installed(self, mock_validate, mock_create): diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get.py index 09c63fd43..0f0904355 100644 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get.py +++ b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get.py @@ -119,19 +119,18 @@ async def get_lease( "--type", type=click.Choice(["kind", "minikube", "remote", "all"]), default="all", help="Filter clusters by type" ) @click.option("--kubectl", type=str, help="Path or name of kubectl executable", default="kubectl") -@click.option("--helm", type=str, help="Path or name of helm executable", default="helm") @click.option("--kind", type=str, help="Path or name of kind executable", default="kind") @click.option("--minikube", type=str, help="Path or name of minikube executable", default="minikube") @opt_output_all @blocking async def get_cluster( - name: Optional[str], type: str, kubectl: str, helm: str, kind: str, minikube: str, output: OutputType + name: Optional[str], type: str, kubectl: str, kind: str, minikube: str, output: OutputType ): """Get information about a specific cluster or list all clusters""" try: if name is not None: # Get specific cluster by context name - cluster_info = await get_cluster_info(name, kubectl, helm, minikube) + cluster_info = await get_cluster_info(name, kubectl, minikube) # Check if the cluster context was found if cluster_info.error and "not found" in cluster_info.error: @@ -140,7 +139,7 @@ async def get_cluster( model_print(cluster_info, output) else: # List all clusters if no name provided - cluster_list = await list_clusters(type, kubectl, helm, kind, minikube) + cluster_list = await list_clusters(type, kubectl, kind, minikube) model_print(cluster_list, output) except click.ClickException: raise @@ -153,15 +152,14 @@ async def get_cluster( "--type", type=click.Choice(["kind", "minikube", "remote", "all"]), default="all", help="Filter clusters by type" ) @click.option("--kubectl", type=str, help="Path or name of kubectl executable", default="kubectl") -@click.option("--helm", type=str, help="Path or name of helm executable", default="helm") @click.option("--kind", type=str, help="Path or name of kind executable", default="kind") @click.option("--minikube", type=str, help="Path or name of minikube executable", default="minikube") @opt_output_all @blocking -async def get_clusters(type: str, kubectl: str, helm: str, kind: str, minikube: str, output: OutputType): +async def get_clusters(type: str, kubectl: str, kind: str, minikube: str, output: OutputType): """List all Kubernetes clusters with Jumpstarter status""" try: - cluster_list = await list_clusters(type, kubectl, helm, kind, minikube) + cluster_list = await list_clusters(type, kubectl, kind, minikube) # Use model_print for all output formats model_print(cluster_list, output) diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/install.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/install.py deleted file mode 100644 index 6f3ced67a..000000000 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/install.py +++ /dev/null @@ -1,255 +0,0 @@ -from typing import Literal, Optional - -import click -from jumpstarter_cli_common.blocking import blocking -from jumpstarter_cli_common.opt import opt_context, opt_kubeconfig -from jumpstarter_cli_common.version import get_client_version -from jumpstarter_kubernetes import ( - get_latest_compatible_controller_version, - helm_installed, - install_helm_chart, - minikube_installed, - uninstall_helm_chart, -) - -from jumpstarter.common.ipaddr import get_ip_address, get_minikube_ip - - -def _validate_cluster_type( - kind: Optional[str], minikube: Optional[str] -) -> Optional[Literal["kind"] | Literal["minikube"]]: - """Validate cluster type selection - returns None if neither is specified""" - if kind and minikube: - raise click.ClickException('You can only select one local cluster type "kind" or "minikube"') - - if kind is not None: - return "kind" - elif minikube is not None: - return "minikube" - else: - return None - - -def _validate_prerequisites(helm: str) -> None: - if helm_installed(helm) is False: - raise click.ClickException( - "helm is not installed (or not in your PATH), please specify a helm executable with --helm " - ) - - -async def _configure_endpoints( - cluster_type: Optional[str], - minikube: str, - cluster_name: str, - ip: Optional[str], - basedomain: Optional[str], - grpc_endpoint: Optional[str], - router_endpoint: Optional[str], -) -> tuple[str, str, str, str]: - if ip is None: - ip = await get_ip_generic(cluster_type, minikube, cluster_name) - if basedomain is None: - basedomain = f"jumpstarter.{ip}.nip.io" - if grpc_endpoint is None: - grpc_endpoint = f"grpc.{basedomain}:8082" - if router_endpoint is None: - router_endpoint = f"router.{basedomain}:8083" - - return ip, basedomain, grpc_endpoint, router_endpoint - - -async def _install_jumpstarter_helm_chart( - chart: str, - name: str, - namespace: str, - basedomain: str, - grpc_endpoint: str, - router_endpoint: str, - mode: str, - version: str, - kubeconfig: Optional[str], - context: Optional[str], - helm: str, - ip: str, - values_files: Optional[list[str]] = None, -) -> None: - click.echo(f'Installing Jumpstarter service v{version} in namespace "{namespace}" with Helm\n') - click.echo(f"Chart URI: {chart}") - click.echo(f"Chart Version: {version}") - click.echo(f"IP Address: {ip}") - click.echo(f"Basedomain: {basedomain}") - click.echo(f"Service Endpoint: {grpc_endpoint}") - click.echo(f"Router Endpoint: {router_endpoint}") - click.echo(f"gPRC Mode: {mode}\n") - - await install_helm_chart( - chart, - name, - namespace, - basedomain, - grpc_endpoint, - router_endpoint, - mode, - version, - kubeconfig, - context, - helm, - values_files, - ) - - click.echo(f'Installed Helm release "{name}" in namespace "{namespace}"') - - -async def get_ip_generic(cluster_type: Optional[str], minikube: str, cluster_name: str) -> str: - if cluster_type == "minikube": - if not minikube_installed(minikube): - raise click.ClickException("minikube is not installed (or not in your PATH)") - try: - ip = await get_minikube_ip(cluster_name, minikube) - except Exception as e: - raise click.ClickException(f"Could not determine Minikube IP address.\n{e}") from e - else: - ip = get_ip_address() - if ip == "0.0.0.0": - raise click.ClickException("Could not determine IP address, use --ip to specify an IP address") - - return ip - - -@click.command -@click.option("--helm", type=str, help="Path or name of a helm executable", default="helm") -@click.option("--name", type=str, help="The name of the chart installation", default="jumpstarter") -@click.option( - "-c", - "--chart", - type=str, - help="The URL of a Jumpstarter helm chart to install", - default="oci://quay.io/jumpstarter-dev/helm/jumpstarter", -) -@click.option( - "-n", "--namespace", type=str, help="Namespace to install Jumpstarter components in", default="jumpstarter-lab" -) -@click.option("-i", "--ip", type=str, help="IP address of your host machine", default=None) -@click.option("-b", "--basedomain", type=str, help="Base domain of the Jumpstarter service", default=None) -@click.option("-g", "--grpc-endpoint", type=str, help="The gRPC endpoint to use for the Jumpstarter API", default=None) -@click.option("-r", "--router-endpoint", type=str, help="The gRPC endpoint to use for the router", default=None) -@click.option("--nodeport", "mode", flag_value="nodeport", help="Use Nodeport routing (recommended)", default=True) -@click.option("--ingress", "mode", flag_value="ingress", help="Use a Kubernetes ingress") -@click.option("--route", "mode", flag_value="route", help="Use an OpenShift route") -@click.option( - "--kind", is_flag=False, flag_value="kind", default=None, help="Use default settings for a local Kind cluster" -) -@click.option( - "--minikube", - is_flag=False, - flag_value="minikube", - default=None, - help="Use default settings for a local Minikube cluster", -) -@click.option("-v", "--version", help="The version of the service to install", default=None) -@click.option( - "-f", - "--values-file", - "values_files", - type=click.Path(exists=True, readable=True, dir_okay=False, resolve_path=True), - multiple=True, - help="Path to custom helm values file (can be specified multiple times)", -) -@opt_kubeconfig -@opt_context -@blocking -async def install( - helm: str, - chart: str, - name: str, - namespace: str, - ip: Optional[str], - basedomain: Optional[str], - grpc_endpoint: Optional[str], - router_endpoint: Optional[str], - mode: Literal["nodeport"] | Literal["ingress"] | Literal["route"], - kind: Optional[str], - minikube: Optional[str], - version: str, - values_files: tuple[str, ...], - kubeconfig: Optional[str], - context: Optional[str], -): - """Install the Jumpstarter service in a Kubernetes cluster""" - _validate_prerequisites(helm) - - cluster_type = _validate_cluster_type(kind, minikube) - - ip, basedomain, grpc_endpoint, router_endpoint = await _configure_endpoints( - cluster_type, minikube or "minikube", "jumpstarter-lab", ip, basedomain, grpc_endpoint, router_endpoint - ) - - if version is None: - version = await get_latest_compatible_controller_version(get_client_version()) - - await _install_jumpstarter_helm_chart( - chart, - name, - namespace, - basedomain, - grpc_endpoint, - router_endpoint, - mode, - version, - kubeconfig, - context, - helm, - ip, - list(values_files) if values_files else None, - ) - - -@click.command -@click.option( - "--kind", is_flag=False, flag_value="kind", default=None, help="Use default settings for a local Kind cluster" -) -@click.option( - "--minikube", - is_flag=False, - flag_value="minikube", - default=None, - help="Use default settings for a local Minikube cluster", -) -@click.option("--cluster-name", type=str, help="The name of the cluster", default="jumpstarter-lab") -@blocking -async def ip( - kind: Optional[str], - minikube: Optional[str], - cluster_name: str, -): - """Attempt to determine the IP address of your computer""" - cluster_type = _validate_cluster_type(kind, minikube) - minikube_binary = minikube or "minikube" - ip = await get_ip_generic(cluster_type, minikube_binary, cluster_name) - click.echo(ip) - - -@click.command -@click.option("--helm", type=str, help="Path or name of a helm executable", default="helm") -@click.option("--name", type=str, help="The name of the chart installation", default="jumpstarter") -@click.option( - "-n", "--namespace", type=str, help="Namespace to install Jumpstarter components in", default="jumpstarter-lab" -) -@opt_kubeconfig -@opt_context -@blocking -async def uninstall( - helm: str, - name: str, - namespace: str, - kubeconfig: Optional[str], - context: Optional[str], -): - """Uninstall the Jumpstarter service in a Kubernetes cluster""" - _validate_prerequisites(helm) - - click.echo(f'Uninstalling Jumpstarter service in namespace "{namespace}" with Helm') - - await uninstall_helm_chart(name, namespace, kubeconfig, context, helm) - - click.echo(f'Uninstalled Helm release "{name}" from namespace "{namespace}"') diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/install_test.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/install_test.py deleted file mode 100644 index 5a9f1ff61..000000000 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/install_test.py +++ /dev/null @@ -1,137 +0,0 @@ -from unittest.mock import patch - -import click -import pytest -from click.testing import CliRunner -from jumpstarter_kubernetes.callbacks import SilentCallback -from jumpstarter_kubernetes.cluster.kind import create_kind_cluster_with_options -from jumpstarter_kubernetes.cluster.minikube import create_minikube_cluster_with_options -from jumpstarter_kubernetes.cluster.operations import validate_cluster_type_selection - -from jumpstarter_cli_admin.install import ( - _validate_prerequisites, - install, - uninstall, -) - - -class TestValidationFunctions: - """Test validation helper functions.""" - - @patch("jumpstarter_cli_admin.install.helm_installed") - def test_validate_prerequisites_helm_installed(self, mock_helm_installed): - mock_helm_installed.return_value = True - # Should not raise any exception - _validate_prerequisites("helm") - - @patch("jumpstarter_cli_admin.install.helm_installed") - def test_validate_prerequisites_helm_not_installed(self, mock_helm_installed): - mock_helm_installed.return_value = False - with pytest.raises(click.ClickException, match="helm is not installed"): - _validate_prerequisites("helm") - - def test_validate_cluster_type_both_specified(self): - """Test that error is raised when both kind and minikube are specified.""" - from jumpstarter_kubernetes.exceptions import ClusterTypeValidationError - - with pytest.raises( - ClusterTypeValidationError, match='You can only select one cluster type' - ): - validate_cluster_type_selection("kind", "minikube") - - def test_validate_cluster_type_kind_only(self): - """Test that 'kind' is returned when only kind is specified.""" - result = validate_cluster_type_selection("kind", None) - assert result == "kind" - - def test_validate_cluster_type_minikube_only(self): - """Test that 'minikube' is returned when only minikube is specified.""" - result = validate_cluster_type_selection(None, "minikube") - assert result == "minikube" - - # Note: test_validate_cluster_type_auto_detect removed as this function - # is now tested in the jumpstarter-kubernetes library - - -class TestEndpointConfiguration: - """Test endpoint configuration functions.""" - - # Note: test_configure_endpoints_minikube removed as this function - # is now tested in the jumpstarter-kubernetes library - - -class TestClusterCreation: - """Test cluster creation functions.""" - - # Note: Tests for _handle_cluster_creation, _create_kind_cluster, and _create_minikube_cluster - # have been removed as these functions no longer exist after the refactoring. - # The functionality is now tested through the new create_kind_cluster_with_options - # and create_minikube_cluster_with_options functions in their respective modules. - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.kind.kind_installed") - @patch("jumpstarter_kubernetes.cluster.kind.create_kind_cluster") - async def test_create_kind_cluster_with_options_success(self, mock_create_kind, mock_kind_installed): - """Test creating a Kind cluster with the new function structure.""" - - mock_kind_installed.return_value = True - mock_create_kind.return_value = True - callback = SilentCallback() - - await create_kind_cluster_with_options( - "kind", "test-cluster", "--verbosity=1", False, None, callback - ) - - mock_create_kind.assert_called_once() - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.kind.kind_installed") - async def test_create_kind_cluster_with_options_not_installed(self, mock_kind_installed): - """Test that ToolNotInstalledError is raised when kind is not installed.""" - from jumpstarter_kubernetes.exceptions import ToolNotInstalledError - - mock_kind_installed.return_value = False - callback = SilentCallback() - - with pytest.raises(ToolNotInstalledError, match="kind is not installed"): - await create_kind_cluster_with_options( - "kind", "test-cluster", "", False, None, callback - ) - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.minikube.minikube_installed") - @patch("jumpstarter_kubernetes.cluster.minikube.create_minikube_cluster") - async def test_create_minikube_cluster_with_options_success(self, mock_create_minikube, mock_minikube_installed): - """Test creating a Minikube cluster with the new function structure.""" - mock_minikube_installed.return_value = True - mock_create_minikube.return_value = True - callback = SilentCallback() - - await create_minikube_cluster_with_options( - "minikube", "test-cluster", "--memory=4096", False, None, callback - ) - - mock_create_minikube.assert_called_once() - - -class TestIPDetection: - """Test IP address detection functions.""" - - # Note: test_get_ip_generic_minikube and test_get_ip_generic_fallback removed - # as these functions are now tested in the jumpstarter-kubernetes library - - -class TestCLICommands: - """Test CLI command execution.""" - - def test_install_command_help(self): - runner = CliRunner() - result = runner.invoke(install, ["--help"]) - assert result.exit_code == 0 - assert "Install the Jumpstarter service" in result.output - - def test_uninstall_command_help(self): - runner = CliRunner() - result = runner.invoke(uninstall, ["--help"]) - assert result.exit_code == 0 - assert "Uninstall the Jumpstarter service" in result.output diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py index 668093345..dd785de75 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py @@ -26,11 +26,6 @@ V1Alpha1ExporterList, V1Alpha1ExporterStatus, ) -from .install import ( - helm_installed, - install_helm_chart, - uninstall_helm_chart, -) from .leases import ( LeasesV1Alpha1Api, V1Alpha1Lease, @@ -61,9 +56,6 @@ "V1Alpha1LeaseSelector", "V1Alpha1LeaseSpec", "V1Alpha1List", - "helm_installed", - "install_helm_chart", - "uninstall_helm_chart", "minikube_installed", "kind_installed", "create_minikube_cluster", diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/__init__.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/__init__.py index 1f4535426..dbbfb0def 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/__init__.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/__init__.py @@ -2,7 +2,6 @@ This module provides comprehensive cluster management functionality including: - Kind and Minikube cluster operations -- Helm chart management - Kubectl operations - Cluster detection and endpoint configuration - High-level orchestration operations @@ -15,7 +14,6 @@ # Common utilities and types from .common import ( ClusterType, - InstallMethod, extract_host_from_ssh, format_cluster_name, get_extra_certs_path, @@ -29,9 +27,6 @@ from .detection import auto_detect_cluster_type, detect_cluster_type, detect_existing_cluster_type from .endpoints import configure_endpoints, get_ip_generic -# Helm operations -from .helm import install_jumpstarter_helm_chart - # k3s cluster operations from .k3s import ( create_k3s_cluster, @@ -87,7 +82,6 @@ __all__ = [ # Types "ClusterType", - "InstallMethod", # Common utilities "extract_host_from_ssh", "validate_cluster_name", @@ -113,8 +107,6 @@ "delete_minikube_cluster", "list_minikube_clusters", "get_minikube_cluster_ip", - # Helm operations - "install_jumpstarter_helm_chart", # Operator operations "install_jumpstarter_operator", # Kubectl operations diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/common.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/common.py index 36899d8d6..9ba5ef0f8 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/common.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/common.py @@ -7,7 +7,6 @@ from ..exceptions import ClusterTypeValidationError ClusterType = Literal["kind"] | Literal["minikube"] | Literal["k3s"] -InstallMethod = Literal["operator", "helm"] # NodePort assignments (must match kind_cluster.yaml extraPortMappings and operator CR) GRPC_NODEPORT = 30010 diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/helm.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/helm.py deleted file mode 100644 index 0b3c6b903..000000000 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/helm.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Helm chart management operations.""" - -from typing import Optional - -from ..callbacks import OutputCallback, SilentCallback -from ..install import install_helm_chart - - -async def install_jumpstarter_helm_chart( - chart: str, - name: str, - namespace: str, - basedomain: str, - grpc_endpoint: str, - router_endpoint: str, - mode: str, - version: str, - kubeconfig: Optional[str], - context: Optional[str], - helm: str, - ip: str, - callback: OutputCallback = None, - values_files: Optional[list[str]] = None, -) -> None: - """Install Jumpstarter Helm chart.""" - if callback is None: - callback = SilentCallback() - - callback.progress(f'Installing Jumpstarter service v{version} in namespace "{namespace}" with Helm\n') - callback.progress(f"Chart URI: {chart}") - callback.progress(f"Chart Version: {version}") - callback.progress(f"IP Address: {ip}") - callback.progress(f"Basedomain: {basedomain}") - callback.progress(f"Service Endpoint: {grpc_endpoint}") - callback.progress(f"Router Endpoint: {router_endpoint}") - callback.progress(f"gRPC Mode: {mode}\n") - - await install_helm_chart( - chart, - name, - namespace, - basedomain, - grpc_endpoint, - router_endpoint, - mode, - version, - kubeconfig, - context, - helm, - values_files, - ) - - callback.success(f'Installed Helm release "{name}" in namespace "{namespace}"') diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/helm_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/helm_test.py deleted file mode 100644 index 3dce9f60d..000000000 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/helm_test.py +++ /dev/null @@ -1,243 +0,0 @@ -"""Tests for Helm chart management operations.""" - -from unittest.mock import patch - -import pytest - -from jumpstarter_kubernetes.cluster.helm import install_jumpstarter_helm_chart - - -class TestInstallJumpstarterHelmChart: - """Test Jumpstarter Helm chart installation.""" - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.helm.install_helm_chart") - async def test_install_jumpstarter_helm_chart_all_params(self, mock_install_helm_chart): - from unittest.mock import MagicMock - - mock_install_helm_chart.return_value = None - mock_callback = MagicMock() - - await install_jumpstarter_helm_chart( - chart="oci://registry.example.com/jumpstarter", - name="jumpstarter", - namespace="jumpstarter-system", - basedomain="jumpstarter.192.168.1.100.nip.io", - grpc_endpoint="grpc.jumpstarter.192.168.1.100.nip.io:8082", - router_endpoint="router.jumpstarter.192.168.1.100.nip.io:8083", - mode="insecure", - version="1.0.0", - kubeconfig="/path/to/kubeconfig", - context="test-context", - helm="helm", - ip="192.168.1.100", - callback=mock_callback, - ) - - # Verify that install_helm_chart was called with correct parameters - mock_install_helm_chart.assert_called_once_with( - "oci://registry.example.com/jumpstarter", - "jumpstarter", - "jumpstarter-system", - "jumpstarter.192.168.1.100.nip.io", - "grpc.jumpstarter.192.168.1.100.nip.io:8082", - "router.jumpstarter.192.168.1.100.nip.io:8083", - "insecure", - "1.0.0", - "/path/to/kubeconfig", - "test-context", - "helm", - None, - ) - - # Verify callback was called - assert mock_callback.progress.call_count >= 7 # Multiple progress messages - assert mock_callback.success.call_count == 1 # One success message - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.helm.install_helm_chart") - async def test_install_jumpstarter_helm_chart_with_none_values(self, mock_install_helm_chart): - mock_install_helm_chart.return_value = None - - await install_jumpstarter_helm_chart( - chart="jumpstarter/jumpstarter", - name="my-jumpstarter", - namespace="default", - basedomain="test.example.com", - grpc_endpoint="grpc.test.example.com:443", - router_endpoint="router.test.example.com:443", - mode="secure", - version="2.1.0", - kubeconfig=None, - context=None, - helm="helm3", - ip="10.0.0.1", - ) - - # Verify that install_helm_chart was called with None values preserved - mock_install_helm_chart.assert_called_once_with( - "jumpstarter/jumpstarter", - "my-jumpstarter", - "default", - "test.example.com", - "grpc.test.example.com:443", - "router.test.example.com:443", - "secure", - "2.1.0", - None, - None, - "helm3", - None, - ) - - # Verify success message with correct values - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.helm.install_helm_chart") - async def test_install_jumpstarter_helm_chart_secure_mode(self, mock_install_helm_chart): - mock_install_helm_chart.return_value = None - - await install_jumpstarter_helm_chart( - chart="https://example.com/charts/jumpstarter-1.5.0.tgz", - name="production-jumpstarter", - namespace="production", - basedomain="jumpstarter.prod.example.com", - grpc_endpoint="grpc.jumpstarter.prod.example.com:443", - router_endpoint="router.jumpstarter.prod.example.com:443", - mode="secure", - version="1.5.0", - kubeconfig="/etc/kubernetes/admin.conf", - context="production-cluster", - helm="/usr/local/bin/helm", - ip="203.0.113.1", - ) - - # Verify gRPC mode is correctly displayed - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.helm.install_helm_chart") - async def test_install_jumpstarter_helm_chart_custom_endpoints(self, mock_install_helm_chart): - mock_install_helm_chart.return_value = None - - await install_jumpstarter_helm_chart( - chart="jumpstarter", - name="dev-jumpstarter", - namespace="development", - basedomain="dev.local", - grpc_endpoint="grpc-custom.dev.local:9090", - router_endpoint="router-custom.dev.local:9091", - mode="insecure", - version="0.9.0-beta", - kubeconfig="~/.kube/config", - context="dev-context", - helm="helm", - ip="172.16.0.10", - ) - - # Verify custom endpoints are displayed correctly - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.helm.install_helm_chart") - async def test_install_jumpstarter_helm_chart_install_helm_chart_error(self, mock_install_helm_chart): - # Test that exceptions from install_helm_chart propagate - mock_install_helm_chart.side_effect = Exception("Helm installation failed") - - with pytest.raises(Exception, match="Helm installation failed"): - await install_jumpstarter_helm_chart( - chart="jumpstarter", - name="test-jumpstarter", - namespace="test", - basedomain="test.local", - grpc_endpoint="grpc.test.local:8082", - router_endpoint="router.test.local:8083", - mode="insecure", - version="1.0.0", - kubeconfig=None, - context=None, - helm="helm", - ip="192.168.1.1", - ) - - # Exception was raised correctly - test complete - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.helm.install_helm_chart") - async def test_install_jumpstarter_helm_chart_minimal_params(self, mock_install_helm_chart): - mock_install_helm_chart.return_value = None - - await install_jumpstarter_helm_chart( - chart="minimal", - name="min", - namespace="min-ns", - basedomain="min.io", - grpc_endpoint="grpc.min.io:80", - router_endpoint="router.min.io:80", - mode="test", - version="0.1.0", - kubeconfig=None, - context=None, - helm="h", - ip="1.1.1.1", - ) - - # Verify all required parameters work with minimal values - mock_install_helm_chart.assert_called_once_with( - "minimal", - "min", - "min-ns", - "min.io", - "grpc.min.io:80", - "router.min.io:80", - "test", - "0.1.0", - None, - None, - "h", - None, - ) - - # Verify appropriate echo calls were made - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.helm.install_helm_chart") - async def test_install_jumpstarter_helm_chart_with_values_files(self, mock_install_helm_chart): - """Test that values_files parameter is passed through correctly.""" - from unittest.mock import MagicMock - - mock_install_helm_chart.return_value = None - mock_callback = MagicMock() - - values_files = ["/path/to/values1.yaml", "/path/to/values2.yaml"] - - await install_jumpstarter_helm_chart( - chart="oci://registry.example.com/jumpstarter", - name="jumpstarter", - namespace="jumpstarter-system", - basedomain="jumpstarter.192.168.1.100.nip.io", - grpc_endpoint="grpc.jumpstarter.192.168.1.100.nip.io:8082", - router_endpoint="router.jumpstarter.192.168.1.100.nip.io:8083", - mode="insecure", - version="1.0.0", - kubeconfig="/path/to/kubeconfig", - context="test-context", - helm="helm", - ip="192.168.1.100", - callback=mock_callback, - values_files=values_files, - ) - - # Verify that install_helm_chart was called with values_files - mock_install_helm_chart.assert_called_once_with( - "oci://registry.example.com/jumpstarter", - "jumpstarter", - "jumpstarter-system", - "jumpstarter.192.168.1.100.nip.io", - "grpc.jumpstarter.192.168.1.100.nip.io:8082", - "router.jumpstarter.192.168.1.100.nip.io:8083", - "insecure", - "1.0.0", - "/path/to/kubeconfig", - "test-context", - "helm", - values_files, - ) diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py index 001c1aef1..d9e12804d 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py @@ -73,9 +73,9 @@ async def get_kubectl_contexts(kubectl: str = "kubectl") -> List[Dict[str, str]] async def check_jumpstarter_installation( # noqa: C901 - context: str, namespace: Optional[str] = None, helm: str = "helm", kubectl: str = "kubectl" + context: str, namespace: Optional[str] = None, kubectl: str = "kubectl" ) -> V1Alpha1JumpstarterInstance: - """Check if Jumpstarter is installed in the cluster.""" + """Check if Jumpstarter is installed in the cluster using CRD detection.""" result_data = { "installed": False, "version": None, @@ -90,86 +90,18 @@ async def check_jumpstarter_installation( # noqa: C901 } try: - # Check for Helm installation first - helm_cmd = [helm, "list", "--all-namespaces", "-o", "json", "--kube-context", context] - returncode, stdout, _ = await run_command(helm_cmd) - - if returncode == 0: - # Extract JSON from output (handle case where warnings are printed before JSON) - json_start = stdout.find("[") - if json_start >= 0: - json_output = stdout[json_start:] - releases = json.loads(json_output) - else: - releases = json.loads(stdout) # Fallback to original parsing - for release in releases: - # Look for Jumpstarter chart - if "jumpstarter" in release.get("chart", "").lower(): - result_data["installed"] = True - result_data["version"] = release.get("app_version") or release.get("chart", "").split("-")[-1] - result_data["namespace"] = release.get("namespace") - result_data["chart_name"] = release.get("name") - result_data["status"] = release.get("status") - - # Try to get Helm values to extract basedomain and endpoints - try: - values_cmd = [ - helm, - "get", - "values", - release.get("name"), - "-n", - release.get("namespace"), - "-o", - "json", - "--kube-context", - context, - ] - values_returncode, values_stdout, _ = await run_command(values_cmd) - - if values_returncode == 0: - # Extract JSON from values output (handle warnings) - json_start = values_stdout.find("{") - if json_start >= 0: - json_output = values_stdout[json_start:] - values = json.loads(json_output) - else: - values = json.loads(values_stdout) # Fallback - - # Extract basedomain - basedomain = values.get("global", {}).get("baseDomain") - if basedomain: - result_data["basedomain"] = basedomain - # Construct default endpoints from basedomain - result_data["controller_endpoint"] = f"grpc.{basedomain}:8082" - result_data["router_endpoint"] = f"router.{basedomain}:8083" - - # Check for explicit endpoints in values - controller_config = values.get("jumpstarter-controller", {}).get("grpc", {}) - if controller_config.get("endpoint"): - result_data["controller_endpoint"] = controller_config["endpoint"] - if controller_config.get("routerEndpoint"): - result_data["router_endpoint"] = controller_config["routerEndpoint"] - - except (json.JSONDecodeError, RuntimeError): - # Failed to get Helm values, but we still have basic info - pass - - break - - # Check for Jumpstarter CRDs as secondary verification + # Check for Jumpstarter CRDs try: crd_cmd = [kubectl, "--context", context, "get", "crd", "-o", "json"] returncode, stdout, _ = await run_command(crd_cmd) if returncode == 0: - # Extract JSON from CRD output (handle warnings) json_start = stdout.find("{") if json_start >= 0: json_output = stdout[json_start:] crds = json.loads(json_output) else: - crds = json.loads(stdout) # Fallback + crds = json.loads(stdout) jumpstarter_crds = [] for item in crds.get("items", []): name = item.get("metadata", {}).get("name", "") @@ -178,17 +110,14 @@ async def check_jumpstarter_installation( # noqa: C901 if jumpstarter_crds: result_data["has_crds"] = True - if not result_data["installed"]: - # CRDs exist but no Helm release found - manual installation? - result_data["installed"] = True - result_data["version"] = "unknown" - result_data["namespace"] = namespace or "unknown" - result_data["status"] = "manual-install" + result_data["installed"] = True + result_data["namespace"] = namespace or "unknown" + result_data["status"] = "installed" except RuntimeError: - pass # CRD check failed, continue with Helm results + pass except json.JSONDecodeError as e: - result_data["error"] = f"Failed to parse Helm output: {e}" + result_data["error"] = f"Failed to parse output: {e}" except RuntimeError as e: result_data["error"] = f"Command failed: {e}" @@ -198,7 +127,6 @@ async def check_jumpstarter_installation( # noqa: C901 async def get_cluster_info( context: str, kubectl: str = "kubectl", - helm: str = "helm", minikube: str = "minikube", ) -> V1Alpha1ClusterInfo: """Get comprehensive cluster information.""" @@ -249,7 +177,7 @@ async def get_cluster_info( # Check Jumpstarter installation if cluster_accessible: - jumpstarter_info = await check_jumpstarter_installation(context, None, helm, kubectl) + jumpstarter_info = await check_jumpstarter_installation(context, None, kubectl) else: jumpstarter_info = V1Alpha1JumpstarterInstance(installed=False, error="Cluster not accessible") @@ -284,7 +212,6 @@ async def get_cluster_info( async def list_clusters( cluster_type_filter: str = "all", kubectl: str = "kubectl", - helm: str = "helm", kind: str = "kind", minikube: str = "minikube", ) -> V1Alpha1ClusterList: @@ -294,7 +221,7 @@ async def list_clusters( cluster_infos = [] for context in contexts: - cluster_info = await get_cluster_info(context["name"], kubectl, helm, minikube) + cluster_info = await get_cluster_info(context["name"], kubectl, minikube) # Filter by type if specified if cluster_type_filter != "all" and cluster_info.type != cluster_type_filter: diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py index 284bce382..5689869a6 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py @@ -190,44 +190,25 @@ async def test_get_kubectl_contexts_custom_kubectl(self, mock_run_command): class TestCheckJumpstarterInstallation: - """Test Jumpstarter installation checking.""" + """Test Jumpstarter installation checking via CRD detection.""" @pytest.mark.asyncio @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") - async def test_check_jumpstarter_installation_helm_found(self, mock_run_command): - helm_releases = [ - { - "chart": "jumpstarter-1.0.0", - "app_version": "1.0.0", - "namespace": "jumpstarter-system", - "name": "jumpstarter-release", - "status": "deployed", - } - ] - # Mock calls: helm list, kubectl get namespaces, kubectl get crds - mock_run_command.side_effect = [ - (0, json.dumps(helm_releases), ""), # helm list success - (0, '{"items": []}', ""), # kubectl get namespaces - (0, '{"items": []}', ""), # kubectl get crds - ] + async def test_check_jumpstarter_installation_crds_found(self, mock_run_command): + crds_response = {"items": [{"metadata": {"name": "exporters.jumpstarter.dev"}}]} + + mock_run_command.return_value = (0, json.dumps(crds_response), "") result = await check_jumpstarter_installation("test-context") assert result.installed is True - assert result.version == "1.0.0" - assert result.namespace == "jumpstarter-system" - assert result.chart_name == "jumpstarter-release" - assert result.status == "deployed" + assert result.has_crds is True + assert result.status == "installed" @pytest.mark.asyncio @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") - async def test_check_jumpstarter_installation_no_helm(self, mock_run_command): - # Helm command fails, fallback to kubectl - mock_run_command.side_effect = [ - (1, "", "helm not found"), # helm list fails - (0, '{"items": []}', ""), # kubectl get namespaces - (1, "", "not found"), # kubectl get crds - ] + async def test_check_jumpstarter_installation_no_crds(self, mock_run_command): + mock_run_command.return_value = (0, '{"items": []}', "") result = await check_jumpstarter_installation("test-context") @@ -236,35 +217,24 @@ async def test_check_jumpstarter_installation_no_helm(self, mock_run_command): @pytest.mark.asyncio @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") - async def test_check_jumpstarter_installation_namespace_found(self, mock_run_command): - crds_response = {"items": [{"metadata": {"name": "exporter.jumpstarter.dev"}}]} - - mock_run_command.side_effect = [ - (1, "", "helm not found"), # helm list fails - (0, json.dumps(crds_response), ""), # kubectl get crds - ] + async def test_check_jumpstarter_installation_command_failure(self, mock_run_command): + mock_run_command.side_effect = RuntimeError("kubectl not found") result = await check_jumpstarter_installation("test-context") - assert result.installed is True - assert result.namespace == "unknown" - assert result.has_crds is True - assert result.status == "manual-install" + assert result.installed is False + assert result.has_crds is False @pytest.mark.asyncio @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") async def test_check_jumpstarter_installation_custom_namespace(self, mock_run_command): - mock_run_command.side_effect = [ - (0, "[]", ""), # helm list - (0, '{"items": []}', ""), # kubectl get crds - ] + crds_response = {"items": [{"metadata": {"name": "exporters.jumpstarter.dev"}}]} + mock_run_command.return_value = (0, json.dumps(crds_response), "") - await check_jumpstarter_installation("test-context", namespace="custom-ns") + result = await check_jumpstarter_installation("test-context", namespace="custom-ns") - # Verify the helm command was called (namespace parameter not used in current implementation) - helm_call = mock_run_command.call_args_list[0] - assert "helm" in helm_call[0][0] - assert "list" in helm_call[0][0] + assert result.installed is True + assert result.namespace == "custom-ns" class TestGetClusterInfo: @@ -387,6 +357,6 @@ async def test_list_clusters_context_error(self, mock_get_contexts): async def test_list_clusters_custom_parameters(self, mock_get_contexts): mock_get_contexts.return_value = [] - await list_clusters(kubectl="custom-kubectl", helm="custom-helm", minikube="custom-minikube") + await list_clusters(kubectl="custom-kubectl", minikube="custom-minikube") mock_get_contexts.assert_called_once_with("custom-kubectl") diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations.py index 8967f7430..40b50f157 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations.py @@ -10,11 +10,9 @@ ClusterTypeValidationError, ToolNotInstalledError, ) -from ..install import helm_installed -from .common import ClusterType, InstallMethod, extract_host_from_ssh, validate_cluster_name +from .common import ClusterType, extract_host_from_ssh, validate_cluster_name from .detection import auto_detect_cluster_type, detect_existing_cluster_type from .endpoints import configure_endpoints -from .helm import install_jumpstarter_helm_chart from .k3s import ( create_k3s_cluster_with_options, ) @@ -119,9 +117,6 @@ async def create_cluster_and_install( # noqa: C901 minikube: str, extra_certs: Optional[str] = None, install_jumpstarter: bool = True, - helm: str = "helm", - chart: str = "oci://quay.io/jumpstarter-dev/helm/jumpstarter", - chart_name: str = "jumpstarter", namespace: str = "jumpstarter-lab", version: Optional[str] = None, kubeconfig: Optional[str] = None, @@ -131,9 +126,7 @@ async def create_cluster_and_install( # noqa: C901 grpc_endpoint: Optional[str] = None, router_endpoint: Optional[str] = None, callback: OutputCallback = None, - values_files: Optional[list[str]] = None, k3s_ssh_host: Optional[str] = None, - install_method: Optional[InstallMethod] = None, operator_installer: Optional[str] = None, ) -> None: """Create a cluster and optionally install Jumpstarter.""" @@ -147,12 +140,12 @@ async def create_cluster_and_install( # noqa: C901 raise ClusterNameValidationError(cluster_name, str(e)) from e if force_recreate_cluster: - callback.warning(f'⚠️ WARNING: Force recreating cluster "{cluster_name}" will destroy ALL data in the cluster!') + callback.warning(f'WARNING: Force recreating cluster "{cluster_name}" will destroy ALL data in the cluster!') callback.warning("This includes:") - callback.warning(" • All deployed applications and services") - callback.warning(" • All persistent volumes and data") - callback.warning(" • All configurations and secrets") - callback.warning(" • All custom resources") + callback.warning(" - All deployed applications and services") + callback.warning(" - All persistent volumes and data") + callback.warning(" - All configurations and secrets") + callback.warning(" - All custom resources") if not callback.confirm(f'Are you sure you want to recreate cluster "{cluster_name}"?'): callback.progress("Cluster recreation cancelled.") raise ClusterOperationError("recreate", cluster_name, cluster_type, Exception("User cancelled")) @@ -183,12 +176,6 @@ async def create_cluster_and_install( # noqa: C901 # Install Jumpstarter if requested if install_jumpstarter: - # k3s always uses the operator; kind/minikube default to helm - if cluster_type == "k3s": - install_method = "operator" - elif install_method is None: - install_method = "helm" - # For k3s, derive IP from SSH host if not specified if cluster_type == "k3s" and ip is None and k3s_ssh_host is not None: ip = extract_host_from_ssh(k3s_ssh_host) @@ -207,39 +194,18 @@ async def create_cluster_and_install( # noqa: C901 Exception("Version must be specified when installing Jumpstarter"), ) - if install_method == "operator": - await install_jumpstarter_operator( - version=version, - namespace=namespace, - basedomain=actual_basedomain, - grpc_endpoint=actual_grpc, - router_endpoint=actual_router, - mode="nodeport", - kubeconfig=kubeconfig, - context=context, - operator_installer=operator_installer, - callback=callback, - ) - else: - if not helm_installed(helm): - raise ToolNotInstalledError("helm", f"helm is not installed (or not in your PATH): {helm}") - - await install_jumpstarter_helm_chart( - chart, - chart_name, - namespace, - actual_basedomain, - actual_grpc, - actual_router, - "nodeport", - version, - kubeconfig, - context, - helm, - actual_ip, - callback, - values_files, - ) + await install_jumpstarter_operator( + version=version, + namespace=namespace, + basedomain=actual_basedomain, + grpc_endpoint=actual_grpc, + router_endpoint=actual_router, + mode="nodeport", + kubeconfig=kubeconfig, + context=context, + operator_installer=operator_installer, + callback=callback, + ) async def create_cluster_only( diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py index 03ff3cd97..5e966fbdb 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py @@ -126,17 +126,15 @@ async def test_create_cluster_only_minikube(self, mock_create_and_install): class TestCreateClusterAndInstall: - """Test cluster creation with installation.""" + """Test cluster creation with operator installation.""" @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.operations.helm_installed") @patch("jumpstarter_kubernetes.cluster.operations.create_kind_cluster_with_options") @patch("jumpstarter_kubernetes.cluster.operations.configure_endpoints") - @patch("jumpstarter_kubernetes.cluster.operations.install_jumpstarter_helm_chart") + @patch("jumpstarter_kubernetes.cluster.operations.install_jumpstarter_operator") async def test_create_cluster_and_install_success( - self, mock_install, mock_configure, mock_create, mock_helm_installed + self, mock_install, mock_configure, mock_create ): - mock_helm_installed.return_value = True mock_configure.return_value = ("192.168.1.100", "test.domain", "grpc.test:8082", "router.test:8083") await create_cluster_and_install("kind", False, "test-cluster", "", "", "kind", "minikube", version="1.0.0") @@ -146,32 +144,12 @@ async def test_create_cluster_and_install_success( mock_install.assert_called_once() @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.operations.helm_installed") @patch("jumpstarter_kubernetes.cluster.operations.create_kind_cluster_with_options") @patch("jumpstarter_kubernetes.cluster.operations.configure_endpoints") - async def test_create_cluster_and_install_no_helm( - self, mock_configure, mock_create_wrapper, mock_helm_installed - ): - from jumpstarter_kubernetes.exceptions import ToolNotInstalledError - - mock_create_wrapper.return_value = None - mock_helm_installed.return_value = False - mock_configure.return_value = ("192.168.1.100", "test.domain", "grpc.test:8082", "router.test:8083") - - with pytest.raises(ToolNotInstalledError): - await create_cluster_and_install( - "kind", False, "test-cluster", "", "", "kind", "minikube", version="v0.1.0" - ) - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.cluster.operations.helm_installed") - @patch("jumpstarter_kubernetes.cluster.operations.create_kind_cluster_with_options") - @patch("jumpstarter_kubernetes.cluster.operations.configure_endpoints") - async def test_create_cluster_and_install_no_version(self, mock_configure, mock_create, mock_helm_installed): + async def test_create_cluster_and_install_no_version(self, mock_configure, mock_create): from jumpstarter_kubernetes.exceptions import ClusterOperationError mock_create.return_value = None - mock_helm_installed.return_value = True mock_configure.return_value = ("192.168.1.100", "test.domain", "grpc.test:8082", "router.test:8083") with pytest.raises(ClusterOperationError): diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exceptions.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exceptions.py index ce56cd2c7..fbc2b4cbb 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exceptions.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exceptions.py @@ -12,7 +12,7 @@ class JumpstarterKubernetesError(Exception): class ToolNotInstalledError(JumpstarterKubernetesError): - """Raised when a required tool (kind, minikube, helm, kubectl) is not installed.""" + """Raised when a required tool (kind, minikube, kubectl) is not installed.""" def __init__(self, tool_name: str, additional_info: str = ""): self.tool_name = tool_name diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/install.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/install.py deleted file mode 100644 index 34514f9e5..000000000 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/install.py +++ /dev/null @@ -1,91 +0,0 @@ -import asyncio -import shutil -from typing import Literal, Optional - - -def helm_installed(name: str) -> bool: - """Check if Helm is installed and available in the PATH.""" - return shutil.which(name) is not None - - -async def install_helm_chart( - chart: str, - name: str, - namespace: str, - basedomain: str, - grpc_endpoint: str, - router_endpoint: str, - mode: Literal["nodeport"] | Literal["ingress"] | Literal["route"], - version: str, - kubeconfig: Optional[str], - context: Optional[str], - helm: Optional[str] = "helm", - values_files: Optional[list[str]] = None, -): - args = [ - helm, - "upgrade", - name, - "--install", - chart, - "--create-namespace", - "--namespace", - namespace, - "--set", - f"global.baseDomain={basedomain}", - "--set", - f"jumpstarter-controller.grpc.endpoint={grpc_endpoint}", - "--set", - f"jumpstarter-controller.grpc.routerEndpoint={router_endpoint}", - "--set", - "global.metrics.enabled=false", - "--set", - f"jumpstarter-controller.grpc.nodeport.enabled={'true' if mode == 'nodeport' else 'false'}", - "--set", - f"jumpstarter-controller.grpc.mode={mode}", - "--version", - version, - "--wait", - ] - - if kubeconfig is not None: - args.append("--kubeconfig") - args.append(kubeconfig) - - if context is not None: - args.append("--kube-context") - args.append(context) - - if values_files is not None: - for values_file in values_files: - args.append("-f") - args.append(values_file) - - # Attempt to install Jumpstarter using Helm - process = await asyncio.create_subprocess_exec(*args) - await process.wait() - - -async def uninstall_helm_chart( - name: str, namespace: str, kubeconfig: Optional[str], context: Optional[str], helm: Optional[str] = "helm" -): - args = [ - helm, - "uninstall", - name, - "--namespace", - namespace, - "--wait", - ] - - if kubeconfig is not None: - args.append("--kubeconfig") - args.append(kubeconfig) - - if context is not None: - args.append("--kube-context") - args.append(context) - - # Attempt to install Jumpstarter using Helm - process = await asyncio.create_subprocess_exec(*args) - await process.wait() diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/install_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/install_test.py deleted file mode 100644 index cf0668960..000000000 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/install_test.py +++ /dev/null @@ -1,258 +0,0 @@ -"""Tests for Helm installation functions.""" - -from unittest.mock import AsyncMock, patch - -import pytest - -from jumpstarter_kubernetes.install import helm_installed, install_helm_chart - - -class TestHelmInstalled: - """Test helm_installed function.""" - - @patch("jumpstarter_kubernetes.install.shutil.which") - def test_helm_installed_true(self, mock_which): - mock_which.return_value = "/usr/local/bin/helm" - assert helm_installed("helm") is True - mock_which.assert_called_once_with("helm") - - @patch("jumpstarter_kubernetes.install.shutil.which") - def test_helm_installed_false(self, mock_which): - mock_which.return_value = None - assert helm_installed("helm") is False - mock_which.assert_called_once_with("helm") - - @patch("jumpstarter_kubernetes.install.shutil.which") - def test_helm_installed_custom_path(self, mock_which): - mock_which.return_value = "/custom/path/helm" - assert helm_installed("/custom/path/helm") is True - mock_which.assert_called_once_with("/custom/path/helm") - - -class TestInstallHelmChart: - """Test install_helm_chart function.""" - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.install.asyncio.create_subprocess_exec") - async def test_install_helm_chart_basic(self, mock_subprocess): - """Test basic helm chart installation without values files.""" - mock_process = AsyncMock() - mock_process.wait = AsyncMock(return_value=0) - mock_subprocess.return_value = mock_process - - await install_helm_chart( - chart="oci://quay.io/jumpstarter/helm", - name="jumpstarter", - namespace="jumpstarter-lab", - basedomain="jumpstarter.192.168.1.100.nip.io", - grpc_endpoint="grpc.jumpstarter.192.168.1.100.nip.io:8082", - router_endpoint="router.jumpstarter.192.168.1.100.nip.io:8083", - mode="nodeport", - version="1.0.0", - kubeconfig=None, - context=None, - helm="helm", - values_files=None, - ) - - # Verify the subprocess was called with correct arguments - args = mock_subprocess.call_args[0] - assert args[0] == "helm" - assert args[1] == "upgrade" - assert args[2] == "jumpstarter" - assert "--install" in args - assert "oci://quay.io/jumpstarter/helm" in args - assert "--namespace" in args - assert "jumpstarter-lab" in args - assert "--version" in args - assert "1.0.0" in args - assert "--wait" in args - - # Verify no -f flags when values_files is None - assert "-f" not in args - - mock_process.wait.assert_called_once() - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.install.asyncio.create_subprocess_exec") - async def test_install_helm_chart_with_single_values_file(self, mock_subprocess): - """Test helm chart installation with a single values file.""" - mock_process = AsyncMock() - mock_process.wait = AsyncMock(return_value=0) - mock_subprocess.return_value = mock_process - - values_files = ["/path/to/values.yaml"] - - await install_helm_chart( - chart="oci://quay.io/jumpstarter/helm", - name="jumpstarter", - namespace="jumpstarter-lab", - basedomain="jumpstarter.192.168.1.100.nip.io", - grpc_endpoint="grpc.jumpstarter.192.168.1.100.nip.io:8082", - router_endpoint="router.jumpstarter.192.168.1.100.nip.io:8083", - mode="nodeport", - version="1.0.0", - kubeconfig=None, - context=None, - helm="helm", - values_files=values_files, - ) - - # Verify the subprocess was called with correct arguments including -f - args = mock_subprocess.call_args[0] - assert "-f" in args - f_index = args.index("-f") - assert args[f_index + 1] == "/path/to/values.yaml" - - mock_process.wait.assert_called_once() - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.install.asyncio.create_subprocess_exec") - async def test_install_helm_chart_with_multiple_values_files(self, mock_subprocess): - """Test helm chart installation with multiple values files.""" - mock_process = AsyncMock() - mock_process.wait = AsyncMock(return_value=0) - mock_subprocess.return_value = mock_process - - values_files = ["/path/to/values.yaml", "/path/to/values.kind.yaml"] - - await install_helm_chart( - chart="oci://quay.io/jumpstarter/helm", - name="jumpstarter", - namespace="jumpstarter-lab", - basedomain="jumpstarter.192.168.1.100.nip.io", - grpc_endpoint="grpc.jumpstarter.192.168.1.100.nip.io:8082", - router_endpoint="router.jumpstarter.192.168.1.100.nip.io:8083", - mode="nodeport", - version="1.0.0", - kubeconfig=None, - context=None, - helm="helm", - values_files=values_files, - ) - - # Verify the subprocess was called with correct arguments including multiple -f flags - args = mock_subprocess.call_args[0] - - # Find all -f flags - f_indices = [i for i, arg in enumerate(args) if arg == "-f"] - assert len(f_indices) == 2 - - # Verify the values files are in the correct order - assert args[f_indices[0] + 1] == "/path/to/values.yaml" - assert args[f_indices[1] + 1] == "/path/to/values.kind.yaml" - - mock_process.wait.assert_called_once() - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.install.asyncio.create_subprocess_exec") - async def test_install_helm_chart_with_kubeconfig_and_context(self, mock_subprocess): - """Test helm chart installation with kubeconfig and context.""" - mock_process = AsyncMock() - mock_process.wait = AsyncMock(return_value=0) - mock_subprocess.return_value = mock_process - - await install_helm_chart( - chart="oci://quay.io/jumpstarter/helm", - name="jumpstarter", - namespace="jumpstarter-lab", - basedomain="jumpstarter.192.168.1.100.nip.io", - grpc_endpoint="grpc.jumpstarter.192.168.1.100.nip.io:8082", - router_endpoint="router.jumpstarter.192.168.1.100.nip.io:8083", - mode="nodeport", - version="1.0.0", - kubeconfig="/path/to/kubeconfig", - context="test-context", - helm="helm", - values_files=None, - ) - - # Verify the subprocess was called with kubeconfig and context - args = mock_subprocess.call_args[0] - assert "--kubeconfig" in args - kubeconfig_index = args.index("--kubeconfig") - assert args[kubeconfig_index + 1] == "/path/to/kubeconfig" - - assert "--kube-context" in args - context_index = args.index("--kube-context") - assert args[context_index + 1] == "test-context" - - mock_process.wait.assert_called_once() - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.install.asyncio.create_subprocess_exec") - async def test_install_helm_chart_with_all_options(self, mock_subprocess): - """Test helm chart installation with all options including values files, kubeconfig, and context.""" - mock_process = AsyncMock() - mock_process.wait = AsyncMock(return_value=0) - mock_subprocess.return_value = mock_process - - values_files = ["/path/to/values1.yaml", "/path/to/values2.yaml", "/path/to/values3.yaml"] - - await install_helm_chart( - chart="oci://quay.io/jumpstarter/helm", - name="jumpstarter", - namespace="jumpstarter-lab", - basedomain="jumpstarter.192.168.1.100.nip.io", - grpc_endpoint="grpc.jumpstarter.192.168.1.100.nip.io:8082", - router_endpoint="router.jumpstarter.192.168.1.100.nip.io:8083", - mode="ingress", - version="1.0.0", - kubeconfig="/path/to/kubeconfig", - context="prod-context", - helm="/usr/local/bin/helm", - values_files=values_files, - ) - - # Verify all options are present - args = mock_subprocess.call_args[0] - - # Check helm binary - assert args[0] == "/usr/local/bin/helm" - - # Check kubeconfig and context - assert "--kubeconfig" in args - assert "/path/to/kubeconfig" in args - assert "--kube-context" in args - assert "prod-context" in args - - # Check values files - f_indices = [i for i, arg in enumerate(args) if arg == "-f"] - assert len(f_indices) == 3 - assert args[f_indices[0] + 1] == "/path/to/values1.yaml" - assert args[f_indices[1] + 1] == "/path/to/values2.yaml" - assert args[f_indices[2] + 1] == "/path/to/values3.yaml" - - # Check mode - assert "jumpstarter-controller.grpc.mode=ingress" in args - - mock_process.wait.assert_called_once() - - @pytest.mark.asyncio - @patch("jumpstarter_kubernetes.install.asyncio.create_subprocess_exec") - async def test_install_helm_chart_empty_values_files_list(self, mock_subprocess): - """Test helm chart installation with empty values files list.""" - mock_process = AsyncMock() - mock_process.wait = AsyncMock(return_value=0) - mock_subprocess.return_value = mock_process - - await install_helm_chart( - chart="oci://quay.io/jumpstarter/helm", - name="jumpstarter", - namespace="jumpstarter-lab", - basedomain="jumpstarter.192.168.1.100.nip.io", - grpc_endpoint="grpc.jumpstarter.192.168.1.100.nip.io:8082", - router_endpoint="router.jumpstarter.192.168.1.100.nip.io:8083", - mode="nodeport", - version="1.0.0", - kubeconfig=None, - context=None, - helm="helm", - values_files=[], - ) - - # Verify no -f flags when values_files is empty list - args = mock_subprocess.call_args[0] - assert "-f" not in args - - mock_process.wait.assert_called_once() From af57a565edd1e27bc6112fcc4f401ff47e49fbbd Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 12:27:05 +0200 Subject: [PATCH 02/22] fix: propagate RuntimeError in check_jumpstarter_installation instead of swallowing Flatten nested try/except in check_jumpstarter_installation so that RuntimeError from run_command (e.g. kubectl not found, permission errors) propagates to the outer handler that properly reports the error. Previously the inner `except RuntimeError: pass` silently swallowed the error, making it impossible for callers to distinguish "not installed" from "failed to check". Generated-By: Forge/20260409_114748_239149_5e586097 Co-Authored-By: Claude Opus 4.6 --- .../jumpstarter_kubernetes/cluster/kubectl.py | 46 +++++++++---------- .../cluster/kubectl_test.py | 2 + 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py index d9e12804d..7a6a3957c 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py @@ -90,31 +90,27 @@ async def check_jumpstarter_installation( # noqa: C901 } try: - # Check for Jumpstarter CRDs - try: - crd_cmd = [kubectl, "--context", context, "get", "crd", "-o", "json"] - returncode, stdout, _ = await run_command(crd_cmd) - - if returncode == 0: - json_start = stdout.find("{") - if json_start >= 0: - json_output = stdout[json_start:] - crds = json.loads(json_output) - else: - crds = json.loads(stdout) - jumpstarter_crds = [] - for item in crds.get("items", []): - name = item.get("metadata", {}).get("name", "") - if "jumpstarter.dev" in name: - jumpstarter_crds.append(name) - - if jumpstarter_crds: - result_data["has_crds"] = True - result_data["installed"] = True - result_data["namespace"] = namespace or "unknown" - result_data["status"] = "installed" - except RuntimeError: - pass + crd_cmd = [kubectl, "--context", context, "get", "crd", "-o", "json"] + returncode, stdout, _ = await run_command(crd_cmd) + + if returncode == 0: + json_start = stdout.find("{") + if json_start >= 0: + json_output = stdout[json_start:] + crds = json.loads(json_output) + else: + crds = json.loads(stdout) + jumpstarter_crds = [] + for item in crds.get("items", []): + name = item.get("metadata", {}).get("name", "") + if "jumpstarter.dev" in name: + jumpstarter_crds.append(name) + + if jumpstarter_crds: + result_data["has_crds"] = True + result_data["installed"] = True + result_data["namespace"] = namespace or "unknown" + result_data["status"] = "installed" except json.JSONDecodeError as e: result_data["error"] = f"Failed to parse output: {e}" diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py index 5689869a6..439528c46 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py @@ -224,6 +224,8 @@ async def test_check_jumpstarter_installation_command_failure(self, mock_run_com assert result.installed is False assert result.has_crds is False + assert result.error is not None + assert "kubectl not found" in result.error @pytest.mark.asyncio @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") From faf614eb3baad1baaa94aa54ec2e49bddd20cd54 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 12:27:29 +0200 Subject: [PATCH 03/22] fix: remove stale noqa C901 complexity suppression from check_jumpstarter_installation After removing the nested Helm detection logic, the function complexity is well under the C901 threshold. The suppression comment is no longer needed. Generated-By: Forge/20260409_114748_239149_5e586097 Co-Authored-By: Claude Opus 4.6 --- .../jumpstarter_kubernetes/cluster/kubectl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py index 7a6a3957c..10dd90a1d 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py @@ -72,7 +72,7 @@ async def get_kubectl_contexts(kubectl: str = "kubectl") -> List[Dict[str, str]] raise KubeconfigError(f"Error listing kubectl contexts: {e}") from e -async def check_jumpstarter_installation( # noqa: C901 +async def check_jumpstarter_installation( context: str, namespace: Optional[str] = None, kubectl: str = "kubectl" ) -> V1Alpha1JumpstarterInstance: """Check if Jumpstarter is installed in the cluster using CRD detection.""" From 68150369d7a4065c15fba785ca7df86f13ed2603 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 12:27:41 +0200 Subject: [PATCH 04/22] fix: remove stale METHOD=helm references from root Makefile The Helm deployment method has been removed. Update the Makefile comment and help text to no longer advertise helm as a valid option. Generated-By: Forge/20260409_114748_239149_5e586097 Co-Authored-By: Claude Opus 4.6 --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1dc17acbd..e34e7f629 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ # Subdirectories containing projects SUBDIRS := python protocol controller e2e -# Deployment method for e2e tests: operator (default) or helm +# Deployment method for e2e tests METHOD ?= operator # Default target @@ -33,8 +33,8 @@ help: @echo " make e2e-full - Full setup + run (for CI or first time)" @echo " make e2e-clean - Clean up e2e test environment (delete cluster, certs, etc.)" @echo "" - @echo " Use METHOD=operator (default) or METHOD=helm to select deployment method" - @echo " Example: make e2e-setup METHOD=helm" + @echo " Use METHOD=operator (default) to select deployment method" + @echo " Example: make e2e-setup METHOD=operator" @echo "" @echo "Per-project targets:" @echo " make build- - Build specific project" From eb0ce04c6047938cace4782126fdadf4f9ac7489 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 12:28:12 +0200 Subject: [PATCH 05/22] fix: remove vestigial chart_name field from V1Alpha1JumpstarterInstance The chart_name/chartName field was only populated by the Helm detection code path which has been removed. The field was permanently None, making it dead structure in the API model. Generated-By: Forge/20260409_114748_239149_5e586097 Co-Authored-By: Claude Opus 4.6 --- .../jumpstarter_kubernetes/cluster/kubectl.py | 1 - .../jumpstarter-kubernetes/jumpstarter_kubernetes/clusters.py | 1 - 2 files changed, 2 deletions(-) diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py index 10dd90a1d..51157bae6 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py @@ -80,7 +80,6 @@ async def check_jumpstarter_installation( "installed": False, "version": None, "namespace": None, - "chart_name": None, "status": None, "has_crds": False, "error": None, diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clusters.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clusters.py index 1031712af..bf7cf5e1b 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clusters.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clusters.py @@ -14,7 +14,6 @@ class V1Alpha1JumpstarterInstance(JsonBaseModel): installed: bool version: Optional[str] = None namespace: Optional[str] = None - chart_name: Optional[str] = Field(alias="chartName", default=None) status: Optional[str] = None has_crds: bool = Field(alias="hasCrds", default=False) error: Optional[str] = None From fb9317e4fef330087e8c4f28e336154eb263e25a Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 12:28:20 +0200 Subject: [PATCH 06/22] fix: correct stale default version comment in compat setup script The comment said COMPAT_CONTROLLER_TAG defaults to v0.7.1 but the actual default on line 31 is v0.7.0. Generated-By: Forge/20260409_114748_239149_5e586097 Co-Authored-By: Claude Opus 4.6 --- e2e/compat/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/compat/setup.sh b/e2e/compat/setup.sh index 953edd0b8..b4c9deaf6 100755 --- a/e2e/compat/setup.sh +++ b/e2e/compat/setup.sh @@ -7,7 +7,7 @@ # # Environment variables: # COMPAT_SCENARIO - "old-controller" or "old-client" (required) -# COMPAT_CONTROLLER_TAG - Controller image tag for old-controller scenario (default: v0.7.1) +# COMPAT_CONTROLLER_TAG - Controller image tag for old-controller scenario (default: v0.7.0) # COMPAT_CLIENT_VERSION - PyPI version for old-client scenario (default: 0.7.1) set -euo pipefail From d0dff94867eb8249563cd8bf79bc1cad56f386dc Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 12:29:13 +0200 Subject: [PATCH 07/22] fix: update version discovery to query operator image repo instead of Helm chart repo The Helm chart publishing CI job is being removed, so the function querying quay.io/jumpstarter-dev/helm/jumpstarter for version tags will return stale results. Switch to querying the operator image repository (jumpstarter-dev/jumpstarter-operator) which continues to receive new version tags with each release. Generated-By: Forge/20260409_114748_239149_5e586097 Co-Authored-By: Claude Opus 4.6 --- .../jumpstarter-kubernetes/jumpstarter_kubernetes/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/controller.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/controller.py index 6d532468c..1cb99fa27 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/controller.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/controller.py @@ -29,7 +29,7 @@ async def get_latest_compatible_controller_version(client_version: Optional[str] ) as session: try: async with session.get( - "https://quay.io/api/v1/repository/jumpstarter-dev/helm/jumpstarter/tag/", + "https://quay.io/api/v1/repository/jumpstarter-dev/jumpstarter-operator/tag/", timeout=aiohttp.ClientTimeout(total=30), ) as resp: resp = await resp.json() From 2a6040365953c0e30cfac11c48074ddbb482ea96 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 12:29:31 +0200 Subject: [PATCH 08/22] fix: fail fast when Jumpstarter CR baseDomain is not found in compat setup Instead of silently falling back to a default domain when the Jumpstarter CR cannot be found, fail immediately with an actionable error message. This prevents misleading test failures caused by connecting to the wrong endpoint. Generated-By: Forge/20260409_114748_239149_5e586097 Co-Authored-By: Claude Opus 4.6 --- e2e/compat/setup.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/e2e/compat/setup.sh b/e2e/compat/setup.sh index b4c9deaf6..c9c948672 100755 --- a/e2e/compat/setup.sh +++ b/e2e/compat/setup.sh @@ -180,7 +180,13 @@ setup_test_environment() { # Get the controller endpoint from Jumpstarter CR export ENDPOINT - ENDPOINT="grpc.$(kubectl get jumpstarter -n "${JS_NAMESPACE}" jumpstarter -o jsonpath='{.spec.baseDomain}' 2>/dev/null || echo 'jumpstarter.127.0.0.1.nip.io'):8082" + local BASEDOMAIN + BASEDOMAIN=$(kubectl get jumpstarter -n "${JS_NAMESPACE}" jumpstarter -o jsonpath='{.spec.baseDomain}' 2>/dev/null) || true + if [ -z "${BASEDOMAIN}" ]; then + log_error "Failed to get baseDomain from Jumpstarter CR in namespace ${JS_NAMESPACE}. Is the controller deployed with a Jumpstarter CR?" + exit 1 + fi + ENDPOINT="grpc.${BASEDOMAIN}:8082" log_info "Controller endpoint: $ENDPOINT" From 919575cd7ce24bfb69d7e53fb02e4346d90c9e9c Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 12:40:44 +0200 Subject: [PATCH 09/22] fix: remove vestigial METHOD variable from Makefiles Only one deployment method (operator) remains, so the METHOD variable is unnecessary indirection. Remove it from the root Makefile and the controller Makefile, and drop redundant METHOD=operator arguments from deploy-with-operator targets. Generated-By: Forge/20260409_114748_239149_5e586097 Co-Authored-By: Claude Opus 4.6 --- Makefile | 16 +++++----------- controller/Makefile | 6 ++---- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index e34e7f629..85cc645de 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,6 @@ # Subdirectories containing projects SUBDIRS := python protocol controller e2e -# Deployment method for e2e tests -METHOD ?= operator - # Default target .PHONY: all all: build @@ -33,9 +30,6 @@ help: @echo " make e2e-full - Full setup + run (for CI or first time)" @echo " make e2e-clean - Clean up e2e test environment (delete cluster, certs, etc.)" @echo "" - @echo " Use METHOD=operator (default) to select deployment method" - @echo " Example: make e2e-setup METHOD=operator" - @echo "" @echo "Per-project targets:" @echo " make build- - Build specific project" @echo " make test- - Test specific project" @@ -121,14 +115,14 @@ test-controller: # Setup e2e testing environment (one-time) .PHONY: e2e-setup e2e-setup: - @echo "Setting up e2e test environment (method: $(METHOD))..." - @METHOD=$(METHOD) bash e2e/setup-e2e.sh + @echo "Setting up e2e test environment..." + @bash e2e/setup-e2e.sh # Run e2e tests .PHONY: e2e-run e2e-run: - @echo "Running e2e tests (method: $(METHOD))..." - @METHOD=$(METHOD) bash e2e/run-e2e.sh + @echo "Running e2e tests..." + @bash e2e/run-e2e.sh # Convenience alias for running e2e tests .PHONY: e2e @@ -137,7 +131,7 @@ e2e: e2e-run # Full e2e setup + run .PHONY: e2e-full e2e-full: - @METHOD=$(METHOD) bash e2e/run-e2e.sh --full + @bash e2e/run-e2e.sh --full # Clean up e2e test environment .PHONY: e2e-clean diff --git a/controller/Makefile b/controller/Makefile index b7da7c872..33934b3f8 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -30,9 +30,6 @@ endif # tools. (i.e. podman) CONTAINER_TOOL ?= podman -# Deployment method -METHOD ?= operator - # Cluster type: kind (default) or k3s CLUSTER_TYPE ?= kind export CLUSTER_TYPE @@ -186,6 +183,7 @@ ifeq ($(SKIP_BUILD),) endif ./hack/deploy_with_operator.sh + .PHONY: deploy-operator deploy-operator: docker-build build-operator cluster grpcurl ## Deploy only the operator (without Jumpstarter CR) NETWORKING_MODE=ingress DEPLOY_JUMPSTARTER=false ./hack/deploy_with_operator.sh @@ -199,7 +197,7 @@ operator-logs: .PHONY: deploy-with-operator-parallel deploy-with-operator-parallel: - make deploy METHOD=operator -j5 --output-sync=target + make deploy -j5 --output-sync=target .PHONY: deploy-exporters deploy-exporters: From 6a9e41f3a579ff5391e67e49e5a79f976d7cefda Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 13:58:54 +0200 Subject: [PATCH 10/22] fix: remove vestigial METHOD deployment abstraction Since Helm support was removed, "operator" is the only deployment method. Remove the METHOD variable, its branching logic, and the parameterized print_deployment_success function throughout the e2e scripts, CI workflows, and hack utilities. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/e2e.yaml | 2 -- controller/hack/deploy_with_operator.sh | 2 +- controller/hack/utils | 11 +---------- e2e/compat/setup.sh | 2 -- e2e/setup-e2e.sh | 7 +------ 5 files changed, 3 insertions(+), 21 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 292d219e1..94b41906a 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -220,7 +220,6 @@ jobs: run: make e2e-setup env: CI: true - METHOD: operator SKIP_BUILD: "true" PREBUILT_WHEELS_DIR: /tmp/python-wheels OPERATOR_IMG: quay.io/jumpstarter-dev/jumpstarter-operator:latest @@ -229,7 +228,6 @@ jobs: run: make e2e-run env: CI: true - METHOD: operator # ============================================================================ # Compatibility tests: cross-version interop between controller and client/exporter diff --git a/controller/hack/deploy_with_operator.sh b/controller/hack/deploy_with_operator.sh index 2f3e60ae6..9df80c3ff 100755 --- a/controller/hack/deploy_with_operator.sh +++ b/controller/hack/deploy_with_operator.sh @@ -217,5 +217,5 @@ wait_for_jumpstarter_resources check_grpc_endpoints # Print success banner -print_deployment_success "operator" +print_deployment_success diff --git a/controller/hack/utils b/controller/hack/utils index a0c55ff4e..0b142398c 100755 --- a/controller/hack/utils +++ b/controller/hack/utils @@ -251,17 +251,8 @@ check_grpc_endpoints() { } # Print deployment success banner -# Args: -# $1: deployment method (e.g., "operator") - optional print_deployment_success() { - local method=${1:-""} - local method_text="" - - if [ -n "${method}" ]; then - method_text=" via ${method}" - fi - - echo -e "${GREEN}Jumpstarter controller deployed successfully${method_text}!${NC}" + echo -e "${GREEN}Jumpstarter controller deployed successfully!${NC}" echo -e " gRPC endpoint: ${GRPC_ENDPOINT}" echo -e " gRPC router endpoint: ${GRPC_ROUTER_ENDPOINT}" } diff --git a/e2e/compat/setup.sh b/e2e/compat/setup.sh index c9c948672..85306ac6c 100755 --- a/e2e/compat/setup.sh +++ b/e2e/compat/setup.sh @@ -24,8 +24,6 @@ REPO_ROOT="$(cd "$E2E_DIR/.." && pwd)" # Default namespace for tests export JS_NAMESPACE="${JS_NAMESPACE:-jumpstarter-lab}" -export METHOD="operator" - # Scenario configuration COMPAT_SCENARIO="${COMPAT_SCENARIO:-old-controller}" COMPAT_CONTROLLER_TAG="${COMPAT_CONTROLLER_TAG:-v0.7.0}" diff --git a/e2e/setup-e2e.sh b/e2e/setup-e2e.sh index 0579e56da..8df7e5ad6 100755 --- a/e2e/setup-e2e.sh +++ b/e2e/setup-e2e.sh @@ -13,9 +13,6 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Default namespace for tests export JS_NAMESPACE="${JS_NAMESPACE:-jumpstarter-lab}" -# Deployment method -export METHOD="${METHOD:-operator}" - # Color output RED='\033[0;31m' GREEN='\033[0;32m' @@ -262,7 +259,7 @@ deploy_dex() { # Step 4: Deploy jumpstarter controller deploy_controller() { - log_info "Deploying jumpstarter controller (method: $METHOD)..." + log_info "Deploying jumpstarter controller..." cd "$REPO_ROOT" @@ -312,7 +309,6 @@ setup_test_environment() { echo "E2E_TEST_NS=$JS_NAMESPACE" >> "$REPO_ROOT/.e2e-setup-complete" echo "REPO_ROOT=$REPO_ROOT" >> "$REPO_ROOT/.e2e-setup-complete" echo "SCRIPT_DIR=$SCRIPT_DIR" >> "$REPO_ROOT/.e2e-setup-complete" - # Set SSL certificate paths for Python to use the generated CA echo "SSL_CERT_FILE=$REPO_ROOT/ca.pem" >> "$REPO_ROOT/.e2e-setup-complete" echo "REQUESTS_CA_BUNDLE=$REPO_ROOT/ca.pem" >> "$REPO_ROOT/.e2e-setup-complete" @@ -327,7 +323,6 @@ setup_test_environment() { main() { log_info "=== Jumpstarter E2E Setup ===" log_info "Namespace: $JS_NAMESPACE" - log_info "Deployment Method: $METHOD" log_info "Repository Root: $REPO_ROOT" log_info "Script Directory: $SCRIPT_DIR" echo "" From e4c5c6fdc085179a415c72b88339171ffe83718f Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 9 Apr 2026 14:49:52 +0200 Subject: [PATCH 11/22] fix: remove old-controller compat test (v0.7.0 has no operator installer) v0.7.0 predates the monorepo merge and was only deployable via OCI Helm chart. With Helm fully removed, the old-controller compat test cannot deploy a v0.7.0 controller. Remove the test, its CI job, and simplify the compat infrastructure to only support the old-client scenario. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/e2e.yaml | 46 +----- Makefile | 12 +- e2e/compat/run.sh | 11 +- e2e/compat/setup.sh | 88 ++---------- e2e/test/compat_old_controller_test.go | 192 ------------------------- 5 files changed, 20 insertions(+), 329 deletions(-) delete mode 100644 e2e/test/compat_old_controller_test.go diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 94b41906a..342a205d1 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -229,46 +229,6 @@ jobs: env: CI: true - # ============================================================================ - # Compatibility tests: cross-version interop between controller and client/exporter - # These jobs can be removed once 0.7.x controller support is no longer needed. - # ============================================================================ - - e2e-compat-old-controller: - needs: [changes, build-python-wheels] - if: needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-24.04 - timeout-minutes: 60 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: "1.22" - - - name: Download python wheels - uses: actions/download-artifact@v4 - with: - name: python-wheels - path: /tmp/python-wheels - - - name: Setup compat environment (old controller v0.7.0) - run: make e2e-compat-setup COMPAT_SCENARIO=old-controller - env: - CI: true - COMPAT_CONTROLLER_TAG: v0.7.0 - PREBUILT_WHEELS_DIR: /tmp/python-wheels - - - name: Run compat tests (old controller + new client/exporter) - run: make e2e-compat-run COMPAT_TEST=old-controller - env: - CI: true - e2e-compat-old-client: needs: [changes, build-controller-image, build-python-wheels] if: needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch' @@ -301,8 +261,8 @@ jobs: - name: Load controller image run: docker load < /tmp/artifacts/controller-image.tar - - name: Setup compat environment (old client v0.7.0) - run: make e2e-compat-setup COMPAT_SCENARIO=old-client + - name: Setup compat environment (old client v0.7.1) + run: make e2e-compat-setup env: CI: true COMPAT_CLIENT_VERSION: "0.7.1" @@ -310,6 +270,6 @@ jobs: PREBUILT_WHEELS_DIR: /tmp/python-wheels - name: Run compat tests (new controller + old client/exporter) - run: make e2e-compat-run COMPAT_TEST=old-client + run: make e2e-compat-run env: CI: true diff --git a/Makefile b/Makefile index 85cc645de..f39f541b0 100644 --- a/Makefile +++ b/Makefile @@ -164,21 +164,17 @@ e2e-clean: test-e2e: e2e-run # Compatibility E2E testing (cross-version tests, separate from main e2e) -COMPAT_SCENARIO ?= old-controller -COMPAT_TEST ?= old-controller -COMPAT_CONTROLLER_TAG ?= v0.7.0 COMPAT_CLIENT_VERSION ?= 0.7.1 .PHONY: e2e-compat-setup e2e-compat-setup: - @echo "Setting up compat e2e (scenario: $(COMPAT_SCENARIO))..." - @COMPAT_SCENARIO=$(COMPAT_SCENARIO) COMPAT_CONTROLLER_TAG=$(COMPAT_CONTROLLER_TAG) \ - COMPAT_CLIENT_VERSION=$(COMPAT_CLIENT_VERSION) bash e2e/compat/setup.sh + @echo "Setting up compat e2e (old client v$(COMPAT_CLIENT_VERSION))..." + @COMPAT_CLIENT_VERSION=$(COMPAT_CLIENT_VERSION) bash e2e/compat/setup.sh .PHONY: e2e-compat-run e2e-compat-run: - @echo "Running compat e2e (test: $(COMPAT_TEST))..." - @COMPAT_TEST=$(COMPAT_TEST) bash e2e/compat/run.sh + @echo "Running compat e2e (old client)..." + @COMPAT_TEST=old-client bash e2e/compat/run.sh # Per-project clean targets .PHONY: clean-python clean-protocol clean-controller clean-e2e diff --git a/e2e/compat/run.sh b/e2e/compat/run.sh index 741c21b63..301feea83 100755 --- a/e2e/compat/run.sh +++ b/e2e/compat/run.sh @@ -3,7 +3,7 @@ # This script runs the compatibility test suite (assumes setup.sh was run first) # # Environment variables: -# COMPAT_TEST - Which test to run: "old-controller" or "old-client" +# COMPAT_TEST - Which test to run (default: "old-client") set -euo pipefail @@ -48,19 +48,16 @@ run_tests() { # Use insecure GRPC for testing export JUMPSTARTER_GRPC_INSECURE=1 - COMPAT_TEST="${COMPAT_TEST:-old-controller}" + COMPAT_TEST="${COMPAT_TEST:-old-client}" log_info "Running compat test: $COMPAT_TEST" local label_filter="" case "$COMPAT_TEST" in - old-controller) - label_filter="old-controller" - ;; old-client) label_filter="old-client" ;; *) - log_error "Unknown COMPAT_TEST: $COMPAT_TEST (expected 'old-controller' or 'old-client')" + log_error "Unknown COMPAT_TEST: $COMPAT_TEST (expected 'old-client')" exit 1 ;; esac @@ -73,7 +70,7 @@ main() { export E2E_TEST_NS="${E2E_TEST_NS:-jumpstarter-lab}" log_info "=== Jumpstarter Compatibility E2E Test Runner ===" - log_info "Test: ${COMPAT_TEST:-old-controller}" + log_info "Test: ${COMPAT_TEST:-old-client}" log_info "Namespace: $E2E_TEST_NS" log_info "Repository Root: $REPO_ROOT" echo "" diff --git a/e2e/compat/setup.sh b/e2e/compat/setup.sh index 85306ac6c..6444ce711 100755 --- a/e2e/compat/setup.sh +++ b/e2e/compat/setup.sh @@ -6,8 +6,6 @@ # Uses operator-based deployment. # # Environment variables: -# COMPAT_SCENARIO - "old-controller" or "old-client" (required) -# COMPAT_CONTROLLER_TAG - Controller image tag for old-controller scenario (default: v0.7.0) # COMPAT_CLIENT_VERSION - PyPI version for old-client scenario (default: 0.7.1) set -euo pipefail @@ -24,9 +22,6 @@ REPO_ROOT="$(cd "$E2E_DIR/.." && pwd)" # Default namespace for tests export JS_NAMESPACE="${JS_NAMESPACE:-jumpstarter-lab}" -# Scenario configuration -COMPAT_SCENARIO="${COMPAT_SCENARIO:-old-controller}" -COMPAT_CONTROLLER_TAG="${COMPAT_CONTROLLER_TAG:-v0.7.0}" COMPAT_CLIENT_VERSION="${COMPAT_CLIENT_VERSION:-0.7.1}" # Color output @@ -68,17 +63,6 @@ install_dependencies() { log_info "Dependencies installed" } -# Get the external IP for baseDomain -get_external_ip() { - if which ip 2>/dev/null 1>/dev/null; then - ip route get 1.1.1.1 | grep -oP 'src \K\S+' - else - local INTERFACE - INTERFACE=$(route get 1.1.1.1 | grep interface | awk '{print $2}') - ifconfig | grep "$INTERFACE" -A 10 | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' | head -n 1 - fi -} - # Create kind cluster and install grpcurl create_cluster() { log_info "Creating kind cluster..." @@ -89,46 +73,6 @@ create_cluster() { log_info "Kind cluster created" } -deploy_old_controller() { - log_info "Deploying old controller (version: $COMPAT_CONTROLLER_TAG)..." - - cd "$REPO_ROOT" - - # Compute networking variables - local IP - IP=$(get_external_ip) - BASEDOMAIN="jumpstarter.${IP}.nip.io" - GRPC_ENDPOINT="grpc.${BASEDOMAIN}:8082" - GRPC_ROUTER_ENDPOINT="router.${BASEDOMAIN}:8083" - - kubectl config use-context kind-jumpstarter - - # Install old controller using operator installer from the release tag - local INSTALLER_URL="https://raw.githubusercontent.com/jumpstarter-dev/jumpstarter/${COMPAT_CONTROLLER_TAG}/controller/deploy/operator/dist/install.yaml" - log_info "Installing old controller via operator (version: ${COMPAT_CONTROLLER_TAG})..." - kubectl apply -f "${INSTALLER_URL}" || log_warn "Operator installer may not be available for ${COMPAT_CONTROLLER_TAG}, skipping" - - kubectl config set-context --current --namespace=jumpstarter-lab - - # Wait for gRPC endpoints - local GRPCURL="${REPO_ROOT}/controller/bin/grpcurl" - log_info "Waiting for gRPC endpoints..." - for ep in ${GRPC_ENDPOINT} ${GRPC_ROUTER_ENDPOINT}; do - local retries=60 - log_info " Checking ${ep}..." - while ! ${GRPCURL} -insecure "${ep}" list > /dev/null 2>&1; do - sleep 2 - retries=$((retries - 1)) - if [ ${retries} -eq 0 ]; then - log_error "${ep} not ready after 120s" - exit 1 - fi - done - done - - log_info "Old controller deployed" -} - deploy_new_controller() { log_info "Deploying new controller from HEAD..." @@ -211,7 +155,7 @@ EOF # Main execution main() { log_info "=== Jumpstarter Compatibility E2E Setup ===" - log_info "Scenario: $COMPAT_SCENARIO" + log_info "Scenario: New Controller + Old Client/Exporter ($COMPAT_CLIENT_VERSION)" log_info "Namespace: $JS_NAMESPACE" log_info "Repository Root: $REPO_ROOT" echo "" @@ -222,34 +166,20 @@ main() { create_cluster echo "" - case "$COMPAT_SCENARIO" in - old-controller) - log_info "Scenario: Old Controller ($COMPAT_CONTROLLER_TAG) + New Client/Exporter" - deploy_old_controller - echo "" - install_jumpstarter - ;; - old-client) - log_info "Scenario: New Controller + Old Client/Exporter ($COMPAT_CLIENT_VERSION)" - deploy_new_controller - echo "" - install_jumpstarter - echo "" - install_old_client - ;; - *) - log_error "Unknown COMPAT_SCENARIO: $COMPAT_SCENARIO (expected 'old-controller' or 'old-client')" - exit 1 - ;; - esac + deploy_new_controller + echo "" + + install_jumpstarter + echo "" + + install_old_client echo "" setup_test_environment echo "" log_info "=== Compat setup complete! ===" - log_info "Scenario: $COMPAT_SCENARIO" - log_info "To run tests: make e2e-compat-run COMPAT_TEST=" + log_info "To run tests: make e2e-compat-run COMPAT_TEST=old-client" } main "$@" diff --git a/e2e/test/compat_old_controller_test.go b/e2e/test/compat_old_controller_test.go deleted file mode 100644 index 3664ff53b..000000000 --- a/e2e/test/compat_old_controller_test.go +++ /dev/null @@ -1,192 +0,0 @@ -/* -Copyright 2026. The Jumpstarter Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package e2e - -import ( - "os/exec" - "path/filepath" - "syscall" - "time" - - . "github.com/onsi/ginkgo/v2" //nolint:revive - . "github.com/onsi/gomega" //nolint:revive -) - -var _ = Describe("Compat: Old Controller E2E Tests", Label("compat", "old-controller"), Ordered, func() { - var ( - tracker *ProcessTracker - ns string - ) - - waitForCompatExporter := func() { - time.Sleep(exporterPostDelay) - MustKubectl("-n", ns, "wait", "--timeout", "5m", - "--for=condition=Online", "--for=condition=Registered", - "exporters.jumpstarter.dev/compat-exporter") - } - - BeforeAll(func() { - tracker = NewProcessTracker() - ns = Namespace() - }) - - AfterAll(func() { - if tracker != nil { - tracker.StopAll() - } - - _ = exec.Command("pkill", "-9", "-f", "jmp run --exporter compat-").Run() - - _, _ = Jmp("admin", "delete", "client", "--namespace", ns, "compat-client", "--delete") - _, _ = Jmp("admin", "delete", "exporter", "--namespace", ns, "compat-exporter", "--delete") - _, _ = Jmp("admin", "delete", "client", "--namespace", ns, "compat-client-wait", "--delete") - _, _ = Jmp("admin", "delete", "exporter", "--namespace", ns, "compat-exporter-wait", "--delete") - - if tracker != nil { - tracker.Cleanup() - } - }) - - // --- Core compatibility --- - Context("Core compatibility", func() { - It("can create client with admin cli", func() { - out, err := Jmp("admin", "create", "client", "-n", ns, "compat-client", - "--unsafe", "--save") - Expect(err).NotTo(HaveOccurred(), out) - }) - - It("can create exporter with admin cli", func() { - out, err := Jmp("admin", "create", "exporter", "-n", ns, "compat-exporter", - "--save", "--label", "example.com/board=compat") - Expect(err).NotTo(HaveOccurred(), out) - - overlayPath := filepath.Join(RepoRoot(), "e2e", "exporters", "exporter.yaml") - MergeExporterConfig("/etc/jumpstarter/exporters/compat-exporter.yaml", overlayPath) - }) - - It("new exporter registers with old controller", func() { - tracker.StartExporterLoop("compat-exporter") - waitForCompatExporter() - }) - - It("exporter shows as Online and Registered", func() { - waitForCompatExporter() - - out := MustKubectl("-n", ns, "get", "exporters.jumpstarter.dev/compat-exporter", - "-o", `jsonpath={.status.conditions[?(@.type=="Online")].status}`) - Expect(out).To(Equal("True")) - - out = MustKubectl("-n", ns, "get", "exporters.jumpstarter.dev/compat-exporter", - "-o", `jsonpath={.status.conditions[?(@.type=="Registered")].status}`) - Expect(out).To(Equal("True")) - }) - - It("new client can lease and connect through old controller", func() { - waitForCompatExporter() - out, err := Jmp("shell", "--client", "compat-client", - "--selector", "example.com/board=compat", "j", "power", "on") - Expect(err).NotTo(HaveOccurred(), out) - }) - - It("can operate on leases through old controller", func() { - waitForCompatExporter() - - MustJmp("config", "client", "use", "compat-client") - MustJmp("create", "lease", "--selector", "example.com/board=compat", "--duration", "1d") - MustJmp("get", "leases") - MustJmp("get", "exporters") - - out, err := Jmp("get", "leases", "--selector", "example.com/board=compat", "-o", "yaml") - Expect(err).NotTo(HaveOccurred(), out) - Expect(out).To(ContainSubstring("example.com/board=compat")) - - out, err = Jmp("get", "leases", "--selector", "example.com/board=doesnotexist") - Expect(err).NotTo(HaveOccurred(), out) - Expect(out).To(Equal("No resources found.")) - - MustJmp("delete", "leases", "--all") - }) - - It("exporter stays Online after lease cycle", func() { - waitForCompatExporter() - - out := MustKubectl("-n", ns, "get", "exporters.jumpstarter.dev/compat-exporter", - "-o", `jsonpath={.status.conditions[?(@.type=="Online")].status}`) - Expect(out).To(Equal("True")) - - out = MustKubectl("-n", ns, "get", "exporters.jumpstarter.dev/compat-exporter", - "-o", `jsonpath={.status.conditions[?(@.type=="Registered")].status}`) - Expect(out).To(Equal("True")) - }) - }) - - // --- Client started before exporter --- - Context("Client started before exporter", func() { - It("client started before exporter connects", func() { - _ = exec.Command("pkill", "-9", "-f", "jmp run --exporter compat-exporter-wait").Run() - time.Sleep(3 * time.Second) - - _, _ = Jmp("admin", "delete", "exporter", "--namespace", ns, "compat-exporter-wait", "--delete") - _, _ = Jmp("admin", "delete", "client", "--namespace", ns, "compat-client-wait", "--delete") - - out, err := Jmp("admin", "create", "client", "-n", ns, "compat-client-wait", - "--unsafe", "--save") - Expect(err).NotTo(HaveOccurred(), out) - - out, err = Jmp("admin", "create", "exporter", "-n", ns, "compat-exporter-wait", - "--save", "--label", "example.com/board=compat-wait") - Expect(err).NotTo(HaveOccurred(), out) - - overlayPath := filepath.Join(RepoRoot(), "e2e", "exporters", "exporter.yaml") - MergeExporterConfig("/etc/jumpstarter/exporters/compat-exporter-wait.yaml", overlayPath) - - // Start client BEFORE exporter - clientCmd := JmpCmd("shell", "--client", "compat-client-wait", - "--selector", "example.com/board=compat-wait", "j", "power", "on") - clientCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - err = clientCmd.Start() - Expect(err).NotTo(HaveOccurred()) - - time.Sleep(5 * time.Second) - Expect(clientCmd.ProcessState).To(BeNil(), "Client exited before exporter was started") - - // Now start the exporter - tracker.StartExporterLoop("compat-exporter-wait") - - done := make(chan error, 1) - go func() { done <- clientCmd.Wait() }() - - select { - case err := <-done: - Expect(err).NotTo(HaveOccurred(), "client shell failed") - case <-time.After(120 * time.Second): - _ = clientCmd.Process.Kill() - Fail("Client shell timed out waiting for exporter (120s)") - } - }) - }) - - // --- Cleanup --- - Context("Cleanup", func() { - It("cleans up resources", func() { - _, _ = Jmp("admin", "delete", "client", "--namespace", ns, "compat-client", "--delete") - _, _ = Jmp("admin", "delete", "exporter", "--namespace", ns, "compat-exporter", "--delete") - _, _ = Jmp("admin", "delete", "client", "--namespace", ns, "compat-client-wait", "--delete") - _, _ = Jmp("admin", "delete", "exporter", "--namespace", ns, "compat-exporter-wait", "--delete") - }) - }) -}) From 47973aaf48ba768146c7fa5bde3dd0563befbcb7 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Sat, 11 Apr 2026 09:12:26 +0200 Subject: [PATCH 12/22] Revert "fix: remove old-controller compat test (v0.7.0 has no operator installer)" This reverts commit fa102b379ca0353498ecf7b536ba71ed3c107924. --- .github/workflows/e2e.yaml | 37 +++- Makefile | 12 +- e2e/compat/run.sh | 11 +- e2e/compat/setup.sh | 88 +++++++++- e2e/compat/tests-old-controller.bats | 232 +++++++++++++++++++++++++ e2e/test/compat_old_controller_test.go | 192 ++++++++++++++++++++ 6 files changed, 553 insertions(+), 19 deletions(-) create mode 100644 e2e/compat/tests-old-controller.bats create mode 100644 e2e/test/compat_old_controller_test.go diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 342a205d1..cabbf76d1 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -229,6 +229,39 @@ jobs: env: CI: true + # ============================================================================ + # Compatibility tests: cross-version interop between controller and client/exporter + # These jobs can be removed once 0.7.x controller support is no longer needed. + # ============================================================================ + + e2e-compat-old-controller: + needs: changes + if: needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Setup compat environment (old controller v0.7.0) + run: make e2e-compat-setup COMPAT_SCENARIO=old-controller + env: + CI: true + COMPAT_CONTROLLER_TAG: v0.7.0 + + - name: Run compat tests (old controller + new client/exporter) + run: make e2e-compat-run COMPAT_TEST=old-controller + env: + CI: true + e2e-compat-old-client: needs: [changes, build-controller-image, build-python-wheels] if: needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch' @@ -262,7 +295,7 @@ jobs: run: docker load < /tmp/artifacts/controller-image.tar - name: Setup compat environment (old client v0.7.1) - run: make e2e-compat-setup + run: make e2e-compat-setup COMPAT_SCENARIO=old-client env: CI: true COMPAT_CLIENT_VERSION: "0.7.1" @@ -270,6 +303,6 @@ jobs: PREBUILT_WHEELS_DIR: /tmp/python-wheels - name: Run compat tests (new controller + old client/exporter) - run: make e2e-compat-run + run: make e2e-compat-run COMPAT_TEST=old-client env: CI: true diff --git a/Makefile b/Makefile index f39f541b0..85cc645de 100644 --- a/Makefile +++ b/Makefile @@ -164,17 +164,21 @@ e2e-clean: test-e2e: e2e-run # Compatibility E2E testing (cross-version tests, separate from main e2e) +COMPAT_SCENARIO ?= old-controller +COMPAT_TEST ?= old-controller +COMPAT_CONTROLLER_TAG ?= v0.7.0 COMPAT_CLIENT_VERSION ?= 0.7.1 .PHONY: e2e-compat-setup e2e-compat-setup: - @echo "Setting up compat e2e (old client v$(COMPAT_CLIENT_VERSION))..." - @COMPAT_CLIENT_VERSION=$(COMPAT_CLIENT_VERSION) bash e2e/compat/setup.sh + @echo "Setting up compat e2e (scenario: $(COMPAT_SCENARIO))..." + @COMPAT_SCENARIO=$(COMPAT_SCENARIO) COMPAT_CONTROLLER_TAG=$(COMPAT_CONTROLLER_TAG) \ + COMPAT_CLIENT_VERSION=$(COMPAT_CLIENT_VERSION) bash e2e/compat/setup.sh .PHONY: e2e-compat-run e2e-compat-run: - @echo "Running compat e2e (old client)..." - @COMPAT_TEST=old-client bash e2e/compat/run.sh + @echo "Running compat e2e (test: $(COMPAT_TEST))..." + @COMPAT_TEST=$(COMPAT_TEST) bash e2e/compat/run.sh # Per-project clean targets .PHONY: clean-python clean-protocol clean-controller clean-e2e diff --git a/e2e/compat/run.sh b/e2e/compat/run.sh index 301feea83..741c21b63 100755 --- a/e2e/compat/run.sh +++ b/e2e/compat/run.sh @@ -3,7 +3,7 @@ # This script runs the compatibility test suite (assumes setup.sh was run first) # # Environment variables: -# COMPAT_TEST - Which test to run (default: "old-client") +# COMPAT_TEST - Which test to run: "old-controller" or "old-client" set -euo pipefail @@ -48,16 +48,19 @@ run_tests() { # Use insecure GRPC for testing export JUMPSTARTER_GRPC_INSECURE=1 - COMPAT_TEST="${COMPAT_TEST:-old-client}" + COMPAT_TEST="${COMPAT_TEST:-old-controller}" log_info "Running compat test: $COMPAT_TEST" local label_filter="" case "$COMPAT_TEST" in + old-controller) + label_filter="old-controller" + ;; old-client) label_filter="old-client" ;; *) - log_error "Unknown COMPAT_TEST: $COMPAT_TEST (expected 'old-client')" + log_error "Unknown COMPAT_TEST: $COMPAT_TEST (expected 'old-controller' or 'old-client')" exit 1 ;; esac @@ -70,7 +73,7 @@ main() { export E2E_TEST_NS="${E2E_TEST_NS:-jumpstarter-lab}" log_info "=== Jumpstarter Compatibility E2E Test Runner ===" - log_info "Test: ${COMPAT_TEST:-old-client}" + log_info "Test: ${COMPAT_TEST:-old-controller}" log_info "Namespace: $E2E_TEST_NS" log_info "Repository Root: $REPO_ROOT" echo "" diff --git a/e2e/compat/setup.sh b/e2e/compat/setup.sh index 6444ce711..85306ac6c 100755 --- a/e2e/compat/setup.sh +++ b/e2e/compat/setup.sh @@ -6,6 +6,8 @@ # Uses operator-based deployment. # # Environment variables: +# COMPAT_SCENARIO - "old-controller" or "old-client" (required) +# COMPAT_CONTROLLER_TAG - Controller image tag for old-controller scenario (default: v0.7.0) # COMPAT_CLIENT_VERSION - PyPI version for old-client scenario (default: 0.7.1) set -euo pipefail @@ -22,6 +24,9 @@ REPO_ROOT="$(cd "$E2E_DIR/.." && pwd)" # Default namespace for tests export JS_NAMESPACE="${JS_NAMESPACE:-jumpstarter-lab}" +# Scenario configuration +COMPAT_SCENARIO="${COMPAT_SCENARIO:-old-controller}" +COMPAT_CONTROLLER_TAG="${COMPAT_CONTROLLER_TAG:-v0.7.0}" COMPAT_CLIENT_VERSION="${COMPAT_CLIENT_VERSION:-0.7.1}" # Color output @@ -63,6 +68,17 @@ install_dependencies() { log_info "Dependencies installed" } +# Get the external IP for baseDomain +get_external_ip() { + if which ip 2>/dev/null 1>/dev/null; then + ip route get 1.1.1.1 | grep -oP 'src \K\S+' + else + local INTERFACE + INTERFACE=$(route get 1.1.1.1 | grep interface | awk '{print $2}') + ifconfig | grep "$INTERFACE" -A 10 | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' | head -n 1 + fi +} + # Create kind cluster and install grpcurl create_cluster() { log_info "Creating kind cluster..." @@ -73,6 +89,46 @@ create_cluster() { log_info "Kind cluster created" } +deploy_old_controller() { + log_info "Deploying old controller (version: $COMPAT_CONTROLLER_TAG)..." + + cd "$REPO_ROOT" + + # Compute networking variables + local IP + IP=$(get_external_ip) + BASEDOMAIN="jumpstarter.${IP}.nip.io" + GRPC_ENDPOINT="grpc.${BASEDOMAIN}:8082" + GRPC_ROUTER_ENDPOINT="router.${BASEDOMAIN}:8083" + + kubectl config use-context kind-jumpstarter + + # Install old controller using operator installer from the release tag + local INSTALLER_URL="https://raw.githubusercontent.com/jumpstarter-dev/jumpstarter/${COMPAT_CONTROLLER_TAG}/controller/deploy/operator/dist/install.yaml" + log_info "Installing old controller via operator (version: ${COMPAT_CONTROLLER_TAG})..." + kubectl apply -f "${INSTALLER_URL}" || log_warn "Operator installer may not be available for ${COMPAT_CONTROLLER_TAG}, skipping" + + kubectl config set-context --current --namespace=jumpstarter-lab + + # Wait for gRPC endpoints + local GRPCURL="${REPO_ROOT}/controller/bin/grpcurl" + log_info "Waiting for gRPC endpoints..." + for ep in ${GRPC_ENDPOINT} ${GRPC_ROUTER_ENDPOINT}; do + local retries=60 + log_info " Checking ${ep}..." + while ! ${GRPCURL} -insecure "${ep}" list > /dev/null 2>&1; do + sleep 2 + retries=$((retries - 1)) + if [ ${retries} -eq 0 ]; then + log_error "${ep} not ready after 120s" + exit 1 + fi + done + done + + log_info "Old controller deployed" +} + deploy_new_controller() { log_info "Deploying new controller from HEAD..." @@ -155,7 +211,7 @@ EOF # Main execution main() { log_info "=== Jumpstarter Compatibility E2E Setup ===" - log_info "Scenario: New Controller + Old Client/Exporter ($COMPAT_CLIENT_VERSION)" + log_info "Scenario: $COMPAT_SCENARIO" log_info "Namespace: $JS_NAMESPACE" log_info "Repository Root: $REPO_ROOT" echo "" @@ -166,20 +222,34 @@ main() { create_cluster echo "" - deploy_new_controller - echo "" - - install_jumpstarter - echo "" - - install_old_client + case "$COMPAT_SCENARIO" in + old-controller) + log_info "Scenario: Old Controller ($COMPAT_CONTROLLER_TAG) + New Client/Exporter" + deploy_old_controller + echo "" + install_jumpstarter + ;; + old-client) + log_info "Scenario: New Controller + Old Client/Exporter ($COMPAT_CLIENT_VERSION)" + deploy_new_controller + echo "" + install_jumpstarter + echo "" + install_old_client + ;; + *) + log_error "Unknown COMPAT_SCENARIO: $COMPAT_SCENARIO (expected 'old-controller' or 'old-client')" + exit 1 + ;; + esac echo "" setup_test_environment echo "" log_info "=== Compat setup complete! ===" - log_info "To run tests: make e2e-compat-run COMPAT_TEST=old-client" + log_info "Scenario: $COMPAT_SCENARIO" + log_info "To run tests: make e2e-compat-run COMPAT_TEST=" } main "$@" diff --git a/e2e/compat/tests-old-controller.bats b/e2e/compat/tests-old-controller.bats new file mode 100644 index 000000000..0fc46197e --- /dev/null +++ b/e2e/compat/tests-old-controller.bats @@ -0,0 +1,232 @@ +#!/usr/bin/env bats +# Compatibility E2E tests: Old Controller (v0.7.x) + New Client/Exporter +# +# Tests that the new client and exporter code works correctly against +# an older controller version that doesn't support hooks protocol changes. +# Also includes the client-started-before-exporter scenario. + +JS_NAMESPACE="${JS_NAMESPACE:-jumpstarter-lab}" + +# File to track bash wrapper process PIDs across tests +COMPAT_PIDS_FILE="${BATS_RUN_TMPDIR:-/tmp}/compat_old_ctrl_pids.txt" + +setup_file() { + # Initialize the PIDs file at the start of all tests + echo "" > "$COMPAT_PIDS_FILE" +} + +setup() { + bats_load_library bats-support + bats_load_library bats-assert + + bats_require_minimum_version 1.5.0 +} + +teardown_file() { + echo "" >&2 + echo "========================================" >&2 + echo "COMPAT OLD-CONTROLLER TEARDOWN RUNNING" >&2 + echo "========================================" >&2 + + # Kill tracked PIDs + if [ -f "$COMPAT_PIDS_FILE" ]; then + while IFS= read -r pid; do + if [ -n "$pid" ]; then + if ps -p "$pid" > /dev/null 2>&1; then + echo " Killing PID $pid" >&2 + kill -9 "$pid" 2>/dev/null || true + fi + fi + done < "$COMPAT_PIDS_FILE" + fi + + # Kill any orphaned jmp processes for compat exporters + pkill -9 -f "jmp run --exporter compat-" 2>/dev/null || true + + # Clean up the PIDs file + rm -f "$COMPAT_PIDS_FILE" + + # Clean up CRDs (best effort) + jmp admin delete client --namespace "${JS_NAMESPACE}" compat-client --delete 2>/dev/null || true + jmp admin delete exporter --namespace "${JS_NAMESPACE}" compat-exporter --delete 2>/dev/null || true + jmp admin delete client --namespace "${JS_NAMESPACE}" compat-client-wait --delete 2>/dev/null || true + jmp admin delete exporter --namespace "${JS_NAMESPACE}" compat-exporter-wait --delete 2>/dev/null || true + + echo "=== Compat old-controller cleanup complete ===" >&2 +} + +wait_for_compat_exporter() { + # Brief delay to avoid catching pre-disconnect state + sleep 2 + kubectl -n "${JS_NAMESPACE}" wait --timeout 5m \ + --for=condition=Online --for=condition=Registered \ + exporters.jumpstarter.dev/compat-exporter +} + +# ============================================================================ +# Core compatibility tests: Old Controller + New Client/Exporter +# ============================================================================ + +@test "compat-old-ctrl: can create client with admin cli" { + run jmp admin create client -n "${JS_NAMESPACE}" compat-client --unsafe --save + assert_success +} + +@test "compat-old-ctrl: can create exporter with admin cli" { + run jmp admin create exporter -n "${JS_NAMESPACE}" compat-exporter --save \ + --label example.com/board=compat + assert_success + + go run github.com/mikefarah/yq/v4@latest -i ". * load(\"e2e/exporters/exporter.yaml\")" \ + /etc/jumpstarter/exporters/compat-exporter.yaml +} + +@test "compat-old-ctrl: new exporter registers with old controller" { + cat <&- & +while true; do + jmp run --exporter compat-exporter + sleep 2 +done +EOF + echo "$!" >> "$COMPAT_PIDS_FILE" + + wait_for_compat_exporter +} + +@test "compat-old-ctrl: exporter shows as Online and Registered" { + wait_for_compat_exporter + + run kubectl -n "${JS_NAMESPACE}" get exporters.jumpstarter.dev/compat-exporter \ + -o jsonpath='{.status.conditions[?(@.type=="Online")].status}' + assert_success + assert_output "True" + + run kubectl -n "${JS_NAMESPACE}" get exporters.jumpstarter.dev/compat-exporter \ + -o jsonpath='{.status.conditions[?(@.type=="Registered")].status}' + assert_success + assert_output "True" +} + +@test "compat-old-ctrl: new client can lease and connect through old controller" { + wait_for_compat_exporter + + run jmp shell --client compat-client \ + --selector example.com/board=compat j power on + assert_success +} + +@test "compat-old-ctrl: can operate on leases through old controller" { + wait_for_compat_exporter + + jmp config client use compat-client + + jmp create lease --selector example.com/board=compat --duration 1d + jmp get leases + jmp get exporters + + # Verify label selector filtering works + run jmp get leases --selector example.com/board=compat -o yaml + assert_success + assert_output --partial "example.com/board=compat" + + run jmp get leases --selector example.com/board=doesnotexist + assert_success + assert_output "No resources found." + + jmp delete leases --all +} + +@test "compat-old-ctrl: exporter stays Online after lease cycle" { + # After lease operations, the new exporter sends ReportStatus with + # new fields (status_version, previous_status, release_lease) that + # the old controller doesn't understand. The exporter must remain Online. + wait_for_compat_exporter + + run kubectl -n "${JS_NAMESPACE}" get exporters.jumpstarter.dev/compat-exporter \ + -o jsonpath='{.status.conditions[?(@.type=="Online")].status}' + assert_success + assert_output "True" + + run kubectl -n "${JS_NAMESPACE}" get exporters.jumpstarter.dev/compat-exporter \ + -o jsonpath='{.status.conditions[?(@.type=="Registered")].status}' + assert_success + assert_output "True" +} + +# ============================================================================ +# Client started before exporter +# ============================================================================ + +@test "compat-old-ctrl: client started before exporter connects" { + # Stop any running compat exporters for a clean test + pkill -9 -f "jmp run --exporter compat-exporter-wait" 2>/dev/null || true + sleep 3 + + # Create fresh resources for this test + jmp admin delete exporter --namespace "${JS_NAMESPACE}" compat-exporter-wait --delete 2>/dev/null || true + jmp admin delete client --namespace "${JS_NAMESPACE}" compat-client-wait --delete 2>/dev/null || true + + run jmp admin create client -n "${JS_NAMESPACE}" compat-client-wait --unsafe --save + assert_success + + run jmp admin create exporter -n "${JS_NAMESPACE}" compat-exporter-wait --save \ + --label example.com/board=compat-wait + assert_success + + go run github.com/mikefarah/yq/v4@latest -i ". * load(\"e2e/exporters/exporter.yaml\")" \ + /etc/jumpstarter/exporters/compat-exporter-wait.yaml + + # Start client shell BEFORE exporter is running (in background) + # The client should wait for a matching exporter to become available + jmp shell --client compat-client-wait \ + --selector example.com/board=compat-wait j power on & + local CLIENT_PID=$! + + # Wait a few seconds to ensure the client is actively waiting for lease fulfillment + sleep 5 + + # Verify the client is still waiting (hasn't exited yet) + if ! kill -0 $CLIENT_PID 2>/dev/null; then + wait $CLIENT_PID || true + fail "Client exited before exporter was started" + fi + + # Now start the exporter + cat <&- & +while true; do + jmp run --exporter compat-exporter-wait + sleep 2 +done +EOF + echo "$!" >> "$COMPAT_PIDS_FILE" + + # Wait for the client command to complete (with timeout) + local timeout=120 + local count=0 + while kill -0 $CLIENT_PID 2>/dev/null && [ $count -lt $timeout ]; do + sleep 1 + count=$((count + 1)) + done + + # Check if client process completed + if kill -0 $CLIENT_PID 2>/dev/null; then + kill -9 $CLIENT_PID 2>/dev/null || true + fail "Client shell timed out waiting for exporter (${timeout}s)" + fi + + # Verify client exited successfully + wait $CLIENT_PID + local exit_code=$? + [ $exit_code -eq 0 ] +} + +# ============================================================================ +# Cleanup +# ============================================================================ + +@test "compat-old-ctrl: cleanup resources" { + jmp admin delete client --namespace "${JS_NAMESPACE}" compat-client --delete 2>/dev/null || true + jmp admin delete exporter --namespace "${JS_NAMESPACE}" compat-exporter --delete 2>/dev/null || true + jmp admin delete client --namespace "${JS_NAMESPACE}" compat-client-wait --delete 2>/dev/null || true + jmp admin delete exporter --namespace "${JS_NAMESPACE}" compat-exporter-wait --delete 2>/dev/null || true +} diff --git a/e2e/test/compat_old_controller_test.go b/e2e/test/compat_old_controller_test.go new file mode 100644 index 000000000..3664ff53b --- /dev/null +++ b/e2e/test/compat_old_controller_test.go @@ -0,0 +1,192 @@ +/* +Copyright 2026. The Jumpstarter Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "os/exec" + "path/filepath" + "syscall" + "time" + + . "github.com/onsi/ginkgo/v2" //nolint:revive + . "github.com/onsi/gomega" //nolint:revive +) + +var _ = Describe("Compat: Old Controller E2E Tests", Label("compat", "old-controller"), Ordered, func() { + var ( + tracker *ProcessTracker + ns string + ) + + waitForCompatExporter := func() { + time.Sleep(exporterPostDelay) + MustKubectl("-n", ns, "wait", "--timeout", "5m", + "--for=condition=Online", "--for=condition=Registered", + "exporters.jumpstarter.dev/compat-exporter") + } + + BeforeAll(func() { + tracker = NewProcessTracker() + ns = Namespace() + }) + + AfterAll(func() { + if tracker != nil { + tracker.StopAll() + } + + _ = exec.Command("pkill", "-9", "-f", "jmp run --exporter compat-").Run() + + _, _ = Jmp("admin", "delete", "client", "--namespace", ns, "compat-client", "--delete") + _, _ = Jmp("admin", "delete", "exporter", "--namespace", ns, "compat-exporter", "--delete") + _, _ = Jmp("admin", "delete", "client", "--namespace", ns, "compat-client-wait", "--delete") + _, _ = Jmp("admin", "delete", "exporter", "--namespace", ns, "compat-exporter-wait", "--delete") + + if tracker != nil { + tracker.Cleanup() + } + }) + + // --- Core compatibility --- + Context("Core compatibility", func() { + It("can create client with admin cli", func() { + out, err := Jmp("admin", "create", "client", "-n", ns, "compat-client", + "--unsafe", "--save") + Expect(err).NotTo(HaveOccurred(), out) + }) + + It("can create exporter with admin cli", func() { + out, err := Jmp("admin", "create", "exporter", "-n", ns, "compat-exporter", + "--save", "--label", "example.com/board=compat") + Expect(err).NotTo(HaveOccurred(), out) + + overlayPath := filepath.Join(RepoRoot(), "e2e", "exporters", "exporter.yaml") + MergeExporterConfig("/etc/jumpstarter/exporters/compat-exporter.yaml", overlayPath) + }) + + It("new exporter registers with old controller", func() { + tracker.StartExporterLoop("compat-exporter") + waitForCompatExporter() + }) + + It("exporter shows as Online and Registered", func() { + waitForCompatExporter() + + out := MustKubectl("-n", ns, "get", "exporters.jumpstarter.dev/compat-exporter", + "-o", `jsonpath={.status.conditions[?(@.type=="Online")].status}`) + Expect(out).To(Equal("True")) + + out = MustKubectl("-n", ns, "get", "exporters.jumpstarter.dev/compat-exporter", + "-o", `jsonpath={.status.conditions[?(@.type=="Registered")].status}`) + Expect(out).To(Equal("True")) + }) + + It("new client can lease and connect through old controller", func() { + waitForCompatExporter() + out, err := Jmp("shell", "--client", "compat-client", + "--selector", "example.com/board=compat", "j", "power", "on") + Expect(err).NotTo(HaveOccurred(), out) + }) + + It("can operate on leases through old controller", func() { + waitForCompatExporter() + + MustJmp("config", "client", "use", "compat-client") + MustJmp("create", "lease", "--selector", "example.com/board=compat", "--duration", "1d") + MustJmp("get", "leases") + MustJmp("get", "exporters") + + out, err := Jmp("get", "leases", "--selector", "example.com/board=compat", "-o", "yaml") + Expect(err).NotTo(HaveOccurred(), out) + Expect(out).To(ContainSubstring("example.com/board=compat")) + + out, err = Jmp("get", "leases", "--selector", "example.com/board=doesnotexist") + Expect(err).NotTo(HaveOccurred(), out) + Expect(out).To(Equal("No resources found.")) + + MustJmp("delete", "leases", "--all") + }) + + It("exporter stays Online after lease cycle", func() { + waitForCompatExporter() + + out := MustKubectl("-n", ns, "get", "exporters.jumpstarter.dev/compat-exporter", + "-o", `jsonpath={.status.conditions[?(@.type=="Online")].status}`) + Expect(out).To(Equal("True")) + + out = MustKubectl("-n", ns, "get", "exporters.jumpstarter.dev/compat-exporter", + "-o", `jsonpath={.status.conditions[?(@.type=="Registered")].status}`) + Expect(out).To(Equal("True")) + }) + }) + + // --- Client started before exporter --- + Context("Client started before exporter", func() { + It("client started before exporter connects", func() { + _ = exec.Command("pkill", "-9", "-f", "jmp run --exporter compat-exporter-wait").Run() + time.Sleep(3 * time.Second) + + _, _ = Jmp("admin", "delete", "exporter", "--namespace", ns, "compat-exporter-wait", "--delete") + _, _ = Jmp("admin", "delete", "client", "--namespace", ns, "compat-client-wait", "--delete") + + out, err := Jmp("admin", "create", "client", "-n", ns, "compat-client-wait", + "--unsafe", "--save") + Expect(err).NotTo(HaveOccurred(), out) + + out, err = Jmp("admin", "create", "exporter", "-n", ns, "compat-exporter-wait", + "--save", "--label", "example.com/board=compat-wait") + Expect(err).NotTo(HaveOccurred(), out) + + overlayPath := filepath.Join(RepoRoot(), "e2e", "exporters", "exporter.yaml") + MergeExporterConfig("/etc/jumpstarter/exporters/compat-exporter-wait.yaml", overlayPath) + + // Start client BEFORE exporter + clientCmd := JmpCmd("shell", "--client", "compat-client-wait", + "--selector", "example.com/board=compat-wait", "j", "power", "on") + clientCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + err = clientCmd.Start() + Expect(err).NotTo(HaveOccurred()) + + time.Sleep(5 * time.Second) + Expect(clientCmd.ProcessState).To(BeNil(), "Client exited before exporter was started") + + // Now start the exporter + tracker.StartExporterLoop("compat-exporter-wait") + + done := make(chan error, 1) + go func() { done <- clientCmd.Wait() }() + + select { + case err := <-done: + Expect(err).NotTo(HaveOccurred(), "client shell failed") + case <-time.After(120 * time.Second): + _ = clientCmd.Process.Kill() + Fail("Client shell timed out waiting for exporter (120s)") + } + }) + }) + + // --- Cleanup --- + Context("Cleanup", func() { + It("cleans up resources", func() { + _, _ = Jmp("admin", "delete", "client", "--namespace", ns, "compat-client", "--delete") + _, _ = Jmp("admin", "delete", "exporter", "--namespace", ns, "compat-exporter", "--delete") + _, _ = Jmp("admin", "delete", "client", "--namespace", ns, "compat-client-wait", "--delete") + _, _ = Jmp("admin", "delete", "exporter", "--namespace", ns, "compat-exporter-wait", "--delete") + }) + }) +}) From 825a43c3e71d476190f718e21bf92dcc087c5def Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Sat, 11 Apr 2026 09:17:06 +0200 Subject: [PATCH 13/22] fix: deploy old controller v0.8.1 via operator installer in compat tests v0.7.0 predated the operator installer and was only deployable via Helm. Replace it with v0.8.1 which ships an operator-installer.yaml release asset. Also bump old-client version from 0.7.1 to 0.8.1 so both compat scenarios test against the previous release. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/e2e.yaml | 9 ++++----- Makefile | 4 ++-- e2e/compat/setup.sh | 14 +++++++------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index cabbf76d1..4fcf0b93b 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -231,7 +231,6 @@ jobs: # ============================================================================ # Compatibility tests: cross-version interop between controller and client/exporter - # These jobs can be removed once 0.7.x controller support is no longer needed. # ============================================================================ e2e-compat-old-controller: @@ -251,11 +250,11 @@ jobs: with: go-version: "1.22" - - name: Setup compat environment (old controller v0.7.0) + - name: Setup compat environment (old controller v0.8.1) run: make e2e-compat-setup COMPAT_SCENARIO=old-controller env: CI: true - COMPAT_CONTROLLER_TAG: v0.7.0 + COMPAT_CONTROLLER_TAG: v0.8.1 - name: Run compat tests (old controller + new client/exporter) run: make e2e-compat-run COMPAT_TEST=old-controller @@ -294,11 +293,11 @@ jobs: - name: Load controller image run: docker load < /tmp/artifacts/controller-image.tar - - name: Setup compat environment (old client v0.7.1) + - name: Setup compat environment (old client v0.8.1) run: make e2e-compat-setup COMPAT_SCENARIO=old-client env: CI: true - COMPAT_CLIENT_VERSION: "0.7.1" + COMPAT_CLIENT_VERSION: "0.8.1" SKIP_BUILD: "true" PREBUILT_WHEELS_DIR: /tmp/python-wheels diff --git a/Makefile b/Makefile index 85cc645de..2fef4bb70 100644 --- a/Makefile +++ b/Makefile @@ -166,8 +166,8 @@ test-e2e: e2e-run # Compatibility E2E testing (cross-version tests, separate from main e2e) COMPAT_SCENARIO ?= old-controller COMPAT_TEST ?= old-controller -COMPAT_CONTROLLER_TAG ?= v0.7.0 -COMPAT_CLIENT_VERSION ?= 0.7.1 +COMPAT_CONTROLLER_TAG ?= v0.8.1 +COMPAT_CLIENT_VERSION ?= 0.8.1 .PHONY: e2e-compat-setup e2e-compat-setup: diff --git a/e2e/compat/setup.sh b/e2e/compat/setup.sh index 85306ac6c..f47dbed47 100755 --- a/e2e/compat/setup.sh +++ b/e2e/compat/setup.sh @@ -7,8 +7,8 @@ # # Environment variables: # COMPAT_SCENARIO - "old-controller" or "old-client" (required) -# COMPAT_CONTROLLER_TAG - Controller image tag for old-controller scenario (default: v0.7.0) -# COMPAT_CLIENT_VERSION - PyPI version for old-client scenario (default: 0.7.1) +# COMPAT_CONTROLLER_TAG - Controller release tag for old-controller scenario (default: v0.8.1) +# COMPAT_CLIENT_VERSION - PyPI version for old-client scenario (default: 0.8.1) set -euo pipefail @@ -26,8 +26,8 @@ export JS_NAMESPACE="${JS_NAMESPACE:-jumpstarter-lab}" # Scenario configuration COMPAT_SCENARIO="${COMPAT_SCENARIO:-old-controller}" -COMPAT_CONTROLLER_TAG="${COMPAT_CONTROLLER_TAG:-v0.7.0}" -COMPAT_CLIENT_VERSION="${COMPAT_CLIENT_VERSION:-0.7.1}" +COMPAT_CONTROLLER_TAG="${COMPAT_CONTROLLER_TAG:-v0.8.1}" +COMPAT_CLIENT_VERSION="${COMPAT_CLIENT_VERSION:-0.8.1}" # Color output RED='\033[0;31m' @@ -103,10 +103,10 @@ deploy_old_controller() { kubectl config use-context kind-jumpstarter - # Install old controller using operator installer from the release tag - local INSTALLER_URL="https://raw.githubusercontent.com/jumpstarter-dev/jumpstarter/${COMPAT_CONTROLLER_TAG}/controller/deploy/operator/dist/install.yaml" + # Install old controller using operator installer from the release assets + local INSTALLER_URL="https://github.com/jumpstarter-dev/jumpstarter/releases/download/${COMPAT_CONTROLLER_TAG}/operator-installer.yaml" log_info "Installing old controller via operator (version: ${COMPAT_CONTROLLER_TAG})..." - kubectl apply -f "${INSTALLER_URL}" || log_warn "Operator installer may not be available for ${COMPAT_CONTROLLER_TAG}, skipping" + kubectl apply -f "${INSTALLER_URL}" kubectl config set-context --current --namespace=jumpstarter-lab From 8c3ca55e6fdcce3c648e348759845027f17f91f3 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Sat, 11 Apr 2026 09:27:15 +0200 Subject: [PATCH 14/22] fix: remove stale bats test file re-added by revert The revert commit brought back tests-old-controller.bats, but upstream converted all bats tests to ginkgo. The ginkgo equivalent (e2e/test/compat_old_controller_test.go) is the canonical test file. Co-Authored-By: Claude Opus 4.6 --- e2e/compat/tests-old-controller.bats | 232 --------------------------- 1 file changed, 232 deletions(-) delete mode 100644 e2e/compat/tests-old-controller.bats diff --git a/e2e/compat/tests-old-controller.bats b/e2e/compat/tests-old-controller.bats deleted file mode 100644 index 0fc46197e..000000000 --- a/e2e/compat/tests-old-controller.bats +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/env bats -# Compatibility E2E tests: Old Controller (v0.7.x) + New Client/Exporter -# -# Tests that the new client and exporter code works correctly against -# an older controller version that doesn't support hooks protocol changes. -# Also includes the client-started-before-exporter scenario. - -JS_NAMESPACE="${JS_NAMESPACE:-jumpstarter-lab}" - -# File to track bash wrapper process PIDs across tests -COMPAT_PIDS_FILE="${BATS_RUN_TMPDIR:-/tmp}/compat_old_ctrl_pids.txt" - -setup_file() { - # Initialize the PIDs file at the start of all tests - echo "" > "$COMPAT_PIDS_FILE" -} - -setup() { - bats_load_library bats-support - bats_load_library bats-assert - - bats_require_minimum_version 1.5.0 -} - -teardown_file() { - echo "" >&2 - echo "========================================" >&2 - echo "COMPAT OLD-CONTROLLER TEARDOWN RUNNING" >&2 - echo "========================================" >&2 - - # Kill tracked PIDs - if [ -f "$COMPAT_PIDS_FILE" ]; then - while IFS= read -r pid; do - if [ -n "$pid" ]; then - if ps -p "$pid" > /dev/null 2>&1; then - echo " Killing PID $pid" >&2 - kill -9 "$pid" 2>/dev/null || true - fi - fi - done < "$COMPAT_PIDS_FILE" - fi - - # Kill any orphaned jmp processes for compat exporters - pkill -9 -f "jmp run --exporter compat-" 2>/dev/null || true - - # Clean up the PIDs file - rm -f "$COMPAT_PIDS_FILE" - - # Clean up CRDs (best effort) - jmp admin delete client --namespace "${JS_NAMESPACE}" compat-client --delete 2>/dev/null || true - jmp admin delete exporter --namespace "${JS_NAMESPACE}" compat-exporter --delete 2>/dev/null || true - jmp admin delete client --namespace "${JS_NAMESPACE}" compat-client-wait --delete 2>/dev/null || true - jmp admin delete exporter --namespace "${JS_NAMESPACE}" compat-exporter-wait --delete 2>/dev/null || true - - echo "=== Compat old-controller cleanup complete ===" >&2 -} - -wait_for_compat_exporter() { - # Brief delay to avoid catching pre-disconnect state - sleep 2 - kubectl -n "${JS_NAMESPACE}" wait --timeout 5m \ - --for=condition=Online --for=condition=Registered \ - exporters.jumpstarter.dev/compat-exporter -} - -# ============================================================================ -# Core compatibility tests: Old Controller + New Client/Exporter -# ============================================================================ - -@test "compat-old-ctrl: can create client with admin cli" { - run jmp admin create client -n "${JS_NAMESPACE}" compat-client --unsafe --save - assert_success -} - -@test "compat-old-ctrl: can create exporter with admin cli" { - run jmp admin create exporter -n "${JS_NAMESPACE}" compat-exporter --save \ - --label example.com/board=compat - assert_success - - go run github.com/mikefarah/yq/v4@latest -i ". * load(\"e2e/exporters/exporter.yaml\")" \ - /etc/jumpstarter/exporters/compat-exporter.yaml -} - -@test "compat-old-ctrl: new exporter registers with old controller" { - cat <&- & -while true; do - jmp run --exporter compat-exporter - sleep 2 -done -EOF - echo "$!" >> "$COMPAT_PIDS_FILE" - - wait_for_compat_exporter -} - -@test "compat-old-ctrl: exporter shows as Online and Registered" { - wait_for_compat_exporter - - run kubectl -n "${JS_NAMESPACE}" get exporters.jumpstarter.dev/compat-exporter \ - -o jsonpath='{.status.conditions[?(@.type=="Online")].status}' - assert_success - assert_output "True" - - run kubectl -n "${JS_NAMESPACE}" get exporters.jumpstarter.dev/compat-exporter \ - -o jsonpath='{.status.conditions[?(@.type=="Registered")].status}' - assert_success - assert_output "True" -} - -@test "compat-old-ctrl: new client can lease and connect through old controller" { - wait_for_compat_exporter - - run jmp shell --client compat-client \ - --selector example.com/board=compat j power on - assert_success -} - -@test "compat-old-ctrl: can operate on leases through old controller" { - wait_for_compat_exporter - - jmp config client use compat-client - - jmp create lease --selector example.com/board=compat --duration 1d - jmp get leases - jmp get exporters - - # Verify label selector filtering works - run jmp get leases --selector example.com/board=compat -o yaml - assert_success - assert_output --partial "example.com/board=compat" - - run jmp get leases --selector example.com/board=doesnotexist - assert_success - assert_output "No resources found." - - jmp delete leases --all -} - -@test "compat-old-ctrl: exporter stays Online after lease cycle" { - # After lease operations, the new exporter sends ReportStatus with - # new fields (status_version, previous_status, release_lease) that - # the old controller doesn't understand. The exporter must remain Online. - wait_for_compat_exporter - - run kubectl -n "${JS_NAMESPACE}" get exporters.jumpstarter.dev/compat-exporter \ - -o jsonpath='{.status.conditions[?(@.type=="Online")].status}' - assert_success - assert_output "True" - - run kubectl -n "${JS_NAMESPACE}" get exporters.jumpstarter.dev/compat-exporter \ - -o jsonpath='{.status.conditions[?(@.type=="Registered")].status}' - assert_success - assert_output "True" -} - -# ============================================================================ -# Client started before exporter -# ============================================================================ - -@test "compat-old-ctrl: client started before exporter connects" { - # Stop any running compat exporters for a clean test - pkill -9 -f "jmp run --exporter compat-exporter-wait" 2>/dev/null || true - sleep 3 - - # Create fresh resources for this test - jmp admin delete exporter --namespace "${JS_NAMESPACE}" compat-exporter-wait --delete 2>/dev/null || true - jmp admin delete client --namespace "${JS_NAMESPACE}" compat-client-wait --delete 2>/dev/null || true - - run jmp admin create client -n "${JS_NAMESPACE}" compat-client-wait --unsafe --save - assert_success - - run jmp admin create exporter -n "${JS_NAMESPACE}" compat-exporter-wait --save \ - --label example.com/board=compat-wait - assert_success - - go run github.com/mikefarah/yq/v4@latest -i ". * load(\"e2e/exporters/exporter.yaml\")" \ - /etc/jumpstarter/exporters/compat-exporter-wait.yaml - - # Start client shell BEFORE exporter is running (in background) - # The client should wait for a matching exporter to become available - jmp shell --client compat-client-wait \ - --selector example.com/board=compat-wait j power on & - local CLIENT_PID=$! - - # Wait a few seconds to ensure the client is actively waiting for lease fulfillment - sleep 5 - - # Verify the client is still waiting (hasn't exited yet) - if ! kill -0 $CLIENT_PID 2>/dev/null; then - wait $CLIENT_PID || true - fail "Client exited before exporter was started" - fi - - # Now start the exporter - cat <&- & -while true; do - jmp run --exporter compat-exporter-wait - sleep 2 -done -EOF - echo "$!" >> "$COMPAT_PIDS_FILE" - - # Wait for the client command to complete (with timeout) - local timeout=120 - local count=0 - while kill -0 $CLIENT_PID 2>/dev/null && [ $count -lt $timeout ]; do - sleep 1 - count=$((count + 1)) - done - - # Check if client process completed - if kill -0 $CLIENT_PID 2>/dev/null; then - kill -9 $CLIENT_PID 2>/dev/null || true - fail "Client shell timed out waiting for exporter (${timeout}s)" - fi - - # Verify client exited successfully - wait $CLIENT_PID - local exit_code=$? - [ $exit_code -eq 0 ] -} - -# ============================================================================ -# Cleanup -# ============================================================================ - -@test "compat-old-ctrl: cleanup resources" { - jmp admin delete client --namespace "${JS_NAMESPACE}" compat-client --delete 2>/dev/null || true - jmp admin delete exporter --namespace "${JS_NAMESPACE}" compat-exporter --delete 2>/dev/null || true - jmp admin delete client --namespace "${JS_NAMESPACE}" compat-client-wait --delete 2>/dev/null || true - jmp admin delete exporter --namespace "${JS_NAMESPACE}" compat-exporter-wait --delete 2>/dev/null || true -} From 6f6e300d2abba09fb5afbec5edbd929aa66863be Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Sat, 11 Apr 2026 09:34:41 +0200 Subject: [PATCH 15/22] fix: address CodeRabbit review findings - Create Jumpstarter CR after applying operator installer in compat setup, since the released installer only contains CRDs and operator deployment, not the CR instance - Align Makefile install/uninstall/build-installer/undeploy targets with the manifests output path (deploy/operator/config/ instead of config/) - Surface non-zero kubectl exit codes as errors in check_jumpstarter_installation instead of silently reporting installed=False - Add test for non-zero exit code path in check_jumpstarter_installation Co-Authored-By: Claude Opus 4.6 --- controller/Makefile | 10 +-- e2e/compat/setup.sh | 64 ++++++++++++++++++- .../jumpstarter_kubernetes/cluster/kubectl.py | 43 +++++++------ .../cluster/kubectl_test.py | 12 ++++ 4 files changed, 102 insertions(+), 27 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index 33934b3f8..45a15971b 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -158,8 +158,8 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build config/default > dist/install.yaml + cd deploy/operator/config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build deploy/operator/config/default > dist/install.yaml ##@ Deployment @@ -169,11 +169,11 @@ endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + $(KUSTOMIZE) build deploy/operator/config/crd | $(KUBECTL) apply -f - .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + $(KUSTOMIZE) build deploy/operator/config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - .PHONY: deploy deploy: cluster grpcurl ## Deploy controller using the operator. Set SKIP_BUILD=1 to skip image builds. Set CLUSTER_TYPE=k3s for k3s. @@ -205,7 +205,7 @@ deploy-exporters: .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + $(KUSTOMIZE) build deploy/operator/config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - ##@ Dependencies diff --git a/e2e/compat/setup.sh b/e2e/compat/setup.sh index f47dbed47..1c049f2bb 100755 --- a/e2e/compat/setup.sh +++ b/e2e/compat/setup.sh @@ -108,13 +108,73 @@ deploy_old_controller() { log_info "Installing old controller via operator (version: ${COMPAT_CONTROLLER_TAG})..." kubectl apply -f "${INSTALLER_URL}" - kubectl config set-context --current --namespace=jumpstarter-lab + log_info "Waiting for operator to be ready..." + kubectl wait --namespace jumpstarter-operator-system \ + --for=condition=available deployment/jumpstarter-operator-controller-manager \ + --timeout=120s + + kubectl create namespace "${JS_NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f - + + log_info "Creating Jumpstarter CR..." + kubectl apply -f - < /dev/null 2>&1; do + sleep 2 + retries=$((retries - 1)) + if [ ${retries} -eq 0 ]; then + log_error "Controller deployment not created after 180s" + exit 1 + fi + done + kubectl wait --namespace "${JS_NAMESPACE}" \ + --for=condition=available deployment/jumpstarter-controller \ + --timeout=180s # Wait for gRPC endpoints local GRPCURL="${REPO_ROOT}/controller/bin/grpcurl" log_info "Waiting for gRPC endpoints..." for ep in ${GRPC_ENDPOINT} ${GRPC_ROUTER_ENDPOINT}; do - local retries=60 + retries=60 log_info " Checking ${ep}..." while ! ${GRPCURL} -insecure "${ep}" list > /dev/null 2>&1; do sleep 2 diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py index 51157bae6..3812b5b8d 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py @@ -90,26 +90,29 @@ async def check_jumpstarter_installation( try: crd_cmd = [kubectl, "--context", context, "get", "crd", "-o", "json"] - returncode, stdout, _ = await run_command(crd_cmd) - - if returncode == 0: - json_start = stdout.find("{") - if json_start >= 0: - json_output = stdout[json_start:] - crds = json.loads(json_output) - else: - crds = json.loads(stdout) - jumpstarter_crds = [] - for item in crds.get("items", []): - name = item.get("metadata", {}).get("name", "") - if "jumpstarter.dev" in name: - jumpstarter_crds.append(name) - - if jumpstarter_crds: - result_data["has_crds"] = True - result_data["installed"] = True - result_data["namespace"] = namespace or "unknown" - result_data["status"] = "installed" + returncode, stdout, stderr = await run_command(crd_cmd) + + if returncode != 0: + result_data["error"] = f"Command failed: {stderr or stdout}" + return V1Alpha1JumpstarterInstance(**result_data) + + json_start = stdout.find("{") + if json_start >= 0: + json_output = stdout[json_start:] + crds = json.loads(json_output) + else: + crds = json.loads(stdout) + jumpstarter_crds = [] + for item in crds.get("items", []): + name = item.get("metadata", {}).get("name", "") + if "jumpstarter.dev" in name: + jumpstarter_crds.append(name) + + if jumpstarter_crds: + result_data["has_crds"] = True + result_data["installed"] = True + result_data["namespace"] = namespace or "unknown" + result_data["status"] = "installed" except json.JSONDecodeError as e: result_data["error"] = f"Failed to parse output: {e}" diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py index 439528c46..6fccde289 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py @@ -227,6 +227,18 @@ async def test_check_jumpstarter_installation_command_failure(self, mock_run_com assert result.error is not None assert "kubectl not found" in result.error + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_check_jumpstarter_installation_nonzero_exit(self, mock_run_command): + mock_run_command.return_value = (1, "", "forbidden") + + result = await check_jumpstarter_installation("test-context") + + assert result.installed is False + assert result.has_crds is False + assert result.error is not None + assert "forbidden" in result.error + @pytest.mark.asyncio @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") async def test_check_jumpstarter_installation_custom_namespace(self, mock_run_command): From 699edc2def10f64f6f8778ed9fb325a99e510434 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Sat, 11 Apr 2026 09:48:39 +0200 Subject: [PATCH 16/22] fix: fail fast when baseDomain is empty in e2e setup If the Jumpstarter CR is missing or baseDomain is not set, exit immediately instead of exporting invalid endpoints like grpc.:8082. Co-Authored-By: Claude Opus 4.6 --- e2e/setup-e2e.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/e2e/setup-e2e.sh b/e2e/setup-e2e.sh index 8df7e5ad6..2e6b8fc08 100755 --- a/e2e/setup-e2e.sh +++ b/e2e/setup-e2e.sh @@ -284,6 +284,10 @@ setup_test_environment() { # failures propagate under set -e (local VAR=$(...) masks exit codes). local BASEDOMAIN BASEDOMAIN=$(kubectl get jumpstarter -n "${JS_NAMESPACE}" jumpstarter -o jsonpath='{.spec.baseDomain}') + if [ -z "${BASEDOMAIN}" ]; then + log_error "Failed to get baseDomain from Jumpstarter CR in namespace ${JS_NAMESPACE}" + exit 1 + fi export ENDPOINT="grpc.${BASEDOMAIN}:8082" export LOGIN_ENDPOINT="login.${BASEDOMAIN}:8086" log_info "Controller endpoint: $ENDPOINT" From fc1ff1f19b75f133c69adc68b6f8b21007c4f69d Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Sat, 11 Apr 2026 09:54:38 +0200 Subject: [PATCH 17/22] fix: load operator image in e2e-compat-old-client CI job The old-client compat test deploys the new controller via deploy_new_controller(), which needs the operator image loaded into kind. Download and load the pre-built operator image and manifest, matching how the main e2e-tests job handles this. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/e2e.yaml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 4fcf0b93b..34a288620 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -262,7 +262,7 @@ jobs: CI: true e2e-compat-old-client: - needs: [changes, build-controller-image, build-python-wheels] + needs: [changes, build-controller-image, build-operator-image, build-python-wheels] if: needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-24.04 timeout-minutes: 60 @@ -284,14 +284,24 @@ jobs: name: controller-image-amd64 path: /tmp/artifacts + - name: Download operator image + uses: actions/download-artifact@v4 + with: + name: operator-image-amd64 + path: /tmp/artifacts + - name: Download python wheels uses: actions/download-artifact@v4 with: name: python-wheels path: /tmp/python-wheels - - name: Load controller image - run: docker load < /tmp/artifacts/controller-image.tar + - name: Load container images and operator manifest + run: | + docker load < /tmp/artifacts/controller-image.tar + docker load < /tmp/artifacts/operator-image.tar + mkdir -p controller/deploy/operator/dist + cp /tmp/artifacts/operator-install.yaml controller/deploy/operator/dist/install.yaml - name: Setup compat environment (old client v0.8.1) run: make e2e-compat-setup COMPAT_SCENARIO=old-client @@ -300,6 +310,7 @@ jobs: COMPAT_CLIENT_VERSION: "0.8.1" SKIP_BUILD: "true" PREBUILT_WHEELS_DIR: /tmp/python-wheels + OPERATOR_IMG: quay.io/jumpstarter-dev/jumpstarter-operator:latest - name: Run compat tests (new controller + old client/exporter) run: make e2e-compat-run COMPAT_TEST=old-client From d70677afd24ffdc1a96f5efc7796f03d32474b42 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Sat, 11 Apr 2026 10:05:10 +0200 Subject: [PATCH 18/22] fix: use client version 0.7.4 for old-client compat test The jumpstarter base package was never published at 0.8.1 on PyPI, making the entire 0.8.1 client stack uninstallable. Fall back to 0.7.4 which is the latest version where all packages are available. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/e2e.yaml | 4 ++-- Makefile | 2 +- e2e/compat/setup.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 34a288620..bc72e2a59 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -303,11 +303,11 @@ jobs: mkdir -p controller/deploy/operator/dist cp /tmp/artifacts/operator-install.yaml controller/deploy/operator/dist/install.yaml - - name: Setup compat environment (old client v0.8.1) + - name: Setup compat environment (old client v0.7.4) run: make e2e-compat-setup COMPAT_SCENARIO=old-client env: CI: true - COMPAT_CLIENT_VERSION: "0.8.1" + COMPAT_CLIENT_VERSION: "0.7.4" SKIP_BUILD: "true" PREBUILT_WHEELS_DIR: /tmp/python-wheels OPERATOR_IMG: quay.io/jumpstarter-dev/jumpstarter-operator:latest diff --git a/Makefile b/Makefile index 2fef4bb70..b8ac36af5 100644 --- a/Makefile +++ b/Makefile @@ -167,7 +167,7 @@ test-e2e: e2e-run COMPAT_SCENARIO ?= old-controller COMPAT_TEST ?= old-controller COMPAT_CONTROLLER_TAG ?= v0.8.1 -COMPAT_CLIENT_VERSION ?= 0.8.1 +COMPAT_CLIENT_VERSION ?= 0.7.4 .PHONY: e2e-compat-setup e2e-compat-setup: diff --git a/e2e/compat/setup.sh b/e2e/compat/setup.sh index 1c049f2bb..a5c74e948 100755 --- a/e2e/compat/setup.sh +++ b/e2e/compat/setup.sh @@ -27,7 +27,7 @@ export JS_NAMESPACE="${JS_NAMESPACE:-jumpstarter-lab}" # Scenario configuration COMPAT_SCENARIO="${COMPAT_SCENARIO:-old-controller}" COMPAT_CONTROLLER_TAG="${COMPAT_CONTROLLER_TAG:-v0.8.1}" -COMPAT_CLIENT_VERSION="${COMPAT_CLIENT_VERSION:-0.8.1}" +COMPAT_CLIENT_VERSION="${COMPAT_CLIENT_VERSION:-0.7.4}" # Color output RED='\033[0;31m' From a72a0d777ae021c3e743b7f27a8cfceb87fc3e66 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Mon, 13 Apr 2026 18:53:17 +0200 Subject: [PATCH 19/22] fix: use correct kustomize image name and require CR instances for install check The build-installer target referenced 'controller' as the image name but the kustomization.yaml uses 'quay.io/jumpstarter-dev/jumpstarter-operator'. Also, check_jumpstarter_installation now queries for actual Jumpstarter CR instances instead of treating CRD presence alone as proof of installation. Co-Authored-By: Claude Opus 4.6 --- controller/Makefile | 2 +- .../jumpstarter_kubernetes/cluster/kubectl.py | 29 +++++++++++++-- .../cluster/kubectl_test.py | 35 ++++++++++++++++--- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/controller/Makefile b/controller/Makefile index 45a15971b..7382e78f4 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -158,7 +158,7 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist - cd deploy/operator/config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + cd deploy/operator/config/manager && $(KUSTOMIZE) edit set image quay.io/jumpstarter-dev/jumpstarter-operator=${IMG} $(KUSTOMIZE) build deploy/operator/config/default > dist/install.yaml ##@ Deployment diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py index 3812b5b8d..cdf4b9d94 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py @@ -72,6 +72,27 @@ async def get_kubectl_contexts(kubectl: str = "kubectl") -> List[Dict[str, str]] raise KubeconfigError(f"Error listing kubectl contexts: {e}") from e +async def _check_cr_instances( + kubectl: str, context: str, namespace: Optional[str] +) -> dict: + """Query for Jumpstarter CR instances to confirm full installation.""" + cr_resource = "jumpstarters.operator.jumpstarter.dev" + try: + cr_cmd = [kubectl, "--context", context, "get", cr_resource, "-A", "-o", "json"] + cr_returncode, cr_stdout, _ = await run_command(cr_cmd) + if cr_returncode == 0: + cr_data = json.loads(cr_stdout) + if cr_data.get("items"): + return { + "installed": True, + "namespace": namespace or "unknown", + "status": "installed", + } + except (json.JSONDecodeError, RuntimeError): + pass + return {} + + async def check_jumpstarter_installation( context: str, namespace: Optional[str] = None, kubectl: str = "kubectl" ) -> V1Alpha1JumpstarterInstance: @@ -110,9 +131,11 @@ async def check_jumpstarter_installation( if jumpstarter_crds: result_data["has_crds"] = True - result_data["installed"] = True - result_data["namespace"] = namespace or "unknown" - result_data["status"] = "installed" + + if "jumpstarters.operator.jumpstarter.dev" in jumpstarter_crds: + result_data.update( + await _check_cr_instances(kubectl, context, namespace) + ) except json.JSONDecodeError as e: result_data["error"] = f"Failed to parse output: {e}" diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py index 6fccde289..d36ae6045 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py @@ -194,15 +194,34 @@ class TestCheckJumpstarterInstallation: @pytest.mark.asyncio @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") - async def test_check_jumpstarter_installation_crds_found(self, mock_run_command): + async def test_check_jumpstarter_installation_crds_only(self, mock_run_command): crds_response = {"items": [{"metadata": {"name": "exporters.jumpstarter.dev"}}]} mock_run_command.return_value = (0, json.dumps(crds_response), "") result = await check_jumpstarter_installation("test-context") - assert result.installed is True assert result.has_crds is True + assert result.installed is False + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_check_jumpstarter_installation_with_cr_instances(self, mock_run_command): + crds_response = {"items": [ + {"metadata": {"name": "exporters.jumpstarter.dev"}}, + {"metadata": {"name": "jumpstarters.operator.jumpstarter.dev"}}, + ]} + cr_response = {"items": [{"metadata": {"name": "jumpstarter", "namespace": "jumpstarter"}}]} + + mock_run_command.side_effect = [ + (0, json.dumps(crds_response), ""), + (0, json.dumps(cr_response), ""), + ] + + result = await check_jumpstarter_installation("test-context") + + assert result.has_crds is True + assert result.installed is True assert result.status == "installed" @pytest.mark.asyncio @@ -242,8 +261,16 @@ async def test_check_jumpstarter_installation_nonzero_exit(self, mock_run_comman @pytest.mark.asyncio @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") async def test_check_jumpstarter_installation_custom_namespace(self, mock_run_command): - crds_response = {"items": [{"metadata": {"name": "exporters.jumpstarter.dev"}}]} - mock_run_command.return_value = (0, json.dumps(crds_response), "") + crds_response = {"items": [ + {"metadata": {"name": "exporters.jumpstarter.dev"}}, + {"metadata": {"name": "jumpstarters.operator.jumpstarter.dev"}}, + ]} + cr_response = {"items": [{"metadata": {"name": "jumpstarter", "namespace": "custom-ns"}}]} + + mock_run_command.side_effect = [ + (0, json.dumps(crds_response), ""), + (0, json.dumps(cr_response), ""), + ] result = await check_jumpstarter_installation("test-context", namespace="custom-ns") From 3bd8ed35698c765c8b5940f81d64ed1d6df253d2 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 16 Apr 2026 15:02:55 +0200 Subject: [PATCH 20/22] test: add coverage for helm-removal changes in kubectl, operations, and get Cover the changed lines flagged by diff-cover after helm chart removal: - kubectl.py: _check_cr_instances function, JSON parsing paths, cluster info context-not-found and version-parse-failure branches - operations.py: force_recreate_cluster warning messages and install_jumpstarter_operator call path - get.py: cluster list command without name argument Generated-By: Forge/20260416_144215_369510_31d1ed1b Co-Authored-By: Claude Opus 4.6 --- .../jumpstarter_cli_admin/get_test.py | 38 ++++ .../cluster/kubectl_test.py | 201 +++++++++++++++++- .../cluster/operations_test.py | 66 ++++++ 3 files changed, 303 insertions(+), 2 deletions(-) diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py index a79ff7d0f..d2caade86 100644 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py +++ b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py @@ -1370,3 +1370,41 @@ def test_get_clusters_error(list_clusters_mock: AsyncMock): result = runner.invoke(get, ["clusters"]) assert result.exit_code == 1 assert "error" in result.output.lower() + + +@patch("jumpstarter_cli_admin.get.list_clusters") +def test_get_cluster_without_name_lists_all(list_clusters_mock: AsyncMock): + from jumpstarter_kubernetes import V1Alpha1ClusterInfo, V1Alpha1ClusterList, V1Alpha1JumpstarterInstance + + runner = CliRunner() + cluster_list = V1Alpha1ClusterList( + items=[ + V1Alpha1ClusterInfo( + name="kind-test", + cluster="kind-kind-test", + server="https://127.0.0.1:6443", + user="kind-kind-test", + namespace="default", + is_current=True, + type="kind", + accessible=True, + version="1.28.0", + jumpstarter=V1Alpha1JumpstarterInstance(installed=False), + ), + ] + ) + list_clusters_mock.return_value = cluster_list + result = runner.invoke(get, ["cluster"]) + assert result.exit_code == 0 + assert "kind-test" in result.output + + +@patch("jumpstarter_cli_admin.get.list_clusters") +def test_get_clusters_command_calls_list_clusters(list_clusters_mock: AsyncMock): + from jumpstarter_kubernetes import V1Alpha1ClusterList + + runner = CliRunner() + list_clusters_mock.return_value = V1Alpha1ClusterList(items=[]) + result = runner.invoke(get, ["clusters"]) + assert result.exit_code == 0 + list_clusters_mock.assert_called_once() diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py index d36ae6045..d76811e78 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py @@ -6,6 +6,7 @@ import pytest from jumpstarter_kubernetes.cluster.kubectl import ( + _check_cr_instances, check_jumpstarter_installation, check_kubernetes_access, get_cluster_info, @@ -189,6 +190,67 @@ async def test_get_kubectl_contexts_custom_kubectl(self, mock_run_command): mock_run_command.assert_called_once_with(["custom-kubectl", "config", "view", "-o", "json"]) +class TestCheckCrInstances: + """Test CR instance detection for Jumpstarter installation.""" + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_cr_instances_found_with_namespace(self, mock_run_command): + cr_response = {"items": [{"metadata": {"name": "jumpstarter", "namespace": "custom-ns"}}]} + mock_run_command.return_value = (0, json.dumps(cr_response), "") + + result = await _check_cr_instances("kubectl", "test-context", "custom-ns") + + assert result == {"installed": True, "namespace": "custom-ns", "status": "installed"} + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_cr_instances_found_without_namespace(self, mock_run_command): + cr_response = {"items": [{"metadata": {"name": "jumpstarter"}}]} + mock_run_command.return_value = (0, json.dumps(cr_response), "") + + result = await _check_cr_instances("kubectl", "test-context", None) + + assert result == {"installed": True, "namespace": "unknown", "status": "installed"} + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_cr_instances_empty_items(self, mock_run_command): + cr_response = {"items": []} + mock_run_command.return_value = (0, json.dumps(cr_response), "") + + result = await _check_cr_instances("kubectl", "test-context", None) + + assert result == {} + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_cr_instances_nonzero_return_code(self, mock_run_command): + mock_run_command.return_value = (1, "", "forbidden") + + result = await _check_cr_instances("kubectl", "test-context", None) + + assert result == {} + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_cr_instances_json_decode_error(self, mock_run_command): + mock_run_command.return_value = (0, "not valid json", "") + + result = await _check_cr_instances("kubectl", "test-context", None) + + assert result == {} + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_cr_instances_runtime_error(self, mock_run_command): + mock_run_command.side_effect = RuntimeError("kubectl not found") + + result = await _check_cr_instances("kubectl", "test-context", None) + + assert result == {} + + class TestCheckJumpstarterInstallation: """Test Jumpstarter installation checking via CRD detection.""" @@ -277,6 +339,56 @@ async def test_check_jumpstarter_installation_custom_namespace(self, mock_run_co assert result.installed is True assert result.namespace == "custom-ns" + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_check_jumpstarter_installation_json_decode_error(self, mock_run_command): + mock_run_command.return_value = (0, "not valid json at all", "") + + result = await check_jumpstarter_installation("test-context") + + assert result.installed is False + assert result.error is not None + assert "Failed to parse output" in result.error + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_check_jumpstarter_installation_stdout_without_json_prefix(self, mock_run_command): + crds_json = json.dumps({"items": [{"metadata": {"name": "exporters.jumpstarter.dev"}}]}) + mock_run_command.return_value = (0, crds_json, "") + + result = await check_jumpstarter_installation("test-context") + + assert result.has_crds is True + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_check_jumpstarter_installation_stdout_with_warning_prefix(self, mock_run_command): + crds_json = json.dumps({"items": [{"metadata": {"name": "exporters.jumpstarter.dev"}}]}) + stdout_with_warning = f"Warning: some kubectl warning\n{crds_json}" + mock_run_command.return_value = (0, stdout_with_warning, "") + + result = await check_jumpstarter_installation("test-context") + + assert result.has_crds is True + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_check_jumpstarter_installation_cr_check_empty_items(self, mock_run_command): + crds_response = {"items": [ + {"metadata": {"name": "jumpstarters.operator.jumpstarter.dev"}}, + ]} + cr_response = {"items": []} + + mock_run_command.side_effect = [ + (0, json.dumps(crds_response), ""), + (0, json.dumps(cr_response), ""), + ] + + result = await check_jumpstarter_installation("test-context") + + assert result.has_crds is True + assert result.installed is False + class TestGetClusterInfo: """Test cluster info retrieval.""" @@ -327,16 +439,75 @@ async def test_get_cluster_info_inaccessible(self, mock_get_contexts): @pytest.mark.asyncio @patch("jumpstarter_kubernetes.cluster.kubectl.get_kubectl_contexts") async def test_get_cluster_info_invalid_json(self, mock_get_contexts): - # Mock get_kubectl_contexts to fail with JSON parse error error_msg = "Failed to parse kubectl config: Expecting value: line 1 column 1 (char 0)" mock_get_contexts.side_effect = JumpstarterKubernetesError(error_msg) result = await get_cluster_info("test-context") - assert result.accessible is False # Function failed + assert result.accessible is False assert "Failed to get cluster info" in result.error assert "Failed to parse kubectl config" in result.error + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.get_kubectl_contexts") + async def test_get_cluster_info_context_not_found(self, mock_get_contexts): + mock_get_contexts.return_value = [ + {"name": "other-context", "cluster": "other", "server": "https://other", "user": "u", "namespace": "default", "current": False} + ] + + result = await get_cluster_info("missing-context") + + assert result.name == "missing-context" + assert result.accessible is False + assert "not found" in result.error + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.get_kubectl_contexts") + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + @patch("jumpstarter_kubernetes.cluster.kubectl.check_jumpstarter_installation") + async def test_get_cluster_info_inaccessible_cluster(self, mock_check_jumpstarter, mock_run_command, mock_get_contexts): + mock_get_contexts.return_value = [ + {"name": "test-context", "cluster": "test-cluster", "server": "https://test.example.com", "user": "test-user", "namespace": "default", "current": False} + ] + mock_run_command.return_value = (1, "", "connection refused") + + result = await get_cluster_info("test-context") + + assert result.accessible is False + assert result.jumpstarter.installed is False + assert result.jumpstarter.error == "Cluster not accessible" + mock_check_jumpstarter.assert_not_called() + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.get_kubectl_contexts") + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + @patch("jumpstarter_kubernetes.cluster.kubectl.check_jumpstarter_installation") + async def test_get_cluster_info_version_parse_failure(self, mock_check_jumpstarter, mock_run_command, mock_get_contexts): + mock_get_contexts.return_value = [ + {"name": "test-context", "cluster": "test-cluster", "server": "https://test.example.com", "user": "test-user", "namespace": "default", "current": False} + ] + mock_run_command.return_value = (0, "not json", "") + mock_check_jumpstarter.return_value = V1Alpha1JumpstarterInstance(installed=False) + + result = await get_cluster_info("test-context") + + assert result.accessible is True + assert result.version == "unknown" + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.get_kubectl_contexts") + @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") + async def test_get_cluster_info_version_command_runtime_error(self, mock_run_command, mock_get_contexts): + mock_get_contexts.return_value = [ + {"name": "test-context", "cluster": "test-cluster", "server": "https://test.example.com", "user": "test-user", "namespace": "default", "current": False} + ] + mock_run_command.side_effect = RuntimeError("command failed") + + result = await get_cluster_info("test-context") + + assert result.accessible is False + assert result.version is None + class TestListClusters: """Test cluster listing functionality.""" @@ -401,3 +572,29 @@ async def test_list_clusters_custom_parameters(self, mock_get_contexts): await list_clusters(kubectl="custom-kubectl", minikube="custom-minikube") mock_get_contexts.assert_called_once_with("custom-kubectl") + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.kubectl.get_kubectl_contexts") + @patch("jumpstarter_kubernetes.cluster.kubectl.get_cluster_info") + async def test_list_clusters_with_type_filter(self, mock_get_cluster_info, mock_get_contexts): + mock_get_contexts.return_value = [ + {"name": "kind-ctx", "cluster": "kind-cluster", "server": "https://kind", "user": "u", "current": True}, + {"name": "remote-ctx", "cluster": "remote-cluster", "server": "https://remote", "user": "u", "current": False}, + ] + + kind_info = V1Alpha1ClusterInfo( + name="kind-ctx", cluster="kind-cluster", server="https://kind", user="u", + namespace="default", is_current=True, type="kind", accessible=True, + jumpstarter=V1Alpha1JumpstarterInstance(installed=False), + ) + remote_info = V1Alpha1ClusterInfo( + name="remote-ctx", cluster="remote-cluster", server="https://remote", user="u", + namespace="default", is_current=False, type="remote", accessible=False, + jumpstarter=V1Alpha1JumpstarterInstance(installed=False), + ) + mock_get_cluster_info.side_effect = [kind_info, remote_info] + + result = await list_clusters(cluster_type_filter="kind") + + assert len(result.items) == 1 + assert result.items[0].name == "kind-ctx" diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py index 5e966fbdb..de9d68a53 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py @@ -162,3 +162,69 @@ async def test_create_cluster_and_install_unsupported_cluster_type(self): await create_cluster_and_install("remote", False, "test-cluster", "", "", "kind", "minikube") assert "Unsupported cluster_type: remote" in str(exc_info.value) + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.operations.create_kind_cluster_with_options") + @patch("jumpstarter_kubernetes.cluster.operations.configure_endpoints") + @patch("jumpstarter_kubernetes.cluster.operations.install_jumpstarter_operator") + async def test_create_cluster_and_install_force_recreate_confirmed( + self, mock_install, mock_configure, mock_create + ): + mock_configure.return_value = ("192.168.1.100", "test.domain", "grpc.test:8082", "router.test:8083") + + from jumpstarter_kubernetes.callbacks import SilentCallback + + callback = SilentCallback() + + await create_cluster_and_install( + "kind", True, "test-cluster", "", "", "kind", "minikube", + version="1.0.0", callback=callback, + ) + + mock_create.assert_called_once() + mock_install.assert_called_once() + + @pytest.mark.asyncio + async def test_create_cluster_and_install_force_recreate_cancelled(self): + from jumpstarter_kubernetes.callbacks import OutputCallback + from jumpstarter_kubernetes.exceptions import ClusterOperationError + + class RejectingCallback: + def progress(self, message): pass + def success(self, message): pass + def warning(self, message): pass + def error(self, message): pass + def confirm(self, prompt): return False + + with pytest.raises(ClusterOperationError, match="User cancelled"): + await create_cluster_and_install( + "kind", True, "test-cluster", "", "", "kind", "minikube", + version="1.0.0", callback=RejectingCallback(), + ) + + @pytest.mark.asyncio + @patch("jumpstarter_kubernetes.cluster.operations.create_kind_cluster_with_options") + @patch("jumpstarter_kubernetes.cluster.operations.configure_endpoints") + @patch("jumpstarter_kubernetes.cluster.operations.install_jumpstarter_operator") + async def test_create_cluster_and_install_calls_operator_installer( + self, mock_install, mock_configure, mock_create + ): + mock_configure.return_value = ("192.168.1.100", "test.domain", "grpc.test:8082", "router.test:8083") + + await create_cluster_and_install( + "kind", False, "test-cluster", "", "", "kind", "minikube", + version="1.0.0", operator_installer="/path/to/installer", + ) + + mock_install.assert_called_once_with( + version="1.0.0", + namespace="jumpstarter-lab", + basedomain="test.domain", + grpc_endpoint="grpc.test:8082", + router_endpoint="router.test:8083", + mode="nodeport", + kubeconfig=None, + context=None, + operator_installer="/path/to/installer", + callback=ANY, + ) From 7ef1b719a38da9ea4ab52d3f0ce4484b0b5c3560 Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Thu, 16 Apr 2026 16:21:13 +0200 Subject: [PATCH 21/22] fix: resolve lint violations and improve test coverage for helm-removal changes Break long lines in kubectl_test.py to stay within 120 character limit and remove unused OutputCallback import in operations_test.py. Generated-By: Forge/20260416_161341_490145_52646d64 Co-Authored-By: Claude Opus 4.6 --- .../cluster/kubectl_test.py | 64 ++++++++++++++++--- .../cluster/operations_test.py | 1 - 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py index d76811e78..e66603aea 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl_test.py @@ -452,7 +452,14 @@ async def test_get_cluster_info_invalid_json(self, mock_get_contexts): @patch("jumpstarter_kubernetes.cluster.kubectl.get_kubectl_contexts") async def test_get_cluster_info_context_not_found(self, mock_get_contexts): mock_get_contexts.return_value = [ - {"name": "other-context", "cluster": "other", "server": "https://other", "user": "u", "namespace": "default", "current": False} + { + "name": "other-context", + "cluster": "other", + "server": "https://other", + "user": "u", + "namespace": "default", + "current": False, + } ] result = await get_cluster_info("missing-context") @@ -465,9 +472,18 @@ async def test_get_cluster_info_context_not_found(self, mock_get_contexts): @patch("jumpstarter_kubernetes.cluster.kubectl.get_kubectl_contexts") @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") @patch("jumpstarter_kubernetes.cluster.kubectl.check_jumpstarter_installation") - async def test_get_cluster_info_inaccessible_cluster(self, mock_check_jumpstarter, mock_run_command, mock_get_contexts): + async def test_get_cluster_info_inaccessible_cluster( + self, mock_check_jumpstarter, mock_run_command, mock_get_contexts + ): mock_get_contexts.return_value = [ - {"name": "test-context", "cluster": "test-cluster", "server": "https://test.example.com", "user": "test-user", "namespace": "default", "current": False} + { + "name": "test-context", + "cluster": "test-cluster", + "server": "https://test.example.com", + "user": "test-user", + "namespace": "default", + "current": False, + } ] mock_run_command.return_value = (1, "", "connection refused") @@ -482,9 +498,18 @@ async def test_get_cluster_info_inaccessible_cluster(self, mock_check_jumpstarte @patch("jumpstarter_kubernetes.cluster.kubectl.get_kubectl_contexts") @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") @patch("jumpstarter_kubernetes.cluster.kubectl.check_jumpstarter_installation") - async def test_get_cluster_info_version_parse_failure(self, mock_check_jumpstarter, mock_run_command, mock_get_contexts): + async def test_get_cluster_info_version_parse_failure( + self, mock_check_jumpstarter, mock_run_command, mock_get_contexts + ): mock_get_contexts.return_value = [ - {"name": "test-context", "cluster": "test-cluster", "server": "https://test.example.com", "user": "test-user", "namespace": "default", "current": False} + { + "name": "test-context", + "cluster": "test-cluster", + "server": "https://test.example.com", + "user": "test-user", + "namespace": "default", + "current": False, + } ] mock_run_command.return_value = (0, "not json", "") mock_check_jumpstarter.return_value = V1Alpha1JumpstarterInstance(installed=False) @@ -497,9 +522,18 @@ async def test_get_cluster_info_version_parse_failure(self, mock_check_jumpstart @pytest.mark.asyncio @patch("jumpstarter_kubernetes.cluster.kubectl.get_kubectl_contexts") @patch("jumpstarter_kubernetes.cluster.kubectl.run_command") - async def test_get_cluster_info_version_command_runtime_error(self, mock_run_command, mock_get_contexts): + async def test_get_cluster_info_version_command_runtime_error( + self, mock_run_command, mock_get_contexts + ): mock_get_contexts.return_value = [ - {"name": "test-context", "cluster": "test-cluster", "server": "https://test.example.com", "user": "test-user", "namespace": "default", "current": False} + { + "name": "test-context", + "cluster": "test-cluster", + "server": "https://test.example.com", + "user": "test-user", + "namespace": "default", + "current": False, + } ] mock_run_command.side_effect = RuntimeError("command failed") @@ -578,8 +612,20 @@ async def test_list_clusters_custom_parameters(self, mock_get_contexts): @patch("jumpstarter_kubernetes.cluster.kubectl.get_cluster_info") async def test_list_clusters_with_type_filter(self, mock_get_cluster_info, mock_get_contexts): mock_get_contexts.return_value = [ - {"name": "kind-ctx", "cluster": "kind-cluster", "server": "https://kind", "user": "u", "current": True}, - {"name": "remote-ctx", "cluster": "remote-cluster", "server": "https://remote", "user": "u", "current": False}, + { + "name": "kind-ctx", + "cluster": "kind-cluster", + "server": "https://kind", + "user": "u", + "current": True, + }, + { + "name": "remote-ctx", + "cluster": "remote-cluster", + "server": "https://remote", + "user": "u", + "current": False, + }, ] kind_info = V1Alpha1ClusterInfo( diff --git a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py index de9d68a53..9edbd7d86 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/operations_test.py @@ -186,7 +186,6 @@ async def test_create_cluster_and_install_force_recreate_confirmed( @pytest.mark.asyncio async def test_create_cluster_and_install_force_recreate_cancelled(self): - from jumpstarter_kubernetes.callbacks import OutputCallback from jumpstarter_kubernetes.exceptions import ClusterOperationError class RejectingCallback: From 2e6d0ea1aadfee8aecf4695f9000fed69fa063ad Mon Sep 17 00:00:00 2001 From: Paul Wallrabe Date: Fri, 17 Apr 2026 16:10:35 +0200 Subject: [PATCH 22/22] fix: add coverage source config for proper diff-cover path resolution Without source = ["."], coverage.xml in isolated mode generates relative paths without a source root, causing diff-cover to fail matching coverage data against git diff paths. Co-Authored-By: Claude Opus 4.6 --- python/packages/jumpstarter-cli-admin/pyproject.toml | 3 +++ python/packages/jumpstarter-cli-common/pyproject.toml | 3 +++ python/packages/jumpstarter-kubernetes/pyproject.toml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/python/packages/jumpstarter-cli-admin/pyproject.toml b/python/packages/jumpstarter-cli-admin/pyproject.toml index 886d97675..1e2f9f20f 100644 --- a/python/packages/jumpstarter-cli-admin/pyproject.toml +++ b/python/packages/jumpstarter-cli-admin/pyproject.toml @@ -23,6 +23,9 @@ dev = [ [project.scripts] jmp-admin = "jumpstarter_cli_admin:admin" +[tool.coverage.run] +source = ["."] + [tool.hatch.build.targets.wheel] packages = ["jumpstarter_cli_admin"] diff --git a/python/packages/jumpstarter-cli-common/pyproject.toml b/python/packages/jumpstarter-cli-common/pyproject.toml index e892545fa..4dd42ecaf 100644 --- a/python/packages/jumpstarter-cli-common/pyproject.toml +++ b/python/packages/jumpstarter-cli-common/pyproject.toml @@ -25,6 +25,9 @@ dev = [ "pytest-cov>=5.0.0", ] +[tool.coverage.run] +source = ["."] + [tool.hatch.build.targets.wheel] packages = ["jumpstarter_cli_common"] diff --git a/python/packages/jumpstarter-kubernetes/pyproject.toml b/python/packages/jumpstarter-kubernetes/pyproject.toml index db3a20d62..a433d5932 100644 --- a/python/packages/jumpstarter-kubernetes/pyproject.toml +++ b/python/packages/jumpstarter-kubernetes/pyproject.toml @@ -24,6 +24,9 @@ dev = [ "pytest-cov>=5.0.0", ] +[tool.coverage.run] +source = ["."] + [tool.hatch.metadata.hooks.vcs.urls] Homepage = "https://jumpstarter.dev" source_archive = "https://github.com/jumpstarter-dev/repo/archive/{commit_hash}.zip"