Imported from openshift-eng/ai-helpers (
plugins/ote-migration/skills/ote-migration-workflow/SKILL.md). Install upstream withnpx skills add openshift-eng/ai-helpers --skill ote-migration-workflow. Copyright stays with the author.
OTE Migration Workflow Skill
This skill provides step-by-step implementation guidance for the complete OTE migration workflow.
When to Use This Skill
Use this skill when executing the /ote-migration:migrate command to automate the migration of OpenShift component repositories to the openshift-tests-extension (OTE) framework.
Prerequisites
- Go toolchain (1.21+)
- Git installed and configured
- Access to openshift-tests-private repository:
- Option 1: Existing local clone (with optional update)
- Option 2: Git credentials to clone from
git@github.com:openshift/openshift-tests-private.git
- Target component repository:
- Option 1: Local path to existing repository
- Option 2: Git URL to clone repository
Overview
The migration is an 8-phase workflow that collects configuration, sets up repositories, creates structure, generates code, migrates tests, resolves dependencies, integrates with Docker, and provides documentation.
Workflow Summary:
ALL 8 PHASES ARE MANDATORY - EXECUTE EACH PHASE IN ORDER:
- User Input Collection (9 inputs - includes Dockerfile integration choice)
- Repository Setup (source and target)
- Structure Creation (directories and files)
- Code Generation (go.mod, main.go, Makefile, bindata.mk, fixtures.go)
- Test Migration (automated with rollback on failure)
- Dependency Resolution (go mod tidy + vendor + build verification)
- Dockerfile Integration (uses choice from Input 9)
- Final Summary and Next Steps
DO NOT skip Phase 7. After Phase 6 completes, proceed immediately to Phase 7.
Key Design Principles:
- No sig filtering: All tests included without filtering logic
- CMD at root (monorepo):
cmd/<extension-name>-tests-ext/main.go(not under test/) - Simple annotations: [OTP] at beginning of Describe, [Level0] at beginning of test name only
- Single go.mod (monorepo): All dependencies in root go.mod (no separate test module)
- Vendor at root (monorepo): Only
vendor/at repository root - No compress/copy targets: Removed from root Makefile
- 🚨 REQUIRED IMPORTS (DO NOT MODIFY): main.go MUST import both packages:
exutil "github.com/openshift/origin/test/extended/util"- provides the actual CLI type (exutil.CLI)compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"- provides helper functions (NewCLI(),KubeConfigPath())- CRITICAL: Use
*exutil.CLIfor type declarations, NOT*compat_otp.CLI(which doesn't exist)
Migration Phases
Phase 1: User Input Collection (9 inputs)
Collect all necessary information from the user before starting the migration.
CRITICAL INSTRUCTIONS:
- Extension name (Input 4): AUTO-DETECT from target repository - do NOT ask user
- All other inputs: Ask user explicitly using AskUserQuestion tool or direct prompts
- WAIT for user response before proceeding to the next input or phase
- Switch to target repository happens after Input 3 (before auto-detecting extension name)
- Dockerfile integration (Input 10): Ask user choice - will be used in Phase 7
Variables collected (shown as <variable-name>) will be used throughout the migration.
Input 1: Directory Structure Strategy
Ask: "Which directory structure strategy do you want to use?"
Option 1: Monorepo strategy (integrate into existing repo)
- Integrates into existing repository structure
- Uses existing
cmd/andtest/directories - CMD location:
cmd/extension/main.go(at repository root, NOT under test/) - Single go.mod: All dependencies in root go.mod (no separate test module)
- Vendor location:
vendor/at root ONLY
Option 2: Single-module strategy (isolated directory)
- Creates isolated
tests-extension/directory - Self-contained with single
go.mod - CMD location:
tests-extension/cmd/main.go - Vendor location:
tests-extension/vendor/
User selects: 1 or 2
Store the selection in variable: <structure-strategy> (value: "monorepo" or "single-module")
Input 2: Working Directory (Workspace)
Ask: "What is the working directory path for migration workspace?
IMPORTANT: This is a temporary workspace for cloning repositories. Your target repository will be collected in the next step (Input 3), and that's where OTE files will be created."
Purpose:
- Temporary location for cloning repositories that don't exist locally
- Recommendation: Parent directory of your target repo or temporary directory
User provides the path:
- Can be absolute or relative
- Can be current directory (
.) - Will create if doesn't exist
Store in variable: <working-dir>
Input 3: Target Repository
Ask: "What is the path to your target repository, or provide a Git URL to clone?"
- Option 1: Local path - Use existing local repository (e.g.,
/home/user/repos/router) - Option 2: Git URL - Clone from remote repository (e.g.,
git@github.com:openshift/router.git)
Store in variable: <target-repo-path> or <target-repo-url>
Input 3a: Update Local Target Repository (if local target provided)
If a local target repository path was provided:
Ask: "Do you want to update the local target repository? (git fetch && git pull) [Y/n]:"
- Default: Yes
- Store in variable:
<update-target>(value: "yes" or "no")
Input 3b: Validate and Switch to Target Repository
Step 1: Validate and update target repository
For local path:
# Validate target repository exists
if [ ! -d "$TARGET_REPO_PATH" ]; then
echo "❌ ERROR: Target repository does not exist"
exit 1
fi
# Check if git repository and update if requested
if [ -d "$TARGET_REPO_PATH/.git" ]; then
cd "$TARGET_REPO_PATH"
if [ "<update-target>" = "yes" ]; then
CURRENT_BRANCH=$(git branch --show-current)
TARGET_REMOTE=$(git remote -v | awk '{print $1}' | head -1)
git fetch "$TARGET_REMOTE"
git pull "$TARGET_REMOTE" "$CURRENT_BRANCH"
fi
fi
For Git URL:
# Extract repository name
REPO_NAME=$(echo "$TARGET_REPO_URL" | sed -E 's|.*/([^/]+)\.git$|\1|')
cd "$WORKING_DIR"
git clone "$TARGET_REPO_URL" "$REPO_NAME"
TARGET_REPO_PATH="$WORKING_DIR/$REPO_NAME"
# Create feature branch
cd "$TARGET_REPO_PATH"
BRANCH_NAME="ote-migration-$(date +%Y%m%d)"
git checkout -b "$BRANCH_NAME"
Step 2: Switch working directory to target repository
cd "$TARGET_REPO_PATH"
WORKING_DIR="$TARGET_REPO_PATH"
echo "========================================="
echo "Switched to target repository"
echo "Working directory is now: $WORKING_DIR"
echo "========================================="
CRITICAL: From this point forward, all operations happen in the target repository.
Input 4: Extension Name (Auto-Detection)
DO NOT ask the user for this - auto-detect it from the target repository.
cd "$WORKING_DIR"
if [ -d ".git" ]; then
DISCOVERED_REMOTE=$(git remote -v | head -1 | awk '{print $1}')
if [ -n "$DISCOVERED_REMOTE" ]; then
REMOTE_URL=$(git remote get-url "$DISCOVERED_REMOTE" 2>/dev/null)
EXTENSION_NAME=$(echo "$REMOTE_URL" | sed 's/.*[:/]\([^/]*\)\/\([^/]*\)\.git$/\2/' | sed 's/\.git$//')
else
EXTENSION_NAME=$(basename "$WORKING_DIR")
fi
else
EXTENSION_NAME=$(basename "$WORKING_DIR")
fi
echo "Extension name auto-detected: $EXTENSION_NAME"
Store in variable: <extension-name>
Input 4a: Target Test Directory Name (Conditional - Monorepo Mode Only)
This input is ONLY asked if:
- Monorepo strategy is selected (from Input 1)
- Target repository already has
test/e2e/directory
Purpose: When migrating to a repository that already has test/e2e/, we need to create a subdirectory to avoid conflicts with existing tests.
Detection logic:
cd "$WORKING_DIR"
if [ "$STRUCTURE_STRATEGY" = "monorepo" ] && [ -d "test/e2e" ]; then
echo "⚠️ Target repository already has test/e2e/ directory"
echo "Tests will be migrated to a subdirectory under test/e2e/"
# Ask for target test directory name
read -p "What subdirectory name should be used under test/e2e/ for migrated tests? (default: extension): " TARGET_TEST_DIR_NAME
TARGET_TEST_DIR_NAME=${TARGET_TEST_DIR_NAME:-extension} # Default to "extension" if empty
else
# No subdirectory needed - tests go directly in test/e2e/
TARGET_TEST_DIR_NAME=""
fi
If test/e2e exists:
- Prompts: "What subdirectory name should be used under test/e2e/ for migrated tests? (default: extension):"
- Default: "extension"
- Example: If you enter "router", tests will be at
test/e2e/router/ - Example: If you press Enter, tests will be at
test/e2e/extension/ - Store in variable:
<target-test-dir>(default: "extension" if empty)
If test/e2e does NOT exist:
- No prompt needed - tests go directly in test/e2e/
- Store in variable:
<target-test-dir>(empty string "")
Input 5: Local Source Repository (Optional)
Ask: "Do you have a local clone of openshift-tests-private? If yes, provide the path (or press Enter to clone):"
Store in variable: <local-source-path> (empty if user wants to clone)
Input 6: Update Local Source Repository (if local source provided)
If local source provided: Ask: "Do you want to update the local source repository? (git fetch && git pull) [Y/n]:"
Store in variable: <update-source> (value: "yes" or "no")
Input 7: Source Test Subfolder
Ask: "What is the test subfolder name under test/extended/?"
- Example: "networking", "router", "storage"
Store in variable: <test-subfolder>
Input 8: Source Testdata Subfolder (Optional)
IMPORTANT: This determines which testdata fixtures are copied from the source OTE repository. The testdata files are embedded into bindata.go and accessed via FixturePath() calls in tests.
Ask: "What is the testdata subfolder name under test/extended/testdata/?"
Options:
- Press Enter to use the same value as the test subfolder (Input 7)
- Enter a subfolder name (e.g., "router", "networking") if different from test subfolder
- Enter "none" if no testdata fixtures exist for these tests
Default: Same as Input 7 (recommended for most cases)
Examples:
- If test subfolder is "router" and testdata is at
test/extended/testdata/router/, press Enter - If test subfolder is "router" but testdata is at
test/extended/testdata/edge/, enter "edge" - If no testdata files exist, enter "none"
AI MUST execute this verification before asking the user:
# List testdata subdirectories to help user answer
if [ -d "<source-repo>/test/extended/testdata" ]; then
echo "Available testdata subdirectories:"
ls -la "<source-repo>/test/extended/testdata/" | grep "^d" | grep -v "^\.$" | awk '{print $NF}'
else
echo "No testdata directory found at <source-repo>/test/extended/testdata"
fi
Then present the discovered subdirectories to the user and ask for their choice.
Store in variable: <testdata-subfolder>
Input 9: Dockerfile Integration Choice
Ask: "Do you want to update Dockerfiles automatically, or do it manually?"
Options:
- Automated - Let the plugin update your Dockerfiles automatically (with backup)
- Manual - Get instructions to update Dockerfiles yourself
Store in variable: <dockerfile-choice> (value: "automated" or "manual")
Input 9a: Select Dockerfiles to Update (conditional - only if automated)
This input is ONLY asked if user chose "automated" in Input 9.
If user chose automated, search for all Dockerfiles in the target repository and ask user to select:
cd <working-dir> # Should already be in target repository from Input 3
echo "Searching for Dockerfiles in target repository..."
# Search for all Dockerfiles recursively
DOCKERFILES=$(find . -type f \( -name "Dockerfile" -o -name "Dockerfile.*" \) ! -path "*/vendor/*" ! -path "*/.git/*" ! -path "*/tests-extension/*" 2>/dev/null)
if [ -z "$DOCKERFILES" ]; then
echo "⚠️ No Dockerfiles found in repository"
echo "You can add Dockerfiles later and integrate manually, or continue without Dockerfile integration"
SELECTED_DOCKERFILES=""
else
# Display found Dockerfiles
echo ""
echo "Found Dockerfiles:"
echo "$DOCKERFILES" | nl -w2 -s'. '
echo ""
fi
If Dockerfiles were found, ask user to select:
Ask: "Which Dockerfile(s) do you want to update?"
Options:
- Enter a number (e.g.,
1for first Dockerfile) - Enter
allto update all Dockerfiles - Enter
noneto skip Dockerfile integration
Example:
Found Dockerfiles:
1. ./Dockerfile
2. ./Dockerfile.rhel8
3. ./build/Dockerfile
Which Dockerfile(s) do you want to update? (number, 'all', or 'none'):
Store user selection:
# Get user choice
CHOICE=<user-input>
if [ -z "$DOCKERFILES" ] || [ "$CHOICE" = "none" ]; then
SELECTED_DOCKERFILES=""
echo "Skipping Dockerfile integration"
elif [ "$CHOICE" = "all" ]; then
SELECTED_DOCKERFILES="$DOCKERFILES"
echo "Selected: All Dockerfiles"
else
# Convert to array and get selected file
DOCKERFILES_ARRAY=($DOCKERFILES)
if [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le "${#DOCKERFILES_ARRAY[@]}" ]; then
SELECTED_DOCKERFILES="${DOCKERFILES_ARRAY[$((CHOICE-1))]}"
echo "Selected: $SELECTED_DOCKERFILES"
else
echo "❌ Invalid choice"
exit 1
fi
fi
Store in variable: <selected-dockerfiles> (space-separated list of Dockerfile paths, or empty if none)
Display Configuration Summary
Show all collected inputs for user confirmation before proceeding:
========================================
Migration Configuration Summary
========================================
Strategy: <structure-strategy>
Workspace: <working-dir>
Target Repository: <target-repo-path>
Update Target Repo: <update-target or "cloned from URL" or "N/A">
Extension Name: <extension-name>
Target Test Directory: <target-test-dir or "test/e2e (no subdirectory)" or "N/A (single-module)">
Source Repository: <local-source-path or "will clone">
Update Source Repo: <update-source or "will clone" or "N/A">
Test Subfolder: <test-subfolder>
Testdata Subfolder: <testdata-subfolder>
Dockerfile Integration: <dockerfile-choice>
Selected Dockerfiles: <selected-dockerfiles or "manual integration" or "none">
========================================
Example output (local target with existing test/e2e, automated Dockerfile):
========================================
Migration Configuration Summary
========================================
Strategy: monorepo
Workspace: /home/user/repos
Target Repository: /home/user/repos/router
Update Target Repo: yes
Extension Name: router
Target Test Directory: test/e2e/extension
Source Repository: /home/user/openshift-tests-private
Update Source Repo: yes
Test Subfolder: router
Testdata Subfolder: router
Dockerfile Integration: automated
Selected Dockerfiles: ./Dockerfile, ./Dockerfile.rhel8
========================================
Example output (local target without test/e2e, automated Dockerfile):
========================================
Migration Configuration Summary
========================================
Strategy: monorepo
Workspace: /home/user/repos
Target Repository: /home/user/repos/mycomponent
Update Target Repo: yes
Extension Name: mycomponent
Target Test Directory: test/e2e (no subdirectory)
Source Repository: /home/user/openshift-tests-private
Update Source Repo: yes
Test Subfolder: mycomponent
Testdata Subfolder: mycomponent
Dockerfile Integration: automated
Selected Dockerfiles: ./Dockerfile
========================================
Example output (cloned target, manual Dockerfile, single-module strategy):
========================================
Migration Configuration Summary
========================================
Strategy: single-module
Workspace: /tmp/migration
Target Repository: /tmp/migration/router
Update Target Repo: cloned from URL
Extension Name: router
Target Test Directory: N/A (single-module)
Source Repository: will clone
Update Source Repo: N/A
Test Subfolder: router
Testdata Subfolder: router
Dockerfile Integration: manual
Selected Dockerfiles: manual integration
========================================
Ask: "Proceed with migration? [Y/n]:"
Phase 1 Validation Checkpoint
MANDATORY VALIDATION:
# Verify extension name detected
if [ -z "$EXTENSION_NAME" ]; then
echo "❌ ERROR: Extension name not detected"
exit 1
fi
# Verify strategy selected
if [ -z "$STRUCTURE_STRATEGY" ]; then
echo "❌ ERROR: Strategy not selected"
exit 1
fi
# Verify target repository path collected
if [ -z "$TARGET_REPO_PATH" ]; then
echo "❌ ERROR: Target repository path not collected"
exit 1
fi
# Verify working directory switched to target
if [ "$WORKING_DIR" != "$TARGET_REPO_PATH" ]; then
echo "❌ ERROR: Working directory not switched to target"
exit 1
fi
echo "✅ Phase 1 Validation Complete"
Phase 2: Repository Setup
Step 1: Setup Source Repository
For local source:
SOURCE_REPO="<local-source-path>"
if [ "<update-source>" = "yes" ]; then
cd "$SOURCE_REPO"
CURRENT_BRANCH=$(git branch --show-current)
# Checkout main/master if on different branch
if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then
if git show-ref --verify --quiet refs/heads/main; then
git checkout main
TARGET_BRANCH="main"
else
git checkout master
TARGET_BRANCH="master"
fi
else
TARGET_BRANCH="$CURRENT_BRANCH"
fi
SOURCE_REMOTE=$(git remote -v | awk '{print $1}' | head -1)
git fetch "$SOURCE_REMOTE"
git pull "$SOURCE_REMOTE" "$TARGET_BRANCH"
fi
For cloning:
cd <working-dir>
if [ -d "openshift-tests-private" ]; then
cd openshift-tests-private
SOURCE_REMOTE=$(git remote -v | grep 'openshift/openshift-tests-private' | head -1 | awk '{print $1}')
git fetch "$SOURCE_REMOTE"
git pull "$SOURCE_REMOTE" master || git pull "$SOURCE_REMOTE" main
cd ..
else
git clone git@github.com:openshift/openshift-tests-private.git openshift-tests-private
fi
SOURCE_REPO="openshift-tests-private"
Set source paths:
if [ -z "<test-subfolder>" ]; then
SOURCE_TEST_PATH="$SOURCE_REPO/test/extended"
else
SOURCE_TEST_PATH="$SOURCE_REPO/test/extended/<test-subfolder>"
fi
if [ "<testdata-subfolder>" = "none" ]; then
SOURCE_TESTDATA_PATH=""
elif [ -z "<testdata-subfolder>" ]; then
SOURCE_TESTDATA_PATH="$SOURCE_REPO/test/extended/testdata"
else
SOURCE_TESTDATA_PATH="$SOURCE_REPO/test/extended/testdata/<testdata-subfolder>"
fi
Phase 3: Structure Creation
Step 1: Create Directory Structure
For Monorepo Strategy:
cd <working-dir>
# Set directory paths based on whether test/e2e already exists
if [ -n "$TARGET_TEST_DIR_NAME" ]; then
# test/e2e exists - use subdirectory
TEST_CODE_DIR="test/e2e/$TARGET_TEST_DIR_NAME"
TESTDATA_DIR="test/e2e/$TARGET_TEST_DIR_NAME/testdata"
echo "Using test subdirectory: test/e2e/$TARGET_TEST_DIR_NAME/"
else
# No test/e2e - use test/e2e directly
TEST_CODE_DIR="test/e2e"
TESTDATA_DIR="test/e2e/testdata"
echo "Using test directory: test/e2e/"
fi
# Create directories
# IMPORTANT: cmd follows pattern cmd/extension/, NOT under test/
mkdir -p "cmd/extension"
mkdir -p bin
mkdir -p "$TEST_CODE_DIR"
mkdir -p "$TESTDATA_DIR"
echo "✅ Created monorepo structure"
echo " CMD directory: cmd/extension/"
echo " Test code: $TEST_CODE_DIR"
echo " Testdata: $TESTDATA_DIR"
For Single-Module Strategy:
cd <working-dir>
mkdir -p tests-extension
cd tests-extension
mkdir -p cmd
mkdir -p bin
mkdir -p test/e2e
mkdir -p test/e2e/testdata
echo "✅ Created single-module structure"
Step 2: Copy Test Files
For Monorepo:
cp -r "$SOURCE_TEST_PATH"/* "$TEST_CODE_DIR"/
echo "Copied $(find "$TEST_CODE_DIR" -name '*_test.go' | wc -l) test files"
For Single-Module:
cp -r "$SOURCE_TEST_PATH"/* test/e2e/
echo "Copied $(find test/e2e -name '*_test.go' | wc -l) test files"
Step 3: Copy Testdata
IMPORTANT: This step copies fixture files from the source OTE repository's testdata directory. If testdata files are not copied, bindata generation will only embed fixtures.go, causing runtime panics when tests load fixture files via FixturePath().
For Monorepo:
if [ -n "$SOURCE_TESTDATA_PATH" ] && [ "$SOURCE_TESTDATA_PATH" != "" ]; then
echo "Copying testdata from: $SOURCE_TESTDATA_PATH"
echo "Target testdata directory: $TESTDATA_DIR"
if [ -n "<testdata-subfolder>" ] && [ "<testdata-subfolder>" != "none" ]; then
# Copy with subfolder structure preserved
mkdir -p "$TESTDATA_DIR/<testdata-subfolder>"
cp -rv "$SOURCE_TESTDATA_PATH"/* "$TESTDATA_DIR/<testdata-subfolder>/" || {
echo "❌ Failed to copy testdata files"
exit 1
}
echo "✅ Copied testdata files to $TESTDATA_DIR/<testdata-subfolder>/"
ls -la "$TESTDATA_DIR/<testdata-subfolder>/" | head -10
else
# Copy without subfolder (flatten)
cp -rv "$SOURCE_TESTDATA_PATH"/* "$TESTDATA_DIR/" || {
echo "❌ Failed to copy testdata files"
exit 1
}
echo "✅ Copied testdata files to $TESTDATA_DIR/"
ls -la "$TESTDATA_DIR/" | head -10
fi
else
echo "⚠️ No testdata files to copy (SOURCE_TESTDATA_PATH is empty or 'none')"
fi
For Single-Module:
if [ -n "$SOURCE_TESTDATA_PATH" ] && [ "$SOURCE_TESTDATA_PATH" != "" ]; then
echo "Copying testdata from: $SOURCE_TESTDATA_PATH"
echo "Target testdata directory: test/e2e/testdata"
if [ -n "<testdata-subfolder>" ] && [ "<testdata-subfolder>" != "none" ]; then
# Copy with subfolder structure preserved
mkdir -p "test/e2e/testdata/<testdata-subfolder>"
cp -rv "$SOURCE_TESTDATA_PATH"/* "test/e2e/testdata/<testdata-subfolder>/" || {
echo "❌ Failed to copy testdata files"
exit 1
}
echo "✅ Copied testdata files to test/e2e/testdata/<testdata-subfolder>/"
ls -la "test/e2e/testdata/<testdata-subfolder>/" | head -10
else
# Copy without subfolder (flatten)
cp -rv "$SOURCE_TESTDATA_PATH"/* test/e2e/testdata/ || {
echo "❌ Failed to copy testdata files"
exit 1
}
echo "✅ Copied testdata files to test/e2e/testdata/"
ls -la "test/e2e/testdata/" | head -10
fi
else
echo "⚠️ No testdata files to copy (SOURCE_TESTDATA_PATH is empty or 'none')"
fi
# Verify testdata files were copied (excluding fixtures.go and bindata.go)
TESTDATA_FILE_COUNT=$(find "$TESTDATA_DIR" -type f ! -name "fixtures.go" ! -name "bindata.go" 2>/dev/null | wc -l)
if [ "$TESTDATA_FILE_COUNT" -eq 0 ]; then
echo "⚠️ WARNING: No testdata fixture files found in $TESTDATA_DIR"
echo "This may cause test failures if tests use FixturePath() to load fixtures."
echo "Verify that testdata-subfolder input was correct."
fi
For Single-Module:
# Same validation for single-module
TESTDATA_FILE_COUNT=$(find test/e2e/testdata -type f ! -name "fixtures.go" ! -name "bindata.go" 2>/dev/null | wc -l)
if [ "$TESTDATA_FILE_COUNT" -eq 0 ]; then
echo "⚠️ WARNING: No testdata fixture files found in test/e2e/testdata"
echo "This may cause test failures if tests use FixturePath() to load fixtures."
echo "Verify that testdata-subfolder input was correct."
fi
Phase 4: Code Generation
🚨 CRITICAL: DO NOT MODIFY IMPORTS 🚨
The generated main.go file contains REQUIRED imports that MUST NOT be changed:
// REQUIRED IMPORTS - DO NOT MODIFY:
import (
exutil "github.com/openshift/origin/test/extended/util" // Provides CLI type
compat_otp "github.com/openshift/origin/test/extended/util/compat_otp" // Provides helper functions
// ...
)
func main() {
exutil.InitStandardFlags() // Initialize flags
// ...
componentSpecs.AddBeforeAll(func() {
if err := compat_otp.InitTest(false); err != nil { // Initialize OTE framework
panic(err)
}
})
}
Why these imports are required:
exutilprovidesInitStandardFlags()to register kubeconfig flags and theCLItypecompat_otpprovidesInitTest()to initialize the test framework and helper functions likeNewCLI()- Both packages exist in
github.com/openshift/originand serve different purposes - The
compat_otppackage is a REAL package path, NOT a placeholder
DO NOT:
- ❌ Change
*exutil.CLIto*compat_otp.CLI(compat_otp.CLI type doesn't exist) - ❌ Remove either import - both are required
- ❌ Remove the import aliases
- ❌ "Fix" what you think are incorrect imports
Verification checks are included in the template generation to catch any modifications.
Step 1: Generate/Update go.mod Files
For Monorepo Strategy:
cd <working-dir>
# Add OTE test dependencies to root go.mod (single module approach)
echo "Adding OTE test dependencies to root go.mod..."
# Add dependencies
OTE_LATEST=$(git ls-remote https://github.com/openshift-eng/openshift-tests-extension.git refs/heads/main | awk '{print $1}')
OTE_SHORT="${OTE_LATEST:0:12}"
echo "Adding OTE dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift-eng/openshift-tests-extension@$OTE_SHORT"; then
echo "❌ Failed to get openshift-tests-extension"
echo "Retrying..."
sleep 2
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift-eng/openshift-tests-extension@$OTE_SHORT"; then
echo "❌ Failed after retry - you may need to run manually: go get github.com/openshift-eng/openshift-tests-extension@latest"
exit 1
fi
fi
echo "✅ OTE dependency added"
echo "Adding origin dependency..."
# Use a known working version instead of @main to avoid breaking changes
ORIGIN_VERSION="v1.5.0-alpha.3.0.20260310231025-5d3fd0545b5d"
if ! GOTOOLCHAIN=auto GOSUMDB=off go get "github.com/openshift/origin@$ORIGIN_VERSION"; then
echo "❌ Failed to get github.com/openshift/origin@$ORIGIN_VERSION"
echo "Retrying..."
sleep 2
if ! GOTOOLCHAIN=auto GOSUMDB=off go get "github.com/openshift/origin@$ORIGIN_VERSION"; then
echo "❌ Failed after retry - you may need to run manually: GOSUMDB=off go get github.com/openshift/origin@$ORIGIN_VERSION"
exit 1
fi
fi
echo "✅ Origin dependency added"
echo "Adding Ginkgo dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/ginkgo/v2@latest; then
echo "❌ Failed to get ginkgo"
echo "Retrying..."
sleep 2
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/ginkgo/v2@latest; then
echo "❌ Failed after retry - you may need to run manually: go get github.com/onsi/ginkgo/v2@latest"
exit 1
fi
fi
echo "✅ Ginkgo dependency added"
echo "Adding Gomega dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/gomega@latest; then
echo "❌ Failed to get gomega"
echo "Retrying..."
sleep 2
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/gomega@latest; then
echo "❌ Failed after retry - you may need to run manually: go get github.com/onsi/gomega@latest"
exit 1
fi
fi
echo "✅ Gomega dependency added"
# Pin opencontainers dependencies to compatible versions BEFORE go mod tidy
# This prevents go mod tidy from upgrading to incompatible versions
echo "Pinning opencontainers dependencies to compatible versions..."
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/cyphar/filepath-securejoin@v0.4.1
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/runtime-spec@v1.2.0
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/cgroups@v0.0.3
echo "✅ Pinned cyphar/filepath-securejoin to v0.4.1"
echo "✅ Pinned opencontainers/runtime-spec to v1.2.0"
echo "✅ Pinned opencontainers/cgroups to v0.0.3"
# Copy replace directives from openshift-tests-private to root go.mod
# IMPORTANT: Filter out openshift-tests-private itself to avoid importing entire test suite
echo "Copying replace directives from openshift-tests-private..."
if [ -n "$SOURCE_REPO" ]; then
grep -A 1000 "^replace" "$SOURCE_REPO/go.mod" | grep -B 1000 "^)" | \
grep -v "^replace" | grep -v "^)" | \
grep -v "github.com/openshift/openshift-tests-private" > /tmp/replace_directives.txt
# Check if replace block already exists in root go.mod
if ! grep -q "^replace (" go.mod; then
echo "" >> go.mod
echo "replace (" >> go.mod
cat /tmp/replace_directives.txt >> go.mod
echo ")" >> go.mod
else
# Append to existing replace block (before closing parenthesis)
# Find the line number of the closing ) for replace block
REPLACE_CLOSE_LINE=$(grep -n "^replace (" go.mod | head -1 | cut -d: -f1)
# Find next closing ) after replace (
NEXT_CLOSE=$(tail -n +$((REPLACE_CLOSE_LINE + 1)) go.mod | grep -n "^)" | head -1 | cut -d: -f1)
REPLACE_CLOSE_LINE=$((REPLACE_CLOSE_LINE + NEXT_CLOSE))
# Insert before closing )
head -n $((REPLACE_CLOSE_LINE - 1)) go.mod > /tmp/go.mod.tmp
cat /tmp/replace_directives.txt >> /tmp/go.mod.tmp
tail -n +$REPLACE_CLOSE_LINE go.mod >> /tmp/go.mod.tmp
mv /tmp/go.mod.tmp go.mod
fi
rm -f /tmp/replace_directives.txt
fi
# Step 4b: Align Ginkgo version with OTE framework (newer version is backward compatible)
# IMPORTANT: Use OTE's Ginkgo version (December 2024), NOT OTP's older version (August 2024)
# The December 2024 fork is backward compatible with August 2024 code from OTP
echo "Aligning Ginkgo version with OTE framework..."
OTE_REPO="https://github.com/openshift-eng/openshift-tests-extension.git"
OTE_GINKGO_VERSION=$(git ls-remote "$OTE_REPO" refs/heads/main | xargs -I {} git ls-remote https://github.com/openshift-eng/openshift-tests-extension {} | git archive --remote=https://github.com/openshift-eng/openshift-tests-extension HEAD go.mod 2>/dev/null | tar -xO | grep "github.com/onsi/ginkgo/v2 =>" | awk '{print $NF}' 2>/dev/null || echo "v2.6.1-0.20241205171354-8006f302fd12")
# Fallback to known working version if detection fails
if [ -z "$OTE_GINKGO_VERSION" ]; then
OTE_GINKGO_VERSION="v2.6.1-0.20241205171354-8006f302fd12"
echo "ℹ️ Using fallback OTE Ginkgo version: $OTE_GINKGO_VERSION"
else
echo "ℹ️ Detected OTE Ginkgo version: $OTE_GINKGO_VERSION"
fi
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift/onsi-ginkgo/v2@$OTE_GINKGO_VERSION"
echo "✅ Ginkgo aligned to OTE framework version (backward compatible with OTP)"
echo "✅ Monorepo go.mod setup complete (single module with all dependencies)"
For Single-Module Strategy:
cd <working-dir>/tests-extension
# Extract Go version from target repo or use default
if [ -f "$TARGET_REPO/go.mod" ]; then
GO_VERSION=$(grep '^go ' "$TARGET_REPO/go.mod" | awk '{print $2}')
else
GO_VERSION="1.21"
fi
go mod init github.com/openshift/<extension-name>-tests-extension
sed -i "s/^go .*/go $GO_VERSION/" go.mod
# Add dependencies
OTE_LATEST=$(git ls-remote https://github.com/openshift-eng/openshift-tests-extension.git refs/heads/main | awk '{print $1}')
OTE_SHORT="${OTE_LATEST:0:12}"
echo "Adding OTE dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift-eng/openshift-tests-extension@$OTE_SHORT"; then
echo "❌ Failed to get openshift-tests-extension"
echo "Retrying..."
sleep 2
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift-eng/openshift-tests-extension@$OTE_SHORT"; then
echo "❌ Failed after retry - you may need to run manually: go get github.com/openshift-eng/openshift-tests-extension@latest"
exit 1
fi
fi
echo "✅ OTE dependency added"
echo "Adding origin dependency..."
# Use a known working version instead of @main to avoid breaking changes
ORIGIN_VERSION="v1.5.0-alpha.3.0.20260310231025-5d3fd0545b5d"
if ! GOTOOLCHAIN=auto GOSUMDB=off go get "github.com/openshift/origin@$ORIGIN_VERSION"; then
echo "❌ Failed to get github.com/openshift/origin@$ORIGIN_VERSION"
echo "Retrying..."
sleep 2
if ! GOTOOLCHAIN=auto GOSUMDB=off go get "github.com/openshift/origin@$ORIGIN_VERSION"; then
echo "❌ Failed after retry - you may need to run manually: GOSUMDB=off go get github.com/openshift/origin@$ORIGIN_VERSION"
exit 1
fi
fi
echo "✅ Origin dependency added"
echo "Adding Ginkgo dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/ginkgo/v2@latest; then
echo "❌ Failed to get ginkgo"
echo "Retrying..."
sleep 2
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/ginkgo/v2@latest; then
echo "❌ Failed after retry - you may need to run manually: go get github.com/onsi/ginkgo/v2@latest"
exit 1
fi
fi
echo "✅ Ginkgo dependency added"
echo "Adding Gomega dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/gomega@latest; then
echo "❌ Failed to get gomega"
echo "Retrying..."
sleep 2
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/gomega@latest; then
echo "❌ Failed after retry - you may need to run manually: go get github.com/onsi/gomega@latest"
exit 1
fi
fi
echo "✅ Gomega dependency added"
# Pin opencontainers dependencies to compatible versions BEFORE go mod tidy
# This prevents go mod tidy from upgrading to incompatible versions
echo "Pinning opencontainers dependencies to compatible versions..."
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/cyphar/filepath-securejoin@v0.4.1
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/runtime-spec@v1.2.0
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/cgroups@v0.0.3
echo "✅ Pinned cyphar/filepath-securejoin to v0.4.1"
echo "✅ Pinned opencontainers/runtime-spec to v1.2.0"
echo "✅ Pinned opencontainers/cgroups to v0.0.3"
# Copy replace directives
# IMPORTANT: Filter out openshift-tests-private itself to avoid importing entire test suite
SOURCE_PATH="../$SOURCE_REPO"
grep -A 1000 "^replace" "$SOURCE_PATH/go.mod" | grep -B 1000 "^)" | \
grep -v "^replace" | grep -v "^)" | \
grep -v "github.com/openshift/openshift-tests-private" > /tmp/replace_directives.txt
echo "" >> go.mod
echo "replace (" >> go.mod
cat /tmp/replace_directives.txt >> go.mod
echo ")" >> go.mod
rm -f /tmp/replace_directives.txt
# Step 4b: Align Ginkgo version with OTE framework (newer version is backward compatible)
# IMPORTANT: Use OTE's Ginkgo version (December 2024), NOT OTP's older version (August 2024)
# The December 2024 fork is backward compatible with August 2024 code from OTP
echo "Aligning Ginkgo version with OTE framework..."
OTE_REPO="https://github.com/openshift-eng/openshift-tests-extension.git"
OTE_GINKGO_VERSION=$(git ls-remote "$OTE_REPO" refs/heads/main | xargs -I {} git ls-remote https://github.com/openshift-eng/openshift-tests-extension {} | git archive --remote=https://github.com/openshift-eng/openshift-tests-extension HEAD go.mod 2>/dev/null | tar -xO | grep "github.com/onsi/ginkgo/v2 =>" | awk '{print $NF}' 2>/dev/null || echo "v2.6.1-0.20241205171354-8006f302fd12")
# Fallback to known working version if detection fails
if [ -z "$OTE_GINKGO_VERSION" ]; then
OTE_GINKGO_VERSION="v2.6.1-0.20241205171354-8006f302fd12"
echo "ℹ️ Using fallback OTE Ginkgo version: $OTE_GINKGO_VERSION"
else
echo "ℹ️ Detected OTE Ginkgo version: $OTE_GINKGO_VERSION"
fi
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift/onsi-ginkgo/v2@$OTE_GINKGO_VERSION"
echo "✅ Ginkgo aligned to OTE framework version (backward compatible with OTP)"
# Generate minimal go.sum
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go mod download || echo "⚠️ Will retry in Phase 6"
cd ..
Step 2: Generate Extension Binary (main.go)
For Monorepo Strategy:
IMPORTANT:
- CMD Location:
cmd/extension/main.go(at repository root, NOT under test/) - NO sig filtering logic
- Single module approach: imports test package from same module
cd <working-dir>
MODULE_NAME=$(grep '^module ' go.mod | awk '{print $2}')
# Re-derive variables from Phase 1/3 using IDENTICAL logic
# (Variables don't persist between phases - need to re-calculate)
# EXTENSION_NAME: Use same logic as Phase 1 Input 4
if [ -d ".git" ]; then
DISCOVERED_REMOTE=$(git remote -v | head -1 | awk '{print $1}')
if [ -n "$DISCOVERED_REMOTE" ]; then
REMOTE_URL=$(git remote get-url "$DISCOVERED_REMOTE" 2>/dev/null)
EXTENSION_NAME=$(echo "$REMOTE_URL" | sed 's/.*[:/]\([^/]*\)\/\([^/]*\)\.git$/\2/' | sed 's/\.git$//')
else
EXTENSION_NAME=$(basename "$(pwd)")
fi
else
EXTENSION_NAME=$(basename "$(pwd)")
fi
# TARGET_TEST_DIR_NAME: Detect from filesystem (user-created directory from Phase 3)
TARGET_TEST_DIR_NAME=""
if [ -d "test/e2e" ]; then
# Check if test/e2e has subdirectories besides testdata
SUBDIRS=$(find test/e2e -mindepth 1 -maxdepth 1 -type d ! -name testdata 2>/dev/null)
if [ -n "$SUBDIRS" ]; then
# Has subdirectories - find the one with Go test files
for dir in $SUBDIRS; do
if ls "$dir"/*_test.go >/dev/null 2>&1; then
TARGET_TEST_DIR_NAME=$(basename "$dir")
break
fi
done
fi
fi
# Determine test import path based on whether test/e2e has subdirectory
if [ -n "$TARGET_TEST_DIR_NAME" ]; then
# test/e2e exists with subdirectory (e.g., github.com/openshift/router/test/e2e/extension)
TEST_IMPORT="$MODULE_NAME/test/e2e/$TARGET_TEST_DIR_NAME"
TEST_FILTER_PATH="/test/e2e/$TARGET_TEST_DIR_NAME/"
echo "Tests are at: test/e2e/$TARGET_TEST_DIR_NAME/"
else
# No subdirectory - use test/e2e directly (e.g., github.com/openshift/router/test/e2e)
TEST_IMPORT="$MODULE_NAME/test/e2e"
TEST_FILTER_PATH="/test/e2e/"
echo "Tests are at: test/e2e/"
fi
# Create main.go at cmd/extension/main.go
cat > "cmd/extension/main.go" << 'EOF'
package main
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/spf13/cobra"
"k8s.io/component-base/logs"
"github.com/openshift-eng/openshift-tests-extension/pkg/cmd"
e "github.com/openshift-eng/openshift-tests-extension/pkg/extension"
et "github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests"
g "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo"
"github.com/openshift/origin/test/extended/util"
compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"
framework "k8s.io/kubernetes/test/e2e/framework"
// Import testdata package from same module
_ "<TEST_IMPORT>/testdata"
// Import test packages from same module
_ "<TEST_IMPORT>"
)
func main() {
// Initialize test framework flags (required for kubeconfig, provider, etc.)
util.InitStandardFlags()
framework.AfterReadingAllFlags(&framework.TestContext)
logs.InitLogs()
defer logs.FlushLogs()
registry := e.NewRegistry()
ext := e.NewExtension("openshift", "payload", "<extension-name>")
// Register test suites (parallel, serial, disruptive, all)
registerSuites(ext)
// Build test specs from Ginkgo
// Note: ModuleTestsOnly() is applied by default, which filters out /vendor/ and k8s.io/kubernetes tests
allSpecs, err := g.BuildExtensionTestSpecsFromOpenShiftGinkgoSuite()
if err != nil {
panic(fmt.Sprintf("couldn't build extension test specs from ginkgo: %+v", err.Error()))
}
// Filter to only include tests from this module's test directory
// Excludes tests from /go/pkg/mod/ (module cache) and /vendor/
componentSpecs := allSpecs.Select(func(spec *et.ExtensionTestSpec) bool {
for _, loc := range spec.CodeLocations {
// Include tests from local test directory (not from module cache or vendor)
if strings.Contains(loc, "<TEST_FILTER_PATH>") && !strings.Contains(loc, "/go/pkg/mod/") && !strings.Contains(loc, "/vendor/") {
return true
}
}
return false
})
// Initialize test framework before all tests
componentSpecs.AddBeforeAll(func() {
if err := compat_otp.InitTest(false); err != nil {
panic(err)
}
// Set testsStarted = true to allow OTP functions like oc.Run() to work
// WithCleanup sets this flag and it remains true for all subsequent tests
util.WithCleanup(func() {
// Empty function - we just need WithCleanup to set testsStarted = true
})
})
// Process all specs
componentSpecs.Walk(func(spec *et.ExtensionTestSpec) {
// Apply platform filters based on Platform: labels
for label := range spec.Labels {
if strings.HasPrefix(label, "Platform:") {
platformName := strings.TrimPrefix(label, "Platform:")
spec.Include(et.PlatformEquals(platformName))
}
}
// Apply platform filters based on [platform:xxx] in test names
re := regexp.MustCompile(`\[platform:([a-z]+)\]`)
if match := re.FindStringSubmatch(spec.Name); match != nil {
platform := match[1]
spec.Include(et.PlatformEquals(platform))
}
// Set lifecycle to Informing
spec.Lifecycle = et.LifecycleInforming
})
// Add filtered component specs to extension
ext.AddSpecs(componentSpecs)
registry.Register(ext)
root := &cobra.Command{
Long: "<Extension Name> Tests",
}
root.AddCommand(cmd.DefaultExtensionCommands(registry)...)
if err := func() error {
return root.Execute()
}(); err != nil {
os.Exit(1)
}
}
// registerSuites registers test suites with proper categorization
func registerSuites(ext *e.Extension) {
suites := []e.Suite{
{
Name: "<extension-name>/conformance/parallel",
Parents: []string{
"openshift/conformance/parallel",
},
Description: "Parallel conformance tests (Level0, non-serial, non-disruptive)",
Qualifiers: []string{
`name.contains("[Level0]") && !(name.contains("[Serial]") || name.contains("[Disruptive]"))`,
},
},
{
Name: "<extension-name>/conformance/serial",
Parents: []string{
"openshift/conformance/serial",
},
Description: "Serial conformance tests (must run sequentially)",
Qualifiers: []string{
`name.contains("[Level0]") && name.contains("[Serial]") && !name.contains("[Disruptive]")`,
},
},
{
Name: "<extension-name>/disruptive",
Parents: []string{"openshift/disruptive"},
Description: "Disruptive tests (may affect cluster state)",
Qualifiers: []string{
`name.contains("[Disruptive]")`,
},
},
{
Name: "<extension-name>/non-disruptive",
Description: "All non-disruptive tests (safe for development clusters)",
Qualifiers: []string{
`!name.contains("[Disruptive]")`,
},
},
{
Name: "<extension-name>/all",
Description: "All <extension-name> tests",
// No qualifiers means all tests from this extension will be included
},
}
for _, suite := range suites {
ext.AddSuite(suite)
}
}
EOF
# Replace placeholders
sed -i "s|<TEST_IMPORT>|$TEST_IMPORT|g" "cmd/extension/main.go"
sed -i "s|<TEST_FILTER_PATH>|$TEST_FILTER_PATH|g" "cmd/extension/main.go"
sed -i "s|<extension-name>|$EXTENSION_NAME|g" "cmd/extension/main.go"
sed -i "s|<Extension Name>|${EXTENSION_NAME^}|g" "cmd/extension/main.go"
sed -i "s|<MODULE_PATH>|$MODULE_NAME|g" "cmd/extension/main.go"
echo "✅ Created cmd/extension/main.go"
# CRITICAL VERIFICATION: Imports must be EXACTLY as templated
echo "🔍 Verifying critical imports in cmd/extension/main.go..."
if ! grep -q 'compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"' "cmd/extension/main.go"; then
echo "❌ CRITICAL ERROR: compat_otp import is missing or modified"
echo " The import MUST be: compat_otp \"github.com/openshift/origin/test/extended/util/compat_otp\""
echo " DO NOT change this to exutil or any other alias"
exit 1
fi
if ! grep -q '"github.com/openshift/origin/test/extended/util"' "cmd/extension/main.go"; then
echo "❌ CRITICAL ERROR: util import is missing"
echo " Both util and compat_otp imports are REQUIRED"
exit 1
fi
if ! grep -q 'util\.InitStandardFlags()' "cmd/extension/main.go"; then
echo "❌ CRITICAL ERROR: util.InitStandardFlags() call is missing or modified"
echo " MUST use 'util.InitStandardFlags()', NOT 'exutil.InitStandardFlags()'"
exit 1
fi
if ! grep -q 'compat_otp\.InitTest' "cmd/extension/main.go"; then
echo "❌ CRITICAL ERROR: compat_otp.InitTest() call is missing or modified"
echo " MUST use 'compat_otp.InitTest(false)', NOT 'exutil.InitTest()' or 'util.InitTest()'"
exit 1
fi
echo "✅ All critical imports and function calls verified"
For Single-Module Strategy:
cd <working-dir>/tests-extension
cat > cmd/main.go << 'EOF'
package main
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/spf13/cobra"
"k8s.io/component-base/logs"
"github.com/openshift-eng/openshift-tests-extension/pkg/cmd"
e "github.com/openshift-eng/openshift-tests-extension/pkg/extension"
et "github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests"
g "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo"
"github.com/openshift/origin/test/extended/util"
compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"
framework "k8s.io/kubernetes/test/e2e/framework"
// Import testdata package from this module
_ "github.com/openshift/<extension-name>-tests-extension/test/e2e/testdata"
// Import test packages from this module
_ "github.com/openshift/<extension-name>-tests-extension/test/e2e"
)
func main() {
// Initialize test framework flags (required for kubeconfig, provider, etc.)
util.InitStandardFlags()
framework.AfterReadingAllFlags(&framework.TestContext)
logs.InitLogs()
defer logs.FlushLogs()
registry := e.NewRegistry()
ext := e.NewExtension("openshift", "payload", "<extension-name>")
// Register test suites (parallel, serial, disruptive, all)
registerSuites(ext)
// Build test specs from Ginkgo
// Note: ModuleTestsOnly() is applied by default, which filters out /vendor/ and k8s.io/kubernetes tests
allSpecs, err := g.BuildExtensionTestSpecsFromOpenShiftGinkgoSuite()
if err != nil {
panic(fmt.Sprintf("couldn't build extension test specs from ginkgo: %+v", err.Error()))
}
// Filter to only include tests from this module's test/e2e/ directory
// Excludes tests from /go/pkg/mod/ (module cache) and /vendor/
componentSpecs := allSpecs.Select(func(spec *et.ExtensionTestSpec) bool {
for _, loc := range spec.CodeLocations {
// Include tests from local test/e2e/ directory (not from module cache or vendor)
if strings.Contains(loc, "/test/e2e/") && !strings.Contains(loc, "/go/pkg/mod/") && !strings.Contains(loc, "/vendor/") {
return true
}
}
return false
})
// Initialize test framework before all tests
componentSpecs.AddBeforeAll(func() {
if err := compat_otp.InitTest(false); err != nil {
panic(err)
}
// Set testsStarted = true to allow OTP functions like oc.Run() to work
// WithCleanup sets this flag and it remains true for all subsequent tests
util.WithCleanup(func() {
// Empty function - we just need WithCleanup to set testsStarted = true
})
})
// Process all specs
componentSpecs.Walk(func(spec *et.ExtensionTestSpec) {
// Apply platform filters based on Platform: labels
for label := range spec.Labels {
if strings.HasPrefix(label, "Platform:") {
platformName := strings.TrimPrefix(label, "Platform:")
spec.Include(et.PlatformEquals(platformName))
}
}
// Apply platform filters based on [platform:xxx] in test names
re := regexp.MustCompile(`\[platform:([a-z]+)\]`)
if match := re.FindStringSubmatch(spec.Name); match != nil {
platform := match[1]
spec.Include(et.PlatformEquals(platform))
}
// Set lifecycle to Informing
spec.Lifecycle = et.LifecycleInforming
})
// Add filtered component specs to extension
ext.AddSpecs(componentSpecs)
registry.Register(ext)
root := &cobra.Command{
Long: "<Extension Name> Tests",
}
root.AddCommand(cmd.DefaultExtensionCommands(registry)...)
if err := func() error {
return root.Execute()
}(); err != nil {
os.Exit(1)
}
}
// registerSuites registers test suites with proper categorization
func registerSuites(ext *e.Extension) {
suites := []e.Suite{
{
Name: "<extension-name>/conformance/parallel",
Parents: []string{
"openshift/conformance/parallel",
},
Description: "Parallel conformance tests (Level0, non-serial, non-disruptive)",
Qualifiers: []string{
`name.contains("[Level0]") && !(name.contains("[Serial]") || name.contains("[Disruptive]"))`,
},
},
{
Name: "<extension-name>/conformance/serial",
Parents: []string{
"openshift/conformance/serial",
},
Description: "Serial conformance tests (must run sequentially)",
Qualifiers: []string{
`name.contains("[Level0]") && name.contains("[Serial]") && !name.contains("[Disruptive]")`,
},
},
{
Name: "<extension-name>/disruptive",
Parents: []string{"openshift/disruptive"},
Description: "Disruptive tests (may affect cluster state)",
Qualifiers: []string{
`name.contains("[Disruptive]")`,
},
},
{
Name: "<extension-name>/non-disruptive",
Description: "All non-disruptive tests (safe for development clusters)",
Qualifiers: []string{
`!name.contains("[Disruptive]")`,
},
},
{
Name: "<extension-name>/all",
Description: "All <extension-name> tests",
// No qualifiers means all tests from this extension will be included
},
}
for _, suite := range suites {
ext.AddSuite(suite)
}
}
EOF
sed -i "s|<extension-name>|$EXTENSION_NAME|g" cmd/main.go
sed -i "s|<Extension Name>|${EXTENSION_NAME^}|g" cmd/main.go
sed -i "s|<Extension Name>|${EXTENSION_NAME^}|g" cmd/main.go
echo "✅ Created cmd/main.go"
# CRITICAL VERIFICATION: Imports must be EXACTLY as templated
echo "🔍 Verifying critical imports in cmd/main.go..."
if ! grep -q 'compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"' "cmd/main.go"; then
echo "❌ CRITICAL ERROR: compat_otp import is missing or modified"
echo " The import MUST be: compat_otp \"github.com/openshift/origin/test/extended/util/compat_otp\""
echo " DO NOT change this to exutil or any other alias"
exit 1
fi
if ! grep -q '"github.com/openshift/origin/test/extended/util"' "cmd/main.go"; then
echo "❌ CRITICAL ERROR: util import is missing"
echo " Both util and compat_otp imports are REQUIRED"
exit 1
fi
if ! grep -q 'util\.InitStandardFlags()' "cmd/main.go"; then
echo "❌ CRITICAL ERROR: util.InitStandardFlags() call is missing or modified"
echo " MUST use 'util.InitStandardFlags()', NOT 'exutil.InitStandardFlags()'"
exit 1
fi
if ! grep -q 'compat_otp\.InitTest' "cmd/main.go"; then
echo "❌ CRITICAL ERROR: compat_otp.InitTest() call is missing or modified"
echo " MUST use 'compat_otp.InitTest(false)', NOT 'exutil.InitTest()' or 'util.InitTest()'"
exit 1
fi
echo "✅ All critical imports and function calls verified"
Step 3: Create bindata.mk
For Monorepo:
cd <working-dir>
# Re-derive directory paths from Phase 3
# (Variables don't persist between phases - need to re-calculate)
if [ -d "test/e2e" ]; then
# Check if test/e2e has subdirectories besides testdata
SUBDIRS=$(find test/e2e -mindepth 1 -maxdepth 1 -type d ! -name testdata 2>/dev/null)
if [ -n "$SUBDIRS" ]; then
TESTDATA_DIR=""
# Has subdirectories - find the one with testdata
for dir in $SUBDIRS; do
if [ -d "$dir/testdata" ]; then
TESTDATA_DIR="$dir/testdata"
break
fi
done
# Fallback if no matching subdir had testdata
if [ -z "$TESTDATA_DIR" ]; then
TESTDATA_DIR="test/e2e/testdata"
fi
else
# No subdirectories - use test/e2e/testdata directly
TESTDATA_DIR="test/e2e/testdata"
fi
else
echo "❌ Cannot find test/e2e directory"
exit 1
fi
echo "Using testdata directory: $TESTDATA_DIR"
# bindata.mk location: at root for single-module monorepo approach
cat > "bindata.mk" << 'EOF'
TESTDATA_PATH := <TESTDATA_DIR>
GOPATH ?= $(shell go env GOPATH)
GO_BINDATA := $(GOPATH)/bin/go-bindata
$(GO_BINDATA):
@echo "Installing go-bindata..."
@GOFLAGS= go install github.com/go-bindata/go-bindata/v3/go-bindata@latest
.PHONY: update-bindata
update-bindata: $(GO_BINDATA)
@echo "Generating bindata for testdata files..."
$(GO_BINDATA) \
-nocompress \
-nometadata \
-prefix "<TESTDATA_DIR>" \
-pkg testdata \
-o <TESTDATA_DIR>/bindata.go \
<TESTDATA_DIR>/...
@gofmt -s -w <TESTDATA_DIR>/bindata.go
@echo "✅ Bindata generated successfully"
.PHONY: verify-bindata
verify-bindata: update-bindata
@echo "Verifying bindata is up to date..."
git diff --exit-code $(TESTDATA_PATH)/bindata.go || (echo "❌ Bindata is out of date" && exit 1)
@echo "✅ Bindata is up to date"
.PHONY: bindata
bindata: clean-bindata update-bindata
.PHONY: clean-bindata
clean-bindata:
@echo "Cleaning bindata..."
@rm -f $(TESTDATA_PATH)/bindata.go
EOF
# Replace placeholders
sed -i "s|<TESTDATA_DIR>|$TESTDATA_DIR|g" "bindata.mk"
echo "✅ Created bindata.mk at root"
For Single-Module:
cd <working-dir>/tests-extension
cat > test/e2e/bindata.mk << 'EOF'
TESTDATA_PATH := testdata
GOPATH ?= $(shell go env GOPATH)
GO_BINDATA := $(GOPATH)/bin/go-bindata
$(GO_BINDATA):
@echo "Installing go-bindata..."
@GOFLAGS= go install github.com/go-bindata/go-bindata/v3/go-bindata@latest
.PHONY: update-bindata
update-bindata: $(GO_BINDATA)
@echo "Generating bindata..."
@mkdir -p $(TESTDATA_PATH)
$(GO_BINDATA) -nocompress -nometadata \
-pkg testdata -o $(TESTDATA_PATH)/bindata.go -prefix "testdata" $(TESTDATA_PATH)/...
@gofmt -s -w $(TESTDATA_PATH)/bindata.go
@echo "✅ Bindata generated successfully"
.PHONY: verify-bindata
verify-bindata: update-bindata
@echo "Verifying bindata is up to date..."
git diff --exit-code $(TESTDATA_PATH)/bindata.go || (echo "❌ Bindata is out of date" && exit 1)
@echo "✅ Bindata is up to date"
.PHONY: bindata
bindata: clean-bindata update-bindata
.PHONY: clean-bindata
clean-bindata:
@rm -f $(TESTDATA_PATH)/bindata.go
EOF
echo "✅ Created test/e2e/bindata.mk"
Step 4: Create/Update Makefile
For Monorepo Strategy:
IMPORTANT: Do NOT add tests-ext-compress or tests-ext-copy targets
cd <working-dir>
# Re-derive EXTENSION_NAME using IDENTICAL logic as Phase 1
# (Variables don't persist between phases - need to re-calculate)
if [ -d ".git" ]; then
DISCOVERED_REMOTE=$(git remote -v | head -1 | awk '{print $1}')
if [ -n "$DISCOVERED_REMOTE" ]; then
REMOTE_URL=$(git remote get-url "$DISCOVERED_REMOTE" 2>/dev/null)
EXTENSION_NAME=$(echo "$REMOTE_URL" | sed 's/.*[:/]\([^/]*\)\/\([^/]*\)\.git$/\2/' | sed 's/\.git$//')
else
EXTENSION_NAME=$(basename "$(pwd)")
fi
else
EXTENSION_NAME=$(basename "$(pwd)")
fi
if [ ! -f "Makefile" ]; then
echo "❌ ERROR: No root Makefile found"
exit 1
fi
if grep -q "tests-ext-build" Makefile; then
echo "⚠️ OTE targets already exist, skipping..."
else
# Single module approach - build from root
cat >> Makefile << EOF
# OTE test extension binary configuration
TESTS_EXT_BINARY := bin/$EXTENSION_NAME-tests-ext
.PHONY: tests-ext-build
tests-ext-build:
@echo "Building OTE test extension binary..."
@\$(MAKE) -f bindata.mk update-bindata
@mkdir -p bin
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go build -mod=vendor -o \$(TESTS_EXT_BINARY) ./cmd/extension
@echo "✅ Extension binary built: \$(TESTS_EXT_BINARY)"
.PHONY: extension
extension: tests-ext-build
.PHONY: clean-extension
clean-extension:
@echo "Cleaning extension binary..."
@rm -f \$(TESTS_EXT_BINARY)
@\$(MAKE) -f bindata.mk clean-bindata 2>/dev/null || true
EOF
echo "✅ Root Makefile updated with OTE targets"
fi
For Single-Module:
cd <working-dir>/tests-extension
cat > Makefile << EOF
BINARY := bin/$EXTENSION_NAME-tests-ext
.PHONY: build
build:
@echo "Building extension binary..."
@cd test/e2e && \$(MAKE) -f bindata.mk update-bindata
@mkdir -p bin
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go build -o \$(BINARY) ./cmd
@echo "✅ Binary built: \$(BINARY)"
.PHONY: clean
clean:
@rm -f \$(BINARY)
@cd test/e2e && \$(MAKE) -f bindata.mk clean-bindata
.PHONY: help
help:
@echo "Available targets:"
@echo " build - Build extension binary"
@echo " clean - Remove binaries and bindata"
EOF
echo "✅ Created Makefile"
Step 5: Create fixtures.go
Create testdata/fixtures.go helper file:
For Monorepo:
cd <working-dir>
cat > "$TESTDATA_DIR/fixtures.go" << 'EOF'
package testdata
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
)
var (
fixtureDir string
)
func init() {
var err error
fixtureDir, err = ioutil.TempDir("", "testdata-fixtures-")
if err != nil {
panic(fmt.Sprintf("failed to create fixture directory: %v", err))
}
// Ensure fixture directory has proper permissions for all users
if err := os.Chmod(fixtureDir, 0755); err != nil {
panic(fmt.Sprintf("failed to set fixture directory permissions: %v", err))
}
}
func FixturePath(elem ...string) string {
relativePath := filepath.Join(elem...)
targetPath
*Truncated - read the full file at https://github.com/openshift-eng/ai-helpers/blob/72044d97c253f2225f6ed929796ffe26a0f4edb8/plugins/ote-migration/skills/ote-migration-workflow/SKILL.