Skip to content

Integration Tests

Integration Tests #3

Workflow file for this run

name: Integration Tests
on:
# Run after CI or Release workflow completes
# - CI on main: catches issues before release (uses 'latest' image)
# - Release on tags: tests the actual released image (uses tag like 'v0.6.0')
workflow_run:
workflows: ["CI", "Release"]
types:
- completed
# Manual trigger for testing specific protocols/tests
workflow_dispatch:
inputs:
protocol:
description: 'Protocol to test'
required: false
default: 'all'
type: choice
options:
- all
- nfs
- nvmeof
- iscsi
- smb
test:
description: 'Specific test to run (leave empty for all tests of selected protocol)'
required: false
default: ''
type: choice
options:
- ''
- basic
- snapshot
- detached-snapshot
- concurrent
- persistence
- statefulset
- volume-expansion
- zfsprops
- name-templating
- clone-volume
- delete-strategy-retain
- block-mode
- snapshot-stress
- dual-mount
- connection-resilience
- pod-restart
- access-modes
- snapshot-restore
- volume-stress
- error-handling
- encryption
pr:
description: 'PR number to test (builds image from PR code, leave empty for normal run)'
required: false
default: ''
type: string
# Restrict permissions to minimum required
permissions:
contents: read
# Ensure only one integration test runs at a time on the new runner
concurrency:
group: integration-tests
cancel-in-progress: false
jobs:
# ==========================================
# Setup Phase
# ==========================================
# Check if driver files changed (skip integration tests for plugin-only changes)
check-driver-changes:
name: Check Driver Changes
runs-on: ubuntu-latest
if: github.event_name == 'workflow_run'
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 2
- name: Check for driver changes
id: check
run: |
# Get changed files in the commit
CHANGED=$(git diff --name-only HEAD^..HEAD 2>/dev/null || echo "")
echo "Changed files: $CHANGED"
# Check if any driver-related files changed (excludes docs/, README, tests/, etc.)
if echo "$CHANGED" | grep -qE '^(cmd/nasty-csi-driver/|pkg/|Dockerfile|go\.(mod|sum)|charts/)'; then
echo "Driver files changed - will run integration tests"
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "No driver files changed - skipping integration tests"
echo "should_run=false" >> $GITHUB_OUTPUT
fi
compute-tag:
name: Compute Image Tag
runs-on: new
needs: check-driver-changes
# Skip if:
# - triggered by workflow_run and the workflow failed
# - triggered by CI workflow but not on main branch (only run integration tests for main)
# - no driver files changed (plugin-only changes)
# Note: always() allows this job to run even when check-driver-changes is skipped (manual dispatch)
if: |
always() &&
(github.event_name != 'workflow_run' ||
(github.event.workflow_run.conclusion == 'success' &&
(github.event.workflow_run.name == 'Release' || github.event.workflow_run.head_branch == 'main'))) &&
(needs.check-driver-changes.outputs.should_run != 'false')
outputs:
image_tag: ${{ steps.tag.outputs.tag }}
steps:
# PR mode: checkout PR code and build image locally
- name: Checkout PR code
if: inputs.pr != ''
uses: actions/checkout@v6
with:
ref: refs/pull/${{ inputs.pr }}/merge
- name: Set up Docker Buildx
if: inputs.pr != ''
uses: docker/setup-buildx-action@v3
- name: Build PR image
if: inputs.pr != ''
run: |
PR_TAG="pr-${{ inputs.pr }}"
IMAGE="ghcr.io/fenio/tns-csi:${PR_TAG}"
echo "=== Building image from PR #${{ inputs.pr }} ==="
docker buildx build \
--load \
--build-arg VERSION="0.0.0-${PR_TAG}" \
--build-arg GIT_COMMIT="$(git rev-parse --short HEAD)" \
--build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-t "${IMAGE}" .
docker save "${IMAGE}" -o /tmp/tns-csi-pr-image.tar
echo "Image saved to /tmp/tns-csi-pr-image.tar"
- name: Upload PR image artifact
if: inputs.pr != ''
uses: actions/upload-artifact@v6
with:
name: pr-image
path: /tmp/tns-csi-pr-image.tar
retention-days: 1
- name: Compute image tag from ref
id: tag
env:
# Use environment variables to safely pass workflow_run context
# This prevents shell injection via malicious branch names
WORKFLOW_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
WORKFLOW_EVENT: ${{ github.event.workflow_run.event }}
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
run: |
# PR mode: use the PR-specific tag
if [[ -n "${{ inputs.pr }}" ]]; then
TAG="pr-${{ inputs.pr }}"
echo "Using PR image tag: $TAG"
echo "tag=$TAG" >> $GITHUB_OUTPUT
exit 0
fi
# For workflow_run events, get ref from the triggering workflow
if [ "${{ github.event_name }}" = "workflow_run" ]; then
REF_NAME="$WORKFLOW_HEAD_BRANCH"
# Check if it was triggered by a tag (Release workflow)
if [ "$WORKFLOW_EVENT" = "push" ] && [[ "$WORKFLOW_HEAD_BRANCH" == v* ]]; then
REF_TYPE="tag"
else
REF_TYPE="branch"
fi
echo "Triggered by workflow: $WORKFLOW_NAME"
else
# For manual dispatch or other events
REF_NAME="${{ github.ref_name }}"
REF_TYPE="${{ github.ref_type }}"
fi
echo "REF_NAME: $REF_NAME, REF_TYPE: $REF_TYPE"
if [ "$REF_TYPE" = "tag" ]; then
# For tags like v0.5.0, strip the 'v' prefix to match Docker image tags
# Release workflow uses semver pattern which produces tags like 0.5.0 (without v)
TAG="${REF_NAME#v}"
echo "Using image tag: $TAG (from tag: $REF_NAME)"
elif [ "$REF_NAME" = "main" ]; then
TAG="latest"
echo "Using image tag: $TAG (from branch: $REF_NAME)"
else
TAG=$(echo "$REF_NAME" | sed 's/\//-/g')
echo "Using image tag: $TAG (from branch: $REF_NAME)"
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
# ==========================================
# E2E Tests
# ==========================================
ginkgo-e2e-nfs:
name: "E2E: NFS"
runs-on: new
timeout-minutes: 60
needs: compute-tag
if: |
!cancelled() && needs.compute-tag.result == 'success' &&
(github.event.inputs.protocol == 'all' || github.event.inputs.protocol == 'nfs' || github.event.inputs.protocol == '')
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.pr != '' && format('refs/pull/{0}/merge', inputs.pr) || '' }}
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- uses: fenio/setup-k3s@v5
with:
k3s-args: '--write-kubeconfig-mode 644 --disable=traefik --disable=local-storage'
- uses: ./.github/actions/setup-snapshots
- name: Download PR image artifact
if: inputs.pr != ''
uses: actions/download-artifact@v7
with:
name: pr-image
path: /tmp
- name: Import PR image into k3s
if: inputs.pr != ''
run: |
sudo k3s ctr images import /tmp/tns-csi-pr-image.tar
echo "Imported PR image into k3s containerd"
sudo k3s crictl images | grep tns-csi || true
- name: Install Ginkgo
run: go install github.com/onsi/ginkgo/v2/ginkgo@latest
- name: Run NFS E2E Tests
env:
NASTY_HOST: ${{ secrets.NASTY_HOST }}
NASTY_API_KEY: ${{ secrets.NASTY_API_KEY }}
NASTY_POOL: ${{ secrets.NASTY_POOL }}
CSI_IMAGE_TAG: ${{ needs.compute-tag.outputs.image_tag }}
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
run: |
echo "=== Running NFS E2E Tests ==="
# Run only NFS-specific tests from the nfs/ directory
ginkgo -v --timeout=55m --flake-attempts=2 ./tests/e2e/nfs/...
- name: Upload diagnostic logs
if: failure()
uses: actions/upload-artifact@v6
with:
name: e2e-nfs-diagnostics
path: /tmp/test-logs/
retention-days: 7
ginkgo-e2e-nvmeof:
name: "E2E: NVMe-oF"
runs-on: new
timeout-minutes: 75
needs: compute-tag
if: |
!cancelled() && needs.compute-tag.result == 'success' &&
(github.event.inputs.protocol == 'all' || github.event.inputs.protocol == 'nvmeof' || github.event.inputs.protocol == '')
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.pr != '' && format('refs/pull/{0}/merge', inputs.pr) || '' }}
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- uses: fenio/setup-k3s@v5
with:
k3s-args: '--write-kubeconfig-mode 644 --disable=traefik --disable=local-storage'
- uses: ./.github/actions/setup-snapshots
- name: Download PR image artifact
if: inputs.pr != ''
uses: actions/download-artifact@v7
with:
name: pr-image
path: /tmp
- name: Import PR image into k3s
if: inputs.pr != ''
run: |
sudo k3s ctr images import /tmp/tns-csi-pr-image.tar
echo "Imported PR image into k3s containerd"
sudo k3s crictl images | grep tns-csi || true
- name: Install Ginkgo
run: go install github.com/onsi/ginkgo/v2/ginkgo@latest
- name: Run NVMe-oF E2E Tests
env:
NASTY_HOST: ${{ secrets.NASTY_HOST }}
NASTY_API_KEY: ${{ secrets.NASTY_API_KEY }}
NASTY_POOL: ${{ secrets.NASTY_POOL }}
CSI_IMAGE_TAG: ${{ needs.compute-tag.outputs.image_tag }}
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
run: |
echo "=== Running NVMe-oF E2E Tests ==="
# Run only NVMe-oF-specific tests from the nvmeof/ directory
ginkgo -v --timeout=70m --flake-attempts=2 ./tests/e2e/nvmeof/...
- name: Upload diagnostic logs
if: failure()
uses: actions/upload-artifact@v6
with:
name: e2e-nvmeof-diagnostics
path: /tmp/test-logs/
retention-days: 7
ginkgo-e2e-iscsi:
name: "E2E: iSCSI"
runs-on: new
timeout-minutes: 75
needs: compute-tag
if: |
!cancelled() && needs.compute-tag.result == 'success' &&
(github.event.inputs.protocol == 'all' || github.event.inputs.protocol == 'iscsi' || github.event.inputs.protocol == '')
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.pr != '' && format('refs/pull/{0}/merge', inputs.pr) || '' }}
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- uses: fenio/setup-k3s@v5
with:
k3s-args: '--write-kubeconfig-mode 644 --disable=traefik --disable=local-storage'
- uses: ./.github/actions/setup-snapshots
- name: Download PR image artifact
if: inputs.pr != ''
uses: actions/download-artifact@v7
with:
name: pr-image
path: /tmp
- name: Import PR image into k3s
if: inputs.pr != ''
run: |
sudo k3s ctr images import /tmp/tns-csi-pr-image.tar
echo "Imported PR image into k3s containerd"
sudo k3s crictl images | grep tns-csi || true
- name: Install Ginkgo
run: go install github.com/onsi/ginkgo/v2/ginkgo@latest
- name: Run iSCSI E2E Tests
env:
NASTY_HOST: ${{ secrets.NASTY_HOST }}
NASTY_API_KEY: ${{ secrets.NASTY_API_KEY }}
NASTY_POOL: ${{ secrets.NASTY_POOL }}
CSI_IMAGE_TAG: ${{ needs.compute-tag.outputs.image_tag }}
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
run: |
echo "=== Running iSCSI E2E Tests ==="
# Run only iSCSI-specific tests from the iscsi/ directory
ginkgo -v --timeout=70m --flake-attempts=2 ./tests/e2e/iscsi/...
- name: Upload diagnostic logs
if: failure()
uses: actions/upload-artifact@v6
with:
name: e2e-iscsi-diagnostics
path: /tmp/test-logs/
retention-days: 7
ginkgo-e2e-smb:
name: "E2E: SMB"
runs-on: new
timeout-minutes: 60
needs: compute-tag
if: |
!cancelled() && needs.compute-tag.result == 'success' &&
(github.event.inputs.protocol == 'all' || github.event.inputs.protocol == 'smb' || github.event.inputs.protocol == '')
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.pr != '' && format('refs/pull/{0}/merge', inputs.pr) || '' }}
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- uses: fenio/setup-k3s@v5
with:
k3s-args: '--write-kubeconfig-mode 644 --disable=traefik --disable=local-storage'
- uses: ./.github/actions/setup-snapshots
- name: Download PR image artifact
if: inputs.pr != ''
uses: actions/download-artifact@v7
with:
name: pr-image
path: /tmp
- name: Import PR image into k3s
if: inputs.pr != ''
run: |
sudo k3s ctr images import /tmp/tns-csi-pr-image.tar
echo "Imported PR image into k3s containerd"
sudo k3s crictl images | grep tns-csi || true
- name: Install Ginkgo and smbclient
run: |
go install github.com/onsi/ginkgo/v2/ginkgo@latest
sudo apt-get install -y smbclient 2>/dev/null || true
- name: Run SMB E2E Tests
env:
NASTY_HOST: ${{ secrets.NASTY_HOST }}
NASTY_API_KEY: ${{ secrets.NASTY_API_KEY }}
NASTY_POOL: ${{ secrets.NASTY_POOL }}
CSI_IMAGE_TAG: ${{ needs.compute-tag.outputs.image_tag }}
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
SMB_USERNAME: ${{ secrets.SMB_USERNAME }}
SMB_PASSWORD: ${{ secrets.SMB_PASSWORD }}
run: |
echo "=== Running SMB E2E Tests ==="
# Run only SMB-specific tests from the smb/ directory
ginkgo -v --timeout=55m --flake-attempts=2 ./tests/e2e/smb/...
- name: Collect diagnostic logs
if: failure()
env:
NASTY_HOST: ${{ secrets.NASTY_HOST }}
NASTY_API_KEY: ${{ secrets.NASTY_API_KEY }}
SMB_USERNAME: ${{ secrets.SMB_USERNAME }}
SMB_PASSWORD: ${{ secrets.SMB_PASSWORD }}
run: |
mkdir -p /tmp/test-logs
echo "=== CSI Controller Logs ===" > /tmp/test-logs/controller.log
kubectl logs -n kube-system -l app.kubernetes.io/component=controller -c nasty-csi-plugin --tail=500 >> /tmp/test-logs/controller.log 2>&1 || true
echo "=== CSI Node Logs ===" > /tmp/test-logs/node.log
kubectl logs -n kube-system -l app.kubernetes.io/component=node -c nasty-csi-plugin --tail=500 >> /tmp/test-logs/node.log 2>&1 || true
echo "=== Pod Events ===" > /tmp/test-logs/events.log
kubectl get events --all-namespaces --sort-by='.lastTimestamp' >> /tmp/test-logs/events.log 2>&1 || true
echo "=== PVCs ===" >> /tmp/test-logs/events.log
kubectl get pvc --all-namespaces >> /tmp/test-logs/events.log 2>&1 || true
echo "=== CSI Driver Pods ===" >> /tmp/test-logs/events.log
kubectl get pods -n kube-system -l app.kubernetes.io/name=nasty-csi-driver -o wide >> /tmp/test-logs/events.log 2>&1 || true
# SMB-specific diagnostics (note: runs after test cleanup, so shares may be gone)
echo "=== SMB Share Listing (smbclient -L) ===" > /tmp/test-logs/smb-diag.log
smbclient -L "//${NASTY_HOST}" -U "${SMB_USERNAME}%${SMB_PASSWORD}" --option="client min protocol=SMB3" 2>&1 >> /tmp/test-logs/smb-diag.log || true
echo "" >> /tmp/test-logs/smb-diag.log
echo "=== NOTE: Real-time share diagnostics are in controller.log [SMB clone diag] lines ===" >> /tmp/test-logs/smb-diag.log
- name: Upload diagnostic logs
if: failure()
uses: actions/upload-artifact@v6
with:
name: e2e-smb-diagnostics
path: /tmp/test-logs/
retention-days: 7
ginkgo-e2e-shared:
name: "E2E: Shared"
runs-on: new
timeout-minutes: 75
needs: compute-tag
if: |
!cancelled() && needs.compute-tag.result == 'success'
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.pr != '' && format('refs/pull/{0}/merge', inputs.pr) || '' }}
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- uses: fenio/setup-k3s@v5
with:
k3s-args: '--write-kubeconfig-mode 644 --disable=traefik --disable=local-storage'
- uses: ./.github/actions/setup-snapshots
- name: Download PR image artifact
if: inputs.pr != ''
uses: actions/download-artifact@v7
with:
name: pr-image
path: /tmp
- name: Import PR image into k3s
if: inputs.pr != ''
run: |
sudo k3s ctr images import /tmp/tns-csi-pr-image.tar
echo "Imported PR image into k3s containerd"
sudo k3s crictl images | grep tns-csi || true
- name: Install Ginkgo
run: go install github.com/onsi/ginkgo/v2/ginkgo@latest
- name: Run Shared E2E Tests
env:
NASTY_HOST: ${{ secrets.NASTY_HOST }}
NASTY_API_KEY: ${{ secrets.NASTY_API_KEY }}
NASTY_POOL: ${{ secrets.NASTY_POOL }}
CSI_IMAGE_TAG: ${{ needs.compute-tag.outputs.image_tag }}
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
SMB_USERNAME: ${{ secrets.SMB_USERNAME }}
SMB_PASSWORD: ${{ secrets.SMB_PASSWORD }}
run: |
echo "=== Running Shared E2E Tests ==="
# Run shared tests from the root e2e/ directory (not nfs/ or nvmeof/)
# These tests use "both" protocol setup (NFS, NVMe-oF, and iSCSI)
ginkgo -v --timeout=70m --flake-attempts=2 ./tests/e2e/
- name: Upload diagnostic logs
if: failure()
uses: actions/upload-artifact@v6
with:
name: e2e-shared-diagnostics
path: /tmp/test-logs/
retention-days: 7
# ==========================================
# Summary
# ==========================================
# Note: All integration tests have been migrated to Ginkgo E2E tests.
# - Protocol-specific tests: tests/e2e/nfs/, tests/e2e/nvmeof/, tests/e2e/iscsi/, tests/e2e/smb/
# - Shared tests (all protocols): tests/e2e/*.go (snapshot_restore, stress, name_templating, etc.)
test-summary:
name: "Test Summary"
runs-on: new
timeout-minutes: 5
needs: [ginkgo-e2e-nfs, ginkgo-e2e-nvmeof, ginkgo-e2e-iscsi, ginkgo-e2e-smb, ginkgo-e2e-shared]
if: always()
steps:
- name: Generate Test Summary
uses: actions/github-script@v8
with:
script: |
const e2eResults = {
'NFS E2E': '${{ needs.ginkgo-e2e-nfs.result }}',
'NVMe-oF E2E': '${{ needs.ginkgo-e2e-nvmeof.result }}',
'iSCSI E2E': '${{ needs.ginkgo-e2e-iscsi.result }}',
'SMB E2E': '${{ needs.ginkgo-e2e-smb.result }}',
'Shared E2E': '${{ needs.ginkgo-e2e-shared.result }}',
};
const getIcon = (result) => {
switch(result) {
case 'success': return '✅';
case 'failure': return '❌';
case 'skipped': return '⏭️';
case 'cancelled': return '🚫';
default: return '⚪';
}
};
let passed = 0, failed = 0, skipped = 0;
for (const result of Object.values(e2eResults)) {
if (result === 'success') passed++;
else if (result === 'failure') failed++;
else if (result === 'skipped') skipped++;
}
const total = Object.keys(e2eResults).length;
let summary = `# Integration Test Results\n\n`;
// Overall status banner
if (failed > 0) {
summary += `> ### ${failed} test(s) failed\n\n`;
} else if (passed === total) {
summary += `> ### All ${total} tests passed!\n\n`;
} else {
summary += `> ### ${passed}/${passed + failed} tests passed (${skipped} skipped)\n\n`;
}
// Quick stats
summary += `| Passed | Failed | Skipped | Total |\n`;
summary += `|:------:|:------:|:-------:|:-----:|\n`;
summary += `| ${passed} | ${failed} | ${skipped} | ${total} |\n\n`;
// E2E Tests
summary += `## E2E Tests\n\n`;
summary += `| Test Suite | Status |\n`;
summary += `|------------|:------:|\n`;
for (const [test, result] of Object.entries(e2eResults)) {
summary += `| ${test} | ${getIcon(result)} |\n`;
}
summary += `\n_Note: Shared E2E includes Snapshot Restore, Volume Stress, and Snapshot Stress tests for NFS, NVMe-oF, iSCSI, and SMB._\n`;
// Write to job summary
await core.summary.addRaw(summary).write();
// Fail if any test failed
if (failed > 0) {
core.setFailed(`${failed} test(s) failed`);
}