plugins/ote-migration/skills/ote-migration-workflow/SKILL.md
Automated workflow for migrating OpenShift component repositories to OTE framework
npx skillsauth add openshift-eng/ai-helpers ote-migration-workflowInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill provides step-by-step implementation guidance for the complete OTE migration workflow.
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.
[email protected]:openshift/openshift-tests-private.gitThe 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:
DO NOT skip Phase 7. After Phase 6 completes, proceed immediately to Phase 7.
Key Design Principles:
cmd/<extension-name>-tests-ext/main.go (not under test/)vendor/ at repository rootexutil "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())*exutil.CLI for type declarations, NOT *compat_otp.CLI (which doesn't exist)Collect all necessary information from the user before starting the migration.
CRITICAL INSTRUCTIONS:
Variables collected (shown as <variable-name>) will be used throughout the migration.
Ask: "Which directory structure strategy do you want to use?"
Option 1: Monorepo strategy (integrate into existing repo)
cmd/ and test/ directoriescmd/extension/main.go (at repository root, NOT under test/)vendor/ at root ONLYOption 2: Single-module strategy (isolated directory)
tests-extension/ directorygo.modtests-extension/cmd/main.gotests-extension/vendor/User selects: 1 or 2
Store the selection in variable: <structure-strategy> (value: "monorepo" or "single-module")
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:
User provides the path:
.)Store in variable: <working-dir>
Ask: "What is the path to your target repository, or provide a Git URL to clone?"
/home/user/repos/router)[email protected]:openshift/router.git)Store in variable: <target-repo-path> or <target-repo-url>
If a local target repository path was provided:
Ask: "Do you want to update the local target repository? (git fetch && git pull) [Y/n]:"
<update-target> (value: "yes" or "no")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.
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>
This input is ONLY asked if:
test/e2e/ directoryPurpose: 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:
test/e2e/router/test/e2e/extension/<target-test-dir> (default: "extension" if empty)If test/e2e does NOT exist:
<target-test-dir> (empty string "")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)
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")
Ask: "What is the test subfolder name under test/extended/?"
Store in variable: <test-subfolder>
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:
Default: Same as Input 7 (recommended for most cases)
Examples:
test/extended/testdata/router/, press Entertest/extended/testdata/edge/, enter "edge"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>
Ask: "Do you want to update Dockerfiles automatically, or do it manually?"
Options:
Store in variable: <dockerfile-choice> (value: "automated" or "manual")
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:
1 for first Dockerfile)all to update all Dockerfilesnone to skip Dockerfile integrationExample:
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)
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]:"
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"
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 [email protected]: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
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"
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"
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
🚨 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:
exutil provides InitStandardFlags() to register kubeconfig flags and the CLI typecompat_otp provides InitTest() to initialize the test framework and helper functions like NewCLI()github.com/openshift/origin and serve different purposescompat_otp package is a REAL package path, NOT a placeholderDO NOT:
*exutil.CLI to *compat_otp.CLI (compat_otp.CLI type doesn't exist)Verification checks are included in the template generation to catch any modifications.
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/[email protected]
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/[email protected]
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/[email protected]
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/[email protected]
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/[email protected]
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/[email protected]
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 ..
For Monorepo Strategy:
IMPORTANT:
cmd/extension/main.go (at repository root, NOT under test/)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"
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"
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"
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 := filepath.Join(fixtureDir, relativePath)
if _, err := os.Stat(targetPath); err == nil {
return targetPath
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
panic(fmt.Sprintf("failed to create directory for %s: %v", relativePath, err))
}
bindataPath := relativePath
tempDir, err := os.MkdirTemp("", "bindata-extract-")
if err != nil {
panic(fmt.Sprintf("failed to create temp directory: %v", err))
}
defer os.RemoveAll(tempDir)
if err := RestoreAsset(tempDir, bindataPath); err != nil {
if err := RestoreAssets(tempDir, bindataPath); err != nil {
panic(fmt.Sprintf("failed to restore fixture %s: %v", relativePath, err))
}
}
extractedPath := filepath.Join(tempDir, bindataPath)
// Set permissions on extracted files/directories before moving
filepath.Walk(extractedPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
os.Chmod(path, 0755)
} else {
os.Chmod(path, 0644)
}
return nil
})
if err := os.Rename(extractedPath, targetPath); err != nil {
panic(fmt.Sprintf("failed to move extracted files: %v", err))
}
// Ensure final path has correct permissions
if info, err := os.Stat(targetPath); err == nil {
if info.IsDir() {
os.Chmod(targetPath, 0755)
} else {
os.Chmod(targetPath, 0644)
}
}
return targetPath
}
func CleanupFixtures() error {
if fixtureDir != "" {
return os.RemoveAll(fixtureDir)
}
return nil
}
func GetFixtureData(elem ...string) ([]byte, error) {
relativePath := filepath.Join(elem...)
cleanPath := relativePath
if len(cleanPath) > 0 && cleanPath[0] == '/' {
cleanPath = cleanPath[1:]
}
return Asset(cleanPath)
}
func MustGetFixtureData(elem ...string) []byte {
data, err := GetFixtureData(elem...)
if err != nil {
panic(fmt.Sprintf("failed to get fixture data: %v", err))
}
return data
}
func FixtureExists(elem ...string) bool {
relativePath := filepath.Join(elem...)
cleanPath := relativePath
if len(cleanPath) > 0 && cleanPath[0] == '/' {
cleanPath = cleanPath[1:]
}
_, err := Asset(cleanPath)
return err == nil
}
func ListFixtures() []string {
names := AssetNames()
fixtures := make([]string, 0, len(names))
for _, name := range names {
if strings.HasPrefix(name, "testdata/") {
fixtures = append(fixtures, strings.TrimPrefix(name, "testdata/"))
}
}
sort.Strings(fixtures)
return fixtures
}
EOF
echo "✅ Created $TESTDATA_DIR/fixtures.go"
For Single-Module: Same content, different path (tests-extension/test/e2e/testdata/fixtures.go)
This phase migrates all Go files (test files AND helper/utility files) with atomic error handling and rollback capability. It replaces FixturePath calls, adds testdata imports, preserves all existing imports, and adds test annotations.
echo "========================================="
echo "Phase 5: Test Migration (atomic)"
echo "========================================="
# 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
TEST_CODE_DIR=""
TESTDATA_DIR=""
# Has subdirectories - find the one with Go test files
for dir in $SUBDIRS; do
if ls "$dir"/*_test.go >/dev/null 2>&1; then
TEST_CODE_DIR="$dir"
TESTDATA_DIR="$dir/testdata"
break
fi
done
# Fallback if no subdir contains tests
if [ -z "$TEST_CODE_DIR" ]; then
TEST_CODE_DIR="test/e2e"
TESTDATA_DIR="test/e2e/testdata"
fi
else
# No subdirectories - use test/e2e directly
TEST_CODE_DIR="test/e2e"
TESTDATA_DIR="test/e2e/testdata"
fi
else
echo "❌ Cannot find test/e2e directory"
exit 1
fi
echo "Using test directory: $TEST_CODE_DIR"
echo "Using testdata directory: $TESTDATA_DIR"
BACKUP_DIR=$(mktemp -d)
if [ -d "$TEST_CODE_DIR" ]; then
cp -r "$TEST_CODE_DIR" "$BACKUP_DIR/test-backup"
echo "Backup created at: $BACKUP_DIR/test-backup"
fi
PHASE5_FAILED=0
cleanup_on_error() {
if [ $PHASE5_FAILED -eq 1 ]; then
echo "❌ Phase 5 failed - rolling back..."
if [ -d "$BACKUP_DIR/test-backup" ]; then
rm -rf "$TEST_CODE_DIR"
cp -r "$BACKUP_DIR/test-backup" "$TEST_CODE_DIR"
echo "✅ Test files restored from backup"
fi
fi
rm -rf "$BACKUP_DIR"
}
trap cleanup_on_error EXIT
echo "Step 1: Replacing FixturePath calls..."
# Process ALL .go files (including helper/utility files, not just *_test.go)
ALL_GO_FILES=$(grep -rl "FixturePath" "$TEST_CODE_DIR" --include="*.go" 2>/dev/null || true)
if [ -n "$ALL_GO_FILES" ]; then
for file in $ALL_GO_FILES; do
# Replace compat_otp.FixturePath
sed -i 's/compat_otp\.FixturePath/testdata.FixturePath/g' "$file"
# Replace exutil.FixturePath
sed -i 's/exutil\.FixturePath/testdata.FixturePath/g' "$file"
# Remove redundant "testdata" prefix (supports any subfolder name)
if [ -n "$TESTDATA_SUBFOLDER" ] && [ "$TESTDATA_SUBFOLDER" != "none" ]; then
# Remove testdata subfolder prefix: FixturePath("testdata", "router") -> FixturePath("router")
sed -i "s/testdata\.FixturePath(\"testdata\", \"$TESTDATA_SUBFOLDER\"/testdata.FixturePath(\"$TESTDATA_SUBFOLDER\"/g" "$file"
# Also handle the generic case for any other testdata references
sed -i 's/testdata\.FixturePath("testdata", /testdata.FixturePath(/g' "$file"
else
# Generic removal for any testdata prefix
sed -i 's/testdata\.FixturePath("testdata", /testdata.FixturePath(/g' "$file"
fi
done
echo "✅ FixturePath calls replaced in all .go files (including helpers)"
else
echo "⚠️ No FixturePath usage found"
fi
echo "Step 2: Adding testdata imports..."
MODULE_NAME=$(grep '^module ' go.mod | awk '{print $2}')
# Testdata import path (uses $TESTDATA_DIR from Step 0)
TESTDATA_IMPORT="$MODULE_NAME/$TESTDATA_DIR"
# Process ALL .go files that use testdata.FixturePath (including helper files)
ALL_GO_FILES=$(grep -rl "testdata\.FixturePath" "$TEST_CODE_DIR" --include="*.go" 2>/dev/null || true)
for file in $ALL_GO_FILES; do
# Check if import already exists (avoid ! operator in if statement)
if grep -q "\"$TESTDATA_IMPORT\"" "$file"; then
continue # Skip if already has import
fi
# Add import
if grep -q "^import (" "$file"; then
sed -i "/^import (/a\\ \"$TESTDATA_IMPORT\"" "$file"
else
sed -i "/^package /a\\\\nimport (\n\t\"$TESTDATA_IMPORT\"\n)" "$file"
fi
done
echo "✅ Testdata imports added to all .go files (including helpers)"
# Fix import ordering using goimports (Go standard tool)
if command -v goimports >/dev/null 2>&1; then
echo "Step 2b: Fixing import ordering with goimports..."
goimports -w "$TEST_CODE_DIR"/*.go 2>/dev/null || true
echo "✅ Import ordering fixed"
else
echo "⚠️ goimports not found - import ordering may not follow Go conventions"
echo " Install with: go install golang.org/x/tools/cmd/goimports@latest"
fi
ANNOTATION LOGIC:
[OTP] at BEGINNING of ALL Describe blocks[Level0] at BEGINNING of It string ONLY for tests with "-LEVEL0-" suffixecho "Step 3: Adding [OTP] and [Level0] annotations..."
# Create Python script for annotation
cat > /tmp/annotate_tests.py << 'PYTHON_SCRIPT'
#!/usr/bin/env python3
import re
import sys
from pathlib import Path
def annotate_file(filepath):
"""
Add [OTP] to Describe blocks and [Level0] to test names.
Logic:
1. Add [OTP] at BEGINNING of all Describe blocks
2. Add [Level0] at BEGINNING of It string (only for tests with -LEVEL0-)
3. Remove -LEVEL0- suffix
"""
with open(filepath, 'r') as f:
lines = f.readlines()
changed = False
# Step 1: Add [OTP] at BEGINNING of ALL Describe blocks
for i, line in enumerate(lines):
if 'g.Describe' in line and '[OTP]' not in line:
# Add [OTP] at the very beginning of the string
lines[i] = re.sub(
r'g\.Describe\("([^"]*)"',
r'g.Describe("[OTP]\1"',
line
)
changed = True
# Step 2: Add [Level0] at BEGINNING of It string ONLY for tests with -LEVEL0-
for i, line in enumerate(lines):
if ('g.It(' in line or 'g.It (' in line) and '-LEVEL0-' in line:
# Add [Level0] at beginning and remove -LEVEL0- suffix
lines[i] = re.sub(
r'g\.It\("([^"]*)-LEVEL0-([^"]*)"',
r'g.It("[Level0] \1-\2"',
line
)
changed = True
if changed:
with open(filepath, 'w') as f:
f.writelines(lines)
return True
return False
if __name__ == '__main__':
test_dir = sys.argv[1]
test_files = list(Path(test_dir).rglob('*.go'))
updated_count = 0
for filepath in test_files:
if annotate_file(str(filepath)):
print(f"✓ {filepath}")
updated_count += 1
else:
print(f"- {filepath} (no changes)")
print(f"\n✅ Updated {updated_count} files")
PYTHON_SCRIPT
chmod +x /tmp/annotate_tests.py
python3 /tmp/annotate_tests.py "$TEST_CODE_DIR"
echo ""
echo "Annotation Summary:"
echo " [OTP] - Added to ALL Describe blocks at beginning"
echo " [Level0] - Added to test names with -LEVEL0- suffix only"
echo " -LEVEL0- - Removed from test names after adding [Level0]"
Expected Results:
Before:
g.Describe("[sig-router] Router tests", func() {
g.It("Author:john-LEVEL0-Critical-Test", func() {})
g.It("Author:jane-High-Test", func() {})
})
After:
g.Describe("[OTP][sig-router] Router tests", func() {
g.It("[Level0] Author:john-Critical-Test", func() {})
g.It("Author:jane-High-Test", func() {})
})
CRITICAL FIX: compat_otp.NewCLI() uses OTP-specific initialization that expects tests to be "started" by the OTP harness. In OTE, this causes "May only be called from within a test case" panics.
The Fix: Replace compat_otp.NewCLI() with exutil.NewCLIWithoutNamespace() and ensure exutil import exists.
echo "Step 3b: Fixing CLI initialization pattern for OTE compatibility..."
# Find all test files with CLI initialization
TEST_FILES=$(find "$TEST_CODE_DIR" -name '*.go' -type f)
for file in $TEST_FILES; do
# Check if file uses compat_otp.NewCLI
if grep -q 'compat_otp\.NewCLI' "$file"; then
echo " Fixing CLI initialization in: $(basename $file)"
# Step 1: Ensure exutil import exists
if ! grep -q 'exutil "github.com/openshift/origin/test/extended/util"' "$file"; then
echo " Adding exutil import..."
# Add after compat_otp import
sed -i '/compat_otp "github.com\/openshift\/origin\/test\/extended\/util\/compat_otp"/a\ exutil "github.com/openshift/origin/test/extended/util"' "$file"
fi
# Step 2: Replace compat_otp.NewCLI with exutil.NewCLIWithoutNamespace
# Handle different patterns:
# Pattern 1: var oc *exutil.CLI + BeforeEach
if grep -q 'var oc \*exutil\.CLI' "$file" && grep -q 'oc = compat_otp\.NewCLI' "$file"; then
# Use Python for multi-line pattern replacement
python3 << 'PYTHON_EOF' "$file"
import sys
import re
file_path = sys.argv[1]
with open(file_path, 'r') as f:
content = f.read()
# Pattern: var oc *exutil.CLI followed by BeforeEach with compat_otp.NewCLI
pattern = r'(\t)var oc \*exutil\.CLI\s*\n\s*\n\s*g\.BeforeEach\(func\(\) \{\s*\n\s*oc = compat_otp\.NewCLI\("([^"]+)",\s*compat_otp\.KubeConfigPath\(\)\)\s*\n\s*\}\)'
# Replace with exutil.NewCLIWithoutNamespace
replacement = r'\1oc := exutil.NewCLIWithoutNamespace("\2")'
new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE)
if new_content != content:
with open(file_path, 'w') as f:
f.write(new_content)
sys.exit(0)
else:
sys.exit(1)
PYTHON_EOF
if [ $? -eq 0 ]; then
echo " ✓ Replaced var + BeforeEach pattern with exutil.NewCLIWithoutNamespace"
fi
fi
# Pattern 2: Direct compat_otp.NewCLI at Describe level
if grep -q 'oc := compat_otp\.NewCLI' "$file"; then
# Extract the CLI name
sed -i 's/oc := compat_otp\.NewCLI("\([^"]*\)", compat_otp\.KubeConfigPath())/oc := exutil.NewCLIWithoutNamespace("\1")/g' "$file"
echo " ✓ Replaced direct compat_otp.NewCLI with exutil.NewCLIWithoutNamespace"
fi
# Pattern 3: var oc = compat_otp.NewCLI
if grep -q 'var oc = compat_otp\.NewCLI' "$file"; then
sed -i 's/var oc = compat_otp\.NewCLI("\([^"]*\)", compat_otp\.KubeConfigPath())/oc := exutil.NewCLIWithoutNamespace("\1")/g' "$file"
echo " ✓ Replaced var oc = pattern with exutil.NewCLIWithoutNamespace"
fi
fi
done
echo "✅ CLI initialization pattern fixed for OTE compatibility"
echo ""
echo "Pattern Changed:"
echo " Before: oc := compat_otp.NewCLI(\"name\", compat_otp.KubeConfigPath())"
echo " # ❌ Uses OTP-specific setup, requires test to be \"started\""
echo ""
echo " After: oc := exutil.NewCLIWithoutNamespace(\"name\")"
echo " # ✅ Works in OTE without OTP harness"
echo ""
echo "Note: compat_otp helpers like By() are still used for test steps"
Why this is critical:
compat_otp.NewCLI() calls SetupProject() which uses OTP-specific requiresTestStart() checkrequiresTestStart() panics with "May only be called from within a test case"exutil.NewCLIWithoutNamespace() creates a CLI without OTP-specific setupcompat_otp.By() and other helpers for test step descriptionsecho "Step 3c: Cleaning up unused exutil imports..."
# Find files with unused exutil imports
TEST_FILES=$(find "$TEST_CODE_DIR" -name '*.go' -type f ! -name '*_util.go' ! -name 'fixtures.go')
CLEANED_COUNT=0
for file in $TEST_FILES; do
if grep -q 'exutil "github.com/openshift/origin/test/extended/util"' "$file"; then
# Check if file actually uses exutil (including NewCLIWithoutNamespace)
# Keep exutil import if either:
# 1. File has exutil. references (e.g., exutil.NewCLIWithoutNamespace)
# 2. File has *exutil.CLI type declarations
if ! grep -q 'exutil\.' "$file" && ! grep -q '\*exutil\.CLI' "$file"; then
echo " Removing unused exutil import from: $(basename $file)"
sed -i '/exutil "github.com\/openshift\/origin\/test\/extended\/util"/d' "$file"
CLEANED_COUNT=$((CLEANED_COUNT + 1))
fi
fi
done
echo "✅ Unused exutil imports cleaned up ($CLEANED_COUNT files)"
Why this is needed:
echo "Step 4: Validating annotations..."
VALIDATION_FAILED=0
# Find all .go files (not just *_test.go) but exclude utility files and vendor
TEST_FILES=$(find "$TEST_CODE_DIR" -name '*.go' -type f | grep -v '_util\.go' | grep -v 'fixtures\.go' | grep -v '/vendor/')
# Check for [OTP] in Describe blocks
MISSING_OTP=0
for file in $TEST_FILES; do
if grep -q "g\.Describe" "$file"; then
if ! grep -q "\[OTP\]" "$file"; then
echo " ❌ Missing [OTP] in: $file"
MISSING_OTP=$((MISSING_OTP + 1))
VALIDATION_FAILED=1
fi
fi
done
if [ $MISSING_OTP -eq 0 ]; then
echo " ✅ All Describe blocks have [OTP]"
fi
# Check that -LEVEL0- suffix is removed
LEVEL0_NOT_REMOVED=0
for file in $TEST_FILES; do
if grep -q -- '-LEVEL0-' "$file"; then
echo " ❌ Still contains -LEVEL0-: $file"
LEVEL0_NOT_REMOVED=$((LEVEL0_NOT_REMOVED + 1))
VALIDATION_FAILED=1
fi
done
if [ $LEVEL0_NOT_REMOVED -eq 0 ]; then
echo " ✅ All -LEVEL0- suffixes removed"
fi
# Check testdata imports for files using FixturePath
MISSING_IMPORT=0
for file in $TEST_FILES; do
if grep -q "testdata\.FixturePath" "$file" && ! grep -q "\"$TESTDATA_IMPORT\"" "$file"; then
echo " ❌ Missing testdata import: $file"
MISSING_IMPORT=$((MISSING_IMPORT + 1))
VALIDATION_FAILED=1
fi
done
if [ $MISSING_IMPORT -eq 0 ]; then
echo " ✅ All testdata imports correct"
fi
if [ $VALIDATION_FAILED -eq 1 ]; then
echo "❌ Validation failed"
PHASE5_FAILED=1
exit 1
fi
echo "✅ Phase 5 validation complete"
PHASE5_FAILED=0
IMPORTANT: Single module approach - all dependencies in root go.mod
CRITICAL INSTRUCTION: Execute ALL bash commands in this phase EXACTLY as written. Do NOT skip steps based on your interpretation or assumptions. Do NOT generate your own messages - use the echo statements provided. Step 1b MUST execute to ensure k8s.io/kms replace directive exists.
cd <working-dir>
echo "========================================="
echo "Phase 6: Dependency Resolution"
echo "========================================="
# Step 1: Detect and fix outdated k8s.io versions
echo "Step 1: Checking for outdated k8s.io versions..."
# Check if go.mod uses old OpenShift kubernetes fork (October 2024 or earlier)
OLD_K8S_COMMITS="1892e4deb967" # October 2, 2024
if grep -q "$OLD_K8S_COMMITS" go.mod; then
echo "⚠️ WARNING: Detected outdated OpenShift kubernetes fork (October 2024)"
echo "OTE framework requires October 2025 fork for compatibility"
echo ""
echo "Applying automatic fix..."
# Backup go.mod before making changes
cp go.mod go.mod.backup.k8s-version-fix
# Update all k8s.io packages to October 2025 version
NEW_K8S_COMMIT_HASH="96593f323733" # October 17, 2025 commit hash
NEW_K8S_VERSION="20251017000000-96593f323733" # Full pseudo-version suffix (timestamp + hash)
sed -i "s/$OLD_K8S_COMMITS/$NEW_K8S_COMMIT_HASH/g" go.mod
echo " ✅ Updated k8s.io packages from October 2024 to October 2025"
# Add otelgrpc replace if missing
if ! grep -q "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc =>" go.mod; then
echo " Adding otelgrpc replace directive..."
# Insert after ginkgo replace
sed -i '/github.com\/onsi\/ginkgo\/v2 =>/a\ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc => go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0' go.mod
echo " ✅ Added otelgrpc v0.53.0 replace directive"
fi
# Add k8s.io/externaljwt if missing
if ! grep -q "k8s.io/externaljwt =>" go.mod; then
echo " Adding k8s.io/externaljwt package..."
# Insert after k8s.io/endpointslice (alphabetically)
sed -i "/k8s.io\/endpointslice =>/a\ k8s.io/externaljwt => github.com/openshift/kubernetes/staging/src/k8s.io/externaljwt v0.0.0-$NEW_K8S_VERSION" go.mod
echo " ✅ Added k8s.io/externaljwt package"
fi
# Add k8s.io/kms if missing (required for Docker build compatibility with Go 1.24)
if ! grep -q "k8s.io/kms =>" go.mod; then
echo " Adding k8s.io/kms package..."
# Insert after k8s.io/kube-scheduler (alphabetically)
sed -i "/k8s.io\/kube-scheduler =>/a\ k8s.io/kms => github.com/openshift/kubernetes/staging/src/k8s.io/kms v0.0.0-$NEW_K8S_VERSION" go.mod
echo " ✅ Added k8s.io/kms package (prevents Docker build Go version errors)"
fi
# Remove deprecated k8s.io/legacy-cloud-providers if present
if grep -q "k8s.io/legacy-cloud-providers" go.mod; then
echo " Removing deprecated k8s.io/legacy-cloud-providers..."
sed -i '/k8s.io\/legacy-cloud-providers/d' go.mod
echo " ✅ Removed deprecated package"
fi
# Update Ginkgo to October 2025 version if using old version
OLD_GINKGO="v2.6.1-0.20241205171354-8006f302fd12" # December 5, 2024
NEW_GINKGO="v2.6.1-0.20251001123353-fd5b1fb35db1" # October 1, 2025
if grep -q "$OLD_GINKGO" go.mod; then
sed -i "s/$OLD_GINKGO/$NEW_GINKGO/g" go.mod
echo " ✅ Updated Ginkgo to October 2025 version"
fi
echo "✅ k8s.io version compatibility fix applied"
echo " Backup saved to: go.mod.backup.k8s-version-fix"
echo ""
else
echo "✅ k8s.io versions are compatible (October 2025 or newer)"
fi
# Step 1b: Ensure k8s.io/kms replace directive exists (required for all repos)
# This must run regardless of k8s.io version to ensure OpenShift fork is used
if ! grep -q "k8s.io/kms =>" go.mod; then
echo "Step 1b: Adding k8s.io/kms replace directive..."
# Get current k8s.io pseudo-version from existing replace directives (keep full version)
CURRENT_K8S_VERSION=$(grep "k8s.io/kubernetes =>" go.mod | grep -o 'v0\.0\.0-[0-9]\{14\}-[a-f0-9]\{12\}' | head -1)
# Check immediately after extraction
if [ -z "$CURRENT_K8S_VERSION" ]; then
echo "❌ CRITICAL: Could not detect k8s.io pseudo-version in go.mod"
echo " Expected format: v0.0.0-YYYYMMDDHHMMSS-COMMITHASH"
echo " This repo might not have k8s.io/kubernetes replace directives"
echo " Cannot determine OpenShift fork version to use for k8s.io/kms"
exit 1
fi
echo " ✅ Detected k8s.io pseudo-version: $CURRENT_K8S_VERSION"
# Check if replace section exists
if grep -q "^replace" go.mod; then
# Replace section exists - insert alphabetically after k8s.io/kube-scheduler
if grep -q "k8s.io/kube-scheduler =>" go.mod; then
sed -i "/k8s.io\/kube-scheduler =>/a\ k8s.io/kms => github.com/openshift/kubernetes/staging/src/k8s.io/kms $CURRENT_K8S_VERSION" go.mod
else
# No kube-scheduler, insert after k8s.io/kubernetes
sed -i "/k8s.io\/kubernetes =>/a\ k8s.io/kms => github.com/openshift/kubernetes/staging/src/k8s.io/kms $CURRENT_K8S_VERSION" go.mod
fi
else
# No replace section - create one at the end
echo "" >> go.mod
echo "replace (" >> go.mod
echo " k8s.io/kms => github.com/openshift/kubernetes/staging/src/k8s.io/kms $CURRENT_K8S_VERSION" >> go.mod
echo ")" >> go.mod
fi
# Verify it was added
if grep -q "k8s.io/kms =>" go.mod; then
echo " ✅ Added k8s.io/kms replace directive (ensures compatibility with OpenShift kubernetes fork)"
else
echo " ❌ Failed to add k8s.io/kms replace directive"
echo " This is required to use OpenShift kubernetes fork instead of upstream"
exit 1
fi
else
echo "Step 1b: k8s.io/kms replace directive already exists ✅"
fi
# CRITICAL VERIFICATION: k8s.io/kms MUST exist after Step 1b
# DO NOT REMOVE THIS DIRECTIVE - IT IS REQUIRED FOR COMPATIBILITY
echo "Verifying k8s.io/kms replace directive exists..."
if ! grep -q "k8s.io/kms =>" go.mod; then
echo "❌ CRITICAL ERROR: k8s.io/kms replace directive was removed or missing"
echo " This directive is MANDATORY to use OpenShift kubernetes fork"
echo " DO NOT remove k8s.io/kms - it prevents build failures"
echo " Redirects from upstream k8s.io/kms to OpenShift fork"
exit 1
fi
echo "✅ Verification passed: k8s.io/kms replace directive exists"
# Step 1c: Ensure k8s.io/kms uses correct pseudo-version format
echo "Step 1c: Verifying k8s.io/kms version format..."
CURRENT_KMS_VERSION=$(grep "k8s.io/kms =>" go.mod | grep -o 'v[0-9]\+\.[0-9]\+\.[0-9]\+-' || echo "")
if [[ "$CURRENT_KMS_VERSION" == "v1."* ]]; then
echo "⚠️ Detected incorrect k8s.io/kms version format: $CURRENT_KMS_VERSION"
echo "Correcting to v0.0.0- format..."
# Extract the timestamp and commit hash
TIMESTAMP_HASH=$(grep "k8s.io/kms =>" go.mod | grep -o '[0-9]\{14\}-[a-f0-9]\{12\}')
if [ -n "$TIMESTAMP_HASH" ]; then
# Replace with correct v0.0.0- format
sed -i "s|k8s.io/kms => github.com/openshift/kubernetes/staging/src/k8s.io/kms v[0-9]\+\.[0-9]\+\.[0-9]\+-$TIMESTAMP_HASH|k8s.io/kms => github.com/openshift/kubernetes/staging/src/k8s.io/kms v0.0.0-$TIMESTAMP_HASH|g" go.mod
echo "✅ k8s.io/kms version corrected to: v0.0.0-$TIMESTAMP_HASH"
grep "k8s.io/kms =>" go.mod
else
echo "❌ Could not extract timestamp/hash from k8s.io/kms version"
exit 1
fi
elif [[ "$CURRENT_KMS_VERSION" == "v0.0.0-"* ]] || [ -z "$CURRENT_KMS_VERSION" ]; then
echo "✅ k8s.io/kms version format is correct"
grep "k8s.io/kms =>" go.mod || echo "(k8s.io/kms uses default v0.0.0- format)"
fi
# Step 2: Tidy root module
echo "Step 2: Running go mod tidy in root module..."
GOTOOLCHAIN=auto GOSUMDB=off go mod tidy
if [ $? -ne 0 ]; then
echo "❌ go mod tidy failed in root module"
exit 1
fi
echo "✅ Root module dependencies resolved"
# Step 3: Vendor at ROOT
echo "Step 3: Running go mod vendor in root module..."
GOTOOLCHAIN=auto GOSUMDB=off go mod vendor
if [ $? -ne 0 ]; then
echo "❌ go mod vendor failed in root module"
exit 1
fi
echo "✅ Root module dependencies vendored (vendor/ at root)"
# Step 4: Build verification
echo "Step 4: Building extension binary for verification..."
make extension
if [ $? -eq 0 ]; then
echo "✅ Extension binary built successfully"
# Test binary execution
./bin/$EXTENSION_NAME-tests-ext --help > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "✅ Binary executes and responds to --help"
else
echo "⚠️ Binary built but execution check failed"
fi
else
echo "❌ Build failed"
exit 1
fi
echo "========================================="
echo "✅ Phase 6 Complete"
echo "========================================="
cd <working-dir>/tests-extension
echo "========================================="
echo "Phase 6: Dependency Resolution"
echo "========================================="
# Step 1: Verify k8s.io versions after fresh go.mod creation
echo "Step 1: Verifying k8s.io versions..."
# For single-module, go.mod is created fresh, but we still check for compatibility
# This is informational - dependencies will be resolved correctly by go mod tidy
if grep -q "k8s.io" go.mod; then
# Check if OTE/origin pulled in compatible k8s.io versions
if grep "k8s.io" go.mod | grep -q "v0.30\|v0.31\|v0.32\|v0.33\|v0.34\|v0.35"; then
echo "⚠️ Note: k8s.io dependencies are at v0.30-v0.35 range"
echo " OTE framework uses OpenShift kubernetes fork (October 2025)"
echo " go mod tidy will resolve compatible versions automatically"
fi
echo "✅ k8s.io versions will be resolved by go mod tidy"
else
echo "✅ No k8s.io dependencies detected yet"
fi
echo "Step 2: Running go mod tidy..."
GOTOOLCHAIN=auto GOSUMDB=off go mod tidy
if [ $? -ne 0 ]; then
echo "❌ go mod tidy failed"
exit 1
fi
echo "Step 3: Running go mod vendor..."
GOTOOLCHAIN=auto GOSUMDB=off go mod vendor
if [ $? -ne 0 ]; then
echo "❌ go mod vendor failed"
exit 1
fi
echo "Step 4: Building extension binary for verification..."
make build
if [ $? -eq 0 ]; then
echo "✅ Extension binary built successfully"
# Test binary execution
./bin/$EXTENSION_NAME-tests-ext --help > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "✅ Binary executes and responds to --help"
fi
else
echo "❌ Build failed"
exit 1
fi
echo "========================================="
echo "✅ Phase 6 Complete"
echo "========================================="
🚨 MANDATORY PHASE - MUST BE EXECUTED 🚨
CRITICAL: This phase is REQUIRED. After Phase 6 completes, you MUST proceed to Phase 7. DO NOT skip this phase.
echo "========================================="
echo "Phase 7: Dockerfile Integration"
echo "========================================="
echo "Using choice from Input 10: <dockerfile-choice>"
This phase executes automated or manual Dockerfile integration based on the user's choice from Input 10.
Use the <dockerfile-choice> and <selected-dockerfiles> variables collected in Phase 1, Input 10 and 10a.
<dockerfile-choice> = "manual", proceed to Step 2<dockerfile-choice> = "automated" and <selected-dockerfiles> is empty, skip Phase 7 (no Dockerfiles selected)<dockerfile-choice> = "automated" and <selected-dockerfiles> is not empty, proceed to Step 3If user chose manual integration:
========================================
Manual Dockerfile Integration Instructions
========================================
To integrate the OTE extension binary into your Docker image, add one builder stage and one COPY command.
**Note**: This works for both single-stage and multi-stage Dockerfiles.
## 1. Test Extension Builder Stage
Add this stage to build and compress the OTE extension binary.
**For multi-stage Dockerfiles**: Add after your existing builder stage.
**For single-stage Dockerfiles**: Add as the first stage before your existing FROM.
FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.21 AS test-extension-builder RUN mkdir -p /go/src/github.com/openshift/<extension-name> WORKDIR /go/src/github.com/openshift/<extension-name> COPY . .
RUN make tests-ext-build &&
cd bin &&
tar -czvf <extension-name>-test-extension.tar.gz <extension-name>-tests-ext &&
rm -f <extension-name>-tests-ext
RUN cd tests-extension &&
make build &&
cd bin &&
tar -czvf <extension-name>-test-extension.tar.gz <extension-name>-tests-ext &&
rm -f <extension-name>-tests-ext
## 2. Copy to Final Image
Add this to your final runtime stage:
COPY --from=test-extension-builder /go/src/github.com/openshift/<extension-name>/bin/<extension-name>-test-extension.tar.gz /usr/bin/
## Example: Multi-Stage Dockerfile
FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.24-openshift-4.22 AS builder WORKDIR /go/src/github.com/openshift/<extension-name> COPY . . RUN make build
FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.25-openshift-4.22 AS test-extension-builder
RUN mkdir -p /go/src/github.com/openshift/<extension-name>
WORKDIR /go/src/github.com/openshift/<extension-name>
COPY . .
RUN make tests-ext-build &&
cd bin &&
tar -czvf <extension-name>-test-extension.tar.gz <extension-name>-tests-ext &&
rm -f <extension-name>-tests-ext
FROM registry.ci.openshift.org/ocp/4.17:base-rhel9 COPY --from=builder /go/src/github.com/openshift/<extension-name>/bin/<extension-name> /usr/bin/
COPY --from=test-extension-builder /go/src/github.com/openshift/<extension-name>/bin/<extension-name>-test-extension.tar.gz /usr/bin/
## Example: Single-Stage Dockerfile
FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.25-openshift-4.22 AS test-extension-builder
RUN mkdir -p /go/src/github.com/openshift/<extension-name>
WORKDIR /go/src/github.com/openshift/<extension-name>
COPY . .
RUN make tests-ext-build &&
cd bin &&
tar -czvf <extension-name>-test-extension.tar.gz <extension-name>-tests-ext &&
rm -f <extension-name>-tests-ext
FROM registry.svc.ci.openshift.org/openshift/origin-v4.0:base-router
RUN INSTALL_PKGS="socat haproxy28 rsyslog" &&
yum install -y $INSTALL_PKGS &&
yum clean all
COPY images/router/haproxy/ /var/lib/haproxy/
COPY --from=test-extension-builder /go/src/github.com/openshift/<extension-name>/bin/<extension-name>-test-extension.tar.gz /usr/bin/
USER 1001 EXPOSE 80 443 ENTRYPOINT ["/usr/bin/openshift-router"]
Replace <extension-name> with your actual extension name.
**Note:** The Makefile uses `-mod=vendor` by default, which means all dependencies are built from the vendored code. This eliminates the need for SSH authentication or network access during Docker builds.
========================================
Exit Phase 7 after providing instructions.
If user chose automated integration, use the <selected-dockerfiles> from Phase 1, Input 10a:
echo "========================================="
echo "Phase 7: Dockerfile Integration (Automated)"
echo "========================================="
# Use the Dockerfiles selected in Phase 1
SELECTED_DOCKERFILES="<selected-dockerfiles>"
echo "Updating selected Dockerfiles: $SELECTED_DOCKERFILES"
echo ""
Convert the stored selection to an array for processing:
# Convert space-separated list to array
SELECTED_DOCKERFILES_ARRAY=($SELECTED_DOCKERFILES)
if [ ${#SELECTED_DOCKERFILES_ARRAY[@]} -eq 0 ]; then
echo "No Dockerfiles selected for update"
exit 0
fi
For each selected Dockerfile:
for DOCKERFILE in "${SELECTED_DOCKERFILES_ARRAY[@]}"; do
echo ""
echo "Updating $DOCKERFILE..."
# Create backup
cp "$DOCKERFILE" "${DOCKERFILE}.pre-ote-migration"
echo "✅ Created backup: ${DOCKERFILE}.pre-ote-migration"
# Determine builder image for test-extension-builder stage
# Strategy: Use existing builder image if found, otherwise derive from go.mod
BUILDER_IMAGE=$(grep "^FROM.*AS builder" "$DOCKERFILE" | head -1 | awk '{print $2}')
if [ -z "$BUILDER_IMAGE" ]; then
# No named builder stage found - need to select appropriate Go builder image
# Extract Go version from root go.mod to match toolchain
GO_VERSION=$(grep "^go " <working-dir>/go.mod | awk '{print $2}' | cut -d. -f1,2)
if [ -z "$GO_VERSION" ]; then
GO_VERSION="1.22"
echo "⚠️ Could not detect Go version from go.mod, defaulting to $GO_VERSION"
fi
# Map Go version to available OpenShift builder image
case "$GO_VERSION" in
1.25)
BUILDER_IMAGE="registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.25-openshift-4.22"
echo "ℹ️ No builder stage found, using Go builder: $BUILDER_IMAGE"
;;
1.24)
BUILDER_IMAGE="registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.24-openshift-4.22"
echo "ℹ️ No builder stage found, using Go builder: $BUILDER_IMAGE"
;;
1.23)
BUILDER_IMAGE="registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.23-openshift-4.20"
echo "ℹ️ No builder stage found, using Go builder: $BUILDER_IMAGE"
;;
1.22)
BUILDER_IMAGE="registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.22-openshift-4.19"
echo "ℹ️ No builder stage found, using Go builder: $BUILDER_IMAGE"
;;
1.21)
BUILDER_IMAGE="registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.21-openshift-4.19"
echo "ℹ️ No builder stage found, using Go builder: $BUILDER_IMAGE"
;;
*)
# Default to Go 1.22 for older or unknown versions
BUILDER_IMAGE="registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.22-openshift-4.19"
echo "⚠️ Unknown Go version $GO_VERSION, defaulting to Go 1.22 builder"
;;
esac
else
echo "✅ Using existing builder image: $BUILDER_IMAGE"
fi
# Check if OTE stage already exists
if grep -q "test-extension-builder" "$DOCKERFILE"; then
echo "⚠️ test-extension-builder stage already exists, skipping"
continue
fi
# Create test-extension-builder stage (builds and compresses)
# Uses -mod=vendor by default (no SSH needed)
TEST_BUILDER_STAGE="
# Test extension builder stage (added by ote-migration)
FROM $BUILDER_IMAGE AS test-extension-builder
RUN mkdir -p /go/src/github.com/openshift/$EXTENSION_NAME
WORKDIR /go/src/github.com/openshift/$EXTENSION_NAME
COPY . .
"
# Add build and compress commands based on strategy
if [ "$STRUCTURE_STRATEGY" = "monorepo" ]; then
TEST_BUILDER_STAGE+="RUN make tests-ext-build && \\
cd bin && \\
tar -czvf $EXTENSION_NAME-test-extension.tar.gz $EXTENSION_NAME-tests-ext && \\
rm -f $EXTENSION_NAME-tests-ext
"
else
TEST_BUILDER_STAGE+="RUN cd tests-extension && \\
make build && \\
cd bin && \\
tar -czvf $EXTENSION_NAME-test-extension.tar.gz $EXTENSION_NAME-tests-ext && \\
rm -f $EXTENSION_NAME-tests-ext
"
fi
# Detect Dockerfile type and find insertion point
BUILDER_LINE=$(grep -n "^FROM.*AS builder" "$DOCKERFILE" | head -1 | cut -d: -f1)
FIRST_FROM_LINE=$(grep -n "^FROM" "$DOCKERFILE" | head -1 | cut -d: -f1)
if [ -n "$BUILDER_LINE" ]; then
# Multi-stage Dockerfile with existing builder stage
echo "Detected multi-stage Dockerfile with builder stage"
# Find end of builder stage (next FROM line)
NEXT_FROM_LINE=$(tail -n +$((BUILDER_LINE + 1)) "$DOCKERFILE" | grep -n "^FROM" | head -1 | cut -d: -f1)
if [ -n "$NEXT_FROM_LINE" ]; then
INSERT_LINE=$((BUILDER_LINE + NEXT_FROM_LINE))
# Insert test-extension-builder stage after builder stage
{
head -n $((INSERT_LINE - 1)) "$DOCKERFILE"
echo "$TEST_BUILDER_STAGE"
tail -n +$INSERT_LINE "$DOCKERFILE"
} > "${DOCKERFILE}.tmp"
mv "${DOCKERFILE}.tmp" "$DOCKERFILE"
echo "✅ Added test-extension-builder stage after existing builder stage"
else
echo "❌ Failed to find end of builder stage"
continue
fi
elif [ -n "$FIRST_FROM_LINE" ]; then
# Single-stage or multi-stage without named builder
echo "Detected single-stage or multi-stage Dockerfile without named builder"
# Insert test-extension-builder stage before first FROM (as first stage)
{
head -n $((FIRST_FROM_LINE - 1)) "$DOCKERFILE"
echo "$TEST_BUILDER_STAGE"
echo ""
tail -n +$FIRST_FROM_LINE "$DOCKERFILE"
} > "${DOCKERFILE}.tmp"
mv "${DOCKERFILE}.tmp" "$DOCKERFILE"
echo "✅ Added test-extension-builder stage as first stage"
else
echo "❌ No FROM line found in Dockerfile, skipping"
continue
fi
# Add COPY command to final stage
FINAL_FROM_LINE=$(grep -n "^FROM" "$DOCKERFILE" | tail -1 | cut -d: -f1)
if [ -n "$FINAL_FROM_LINE" ]; then
# Determine COPY path based on strategy
if [ "$STRUCTURE_STRATEGY" = "monorepo" ]; then
COPY_PATH="/go/src/github.com/openshift/$EXTENSION_NAME/bin/$EXTENSION_NAME-test-extension.tar.gz"
else
COPY_PATH="/go/src/github.com/openshift/$EXTENSION_NAME/tests-extension/bin/$EXTENSION_NAME-test-extension.tar.gz"
fi
COPY_CMD="
# Copy test extension binary (added by ote-migration)
COPY --from=test-extension-builder $COPY_PATH /usr/bin/"
# Insert COPY command after final FROM line
{
head -n $FINAL_FROM_LINE "$DOCKERFILE"
echo "$COPY_CMD"
tail -n +$((FINAL_FROM_LINE + 1)) "$DOCKERFILE"
} > "${DOCKERFILE}.tmp"
mv "${DOCKERFILE}.tmp" "$DOCKERFILE"
echo "✅ Added COPY command to final stage"
fi
echo "✅ Updated $DOCKERFILE"
done
echo ""
echo "========================================="
echo "Dockerfile Integration Complete"
echo "========================================="
echo "Updated Dockerfiles:"
for DF in "${SELECTED_DOCKERFILES_ARRAY[@]}"; do
echo " - $DF"
echo " Backup: ${DF}.pre-ote-migration"
done
echo ""
Generate comprehensive summary based on strategy used.
For Monorepo:
========================================
🎉 OTE Migration Complete!
========================================
## Summary
Successfully migrated **<extension-name>** to OTE framework using **monorepo strategy** with single module approach.
## Created Structure
Directory tree:
<working-dir>/
├── bin/
│ └── <extension-name>-tests-ext
├── cmd/
│ └── extension/
│ └── main.go # OTE entry point (at root)
├── test/
│ └── e2e/
│ ├── *_test.go # Migrated test files
│ └── testdata/
│ ├── bindata.go
│ └── fixtures.go
├── vendor/ # Vendored at ROOT
├── bindata.mk # Bindata generation (at root)
├── go.mod # Single module with all dependencies
├── go.sum # Single go.sum
├── Makefile # Updated with OTE targets
└── Dockerfile # Updated (if automated)
## Key Features
1. **CMD Location**: `cmd/extension/main.go` (at root, not under test/)
2. **Single Module**: All dependencies (component + tests) in root go.mod
3. **No Sig Filtering**: All tests included without filtering
4. **Annotations**:
- [OTP] added to all Describe blocks at beginning
- [Level0] added to test names with -LEVEL0- suffix only
5. **Vendored at Root**: Only `vendor/` at repository root
6. **Dockerfile Integration**: Automated Docker image integration
## Next Steps
### 1. Verify Build
make extension
ls -lh bin/<extension-name>-tests-ext
### 2. List Tests
./bin/<extension-name>-tests-ext list
./bin/<extension-name>-tests-ext list | wc -l
./bin/<extension-name>-tests-ext list | grep -c "[Level0]"
### 3. Run Tests
./bin/<extension-name>-tests-ext run
./bin/<extension-name>-tests-ext run --grep "test-name-pattern"
./bin/<extension-name>-tests-ext run --grep "[Level0]"
### 4. Build Docker Image
podman build -t <component>:test -f <path-to-dockerfile> .
podman run --rm --entrypoint ls <component>:test -lh /usr/bin/*-test-extension.tar.gz
### 5. Verify Test Annotations
grep -r "[OTP]" test/e2e/*_test.go
grep -r "[Level0]" test/e2e/*_test.go
grep -r "-LEVEL0-" test/e2e/*_test.go || echo "✅ All -LEVEL0- removed"
## Files Created/Modified
- ✅ `cmd/extension/main.go` - Created
- ✅ `test/e2e/testdata/fixtures.go` - Created
- ✅ `test/e2e/testdata/bindata.go` - Created (generated, can be in .gitignore)
- ✅ `bindata.mk` - Created (at repository root)
- ✅ `test/e2e/*_test.go` - Modified (annotations, imports)
- ✅ `go.mod` - Updated (all dependencies + replace directives)
- ✅ `go.sum` - Updated
- ✅ `vendor/` - Created at root
- ✅ `Makefile` - Updated (tests-ext-build target)
- ✅ `Dockerfile` - Updated (if automated integration)
## Files to Commit (IMPORTANT!)
Before creating a PR, ensure these files are committed to git:
**Required for reproducible builds:**
- ✅ `go.mod` - Single module with all dependencies
- ✅ `go.sum` - Single go.sum for reproducible builds
- ✅ `cmd/extension/main.go` - OTE entry point
- ✅ `test/e2e/testdata/fixtures.go` - Testdata helper functions
- ✅ `bindata.mk` - Bindata generation makefile (at repository root)
- ✅ `test/e2e/*_test.go` - Migrated test files
- ✅ `Makefile` - Updated with OTE targets
**Can be in .gitignore:**
- ⚠️ `test/e2e/testdata/bindata.go` - Generated file (regenerated during build)
- ⚠️ `bin/<extension-name>-tests-ext` - Binary (build artifact)
- ⚠️ `vendor/` - Optional (some repos commit, others don't)
**Why go.sum is critical:**
Without committed go.sum, Docker builds will:
- ❌ Be slower (must download all modules and generate go.sum)
- ❌ Be less reproducible (module versions can drift)
- ❌ Fail on network issues or GOSUMDB problems
- ❌ Potentially use different dependency versions
With committed go.sum, Docker builds will:
- ✅ Be faster (use checksums from go.sum)
- ✅ Be reproducible (exact same dependencies every time)
- ✅ Work reliably in CI/CD environments
- ✅ Ensure consistent behavior across builds
**Verify file is tracked:**
git status go.sum
git add go.mod go.sum
## Troubleshooting
If you encounter issues, see the troubleshooting guide below.
========================================
Migration completed successfully! 🎉
========================================
For Single-Module:
========================================
🎉 OTE Migration Complete!
========================================
## Summary
Successfully migrated **<extension-name>** to OTE framework using **single-module strategy**.
## Created Structure
Directory tree:
<working-dir>/
└── tests-extension/
├── cmd/
│ └── main.go # OTE entry point
├── bin/
│ └── <extension-name>-tests-ext
├── test/
│ └── e2e/
│ ├── *_test.go # Migrated tests
│ ├── testdata/
│ │ ├── bindata.go
│ │ └── fixtures.go
│ └── bindata.mk
├── vendor/ # Vendored dependencies
├── go.mod
├── go.sum
└── Makefile
## Next Steps
### 1. Build Extension
cd tests-extension make build
### 2. List Tests
./bin/<extension-name>-tests-ext list
### 3. Run Tests
./bin/<extension-name>-tests-ext run
## Files Created
- ✅ `tests-extension/cmd/main.go`
- ✅ `tests-extension/go.mod`
- ✅ `tests-extension/vendor/`
- ✅ `tests-extension/test/e2e/*_test.go`
- ✅ `tests-extension/test/e2e/testdata/fixtures.go`
- ✅ `tests-extension/Makefile`
========================================
Migration completed successfully! 🎉
========================================
Throughout the workflow:
This section provides solutions to common issues encountered during OTE migration based on real-world experience.
Symptom:
undefined: ginkgo.NewWriter
spec.Labels undefined (type types.TestSpec has no field or method Labels)
Root Cause: Test module uses old Ginkgo version from OTP (August 2024), but OTE framework requires newer version (December 2024).
Solution: The December 2024 Ginkgo fork is backward compatible with August 2024 code. Always use OTE's Ginkgo version:
cd test/e2e # or test/e2e/<test-dir-name>
# Get OTE's Ginkgo version (December 2024)
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift/onsi-ginkgo/[email protected]"
# Update root go.mod replace directive
cd ../.. # or ../../.. for subdirectory mode
grep -q "github.com/onsi/ginkgo/v2 =>" go.mod || echo "replace github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12" >> go.mod
GOTOOLCHAIN=auto GOSUMDB=off go mod tidy
GOTOOLCHAIN=auto GOSUMDB=off go mod vendor
Why this works: OTE's December 2024 fork is backward compatible with OTP's August 2024 code. Using OTE's version everywhere prevents API incompatibilities.
Symptom:
k8s.io/cri-client/pkg/remote_image.go:75:39: undefined: otelgrpc.UnaryClientInterceptor
Root Cause: Root module uses old k8s.io/kubernetes replace directives (October 2024), but test module needs newer versions from OTP (October 2025).
Solution: Sync k8s.io replace directives from test module to root:
# Extract k8s.io replace directives from test module
cd test/e2e # or test/e2e/<test-dir-name>
grep "k8s.io/" go.mod | grep "=>" > /tmp/k8s_replaces.txt
# Apply to root go.mod
cd ../.. # or ../../.. for subdirectory mode
while IFS= read -r replace_line; do
PACKAGE=$(echo "$replace_line" | awk '{print $1}')
# Remove old version if exists
sed -i "/^[[:space:]]*$PACKAGE /d" go.mod
# Add new version
sed -i "/^replace (/a\\ $replace_line" go.mod
done < /tmp/k8s_replaces.txt
GOTOOLCHAIN=auto GOSUMDB=off go mod tidy
GOTOOLCHAIN=auto GOSUMDB=off go mod vendor
Why this works: OTP's k8s.io versions (October 2025) are proven stable. The OTE Ginkgo fork (December 2024) is compatible with these versions.
Symptom:
k8s.io/kube-openapi/pkg/util/proto/document_v3.go:291:31: cannot use s.GetDefault().ToRawInfo() (value of type *"go.yaml.in/yaml/v3".Node) as *"gopkg.in/yaml.v3".Node value in argument to parseV3Interface
Root Cause: Old kube-openapi pin from February 2024 conflicts with natural version resolution.
Solution: Remove old kube-openapi pin to allow natural version resolution:
cd <working-dir>
# Remove old kube-openapi pin
sed -i '/k8s.io\/kube-openapi => k8s.io\/kube-openapi v0.0.0-2024/d' go.mod
# Clean and rebuild
rm -rf vendor/
GOTOOLCHAIN=auto GOSUMDB=off go mod tidy
GOTOOLCHAIN=auto GOSUMDB=off go mod vendor
make clean-extension
make extension
Why this works: Removing the old pin allows Go to resolve the correct kube-openapi version naturally based on k8s.io dependencies.
🚨 CRITICAL: Different files use different import patterns
In cmd/extension/main.go:
import (
"github.com/openshift/origin/test/extended/util" // NO alias - imported as 'util'
compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"
)
func main() {
util.InitStandardFlags() // Use 'util' (no alias)
// ...
componentSpecs.AddBeforeAll(func() {
if err := compat_otp.InitTest(false); err != nil {
panic(err)
}
})
}
In test files (e.g., test/e2e/*_test.go):
import (
exutil "github.com/openshift/origin/test/extended/util" // WITH alias - imported as 'exutil'
compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"
)
var _ = g.Describe("[OTP] Test", func() {
var oc *exutil.CLI // Use 'exutil.CLI' as the type (with alias)
g.BeforeEach(func() {
oc = compat_otp.NewCLI(...) // Use 'compat_otp' helper (returns *exutil.CLI)
})
})
Why the difference:
main.go calls util.InitStandardFlags() which expects no alias*exutil.CLI which requires the aliascompat_otp for helper functions like NewCLI() and InitTest()*compat_otp.CLI as a type - it doesn't exist!Symptom:
[PANICKED] Test Panicked
In [BeforeEach] at: .../vendor/github.com/openshift/origin/test/extended/util/test_setup.go:140
May only be called from within a test case
Full Stack Trace
github.com/openshift/origin/test/extended/util.requiresTestStart(...)
.../vendor/github.com/openshift/origin/test/extended/util/test_setup.go:140
github.com/openshift/origin/test/extended/util.(*CLI).setupProject(0xc004ff2100?)
.../vendor/github.com/openshift/origin/test/extended/util/client.go:365
Root Cause:
CLI is initialized at the Describe block level (package-level variable) instead of in a BeforeEach hook. In OTE, the test framework is initialized in BeforeAll, which runs after Describe blocks are evaluated. Calling compat_otp.NewCLI() before the framework is initialized causes this panic.
❌ WRONG - OTP Pattern (does not work in OTE):
var _ = g.Describe("[OTP] Test Suite", func() {
var oc = compat_otp.NewCLI("my-cli", compat_otp.KubeConfigPath()) // ❌ Runs BEFORE BeforeAll
g.It("test case", func() {
// oc is already initialized here
})
})
✅ CORRECT - OTE Pattern:
var _ = g.Describe("[OTP] Test Suite", func() {
var oc *exutil.CLI // ✅ Declare variable with actual type (exutil.CLI)
g.BeforeEach(func() { // ✅ Initialize in BeforeEach (runs AFTER BeforeAll)
oc = compat_otp.NewCLI("my-cli", compat_otp.KubeConfigPath()) // Use compat_otp helper
})
g.It("test case", func() {
// oc is initialized by BeforeEach before this runs
})
})
Why this works:
BeforeAll in main.go calls compat_otp.InitTest() to initialize the frameworkBeforeEach runs after BeforeAll, so the framework is readycompat_otp.NewCLI() can now safely set up project and client hooksUnderstanding the imports:
exutil package defines the CLI type - use *exutil.CLI for variable declarationscompat_otp package provides helper functions that return *exutil.CLI - use compat_otp.NewCLI() for initializationcompat_otp.CLI type - using *compat_otp.CLI will cause compilation errorsMigration Pattern: For ALL test files, change from:
var oc = compat_otp.NewCLI("name", compat_otp.KubeConfigPath())
To:
var oc *exutil.CLI // Type is exutil.CLI (the actual type)
g.BeforeEach(func() {
oc = compat_otp.NewCLI("name", compat_otp.KubeConfigPath()) // compat_otp helper returns *exutil.CLI
})
Symptom:
unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined
[FAILED] in [BeforeEach] - framework.go:222
Root Cause:
Removing util.InitStandardFlags() prevents the test framework from registering kubeconfig flags. Tests fail even when KUBECONFIG is set.
🚨 COMMON MISTAKE - DO NOT DO THIS IN main.go:
// ❌ WRONG FOR main.go - DO NOT use exutil alias in main.go:
import (
exutil "github.com/openshift/origin/test/extended/util" // ❌ WRONG for main.go
framework "k8s.io/kubernetes/test/e2e/framework"
)
func main() {
exutil.InitStandardFlags() // ❌ WRONG for main.go
// ...
componentSpecs.AddBeforeAll(func() {
if err := exutil.InitTest(false); err != nil { // ❌ WRONG for main.go
panic(err)
}
})
}
The above WRONG code removes the required compat_otp package and breaks the framework initialization.
✅ CORRECT Solution:
Both util and compat_otp imports are REQUIRED. The compat_otp package is a REAL package at github.com/openshift/origin/test/extended/util/compat_otp:
// ✅ CORRECT main.go setup:
import (
"k8s.io/component-base/logs"
"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"
)
func main() {
// Initialize test framework flags (required for kubeconfig, provider, etc.)
util.InitStandardFlags()
framework.AfterReadingAllFlags(&framework.TestContext)
logs.InitLogs()
defer logs.FlushLogs()
// ... build and filter specs ...
// 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
})
})
// ... rest of extension setup ...
}
What to keep:
util.InitStandardFlags() - Registers kubeconfig, provider, and other test flags (MUST use util, NOT exutil)framework.AfterReadingAllFlags(&framework.TestContext) - Initializes framework context (REQUIRED)logs.InitLogs() - Initialize loggingcompat_otp.InitTest(false) in BeforeAll - Sets up test framework context (MUST use compat_otp, NOT util or exutil)util and compat_otp import lines - BOTH are requiredutil.WithCleanup() wrapper - Sets testsStarted = true so OTP functions like oc.Run() execute without "test not started" panicsWhy this works:
util.InitStandardFlags() registers framework flags so KUBECONFIG is recognizedframework.AfterReadingAllFlags(&framework.TestContext) initializes the framework context, preventing nil pointer dereference in framework.BeforeEachcompat_otp.InitTest() in BeforeAll sets up the test framework context when tests startutil.WithCleanup() sets the testsStarted flag, which OTP helper functions check before executing — without it, calls like oc.Run() panic with "test not started"Why you CANNOT use exutil alias:
compat_otp package provides InitTest() which is different from util.InitTest()exutil as an alias removes the ability to import both util and compat_otpSymptom:
100+ errors with -mod=vendor:
vendor/k8s.io/apimachinery/pkg/util/managedfields/internal/fieldmanager.go:26:2: imported and not used: "sigs.k8s.io/structured-merge-diff/v4/fieldpath"
Root Cause: Vendor directory contains both v4 and v6 of structured-merge-diff, causing conflicts. This is a specific symptom of vendor directory being out of sync with go.mod.
Solution: See the "Vendor Directory Out of Sync" section below for the complete solution and explanation.
# Check go.mod in test module
cd test/e2e
go mod verify
# Rebuild vendor at root
cd ../..
rm -rf vendor/
GOTOOLCHAIN=auto GOSUMDB=off go mod vendor
# Clean and rebuild
make clean-extension
make extension
# Check testdata imports
grep -r "testdata.FixturePath" test/e2e/
# Verify import paths
grep -r "import" test/e2e/*.go | grep testdata
# Check for missing [OTP]
grep -L "\[OTP\]" test/e2e/*_test.go
# Check for remaining -LEVEL0-
grep -r "\-LEVEL0\-" test/e2e/
# Re-run annotation script if needed
python3 /tmp/annotate_tests.py test/e2e/
# Check Dockerfile stages
docker build --target test-extension-builder .
# Verify binary exists before compression
docker run --rm <image> ls -la bin/
# Check Makefile target
make tests-ext-build
Symptom:
# Build fails with import errors or version conflicts
vendor/k8s.io/apimachinery/pkg/util/managedfields/internal/fieldmanager.go:26:2: imported and not used
Cause: The vendor directory doesn't match the current go.mod (happens after updating k8s.io versions or replace directives).
Solution:
# Regenerate vendor directory to match go.mod
rm -rf vendor/
GOTOOLCHAIN=auto GOSUMDB=off go mod tidy
GOTOOLCHAIN=auto GOSUMDB=off go mod vendor
# Verify vendor is clean
go mod verify
# Retry build
make clean-extension
make tests-ext-build
# If Docker build:
docker build -t <component>:test -f <dockerfile> .
Why this works:
The Makefile uses -mod=vendor by default, which builds from the vendored code. After updating go.mod, the vendor directory must be regenerated to contain the correct resolved dependencies. This ensures reproducible, offline builds without requiring network access during Docker builds.
Symptom:
cannot use *resourceConfig.PidsLimit (variable of type int64) as *int64 value in assignment
Root Cause:
Using github.com/openshift/origin@main pulls the latest changes, which may include breaking API changes. The origin repository evolves rapidly and can introduce incompatibilities.
Solution:
Pin to a known working version instead of using @main:
# Use a specific working version instead of @main
ORIGIN_VERSION="v1.5.0-alpha.3.0.20260310231025-5d3fd0545b5d" # March 10, 2026
# Update with GOSUMDB=off to avoid checksum verification issues
GOTOOLCHAIN=auto GOSUMDB=off go get "github.com/openshift/origin@$ORIGIN_VERSION"
# Clean and rebuild
rm -rf vendor/
GOTOOLCHAIN=auto GOSUMDB=off go mod tidy
GOTOOLCHAIN=auto GOSUMDB=off go mod vendor
make clean-extension
make extension
Why this works:
Pinning to a specific version prevents breaking changes from being pulled automatically. Using GOSUMDB=off skips checksum verification for internal packages that may not be in the public Go module proxy.
Finding a working version:
# View recent origin commits to find a stable version
cd /path/to/openshift-tests-private
git log --oneline --since="1 month ago" | head -20
# Check version in working OTP repository
grep "github.com/openshift/origin" go.mod
Symptom:
verifying github.com/openshift/origin@...: reading https://sum.golang.org/lookup/...: 410 Gone
go: github.com/openshift/origin@...: invalid version: git fetch failed
Root Cause:
Internal OpenShift packages are not available in the public Go checksum database (sum.golang.org). Using GOSUMDB=sum.golang.org causes verification failures.
Solution:
Use GOSUMDB=off for all go get/tidy/vendor commands:
# Disable checksum verification for internal packages
GOTOOLCHAIN=auto GOSUMDB=off go get "github.com/openshift/origin@$VERSION"
GOTOOLCHAIN=auto GOSUMDB=off go mod tidy
GOTOOLCHAIN=auto GOSUMDB=off go mod vendor
# Verify vendor directory is populated
go mod verify
Why this works:
OpenShift internal packages are not published to the public Go module proxy. Setting GOSUMDB=off disables checksum verification, allowing Go to fetch packages directly from their source repositories (e.g., GitHub).
Security note: This is safe for internal development because:
-mod=vendor)Symptom:
panic: failed to restore fixture router/haproxy.cfg: Asset not found
Root Cause: Testdata files not copied from openshift-tests-private during Phase 3, or bindata.go not regenerated.
Solution:
# Check if testdata files exist (excluding generated files)
find test/e2e/testdata -type f ! -name "fixtures.go" ! -name "bindata.go"
# If empty, copy testdata from source
SOURCE_TESTDATA="<source-repo>/test/extended/testdata/<testdata-subfolder>"
cp -rv "$SOURCE_TESTDATA"/* test/e2e/testdata/
# Regenerate bindata.go
cd test/e2e
make -f bindata.mk update-bindata
# Rebuild extension
cd ../..
make clean-extension
make extension
Why this works: bindata.go embeds all files from testdata/ directory. If testdata files weren't copied, bindata.go only contains fixtures.go, causing runtime panics when tests load fixtures via FixturePath().
This skill provides complete automation for OTE migration with:
vendor/ at repository rootFollow each phase sequentially for successful migration. All phases include error handling and validation to ensure migration integrity.
testing
Snapshot OpenShift payload data (release controller, PR diffs, comments, CI jobs, JUnit results, regression tracking) to a local directory for offline analysis
research
Shared engine for analyzing Jira issue activity and generating status summaries
tools
This skill should be used before any Snowflake command to verify MCP connectivity, guide users through access provisioning, and set the session context. Invoke this skill proactively whenever a command needs Snowflake data access.
development
Analyze a payload snapshot to identify root causes of blocking job failures, score candidate PRs, and produce an HTML report with revert recommendations