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 6b0792f6a..5305ab0ab 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/e2e.yaml b/.github/workflows/e2e.yaml index 66dab17c7..b96e373e4 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,15 +228,13 @@ jobs: run: make e2e-run env: CI: true - METHOD: operator # ============================================================================ # 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] + needs: changes if: needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-24.04 timeout-minutes: 60 @@ -253,18 +250,11 @@ jobs: 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) + - 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 - PREBUILT_WHEELS_DIR: /tmp/python-wheels + COMPAT_CONTROLLER_TAG: v0.8.1 - name: Run compat tests (old controller + new client/exporter) run: make e2e-compat-run COMPAT_TEST=old-controller @@ -272,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 @@ -294,22 +284,33 @@ 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.7.0) + - 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.7.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 - name: Run compat tests (new controller + old client/exporter) run: make e2e-compat-run COMPAT_TEST=old-client diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ecafd8f71..ceb188f54 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/Makefile b/Makefile index 1dc17acbd..b8ac36af5 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,6 @@ # Subdirectories containing projects SUBDIRS := python protocol controller e2e -# Deployment method for e2e tests: operator (default) or helm -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) or METHOD=helm to select deployment method" - @echo " Example: make e2e-setup METHOD=helm" - @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 @@ -172,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.7.4 .PHONY: e2e-compat-setup e2e-compat-setup: diff --git a/controller/Makefile b/controller/Makefile index 08c9aa00e..7382e78f4 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -30,9 +30,6 @@ endif # tools. (i.e. podman) CONTAINER_TOOL ?= podman -# Deployment method: operator (default) or helm -METHOD ?= operator - # Cluster type: kind (default) or k3s CLUSTER_TYPE ?= kind export CLUSTER_TYPE @@ -67,17 +64,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 @@ -170,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 quay.io/jumpstarter-dev/jumpstarter-operator=${IMG} + $(KUSTOMIZE) build deploy/operator/config/default > dist/install.yaml ##@ Deployment @@ -181,32 +169,20 @@ 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 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) @@ -221,20 +197,15 @@ 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: ./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 - + $(KUSTOMIZE) build deploy/operator/config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - ##@ Dependencies 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/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/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..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., "Helm", "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/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..a5c74e948 100755 --- a/e2e/compat/setup.sh +++ b/e2e/compat/setup.sh @@ -2,13 +2,13 @@ # 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) -# COMPAT_CONTROLLER_TAG - Controller image tag for old-controller scenario (default: v0.7.1) -# 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 @@ -24,13 +24,10 @@ 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" - # 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.7.4}" # Color output RED='\033[0;31m' @@ -92,15 +89,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,26 +103,78 @@ 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 - - kubectl config set-context --current --namespace=jumpstarter-lab + # 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_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 @@ -144,20 +189,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 +236,15 @@ 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') + 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" diff --git a/e2e/setup-e2e.sh b/e2e/setup-e2e.sh index fc8bce67f..2e6b8fc08 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: operator (default) or helm -export METHOD="${METHOD:-operator}" - # Color output RED='\033[0;31m' GREEN='\033[0;32m' @@ -262,30 +259,12 @@ deploy_dex() { # Step 4: Deploy jumpstarter controller deploy_controller() { - log_info "Deploying jumpstarter controller (method: $METHOD)..." + log_info "Deploying jumpstarter 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 +279,17 @@ 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" + 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" log_info "Login endpoint: $LOGIN_ENDPOINT" @@ -340,7 +313,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" @@ -355,7 +327,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 "" 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..cdf4b9d94 100644 --- a/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py +++ b/python/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/cluster/kubectl.py @@ -72,15 +72,35 @@ 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 - context: str, namespace: Optional[str] = None, helm: str = "helm", kubectl: str = "kubectl" +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: - """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, "namespace": None, - "chart_name": None, "status": None, "has_crds": False, "error": None, @@ -90,105 +110,35 @@ 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 + crd_cmd = [kubectl, "--context", context, "get", "crd", "-o", "json"] + returncode, stdout, stderr = await run_command(crd_cmd) - break + if returncode != 0: + result_data["error"] = f"Command failed: {stderr or stdout}" + return V1Alpha1JumpstarterInstance(**result_data) - # Check for Jumpstarter CRDs as secondary verification - 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 - 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 - 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" - except RuntimeError: - pass # CRD check failed, continue with Helm results + 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 + + 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 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 +148,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 +198,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 +233,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 +242,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..d36ae6045 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,44 @@ 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 + 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.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(helm_releases), ""), # helm list success - (0, '{"items": []}', ""), # kubectl get namespaces - (0, '{"items": []}', ""), # kubectl get crds + (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.version == "1.0.0" - assert result.namespace == "jumpstarter-system" - assert result.chart_name == "jumpstarter-release" - assert result.status == "deployed" + 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 +236,46 @@ 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"}}]} + async def test_check_jumpstarter_installation_command_failure(self, mock_run_command): + mock_run_command.side_effect = RuntimeError("kubectl not found") - mock_run_command.side_effect = [ - (1, "", "helm not found"), # helm list fails - (0, json.dumps(crds_response), ""), # kubectl get crds - ] + 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 "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 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 + 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): + 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, "[]", ""), # helm list - (0, '{"items": []}', ""), # kubectl get crds + (0, json.dumps(crds_response), ""), + (0, json.dumps(cr_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 +398,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/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 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() 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()