diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f64d76743..5fb3e01233 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -282,6 +282,25 @@ jobs: secrets: inherit if: ${{ needs.vars.outputs.helm_changes == 'true' || github.event_name == 'schedule' }} + ipv6-only-tests: + name: IPv6-Only tests + needs: [vars, build-oss] + strategy: + fail-fast: false + matrix: + image: [nginx] # Temporarily removed plus due to credential issues + k8s-version: + [ + "${{ needs.vars.outputs.k8s_latest }}", # Only test against latest k8s for IPv6-only + ] + uses: ./.github/workflows/ipv6-only.yml + with: + image: ${{ matrix.image }} + k8s-version: ${{ matrix.k8s-version }} + secrets: inherit + permissions: + contents: read + publish-helm: name: Package and Publish Helm Chart runs-on: ubuntu-24.04 diff --git a/.github/workflows/ipv6-only.yml b/.github/workflows/ipv6-only.yml new file mode 100644 index 0000000000..0f079e090c --- /dev/null +++ b/.github/workflows/ipv6-only.yml @@ -0,0 +1,222 @@ +name: IPv6-Only Testing + +on: + workflow_call: + inputs: + image: + required: true + type: string + k8s-version: + required: true + type: string + +defaults: + run: + shell: bash + +env: + PLUS_USAGE_ENDPOINT: ${{ secrets.JWT_PLUS_REPORTING_ENDPOINT }} + +permissions: + contents: read + +jobs: + ipv6-only-tests: + name: Run IPv6-Only Tests + runs-on: ubuntu-24.04 + if: ${{ !github.event.pull_request.head.repo.fork || inputs.image != 'plus' }} + env: + DOCKER_BUILD_SUMMARY: false + steps: + - name: Checkout Repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + + - name: Setup Golang Environment + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: stable + + - name: Set GOPATH + run: echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV + + - name: Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + + - name: NGF Docker meta + id: ngf-meta + uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 + with: + images: | + name=ghcr.io/nginx/nginx-gateway-fabric + tags: | + type=semver,pattern={{version}} + type=schedule + type=edge + type=ref,event=pr + type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') }} + + - name: NGINX Docker meta + id: nginx-meta + uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 + with: + images: | + name=ghcr.io/nginx/nginx-gateway-fabric/${{ inputs.image == 'plus' && 'nginx-plus' || inputs.image }} + tags: | + type=semver,pattern={{version}} + type=edge + type=schedule + type=ref,event=pr + type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') }} + + - name: Build binary + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + with: + version: v2.11.2 # renovate: datasource=github-tags depName=goreleaser/goreleaser + args: build --single-target --snapshot --clean + env: + TELEMETRY_ENDPOINT: otel-collector-opentelemetry-collector.collector.svc.cluster.local:4317 + TELEMETRY_ENDPOINT_INSECURE: "true" + + - name: Build NGF Docker Image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + file: build/Dockerfile + tags: ${{ steps.ngf-meta.outputs.tags }} + context: "." + load: true + cache-from: type=gha,scope=ngf-ipv6 + pull: true + target: goreleaser + + - name: Build NGINX Docker Image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + file: build/Dockerfile${{ inputs.image == 'nginx' && '.nginx' || '' }}${{ inputs.image == 'plus' && '.nginxplus' || ''}} + tags: ${{ steps.nginx-meta.outputs.tags }} + context: "." + load: true + cache-from: type=gha,scope=${{ inputs.image }}-ipv6 + pull: true + build-args: | + NJS_DIR=internal/controller/nginx/modules/src + NGINX_CONF_DIR=internal/controller/nginx/conf + BUILD_AGENT=gha + + - name: Setup license file for plus + if: ${{ inputs.image == 'plus' }} + env: + PLUS_LICENSE: ${{ secrets.JWT_PLUS_REPORTING }} + run: echo "${PLUS_LICENSE}" > license.jwt + + - name: Deploy IPv6-Only Kubernetes + id: k8s + run: | + # Enable IPv6 and container network options + sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0 + sudo sysctl -w net.ipv6.conf.all.forwarding=1 + + # Create IPv6-only kind cluster + kind create cluster \ + --name ${{ github.run_id }}-ipv6 \ + --image=kindest/node:${{ inputs.k8s-version }} \ + --config=config/cluster/kind-cluster-ipv6-only.yaml + + # Load images into the cluster + kind load docker-image ${{ join(fromJSON(steps.ngf-meta.outputs.json).tags, ' ') }} ${{ join(fromJSON(steps.nginx-meta.outputs.json).tags, ' ') }} --name ${{ github.run_id }}-ipv6 + + - name: Install NGF with IPv6 Configuration + run: | + ngf_prefix=ghcr.io/nginx/nginx-gateway-fabric + ngf_tag=${{ steps.ngf-meta.outputs.version }} + + # Install with IPv6-specific configuration + CLUSTER_NAME=${{ github.run_id }}-ipv6 \ + HELM_PARAMETERS="--set nginx.config.ipFamily=ipv6 --set nginx.service.type=ClusterIP" \ + make helm-install-local${{ inputs.image == 'plus' && '-with-plus' || ''}} PREFIX=${ngf_prefix} TAG=${ngf_tag} + working-directory: ./tests + + - name: Deploy Test Applications + run: | + kubectl apply -f tests/manifests/ipv6-test-app.yaml + + - name: Wait for NGF and Applications to be Ready + run: | + echo "Waiting for NGF to be ready..." + kubectl wait --for=condition=available --timeout=300s deployment/nginx-gateway -n nginx-gateway + + echo "Waiting for test applications to be ready..." + kubectl wait --for=condition=available --timeout=300s deployment/test-app-ipv6 + + - name: Deploy IPv6 Test Client + run: | + kubectl apply -f tests/manifests/test-client-ipv6.yaml + kubectl wait --for=condition=ready --timeout=300s pod/ipv6-test-client + + - name: Get NGF IPv6 Address + id: ngf-address + run: | + # Get the NGF service IPv6 address + NGF_IPV6=$(kubectl get service nginx-gateway -n nginx-gateway -o jsonpath='{.spec.clusterIP}') + echo "NGF IPv6 Address: $NGF_IPV6" + echo "ngf_ipv6=$NGF_IPV6" >> $GITHUB_OUTPUT + + - name: Run IPv6 Connectivity Tests + run: | + echo "=== Running IPv6-Only Tests ===" + + # Test 1: Basic connectivity test using test client pod + echo "Test 1: Basic IPv6 connectivity" + kubectl exec ipv6-test-client -- curl --version + kubectl exec ipv6-test-client -- nslookup nginx-gateway.nginx-gateway.svc.cluster.local + + # Test 2: Test NGF service directly via IPv6 + echo "Test 2: NGF Service IPv6 connectivity" + kubectl exec ipv6-test-client -- curl -6 --connect-timeout 30 --max-time 60 -v \ + -H "Host: ipv6-test.example.com" \ + "http://[${{ steps.ngf-address.outputs.ngf_ipv6 }}]:80/" || echo "Direct NGF test failed" + + # Test 3: Test via service DNS + echo "Test 3: Service DNS IPv6 connectivity" + kubectl exec ipv6-test-client -- curl -6 --connect-timeout 30 --max-time 60 -v \ + -H "Host: ipv6-test.example.com" \ + "http://nginx-gateway.nginx-gateway.svc.cluster.local:80/" || echo "Service DNS test failed" + + - name: Validate IPv6-Only Configuration + run: | + echo "=== Validating IPv6-Only Configuration ===" + + # Check NGF configuration + echo "NGF Pod IPv6 addresses:" + kubectl get pods -n nginx-gateway -o wide + + echo "NGF Service configuration:" + kubectl get service nginx-gateway -n nginx-gateway -o yaml + + echo "Gateway and HTTPRoute status:" + kubectl get gateway,httproute -A -o wide + + echo "Test application service configuration:" + kubectl get service test-app-ipv6-service -o yaml + + - name: Collect Logs + if: always() + run: | + echo "=== Collecting logs for debugging ===" + echo "NGF Controller logs:" + kubectl logs -n nginx-gateway deployment/nginx-gateway -c nginx-gateway-controller --tail=100 || true + + echo "NGINX logs:" + kubectl logs -n nginx-gateway deployment/nginx-gateway -c nginx --tail=100 || true + + echo "Test client logs:" + kubectl logs ipv6-test-client --tail=100 || true + + echo "Cluster events:" + kubectl get events --sort-by='.lastTimestamp' --all-namespaces --tail=50 || true + + - name: Cleanup + if: always() + run: | + kind delete cluster --name ${{ github.run_id }}-ipv6 || true diff --git a/config/cluster/kind-cluster-ipv6-only.yaml b/config/cluster/kind-cluster-ipv6-only.yaml new file mode 100644 index 0000000000..bb2dce8392 --- /dev/null +++ b/config/cluster/kind-cluster-ipv6-only.yaml @@ -0,0 +1,7 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane +networking: + ipFamily: ipv6 + apiServerAddress: "::1" diff --git a/tests/manifests/ipv6-test-app.yaml b/tests/manifests/ipv6-test-app.yaml new file mode 100644 index 0000000000..ff4a506cd9 --- /dev/null +++ b/tests/manifests/ipv6-test-app.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-app-ipv6 + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: test-app-ipv6 + template: + metadata: + labels: + app: test-app-ipv6 + spec: + containers: + - name: nginx + image: nginx:alpine + ports: + - containerPort: 80 + resources: + limits: + cpu: "100m" + memory: "128Mi" + requests: + cpu: "50m" + memory: "64Mi" +--- +apiVersion: v1 +kind: Service +metadata: + name: test-app-ipv6-service + namespace: default +spec: + selector: + app: test-app-ipv6 + ports: + - port: 80 + targetPort: 80 + ipFamilies: [IPv6] + ipFamilyPolicy: SingleStack +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: test-route-ipv6 + namespace: default +spec: + parentRefs: + - name: nginx-gateway + namespace: nginx-gateway + hostnames: + - "ipv6-test.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: test-app-ipv6-service + port: 80 diff --git a/tests/manifests/test-client-ipv6.yaml b/tests/manifests/test-client-ipv6.yaml new file mode 100644 index 0000000000..49b766e02a --- /dev/null +++ b/tests/manifests/test-client-ipv6.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: Pod +metadata: + name: ipv6-test-client + namespace: default + labels: + app: ipv6-test-client +spec: + restartPolicy: Never + containers: + - name: test-client + image: curlimages/curl:8.11.1 + imagePullPolicy: IfNotPresent + command: ["sleep", "3600"] # Keep pod alive for exec commands + resources: + limits: + cpu: "100m" + memory: "128Mi" + requests: + cpu: "50m" + memory: "64Mi" + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 65534 + capabilities: + drop: + - ALL + dnsConfig: + options: + - name: single-request-reopen + - name: ndots + value: "2" + dnsPolicy: ClusterFirst