Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
222 changes: 222 additions & 0 deletions .github/workflows/ipv6-only.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions config/cluster/kind-cluster-ipv6-only.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
networking:
ipFamily: ipv6
apiServerAddress: "::1"
61 changes: 61 additions & 0 deletions tests/manifests/ipv6-test-app.yaml
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions tests/manifests/test-client-ipv6.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading