diff --git a/.github/workflows/publish-helm-chart.yml b/.github/workflows/publish-helm-chart.yml index 236eeb8..03b386c 100644 --- a/.github/workflows/publish-helm-chart.yml +++ b/.github/workflows/publish-helm-chart.yml @@ -2,8 +2,10 @@ name: Helm Chart CI/CD on: push: + branches: + - '**' tags: - - 'v*' + - 'v*' workflow_dispatch: env: @@ -11,8 +13,368 @@ env: CHART_NAME: headplane jobs: + setup: + runs-on: ubuntu-latest + outputs: + test-cases: ${{ steps.get-test-cases.outputs.test-cases }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get test cases + id: get-test-cases + run: | + test_cases=$(find test/test-cases -name "*.yaml" -type f | sort | jq -R -s -c 'split("\n")[:-1]') + echo "test-cases=$test_cases" >> $GITHUB_OUTPUT + echo "Found test cases: $test_cases" + + test: + needs: setup + runs-on: ubuntu-latest + strategy: + matrix: + test-file: ${{ fromJson(needs.setup.outputs.test-cases) }} + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v3 + with: + version: v3.12.3 + + - name: Install yq + run: | + echo "=== Installing yq ===" + wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + chmod +x /usr/local/bin/yq + echo "✓ yq installed successfully " + + - name: Install k3s + run: | + echo "=== Installing k3s ===" + curl -sfL https://get.k3s.io | sh - + sudo chmod 644 /etc/rancher/k3s/k3s.yaml + mkdir -p ~/.kube + sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config + sudo chown $USER:$USER ~/.kube/config + export KUBECONFIG=~/.kube/config + echo "✓ k3s installed successfully" + + - name: Wait for k3s to be ready + run: | + echo "=== Waiting for k3s to be ready ===" + timeout 60s bash -c 'until kubectl get nodes; do sleep 2; done' + echo "✓ k3s is ready" + + - name: Create test directories + run: | + echo "=== Creating test directories ===" + mkdir -p test/output test/logs + echo "✓ Test directories created successfully" + + - name: Update Helm dependencies + run: | + if [ ! "$(ls -A charts)" ]; then + echo "=== Updating Helm dependencies ===" + helm dependency update . + echo "✓ Helm dependencies updated successfully" + else + echo "=== Using existing dependencies ===" + echo "✓ Using existing dependencies" + fi + + - name: Copy test templates + run: | + echo "=== Copying test templates ===" + if [ -d "test/templates" ]; then + cp -r test/templates/* templates/ + echo "✓ Test templates copied successfully" + else + echo "No test templates directory found" + fi + + - name: Generate template + id: generate + run: | + test_name=$(basename "${{ matrix.test-file }}" .yaml) + echo "=== Generating template for: $test_name ===" + helm template rybbit . -f "${{ matrix.test-file }}" > "test/output/${test_name}-output.yaml" + echo "✓ Template generated successfully for $test_name" + + - name: Validate YAML syntax + run: | + test_name=$(basename "${{ matrix.test-file }}" .yaml) + echo "=== Validating YAML for: $test_name ===" + if ! yq eval '.' "test/output/${test_name}-output.yaml" > /dev/null 2>&1; then + echo "Invalid YAML syntax in ${test_name}-output.yaml" + yq eval '.' "test/output/${test_name}-output.yaml" + exit 1 + fi + echo "✓ YAML syntax validated successfully for $test_name" + + - name: Check for unrendered templates + run: | + test_name=$(basename "${{ matrix.test-file }}" .yaml) + echo "=== Checking for unrendered templates in: $test_name ===" + if grep -q "{{.*}}" "test/output/${test_name}-output.yaml"; then + echo "Found unrendered template variables in ${test_name}-output.yaml" + cat "test/output/${test_name}-output.yaml" + exit 1 + fi + echo "✓ No unrendered templates found in $test_name" + + - name: Validate Kubernetes resources + run: | + test_name=$(basename "${{ matrix.test-file }}" .yaml) + echo "=== Validating Kubernetes resources for: $test_name ===" + + # Check if the test file contains ServiceMonitor resources + if grep -q "kind: ServiceMonitor" "test/output/${test_name}-output.yaml"; then + # Check if Prometheus Operator CRDs are installed + if ! kubectl get crd servicemonitors.monitoring.coreos.com >/dev/null 2>&1; then + echo "Skipping ServiceMonitor validation as Prometheus Operator CRDs are not installed" + # Remove ServiceMonitor resources temporarily for validation + yq eval 'select(.kind != "ServiceMonitor")' "test/output/${test_name}-output.yaml" > "test/output/${test_name}-output-temp.yaml" + # Validate remaining resources + if [ -s "test/output/${test_name}-output-temp.yaml" ]; then + kubectl apply --dry-run=client -f "test/output/${test_name}-output-temp.yaml" + else + echo "No resources to validate after removing ServiceMonitor resources" + fi + rm "test/output/${test_name}-output-temp.yaml" + else + kubectl apply --dry-run=client -f "test/output/${test_name}-output.yaml" + fi + else + kubectl apply --dry-run=client -f "test/output/${test_name}-output.yaml" + fi + echo "✓ Kubernetes resources validated successfully for $test_name" + + - name: Check required resources + run: | + test_name=$(basename "${{ matrix.test-file }}" .yaml) + echo "=== Checking required resources for: $test_name ===" + if ! grep -q "kind: Deployment\|kind: Service\|kind: ConfigMap\|kind: Secret" "test/output/${test_name}-output.yaml"; then + echo "Missing required Kubernetes resources in ${test_name}-output.yaml" + exit 1 + fi + echo "✓ Required resources found in $test_name" + + - name: Deploy and test resources + run: | + test_name=$(basename "${{ matrix.test-file }}" .yaml) + namespace="test-${test_name}-$(date +%m%d%H%M)" + namespace=$(echo "$namespace" | tr '[:upper:]' '[:lower:]' | tr '_' '-' | cut -c 1-63 | sed 's/-$//') + + echo "=== Testing $test_name in namespace: $namespace ===" + kubectl create namespace "$namespace" + + # Apply resources and monitor status + echo "Applying resources..." + helm template rybbit . -f "${{ matrix.test-file }}" --namespace "$namespace" | kubectl apply -f - -n "$namespace" + + # Give resources time to start scheduling + echo "Waiting for resources to start scheduling..." + sleep 3 + + # Immediate check of events and pod status + echo "=== Initial Status Check ===" + echo "Deployments:" + kubectl get deployments -n "$namespace" -o wide + + echo -e "\nJobs:" + kubectl get jobs -n "$namespace" -o wide + + echo -e "\nPods:" + kubectl get pods -n "$namespace" -o wide + + echo -e "\nRecent Events:" + kubectl get events -n "$namespace" --sort-by='.lastTimestamp' | tail -n 20 + + # Monitor deployments and jobs with continuous updates + echo -e "\nMonitoring deployments and jobs..." + start_time=$(date +%s) + timeout_seconds=60 + + while true; do + current_time=$(date +%s) + elapsed=$((current_time - start_time)) + remaining=$((timeout_seconds - elapsed)) + + if [ $elapsed -ge $timeout_seconds ]; then + echo "[$test_name] Timeout reached (${timeout_seconds}s). Test FAILED!" + echo "[$test_name] Final status before timeout:" + kubectl get deployments,jobs,pods -n "$namespace" -o wide + echo "[$test_name] Recent events:" + kubectl get events -n "$namespace" --sort-by='.lastTimestamp' | tail -n 10 + exit 1 + fi + + echo "[$test_name] === Current Status (${elapsed}s elapsed, ${remaining}s remaining) ===" + kubectl get deployments,jobs,pods -n "$namespace" -o wide + + # Check if all deployments are ready + deployment_ready_count=$(kubectl get deployments -n "$namespace" -o jsonpath='{.items[*].status.readyReplicas}' | tr ' ' '\n' | grep -v "^$" | wc -l) + deployment_total_count=$(kubectl get deployments -n "$namespace" -o jsonpath='{.items[*].status.replicas}' | tr ' ' '\n' | grep -v "^$" | wc -l) + + # Check if all jobs are completed + job_completed_count=$(kubectl get jobs -n "$namespace" -o jsonpath='{.items[*].status.succeeded}' | tr ' ' '\n' | grep -v "^$" | wc -l) + job_total_count=$(kubectl get jobs -n "$namespace" -o jsonpath='{.items[*].spec.completions}' | tr ' ' '\n' | grep -v "^$" | wc -l) + + # If no jobs exist, set job counts to 0 + if [ "$job_total_count" -eq 0 ]; then + job_completed_count=0 + job_total_count=0 + fi + + echo "[$test_name] Status: Deployments $deployment_ready_count/$deployment_total_count ready, Jobs $job_completed_count/$job_total_count completed" + + # Proactively capture logs for pods that might be having issues + for pod in $(kubectl get pods -n "$namespace" -o jsonpath='{.items[*].metadata.name}'); do + pod_status=$(kubectl get pod -n "$namespace" "$pod" -o jsonpath='{.status.phase}') + # Only capture logs from pods that are running or have failed + if [ "$pod_status" = "Running" ] || [ "$pod_status" = "Failed" ] || [ "$pod_status" = "Error" ]; then + echo "[$test_name] Logs for $pod (status: $pod_status):" + kubectl logs -n "$namespace" "$pod" --all-containers --tail=5 || echo "No logs available for $pod" + elif [ "$pod_status" = "Pending" ]; then + # For pending pods, just show the status without trying to get logs + echo "[$test_name] Pod $pod is still pending - waiting for container to start..." + fi + done + + # Check if all deployments are ready AND all jobs are completed + deployments_ready=false + jobs_completed=false + + # Check if deployments are ready (if any exist) + if [ "$deployment_total_count" -eq 0 ]; then + deployments_ready=true + elif [ "$deployment_ready_count" -eq "$deployment_total_count" ] && [ "$deployment_total_count" -gt 0 ]; then + deployments_ready=true + fi + + # Check if jobs are completed (if any exist) + if [ "$job_total_count" -eq 0 ]; then + jobs_completed=true + elif [ "$job_completed_count" -eq "$job_total_count" ] && [ "$job_total_count" -gt 0 ]; then + jobs_completed=true + fi + + # Additional check: ensure all containers in running pods are ready + containers_ready=true + for pod in $(kubectl get pods -n "$namespace" -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}'); do + ready_containers=$(kubectl get pod -n "$namespace" "$pod" -o jsonpath='{.status.containerStatuses[?(@.ready==true)].name}' | wc -w) + total_containers=$(kubectl get pod -n "$namespace" "$pod" -o jsonpath='{.status.containerStatuses[*].name}' | wc -w) + if [ "$ready_containers" -ne "$total_containers" ]; then + containers_ready=false + echo "[$test_name] Pod $pod has $ready_containers/$total_containers containers ready" + break + fi + done + + if [ "$deployments_ready" = true ] && [ "$jobs_completed" = true ] && [ "$containers_ready" = true ]; then + echo "[$test_name] All deployments are ready, all jobs are completed, and all containers are ready!" + + # # Verify API key replacement if OIDC is enabled + # if kubectl get secret headplane-config -n "$namespace" >/dev/null 2>&1; then + # CONFIG_CONTENT=$(kubectl get secret headplane-config -n "$namespace" -o jsonpath='{.data.config\.yaml}' | base64 -d) + + # # Only verify API key replacement if OIDC is enabled + # if echo "$CONFIG_CONTENT" | grep -q "oidc:"; then + # echo "[$test_name] Verifying API key replacement in headplane-config secret..." + + # if echo "$CONFIG_CONTENT" | grep -q "headscale_api_key:"; then + # API_KEY=$(echo "$CONFIG_CONTENT" | grep "headscale_api_key:" | sed 's/.*headscale_api_key: "\([^"]*\)".*/\1/') + + # if [[ "$API_KEY" != "hcap_placeholder_key_for_validation_only_12345678901234567890123456789012" ]] && [[ -n "$API_KEY" ]]; then + # echo "[$test_name] ✅ API key successfully replaced with real token: ${API_KEY:0:20}..." + # else + # echo "[$test_name] ❌ API key replacement verification failed" + # echo "[$test_name] Current API key: $API_KEY" + # echo "[$test_name] Expected: Real API token (not placeholder)" + # exit 1 + # fi + # else + # echo "[$test_name] ❌ No headscale_api_key found in config" + # exit 1 + # fi + # else + # echo "[$test_name] OIDC not enabled, skipping API key verification" + # fi + # fi + + break + fi + + # Check for any failed pods + if kubectl get pods -n "$namespace" | grep -q "Error\|CrashLoopBackOff\|ImagePullBackOff\|Terminating\|Failed"; then + echo "[$test_name] Found failed or terminating pods. Checking details..." + kubectl get pods -n "$namespace" | grep -E "Error|CrashLoopBackOff|ImagePullBackOff|Terminating|Failed" + echo "[$test_name] Recent events:" + kubectl get events -n "$namespace" --sort-by='.lastTimestamp' | grep -E "Error|Warning|Failed" | tail -5 + echo "[$test_name] Pod logs:" + for pod in $(kubectl get pods -n "$namespace" -o jsonpath='{.items[*].metadata.name}'); do + echo "[$test_name] --- $pod logs ---" + kubectl logs -n "$namespace" "$pod" --all-containers --tail=10 || echo "No logs available for $pod" + done + exit 1 + fi + + # Check for any pods not in Running state + non_running_pods=$(kubectl get pods -n "$namespace" -o jsonpath='{.items[?(@.status.phase!="Running")].metadata.name}') + if [ -n "$non_running_pods" ]; then + should_fail=false + for pod in $non_running_pods; do + pod_status=$(kubectl get pod -n "$namespace" "$pod" -o jsonpath='{.status.phase}') + + # Only fail if the pod is in a failed state, not if it's still pending or succeeded + if [ "$pod_status" = "Failed" ] || [ "$pod_status" = "Error" ] || [ "$pod_status" = "CrashLoopBackOff" ]; then + should_fail=true + echo "[$test_name] Pod $pod has failed status: $pod_status" + kubectl logs -n "$namespace" "$pod" --all-containers --tail=10 || echo "No logs available for $pod" + elif [ "$pod_status" = "Pending" ]; then + echo "[$test_name] Pod $pod is still pending - normal during startup" + elif [ "$pod_status" = "Succeeded" ]; then + echo "[$test_name] Pod $pod has succeeded - this is expected for completed jobs" + else + echo "[$test_name] Pod $pod has unexpected status: $pod_status" + should_fail=true + fi + done + + if [ "$should_fail" = true ]; then + exit 1 + fi + fi + + # Check for failed jobs + failed_jobs=$(kubectl get jobs -n "$namespace" -o jsonpath='{.items[?(@.status.failed>0)].metadata.name}') + if [ -n "$failed_jobs" ]; then + echo "[$test_name] Found failed jobs: $failed_jobs" + for job in $failed_jobs; do + echo "[$test_name] Job $job pod logs:" + job_pods=$(kubectl get pods -n "$namespace" -l job-name="$job" -o jsonpath='{.items[*].metadata.name}') + for pod in $job_pods; do + echo "[$test_name] --- $pod logs ---" + kubectl logs -n "$namespace" "$pod" --tail=10 || echo "No logs available" + done + done + exit 1 + fi + + sleep 10 + done + + # Cleanup with force delete + echo "=== Cleaning up ===" + kubectl delete namespace "$namespace" --force --grace-period=0 + echo "✓ Cleanup completed for $test_name" + publish: if: startsWith(github.ref, 'refs/tags/') + needs: [setup, test] runs-on: ubuntu-latest permissions: contents: read diff --git a/README.md b/README.md index dc6dd60..cb0d65b 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,14 @@ helm uninstall headplane | headplane.config.server.port | int | `3000` | | | headplane.envFrom | list | `[]` | | | headplane.image | string | `"ghcr.io/tale/headplane:0.6.0"` | | -| headplane.oidc.client_id | string | `"REPLACE_IT_WITH_YOUR_OIDC_CLIENT_ID_FOR_HEADPLANE"` | | +| headplane.oidc.client_id | string | `"PLACEHOLDER_USE_SECRET"` | | | headplane.oidc.disable_api_key_login | bool | `true` | | | headplane.oidc.enabled | bool | `false` | | | headplane.oidc.issuer | string | `"https://your-oidc-issuer-url.com"` | | | headplane.oidc.redirect_uri | string | `"https://your-headplane-admin-domain.com/admin/oidc/callback"` | | | headplane.oidc.secret_name | string | `"oidc-secrets"` | | | headplane.oidc.token_endpoint_auth_method | string | `"client_secret_post"` | | -| headscale.acl | string | `""` | | +| headscale.acl | string | `"{\n \"acls\": []\n}\n"` | | | headscale.config.database.debug | bool | `false` | | | headscale.config.database.sqlite.path | string | `"/etc/headscale/db.sqlite"` | | | headscale.config.database.type | string | `"sqlite"` | | diff --git a/templates/job.yaml b/templates/job.yaml index 27300b5..b5a0a9a 100644 --- a/templates/job.yaml +++ b/templates/job.yaml @@ -23,13 +23,24 @@ spec: sleep 1 done - echo "Checking if Secret 'headscale-api-token' exists..." + echo "Checking if API token needs to be generated..." if kubectl get secret headscale-api-token -n {{ .Release.Namespace }} >/dev/null 2>&1; then - echo "Secret already exists. Skipping token generation." - exit 0 + CURRENT_TOKEN=$(kubectl get secret headscale-api-token -n {{ .Release.Namespace }} -o jsonpath='{.data.HEADPLANE_OIDC__HEADSCALE_API_KEY}' | base64 -d) + echo "Current token value: '$CURRENT_TOKEN'" + echo "Current token length: ${#CURRENT_TOKEN}" + echo "Expected placeholder: 'placeholder-token-will-be-replaced-by-job'" + echo "Expected length: 42" + if [[ "$CURRENT_TOKEN" != "placeholder-token-will-be-replaced-by-job" ]]; then + echo "Real API token already exists. Skipping token generation." + exit 0 + else + echo "Placeholder token found. Generating real API token..." + fi + else + echo "Secret not found. Generating API token..." fi - echo "Secret not found. Generating Headscale API token..." + echo "Generating Headscale API token..." TOKEN=$(kubectl -n {{ .Release.Namespace }} exec -i headplane-0 -c headscale -- headscale apikeys create -e 100y) if [ -z "$TOKEN" ]; then @@ -37,5 +48,7 @@ spec: exit 1 fi - echo "Creating Kubernetes Secret..." - kubectl create secret generic headscale-api-token --from-literal=HEADPLANE_OIDC__HEADSCALE_API_KEY="$TOKEN" -n {{ .Release.Namespace }} + echo "Updating headscale-api-token secret with generated API token..." + kubectl patch secret headscale-api-token -n {{ .Release.Namespace }} -p="{\"data\":{\"HEADPLANE_OIDC__HEADSCALE_API_KEY\":\"$(echo -n "$TOKEN" | base64)\"}}" + + echo "Successfully updated headscale-api-token secret with real API token" diff --git a/templates/roles.yaml b/templates/roles.yaml index 39b2d72..5c0e288 100644 --- a/templates/roles.yaml +++ b/templates/roles.yaml @@ -11,6 +11,9 @@ rules: - apiGroups: ['apps'] resources: ['deployments'] verbs: ['get', 'list'] +- apiGroups: ['batch'] + resources: ['jobs'] + verbs: ['get', 'list', 'watch'] --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -57,4 +60,4 @@ rules: verbs: ["create"] - apiGroups: [""] resources: ["secrets"] - verbs: ["create", "get"] + verbs: ["create", "get", "update", "patch"] diff --git a/templates/secret-headplane.yaml b/templates/secret-headplane.yaml index db6a0f3..8a79075 100644 --- a/templates/secret-headplane.yaml +++ b/templates/secret-headplane.yaml @@ -20,4 +20,5 @@ stringData: token_endpoint_auth_method: {{ .Values.headplane.oidc.token_endpoint_auth_method | quote }} redirect_uri: {{ .Values.headplane.oidc.redirect_uri | quote }} client_id: {{ .Values.headplane.oidc.client_id | quote }} + headscale_api_key: {{ .Values.headplane.oidc.headscale_api_key | default "hcap_placeholder_key_for_validation_only_12345678901234567890123456789012" | quote }} {{- end }} \ No newline at end of file diff --git a/templates/secret-headscale-api-token.yaml b/templates/secret-headscale-api-token.yaml new file mode 100644 index 0000000..3cf9a2d --- /dev/null +++ b/templates/secret-headscale-api-token.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: headscale-api-token + namespace: {{ .Release.Namespace }} +type: Opaque +stringData: + HEADPLANE_OIDC__HEADSCALE_API_KEY: "placeholder-token-will-be-replaced-by-job" \ No newline at end of file diff --git a/templates/statefulset-headplane.yaml b/templates/statefulset-headplane.yaml index 6d4b6bf..790d3fd 100644 --- a/templates/statefulset-headplane.yaml +++ b/templates/statefulset-headplane.yaml @@ -35,10 +35,21 @@ spec: containers: - name: headplane image: {{ .Values.headplane.image }} + command: + - /bin/sh + - -c + - | + echo "Waiting for headscale-generate-token job to complete..." + kubectl wait --for=condition=complete job/headscale-generate-token -n {{ .Release.Namespace }} --timeout=300s + echo "Job completed successfully, starting headplane..." + exec node /app/build/server/index.js envFrom: + - secretRef: + name: headscale-api-token {{- if .Values.headplane.oidc.enabled }} - secretRef: name: {{ .Values.headplane.oidc.secret_name }} + # optional: true {{- end }} {{- with .Values.headplane.envFrom }} {{- toYaml . | nindent 10 }} diff --git a/test/templates/mock-oidc-server.yaml b/test/templates/mock-oidc-server.yaml new file mode 100644 index 0000000..f4d4ec1 --- /dev/null +++ b/test/templates/mock-oidc-server.yaml @@ -0,0 +1,110 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: mock-oidc-server +spec: + selector: + app: mock-oidc-server + ports: + - protocol: TCP + port: 80 + targetPort: 80 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: mock-oidc-config +data: + openid-configuration: | + { + "issuer": "http://mock-oidc-server", + "authorization_endpoint": "http://mock-oidc-server/oauth/authorize", + "token_endpoint": "http://mock-oidc-server/oauth/token", + "userinfo_endpoint": "http://mock-oidc-server/userinfo", + "jwks_uri": "http://mock-oidc-server/.well-known/jwks.json", + "response_types_supported": ["code", "token", "id_token"], + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": ["RS256"], + "scopes_supported": ["openid", "profile", "email"], + "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"], + "claims_supported": ["sub", "iss", "name", "email"] + } + jwks.json: | + { + "keys": [ + { + "kty": "RSA", + "kid": "test-key", + "use": "sig", + "alg": "RS256", + "n": "test-modulus", + "e": "AQAB" + } + ] + } + userinfo: | + { + "sub": "test-user-id", + "name": "Test User", + "email": "test@example.com" + } + nginx.conf: | + events { + worker_connections 1024; + } + http { + server { + listen 80; + + location /.well-known/openid-configuration { + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin *; + return 200 '{"issuer":"http://mock-oidc-server","authorization_endpoint":"http://mock-oidc-server/oauth/authorize","token_endpoint":"http://mock-oidc-server/oauth/token","userinfo_endpoint":"http://mock-oidc-server/userinfo","jwks_uri":"http://mock-oidc-server/.well-known/jwks.json","response_types_supported":["code","token","id_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["openid","profile","email"],"token_endpoint_auth_methods_supported":["client_secret_post","client_secret_basic"],"claims_supported":["sub","iss","name","email"]}'; + } + + location /.well-known/jwks.json { + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin *; + return 200 '{"keys":[{"kty":"RSA","kid":"test-key","use":"sig","alg":"RS256","n":"test-modulus","e":"AQAB"}]}'; + } + + location /userinfo { + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin *; + return 200 '{"sub":"test-user-id","name":"Test User","email":"test@example.com"}'; + } + + location / { + return 404; + } + } + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mock-oidc-server +spec: + replicas: 1 + selector: + matchLabels: + app: mock-oidc-server + template: + metadata: + labels: + app: mock-oidc-server + spec: + containers: + - name: mock-oidc-server + image: nginx:alpine + ports: + - containerPort: 80 + volumeMounts: + - name: nginx-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + volumes: + - name: nginx-config + configMap: + name: mock-oidc-config \ No newline at end of file diff --git a/test/templates/secret-oidc.yaml b/test/templates/secret-oidc.yaml new file mode 100644 index 0000000..7bf6f80 --- /dev/null +++ b/test/templates/secret-oidc.yaml @@ -0,0 +1,17 @@ +--- +{{- if or .Values.headplane.oidc.enabled .Values.headscale.oidc.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.headplane.oidc.secret_name | default .Values.headscale.oidc.secret_name | default "oidc-secrets" }} +type: Opaque +stringData: + {{- if .Values.headplane.oidc.enabled }} + HEADPLANE_OIDC__CLIENT_SECRET: "test-headplane-oidc-client-secret" + HEADPLANE_OIDC__CLIENT_ID: {{ .Values.headplane.oidc.client_id | default "test-headplane-oidc-client-id" | quote }} + {{- end }} + {{- if .Values.headscale.oidc.enabled }} + HEADSCALE_OIDC__CLIENT_SECRET: "test-headscale-oidc-client-secret" + HEADSCALE_OIDC__CLIENT_ID: {{ .Values.headscale.oidc.client_id | default "test-headscale-oidc-client-id" | quote }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/test/test-cases/00-lag0-test-case.yaml b/test/test-cases/00-lag0-test-case.yaml new file mode 100644 index 0000000..8af843e --- /dev/null +++ b/test/test-cases/00-lag0-test-case.yaml @@ -0,0 +1,82 @@ +headplane: + image: ghcr.io/tale/headplane:0.6.0 + config: + server: + host: "0.0.0.0" + port: 3000 + cookie_secure: true + headscale: + url: "https://vpn.lag0.com.br" + config_path: "/etc/headscale/config.yaml" + config_strict: "true" + integration: + kubernetes: + enabled: true + validate_manifest: false + pod_name: "headplane-0" + secret: + name: headplane-secret + create: true +headscale: + image: headscale/headscale:0.26.0 + config: + server_url: https://headscale.lag0.com.br + listen_addr: 0.0.0.0:8080 + metrics_listen_addr: 0.0.0.0:9090 + grpc_listen_addr: 0.0.0.0:50443 + grpc_allow_insecure: false + policy: + mode: database + prefixes: + v4: 100.64.0.0/10 + v6: fd7a:115c:a1e0::/48 + allocation: sequential + database: + type: sqlite + debug: false + sqlite: + path: /etc/headscale/db.sqlite + noise: + private_key_path: /etc/headscale/noise_private.key + derp: + server: + enabled: true + region_id: 999 + region_code: "headscale" + region_name: "Headscale Embedded DERP" + stun_listen_addr: "0.0.0.0:3478" + private_key_path: /var/lib/headscale/derp_server_private.key + automatically_add_embedded_derp_region: true + ipv4: 1.2.3.4 + ipv6: 2001:db8::1 + urls: + - https://controlplane.tailscale.com/derpmap/default + paths: [] + dns: + magic_dns: true + base_domain: clients.lag0.com.br + nameservers: + global: + - 1.1.1.1 + - 8.8.8.8 +relay: + enabled: false +pvc: + enabled: true + name: headscale-config + accessModes: + - ReadWriteOnce + storage: 1Gi + annotations: + kustomize.toolkit.fluxcd.io/prune: disabled +# storageClassName: default + +ingress: + enabled: false + className: nginx + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-production" + labels: [] + headplaneDomain: "headscale.lag0.com.br" + headscaleDomain: "vpn.lag0.com.br" + tlsSecretName: "headplane-tls" diff --git a/test/test-cases/01-definetelynobody-test-case.yaml b/test/test-cases/01-definetelynobody-test-case.yaml new file mode 100644 index 0000000..1bfbc69 --- /dev/null +++ b/test/test-cases/01-definetelynobody-test-case.yaml @@ -0,0 +1,18 @@ +headplane: + config: + headscale: + url: "https://vpn.test.example.com" + oidc: + # client_id: "test-headplane-client-id" + enabled: true + issuer: "http://mock-oidc-server" + redirect_uri: "https://headplane.test.example.com/admin/oidc/callback" +headscale: + config: + server_url: "https://vpn.test.example.com" + dns: + base_domain: "test.vpn" + oidc: + client_id: "test-headscale-client-id" + enabled: true + issuer: "http://mock-oidc-server" \ No newline at end of file diff --git a/test/test-cases/02-AzSiAz-test-case.yaml b/test/test-cases/02-AzSiAz-test-case.yaml new file mode 100644 index 0000000..51ba081 --- /dev/null +++ b/test/test-cases/02-AzSiAz-test-case.yaml @@ -0,0 +1,22 @@ +# Test case for PR #3 +headplane: + config: + headscale: + url: "https://vpn.test.example.com" + oidc: + client_id: "test-headplane-client-id" + enabled: true + issuer: "http://mock-oidc-server" + redirect_uri: "https://headplane.test.example.com/admin/oidc/callback" +headscale: + config: + server_url: "https://vpn.test.example.com" + dns: + base_domain: "test.vpn" + oidc: + pkce: + enabled: true + method: S256 + client_id: "test-headscale-client-id" + enabled: true + issuer: "http://mock-oidc-server" \ No newline at end of file diff --git a/values.yaml b/values.yaml index 3a63fb8..17b873b 100644 --- a/values.yaml +++ b/values.yaml @@ -14,7 +14,7 @@ headplane: # OIDC redirect URI redirect_uri: "https://your-headplane-admin-domain.com/admin/oidc/callback" # OIDC client ID - client_id: "REPLACE_IT_WITH_YOUR_OIDC_CLIENT_ID_FOR_HEADPLANE" + client_id: "PLACEHOLDER_USE_SECRET" #USE secret below instead # Name of the secret containing OIDC credentials secret_name: "oidc-secrets" config: