Skip to content
Draft
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
39 changes: 39 additions & 0 deletions .github/scripts/e2e-prepare-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Writes non-secret env to $RUNNER_TEMP/e2e-env.sh and materializes secrets into temp files.
# Never echoes secret values.

set -euo pipefail

ENV_FILE="${RUNNER_TEMP}/e2e-env.sh"
: >"$ENV_FILE"

write_env() {
printf 'export %s=%q\n' "$1" "$2" >>"$ENV_FILE"
}

if [[ -n "${E2E_SSH_PRIVATE_KEY:-}" ]]; then
KEY_FILE="${RUNNER_TEMP}/e2e_ssh_key"
printf '%s\n' "$E2E_SSH_PRIVATE_KEY" >"$KEY_FILE"
chmod 600 "$KEY_FILE"
write_env SSH_PRIVATE_KEY "$KEY_FILE"
fi

if [[ -n "${E2E_SSH_PUBLIC_KEY:-}" ]]; then
PUB_FILE="${RUNNER_TEMP}/e2e_ssh_pub"
printf '%s\n' "$E2E_SSH_PUBLIC_KEY" >"$PUB_FILE"
chmod 644 "$PUB_FILE"
write_env SSH_PUBLIC_KEY "$PUB_FILE"
fi

if [[ -n "${E2E_CLUSTER_KUBECONFIG:-}" ]]; then
KC_FILE="${RUNNER_TEMP}/e2e_kubeconfig"
printf '%s' "$E2E_CLUSTER_KUBECONFIG" | base64 -d >"$KC_FILE"
chmod 600 "$KC_FILE"
write_env KUBE_CONFIG_PATH "$KC_FILE"
fi

write_env GOMODCACHE "${GOMODCACHE:-${RUNNER_TEMP}/e2e-gomodcache}"
write_env GOCACHE "${GOCACHE:-${RUNNER_TEMP}/e2e-gocache}"
write_env E2E_ARTIFACT_DIR "${E2E_ARTIFACT_DIR:-${RUNNER_TEMP}/e2e-artifacts}"

echo "e2e-env.sh prepared (secrets written to temp files only)"
309 changes: 309 additions & 0 deletions .github/workflows/e2e-reusable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
# Reusable E2E pipeline: create-cluster → run-tests → teardown-cluster (SDK only, no bash cluster setup).
#
# Caller example (module repo):
# jobs:
# e2e:
# uses: deckhouse/storage-e2e/.github/workflows/e2e-reusable.yml@main
# secrets: inherit
# with:
# module_path: e2e
# cluster_provider: alwaysCreateNew
# cluster_config: e2e/tests/cluster_config.yml
# test_package: ./tests/
# test_timeout: 60m

name: Storage E2E (reusable)

permissions:
contents: read
checks: write
pull-requests: read

on:
workflow_call:
inputs:
module_path:
description: "Path to the module e2e Go module root (contains go.mod and tests/)"
type: string
required: true
cluster_provider:
description: "Cluster provider: alwaysCreateNew | alwaysUseExisting | commander"
type: string
required: true
cluster_config:
description: "Path to cluster_config.yml relative to repository root (for alwaysCreateNew)"
type: string
required: false
default: ""
test_package:
description: "Go package for run-tests (e.g. ./tests/)"
type: string
required: true
label_filter:
description: "Ginkgo label filter; empty runs all tests"
type: string
required: false
default: ""
test_timeout:
description: "go test / ginkgo timeout"
type: string
required: false
default: "60m"
storage_e2e_ref:
description: "Git ref of storage-e2e for checkout (branch, tag, or SHA)"
type: string
required: false
default: "main"
runner_labels:
description: "JSON array of runner labels, e.g. [\"self-hosted\",\"regular\"]"
type: string
required: false
default: '["self-hosted","regular"]'
secrets:
E2E_SSH_PRIVATE_KEY:
required: false
E2E_SSH_PUBLIC_KEY:
required: false
E2E_SSH_HOST:
required: false
E2E_SSH_USER:
required: false
E2E_SSH_JUMP_HOST:
required: false
E2E_SSH_JUMP_USER:
required: false
E2E_CLUSTER_KUBECONFIG:
required: false
E2E_TEST_CLUSTER_CREATE_MODE:
required: false
E2E_TEST_CLUSTER_STORAGE_CLASS:
required: false
E2E_TEST_CLUSTER_CLEANUP:
required: false
E2E_DECKHOUSE_LICENSE:
required: false
E2E_REGISTRY_DOCKER_CFG:
required: false
GOPROXY:
required: false

defaults:
run:
shell: bash

env:
E2E_ARTIFACT_DIR: ${{ runner.temp }}/e2e-artifacts
GOMODCACHE: ${{ runner.temp }}/e2e-gomodcache
GOCACHE: ${{ runner.temp }}/e2e-gocache

jobs:
create-cluster:
name: create-cluster
runs-on: ${{ fromJSON(inputs.runner_labels) }}
outputs:
artifact_dir: ${{ env.E2E_ARTIFACT_DIR }}
steps:
- name: Checkout module repository
uses: actions/checkout@v4

- name: Checkout storage-e2e
uses: actions/checkout@v4
with:
repository: deckhouse/storage-e2e
ref: ${{ inputs.storage_e2e_ref }}
path: storage-e2e

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: storage-e2e/go.mod
cache-dependency-path: |
storage-e2e/go.sum
${{ inputs.module_path }}/go.sum

- name: Prepare SSH and kubeconfig (no secret values in logs)
run: storage-e2e/.github/scripts/e2e-prepare-env.sh
env:
E2E_SSH_PRIVATE_KEY: ${{ secrets.E2E_SSH_PRIVATE_KEY }}
E2E_SSH_PUBLIC_KEY: ${{ secrets.E2E_SSH_PUBLIC_KEY }}
E2E_CLUSTER_KUBECONFIG: ${{ secrets.E2E_CLUSTER_KUBECONFIG }}

- name: Build storage-e2e CLI
run: go build -o "${{ runner.temp }}/storage-e2e" ./cmd/e2e
working-directory: storage-e2e

- name: create-cluster (ClusterProvider SDK)
working-directory: ${{ inputs.module_path }}
run: |
set -euo pipefail
source "${{ runner.temp }}/e2e-env.sh"
mkdir -p "${GOMODCACHE}" "${GOCACHE}" "${E2E_ARTIFACT_DIR}"
CONFIG_ARG=""
if [ -n "${{ inputs.cluster_config }}" ]; then
CONFIG_ARG="--config ${{ github.workspace }}/${{ inputs.cluster_config }}"
fi
"${{ runner.temp }}/storage-e2e" create-cluster \
--provider "${{ inputs.cluster_provider }}" \
${CONFIG_ARG} \
--artifact-dir "${E2E_ARTIFACT_DIR}"
env:
TEST_CLUSTER_CREATE_MODE: ${{ inputs.cluster_provider }}
TEST_CLUSTER_NAMESPACE: e2e-${{ github.event.repository.name }}-${{ github.run_id }}
TEST_CLUSTER_STORAGE_CLASS: ${{ secrets.E2E_TEST_CLUSTER_STORAGE_CLASS }}
TEST_CLUSTER_CLEANUP: ${{ secrets.E2E_TEST_CLUSTER_CLEANUP }}
DKP_LICENSE_KEY: ${{ secrets.E2E_DECKHOUSE_LICENSE }}
REGISTRY_DOCKER_CFG: ${{ secrets.E2E_REGISTRY_DOCKER_CFG }}
SSH_HOST: ${{ secrets.E2E_SSH_HOST }}
SSH_USER: ${{ secrets.E2E_SSH_USER }}
SSH_JUMP_HOST: ${{ secrets.E2E_SSH_JUMP_HOST }}
SSH_JUMP_USER: ${{ secrets.E2E_SSH_JUMP_USER }}
COMMANDER_URL: ${{ secrets.COMMANDER_URL }}
COMMANDER_TOKEN: ${{ secrets.COMMANDER_TOKEN }}
LOG_LEVEL: ${{ vars.E2E_LOG_LEVEL || 'info' }}

- name: Upload cluster session artifact
uses: actions/upload-artifact@v4
with:
name: e2e-cluster-session-${{ github.run_id }}
path: ${{ env.E2E_ARTIFACT_DIR }}
retention-days: 2
if-no-files-found: error

run-tests:
name: run-tests
needs: create-cluster
runs-on: ${{ fromJSON(inputs.runner_labels) }}
steps:
- name: Checkout module repository
uses: actions/checkout@v4

- name: Checkout storage-e2e
uses: actions/checkout@v4
with:
repository: deckhouse/storage-e2e
ref: ${{ inputs.storage_e2e_ref }}
path: storage-e2e

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: ${{ inputs.module_path }}/go.mod
cache-dependency-path: ${{ inputs.module_path }}/go.sum

- name: Download cluster session
uses: actions/download-artifact@v4
with:
name: e2e-cluster-session-${{ github.run_id }}
path: ${{ env.E2E_ARTIFACT_DIR }}

- name: Prepare SSH (run-tests)
run: storage-e2e/.github/scripts/e2e-prepare-env.sh
env:
E2E_SSH_PRIVATE_KEY: ${{ secrets.E2E_SSH_PRIVATE_KEY }}
E2E_SSH_PUBLIC_KEY: ${{ secrets.E2E_SSH_PUBLIC_KEY }}
E2E_CLUSTER_KUBECONFIG: ${{ secrets.E2E_CLUSTER_KUBECONFIG }}

- name: Build storage-e2e CLI
run: go build -o "${{ runner.temp }}/storage-e2e" ./cmd/e2e
working-directory: storage-e2e

- name: Pin storage-e2e module to checked-out ref
working-directory: ${{ inputs.module_path }}
run: go mod edit -replace=github.com/deckhouse/storage-e2e=${{ github.workspace }}/storage-e2e

- name: run-tests
working-directory: ${{ inputs.module_path }}
run: |
set -euo pipefail
source "${{ runner.temp }}/e2e-env.sh"
source "${E2E_ARTIFACT_DIR}/run-env.sh"
mkdir -p "${GOMODCACHE}" "${GOCACHE}"
go mod download
LABEL_ARGS=()
if [ -n "${E2E_LABEL_FILTER:-}" ]; then
LABEL_ARGS=(--label-filter "${E2E_LABEL_FILTER}")
fi
"${{ runner.temp }}/storage-e2e" run-tests \
--package "${{ inputs.test_package }}" \
--timeout "${{ inputs.test_timeout }}" \
--artifact-dir "${E2E_ARTIFACT_DIR}" \
"${LABEL_ARGS[@]}"
env:
DKP_LICENSE_KEY: ${{ secrets.E2E_DECKHOUSE_LICENSE }}
REGISTRY_DOCKER_CFG: ${{ secrets.E2E_REGISTRY_DOCKER_CFG }}
SSH_JUMP_HOST: ${{ secrets.E2E_SSH_JUMP_HOST }}
SSH_JUMP_USER: ${{ secrets.E2E_SSH_JUMP_USER }}
E2E_LABEL_FILTER: ${{ inputs.label_filter }}
CI: "true"

- name: Publish JUnit to PR checks
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: ${{ env.E2E_ARTIFACT_DIR }}/junit.xml
check_name: E2E (${{ inputs.test_package }})
comment_mode: off

- name: Upload JUnit artifact
uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-junit-${{ github.run_id }}
path: ${{ env.E2E_ARTIFACT_DIR }}/junit.xml
retention-days: 14
if-no-files-found: ignore

teardown-cluster:
name: teardown-cluster
needs: [create-cluster, run-tests]
if: always()
runs-on: ${{ fromJSON(inputs.runner_labels) }}
steps:
- name: Checkout storage-e2e
uses: actions/checkout@v4
with:
repository: deckhouse/storage-e2e
ref: ${{ inputs.storage_e2e_ref }}
path: storage-e2e

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: storage-e2e/go.mod

- name: Download cluster session
uses: actions/download-artifact@v4
with:
name: e2e-cluster-session-${{ github.run_id }}
path: ${{ env.E2E_ARTIFACT_DIR }}

- name: Prepare SSH (teardown)
run: storage-e2e/.github/scripts/e2e-prepare-env.sh
env:
E2E_SSH_PRIVATE_KEY: ${{ secrets.E2E_SSH_PRIVATE_KEY }}
E2E_SSH_PUBLIC_KEY: ${{ secrets.E2E_SSH_PUBLIC_KEY }}

- name: Build storage-e2e CLI
run: go build -o "${{ runner.temp }}/storage-e2e" ./cmd/e2e
working-directory: storage-e2e

- name: teardown-cluster (ClusterProvider SDK)
run: |
set -euo pipefail
source "${{ runner.temp }}/e2e-env.sh"
mkdir -p "${GOMODCACHE}" "${GOCACHE}"
"${{ runner.temp }}/storage-e2e" teardown-cluster --artifact-dir "${E2E_ARTIFACT_DIR}"
env:
TEST_CLUSTER_CLEANUP: ${{ secrets.E2E_TEST_CLUSTER_CLEANUP }}
SSH_HOST: ${{ secrets.E2E_SSH_HOST }}
SSH_USER: ${{ secrets.E2E_SSH_USER }}
SSH_JUMP_HOST: ${{ secrets.E2E_SSH_JUMP_HOST }}
SSH_JUMP_USER: ${{ secrets.E2E_SSH_JUMP_USER }}
COMMANDER_URL: ${{ secrets.COMMANDER_URL }}
COMMANDER_TOKEN: ${{ secrets.COMMANDER_TOKEN }}
DKP_LICENSE_KEY: ${{ secrets.E2E_DECKHOUSE_LICENSE }}
REGISTRY_DOCKER_CFG: ${{ secrets.E2E_REGISTRY_DOCKER_CFG }}

- name: Cleanup temp credentials
if: always()
run: rm -f "${{ runner.temp }}/e2e-env.sh" "${{ runner.temp }}/e2e_ssh_key" "${{ runner.temp }}/e2e_kubeconfig" 2>/dev/null || true
36 changes: 36 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# storage-e2e — local and CI entrypoints share cmd/e2e

GO ?= go
E2E_BIN ?= bin/e2e
E2E_PROVIDER ?= alwaysCreateNew
E2E_CONFIG ?=
E2E_PACKAGE ?= ./tests/...
E2E_LABEL_FILTER ?=
E2E_ARTIFACT_DIR ?= /tmp/e2e
E2E_TIMEOUT ?= 60m

.PHONY: build-e2e e2e e2e-create e2e-run e2e-teardown help

help:
@echo "Targets:"
@echo " build-e2e Build bin/e2e CLI"
@echo " e2e create-cluster + run-tests + teardown-cluster (full cycle)"
@echo " e2e-create create-cluster only"
@echo " e2e-run run-tests only (expects prior create; uses E2E_ARTIFACT_DIR)"
@echo " e2e-teardown teardown-cluster only"

build-e2e:
$(GO) build -o $(E2E_BIN) ./cmd/e2e

e2e: build-e2e e2e-create e2e-run e2e-teardown

e2e-create: build-e2e
@test -n "$(E2E_CONFIG)" || (echo "E2E_CONFIG must point to cluster_config.yml for alwaysCreateNew"; exit 1)
$(E2E_BIN) create-cluster --provider $(E2E_PROVIDER) --config $(E2E_CONFIG) --artifact-dir $(E2E_ARTIFACT_DIR)

e2e-run: build-e2e
$(E2E_BIN) run-tests --package $(E2E_PACKAGE) --artifact-dir $(E2E_ARTIFACT_DIR) \
$(if $(E2E_LABEL_FILTER),--label-filter "$(E2E_LABEL_FILTER)",)

e2e-teardown: build-e2e
$(E2E_BIN) teardown-cluster --artifact-dir $(E2E_ARTIFACT_DIR)
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

End-to-end tests for Deckhouse storage components.

## CI (reusable workflow)

See [docs/CI.md](docs/CI.md) for the three-stage pipeline (`create-cluster` → `run-tests` → `teardown-cluster`), `cmd/e2e`, and module integration.

## Quick Start

1. Create test with script: `cd tests && ./create-test.sh <your-test-name>`
Expand Down
Loading
Loading