diff --git a/.github/workflows/controller-kind.yaml b/.github/workflows/controller-kind.yaml index fa152b630..f89108081 100644 --- a/.github/workflows/controller-kind.yaml +++ b/.github/workflows/controller-kind.yaml @@ -11,6 +11,11 @@ on: jobs: deploy-kind: + strategy: + matrix: + method: + - helm + - operator runs-on: ubuntu-latest steps: - name: Checkout repository @@ -18,9 +23,11 @@ jobs: with: fetch-depth: 0 - - name: Run make deploy + - name: Run make deploy (${{ matrix.method }}) 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 d13d6472e..af05c7387 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -12,13 +12,42 @@ permissions: contents: read jobs: - e2e-tests: + changes: if: github.repository_owner == 'jumpstarter-dev' + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.filter.outputs.e2e }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: dorny/paths-filter@v3 + id: filter + with: + base: ${{ github.base_ref || github.event.merge_group.base_ref || 'main' }} + filters: | + e2e: + - 'controller/**' + - 'e2e/**' + - 'python/**' + - '.github/workflows/e2e.yaml' + - 'Makefile' + + e2e-tests: + needs: changes + if: needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch' strategy: matrix: os: - ubuntu-24.04 - ubuntu-24.04-arm + method: + - operator + - helm + exclude: + # Only run operator on ARM, skip helm + - os: ubuntu-24.04-arm + method: helm runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: @@ -37,8 +66,10 @@ jobs: run: make e2e-setup env: CI: true + METHOD: ${{ matrix.method }} - name: Run e2e tests run: make e2e-run env: CI: true + METHOD: ${{ matrix.method }} diff --git a/Makefile b/Makefile index a53d5ed3d..2b343d047 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ # 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 @@ -30,6 +33,9 @@ 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" @@ -115,14 +121,14 @@ test-controller: # Setup e2e testing environment (one-time) .PHONY: e2e-setup e2e-setup: - @echo "Setting up e2e test environment..." - @bash e2e/setup-e2e.sh + @echo "Setting up e2e test environment (method: $(METHOD))..." + @METHOD=$(METHOD) bash e2e/setup-e2e.sh # Run e2e tests .PHONY: e2e-run e2e-run: - @echo "Running e2e tests..." - @bash e2e/run-e2e.sh + @echo "Running e2e tests (method: $(METHOD))..." + @METHOD=$(METHOD) bash e2e/run-e2e.sh # Convenience alias for running e2e tests .PHONY: e2e @@ -131,7 +137,7 @@ e2e: e2e-run # Full e2e setup + run .PHONY: e2e-full e2e-full: - @bash e2e/run-e2e.sh --full + @METHOD=$(METHOD) bash e2e/run-e2e.sh --full # Clean up e2e test environment .PHONY: e2e-clean diff --git a/controller/Makefile b/controller/Makefile index 16d4e1ce3..b5248b034 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -30,6 +30,9 @@ endif # tools. (i.e. podman) CONTAINER_TOOL ?= podman +# Deployment method: operator (default) or helm +METHOD ?= operator + # Setting SHELL to bash allows bash commands to be executed by recipes. # Options are set to exit when a recipe line exits non-zero or a piped command fails. SHELL = /usr/bin/env bash -o pipefail @@ -171,15 +174,23 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - .PHONY: deploy -deploy: docker-build cluster grpcurl +deploy: docker-build cluster grpcurl ## Deploy controller using METHOD (operator or helm) +ifeq ($(METHOD),operator) + $(MAKE) build-operator + ./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: docker-build build-operator cluster grpcurl - ./hack/deploy_with_operator.sh +deploy-with-operator: + $(MAKE) deploy METHOD=operator .PHONY: deploy-operator -deploy-operator: docker-build build-operator cluster grpcurl +deploy-operator: docker-build build-operator cluster grpcurl ## Deploy only the operator (without Jumpstarter CR) NETWORKING_MODE=ingress DEPLOY_JUMPSTARTER=false ./hack/deploy_with_operator.sh .PHONY: test-operator-e2e @@ -191,7 +202,7 @@ operator-logs: .PHONY: deploy-with-operator-parallel deploy-with-operator-parallel: - make deploy-with-operator -j5 --output-sync=target + make deploy METHOD=operator -j5 --output-sync=target .PHONY: deploy-exporters deploy-exporters: diff --git a/controller/deploy/operator/api/v1alpha1/jumpstarter_types.go b/controller/deploy/operator/api/v1alpha1/jumpstarter_types.go index c2efa0706..53ec0a2e8 100644 --- a/controller/deploy/operator/api/v1alpha1/jumpstarter_types.go +++ b/controller/deploy/operator/api/v1alpha1/jumpstarter_types.go @@ -344,6 +344,19 @@ type AuthenticationConfig struct { // Enables authentication using external JWT tokens from OIDC providers. // Supports multiple JWT authenticators for different identity providers. JWT []apiserverv1beta1.JWTAuthenticator `json:"jwt,omitempty"` + + // Automatic user provisioning configuration, this is useful for creating + // users authenticated by external identity providers in Jumpstarter. + AutoProvisioning AutoProvisioningConfig `json:"autoProvisioning,omitempty"` +} + +// AutoProvisioningConfig defines auto provisioning configuration. +type AutoProvisioningConfig struct { + // Enable auto provisioning. + // When disabled, users authenticated by external identity providers will + // not be automatically created in Jumpstarter. + // +kubebuilder:default=false + Enabled bool `json:"enabled,omitempty"` } // InternalAuthConfig defines the built-in authentication configuration. diff --git a/controller/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go b/controller/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go index 17cfe820c..96fe6fa03 100644 --- a/controller/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/controller/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go @@ -39,6 +39,7 @@ func (in *AuthenticationConfig) DeepCopyInto(out *AuthenticationConfig) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + out.AutoProvisioning = in.AutoProvisioning } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationConfig. @@ -51,6 +52,21 @@ func (in *AuthenticationConfig) DeepCopy() *AuthenticationConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AutoProvisioningConfig) DeepCopyInto(out *AutoProvisioningConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoProvisioningConfig. +func (in *AutoProvisioningConfig) DeepCopy() *AutoProvisioningConfig { + if in == nil { + return nil + } + out := new(AutoProvisioningConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CertManagerConfig) DeepCopyInto(out *CertManagerConfig) { *out = *in diff --git a/controller/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml b/controller/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml index 0702ebad1..8bbb66f68 100644 --- a/controller/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml +++ b/controller/deploy/operator/bundle/manifests/jumpstarter-operator.clusterserviceversion.yaml @@ -18,7 +18,7 @@ metadata: } ] capabilities: Basic Install - createdAt: "2026-01-28T15:18:09Z" + createdAt: "2026-01-30T11:40:29Z" operators.operatorframework.io/builder: operator-sdk-v1.41.1 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 name: jumpstarter-operator.v0.8.0 diff --git a/controller/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml b/controller/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml index 8c188055c..83dab3e49 100644 --- a/controller/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml +++ b/controller/deploy/operator/bundle/manifests/operator.jumpstarter.dev_jumpstarters.yaml @@ -47,6 +47,19 @@ spec: Authentication configuration for client and exporter authentication. Supports multiple authentication methods including internal tokens, Kubernetes tokens, and JWT. properties: + autoProvisioning: + description: |- + Automatic user provisioning configuration, this is useful for creating + users authenticated by external identity providers in Jumpstarter. + properties: + enabled: + default: false + description: |- + Enable auto provisioning. + When disabled, users authenticated by external identity providers will + not be automatically created in Jumpstarter. + type: boolean + type: object internal: description: |- Internal authentication configuration. diff --git a/controller/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml b/controller/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml index f809ccbd3..ac4b99aa1 100644 --- a/controller/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml +++ b/controller/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml @@ -47,6 +47,19 @@ spec: Authentication configuration for client and exporter authentication. Supports multiple authentication methods including internal tokens, Kubernetes tokens, and JWT. properties: + autoProvisioning: + description: |- + Automatic user provisioning configuration, this is useful for creating + users authenticated by external identity providers in Jumpstarter. + properties: + enabled: + default: false + description: |- + Enable auto provisioning. + When disabled, users authenticated by external identity providers will + not be automatically created in Jumpstarter. + type: boolean + type: object internal: description: |- Internal authentication configuration. diff --git a/controller/deploy/operator/dist/install.yaml b/controller/deploy/operator/dist/install.yaml index 625b12a8a..e82ef6c15 100644 --- a/controller/deploy/operator/dist/install.yaml +++ b/controller/deploy/operator/dist/install.yaml @@ -450,6 +450,19 @@ spec: Authentication configuration for client and exporter authentication. Supports multiple authentication methods including internal tokens, Kubernetes tokens, and JWT. properties: + autoProvisioning: + description: |- + Automatic user provisioning configuration, this is useful for creating + users authenticated by external identity providers in Jumpstarter. + properties: + enabled: + default: false + description: |- + Enable auto provisioning. + When disabled, users authenticated by external identity providers will + not be automatically created in Jumpstarter. + type: boolean + type: object internal: description: |- Internal authentication configuration. diff --git a/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go b/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go index 9f3f91da3..4d406219e 100644 --- a/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go +++ b/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go @@ -980,7 +980,7 @@ func (r *JumpstarterReconciler) createConfigMap(jumpstarter *operatorv1alpha1.Ju func (r *JumpstarterReconciler) buildConfig(jumpstarter *operatorv1alpha1.Jumpstarter) config.Config { cfg := config.Config{ Provisioning: config.Provisioning{ - Enabled: false, + Enabled: jumpstarter.Spec.Authentication.AutoProvisioning.Enabled, }, Grpc: config.Grpc{ Keepalive: config.Keepalive{ diff --git a/controller/deploy/operator/test/e2e/e2e_test.go b/controller/deploy/operator/test/e2e/e2e_test.go index ef89c5a83..5aa176e23 100644 --- a/controller/deploy/operator/test/e2e/e2e_test.go +++ b/controller/deploy/operator/test/e2e/e2e_test.go @@ -680,6 +680,38 @@ provisioning: }, 1*time.Minute).Should(ContainSubstring("router.jumpstarter.127.0.0.1.nip.io:5443")) }) + It("should update provisioning config when autoProvisioning is enabled", func() { + By("updating the Jumpstarter CR to enable auto provisioning") + jumpstarter := &operatorv1alpha1.Jumpstarter{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: "jumpstarter", + Namespace: dynamicTestNamespace, + }, jumpstarter) + Expect(err).NotTo(HaveOccurred()) + + jumpstarter.Spec.Authentication.AutoProvisioning.Enabled = true + err = k8sClient.Update(ctx, jumpstarter) + Expect(err).NotTo(HaveOccurred()) + + By("verifying the ConfigMap contains provisioning.enabled: true") + Eventually(func(g Gomega) { + configmap := &corev1.ConfigMap{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: "jumpstarter-controller", + Namespace: dynamicTestNamespace, + }, configmap) + g.Expect(err).NotTo(HaveOccurred()) + + var configObj map[string]interface{} + err = yaml.Unmarshal([]byte(configmap.Data["config"]), &configObj) + g.Expect(err).NotTo(HaveOccurred()) + + provisioning, ok := configObj["provisioning"].(map[string]interface{}) + g.Expect(ok).To(BeTrue()) + g.Expect(provisioning["enabled"]).To(Equal(true)) + }, 1*time.Minute).Should(Succeed()) + }) + It("should allow access to ingress grpc endpoints", func() { // TODO: fix ingress in kind (not working for helm either) Skip("nginx ingress not working in kind") diff --git a/controller/hack/deploy_with_operator.sh b/controller/hack/deploy_with_operator.sh index 9eec08e81..224e0b246 100755 --- a/controller/hack/deploy_with_operator.sh +++ b/controller/hack/deploy_with_operator.sh @@ -113,9 +113,45 @@ else enabled: false" fi +# Build JWT authentication configuration for dex if OPERATOR_USE_DEX is set +JWT_AUTH_CONFIG="" +if [ "${OPERATOR_USE_DEX:-}" = "true" ]; then + DEX_CA_FILE="${DEX_CA_FILE:-ca.pem}" + if [ ! -f "${DEX_CA_FILE}" ]; then + echo -e "${RED}OPERATOR_USE_DEX is set but DEX_CA_FILE (${DEX_CA_FILE}) not found${NC}" + exit 1 + fi + echo -e "${GREEN}Configuring dex JWT authentication (CA from ${DEX_CA_FILE})...${NC}" + # Read the CA certificate content + DEX_CA_CONTENT=$(cat "${DEX_CA_FILE}") + JWT_AUTH_CONFIG=" + jwt: + - issuer: + url: https://dex.dex.svc.cluster.local:5556 + audiences: + - jumpstarter-cli + audienceMatchPolicy: MatchAny + certificateAuthority: | +$(echo "${DEX_CA_CONTENT}" | sed 's/^/ /') + claimMappings: + username: + claim: \"name\" + prefix: \"dex:\"" +fi + # Apply the Jumpstarter CR with the appropriate endpoint configuration # Create a temporary file which is useful for debugging -TMPFILE=$(mktemp /tmp/jumpstarter-cr.XXXXXX.yaml) +TMPFILE=$(mktemp /tmp/jumpstarter-cr-XXXXXX) +mv "${TMPFILE}" "${TMPFILE}.yaml" +TMPFILE="${TMPFILE}.yaml" +# Build the authentication section +AUTH_CONFIG=" authentication: + internal: + prefix: \"internal:\" + enabled: true + autoProvisioning: + enabled: true${JWT_AUTH_CONFIG}" + cat < "${TMPFILE}" apiVersion: operator.jumpstarter.dev/v1alpha1 kind: Jumpstarter @@ -125,10 +161,7 @@ metadata: spec: baseDomain: ${BASEDOMAIN} ${CERTMANAGER_CONFIG} - authentication: - internal: - prefix: "internal:" - enabled: true +${AUTH_CONFIG} controller: image: ${IMAGE_REPO} imagePullPolicy: IfNotPresent diff --git a/e2e/run-e2e.sh b/e2e/run-e2e.sh index 203fad286..1c2f05f3a 100755 --- a/e2e/run-e2e.sh +++ b/e2e/run-e2e.sh @@ -100,7 +100,7 @@ run_tests() { # Run bats tests log_info "Running bats tests..." - bats --show-output-of-passing-tests --verbose-run "$SCRIPT_DIR"/tests.bats + bats -x --show-output-of-passing-tests --verbose-run "$SCRIPT_DIR"/tests.bats } # Full setup and run (for CI or first-time use) diff --git a/e2e/setup-e2e.sh b/e2e/setup-e2e.sh index 96a4c7e69..6b8090ece 100755 --- a/e2e/setup-e2e.sh +++ b/e2e/setup-e2e.sh @@ -13,6 +13,9 @@ 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' @@ -261,13 +264,25 @@ deploy_dex() { # Step 3: Deploy jumpstarter controller deploy_controller() { - log_info "Deploying jumpstarter controller..." + log_info "Deploying jumpstarter controller (method: $METHOD)..." cd "$REPO_ROOT" - # Deploy with modified values using EXTRA_VALUES environment variable - log_info "Deploying controller with CA certificate..." - EXTRA_VALUES="--values $REPO_ROOT/.e2e/values.kind.yaml" make -C controller deploy + # Validate METHOD + if [ "$METHOD" != "operator" ] && [ "$METHOD" != "helm" ]; then + log_error "Unknown deployment method: $METHOD (expected 'operator' or 'helm')" + exit 1 + fi + + # Deploy with CA certificate + log_info "Deploying controller with CA certificate using $METHOD..." + if [ "$METHOD" = "operator" ]; then + # For operator: use OPERATOR_USE_DEX to inject dex config directly + OPERATOR_USE_DEX=true DEX_CA_FILE="$REPO_ROOT/ca.pem" METHOD=$METHOD make -C controller deploy + else + # For helm: use EXTRA_VALUES to pass the values file + EXTRA_VALUES="--values $REPO_ROOT/.e2e/values.kind.yaml" METHOD=$METHOD make -C controller deploy + fi log_info "✓ Controller deployed" } @@ -297,14 +312,26 @@ setup_test_environment() { cd "$REPO_ROOT" - # Get the controller endpoint - export ENDPOINT=$(helm get values jumpstarter --output json | jq -r '."jumpstarter-controller".grpc.endpoint') + # Get the controller endpoint based on deployment method + if [ "$METHOD" = "operator" ]; then + # For operator deployment, construct the endpoint from the Jumpstarter CR + # The operator uses nodeport mode by default with port 8082 + local BASEDOMAIN=$(kubectl get jumpstarter -n "${JS_NAMESPACE}" jumpstarter -o jsonpath='{.spec.baseDomain}') + export ENDPOINT="grpc.${BASEDOMAIN}:8082" + else + # For helm deployment, get the endpoint from helm values + export ENDPOINT=$(helm get values jumpstarter --output json | jq -r '."jumpstarter-controller".grpc.endpoint') + fi log_info "Controller endpoint: $ENDPOINT" - # Setup exporters directory - echo "Setting up exporters directory in /etc/jumpstarter/exporters..., will need permissions" - sudo mkdir -p /etc/jumpstarter/exporters - sudo chown "$USER" /etc/jumpstarter/exporters + # Setup exporters directory (only use sudo if needed) + if [ ! -d /etc/jumpstarter/exporters ] || [ ! -w /etc/jumpstarter/exporters ]; then + log_info "Setting up exporters directory in /etc/jumpstarter/exporters (requires sudo)..." + sudo mkdir -p /etc/jumpstarter/exporters + sudo chown "$USER" /etc/jumpstarter/exporters + else + log_info "Exporters directory already exists and is writable" + fi # Create service accounts log_info "Creating service accounts..." @@ -316,6 +343,7 @@ setup_test_environment() { echo "JS_NAMESPACE=$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" + echo "METHOD=$METHOD" >> "$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" @@ -331,6 +359,7 @@ 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/e2e/tests.bats b/e2e/tests.bats index a52f9b62b..09d3a6a2c 100644 --- a/e2e/tests.bats +++ b/e2e/tests.bats @@ -60,12 +60,23 @@ wait_for_exporter() { # After a lease operation the exporter is disconnecting from controller and reconnecting. # The disconnect can take a short while so let's avoid catching the pre-disconnect state and early return sleep 2 - kubectl -n "${JS_NAMESPACE}" wait --timeout 20m --for=condition=Online --for=condition=Registered \ - exporters.jumpstarter.dev/test-exporter-oidc - kubectl -n "${JS_NAMESPACE}" wait --timeout 20m --for=condition=Online --for=condition=Registered \ - exporters.jumpstarter.dev/test-exporter-sa - kubectl -n "${JS_NAMESPACE}" wait --timeout 20m --for=condition=Online --for=condition=Registered \ - exporters.jumpstarter.dev/test-exporter-legacy + # Run waits in parallel + kubectl -n "${JS_NAMESPACE}" wait --timeout 5m --for=condition=Online --for=condition=Registered \ + exporters.jumpstarter.dev/test-exporter-oidc & + local pid1=$! + kubectl -n "${JS_NAMESPACE}" wait --timeout 5m --for=condition=Online --for=condition=Registered \ + exporters.jumpstarter.dev/test-exporter-sa & + local pid2=$! + kubectl -n "${JS_NAMESPACE}" wait --timeout 5m --for=condition=Online --for=condition=Registered \ + exporters.jumpstarter.dev/test-exporter-legacy & + local pid3=$! + + # Wait for all to complete and capture failures + local rc=0 + wait "$pid1" || rc=$? + wait "$pid2" || rc=$? + wait "$pid3" || rc=$? + return $rc } @test "can create clients with admin cli" { @@ -118,6 +129,7 @@ wait_for_exporter() { --connector-id kubernetes \ --token $(kubectl create -n "${JS_NAMESPACE}" token test-exporter-sa) + # add the mock export paths to those files go run github.com/mikefarah/yq/v4@latest -i ". * load(\"e2e/exporter.yaml\")" \ /etc/jumpstarter/exporters/test-exporter-oidc.yaml go run github.com/mikefarah/yq/v4@latest -i ". * load(\"e2e/exporter.yaml\")" \