From a9f5535b42659fceb95ceca5d0254f5e810c6639 Mon Sep 17 00:00:00 2001 From: supernova Date: Fri, 23 May 2025 21:28:16 +0530 Subject: [PATCH 01/12] WIP : linux compatibility and testing --- .../linux-compatibility-linux-amd64.yml | 101 +++++++ .github/workflows/linux-compatibility.yml | 62 +++++ Makefile | 8 +- docker/anvil/docker-compose.yaml | 2 + docker/linux-test/Dockerfile | 253 ++++++++++++++++++ docker/linux-test/docker-compose.yml | 17 ++ pkg/commands/devnet_actions.go | 9 +- pkg/common/devnet/constants.go | 22 +- pkg/common/devnet/utils.go | 80 ++++++ scripts/linux-compatibility.sh | 64 +++++ scripts/test-networking-regression.sh | 219 +++++++++++++++ 11 files changed, 829 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/linux-compatibility-linux-amd64.yml create mode 100644 .github/workflows/linux-compatibility.yml create mode 100644 docker/linux-test/Dockerfile create mode 100644 docker/linux-test/docker-compose.yml create mode 100755 scripts/linux-compatibility.sh create mode 100755 scripts/test-networking-regression.sh diff --git a/.github/workflows/linux-compatibility-linux-amd64.yml b/.github/workflows/linux-compatibility-linux-amd64.yml new file mode 100644 index 00000000..d551195d --- /dev/null +++ b/.github/workflows/linux-compatibility-linux-amd64.yml @@ -0,0 +1,101 @@ +name: linux-compatibility-linux-amd64 + +on: + push: + branches: + - main + pull_request: + branches: ["**"] + # Allow manual triggering + workflow_dispatch: + +jobs: + linux-amd64-compatibility: + name: Linux AMD64 Compatibility Testing + runs-on: ubuntu-latest + timeout-minutes: 25 + + strategy: + matrix: + # Test different Ubuntu versions to ensure broader compatibility + ubuntu-version: ["20.04", "22.04", "24.04"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64 + + - name: Free up disk space + run: | + docker system prune -f + df -h + + - name: Build Linux AMD64 specific test image + run: | + docker build \ + --platform linux/amd64 \ + --add-host=host.docker.internal:host-gateway \ + -f docker/linux-test/Dockerfile \ + -t devkit-linux-amd64-test:${{ matrix.ubuntu-version }} \ + --build-arg BASE_IMAGE=ubuntu:${{ matrix.ubuntu-version }} \ + . + + - name: Make script executable + run: chmod +x ./scripts/linux-compatibility.sh + + - name: Run Linux AMD64 compatibility tests + env: + DOCKER_IMAGE_TAG: ${{ matrix.ubuntu-version }} + run: | + echo "πŸ”§ Testing on Ubuntu ${{ matrix.ubuntu-version }} (linux/amd64)" + # Modify the script to use our specific image + sed "s/devkit-linux-test/devkit-linux-amd64-test:${{ matrix.ubuntu-version }}/g" \ + scripts/linux-compatibility.sh > scripts/linux-compatibility-amd64.sh + chmod +x scripts/linux-compatibility-amd64.sh + ./scripts/linux-compatibility-amd64.sh + + - name: Test cross-compilation for Linux AMD64 + run: | + echo "πŸ”¨ Testing cross-compilation to linux/amd64..." + make build/linux-amd64 + file release/linux-amd64/devkit | grep "ELF 64-bit LSB executable, x86-64" + + - name: Save test results as artifact + if: always() + run: | + # Try to extract test results from the container if it exists + docker run --rm --add-host=host.docker.internal:host-gateway \ + devkit-linux-amd64-test:${{ matrix.ubuntu-version }} \ + cat test-results.txt > test-results-${{ matrix.ubuntu-version }}.txt 2>/dev/null || \ + echo "Could not extract test results for Ubuntu ${{ matrix.ubuntu-version }}" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: linux-amd64-compatibility-results-ubuntu-${{ matrix.ubuntu-version }} + path: test-results-${{ matrix.ubuntu-version }}.txt + retention-days: 30 + + - name: Archive test results on failure + if: failure() + run: | + echo "πŸ“ Collecting debug information for Ubuntu ${{ matrix.ubuntu-version }}..." + docker images | grep devkit || echo "No devkit images found" + docker ps -a | grep devkit || echo "No devkit containers found" + docker system df + echo "Platform info:" + uname -a + echo "Docker version:" + docker version + + - name: Cleanup Docker resources + if: always() + run: | + docker container prune -f || true + docker image prune -f || true + docker rmi devkit-linux-amd64-test:${{ matrix.ubuntu-version }} || true \ No newline at end of file diff --git a/.github/workflows/linux-compatibility.yml b/.github/workflows/linux-compatibility.yml new file mode 100644 index 00000000..9b756f72 --- /dev/null +++ b/.github/workflows/linux-compatibility.yml @@ -0,0 +1,62 @@ +name: Linux Compatibility Tests + +on: + push: + branches: + - main + pull_request: + branches: ["**"] + # Allow manual triggering + workflow_dispatch: + +jobs: + linux-compatibility: + name: Linux Compatibility Testing + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Free up disk space + run: | + docker system prune -f + df -h + + - name: Make script executable + run: chmod +x ./scripts/linux-compatibility.sh + + - name: Run Linux compatibility tests + run: ./scripts/linux-compatibility.sh + + - name: Save test results as artifact + if: always() + run: | + # Try to extract test results from the container if it exists + docker run --rm --add-host=host.docker.internal:host-gateway devkit-linux-test cat test-results.txt > test-results.txt 2>/dev/null || echo "Could not extract test results" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: linux-compatibility-results + path: test-results.txt + retention-days: 30 + + - name: Archive test results on failure + if: failure() + run: | + echo "πŸ“ Collecting debug information..." + docker images | grep devkit || echo "No devkit images found" + docker ps -a | grep devkit || echo "No devkit containers found" + docker system df + + - name: Cleanup Docker resources + if: always() + run: | + docker container prune -f || true + docker image prune -f || true \ No newline at end of file diff --git a/Makefile b/Makefile index d9006123..d788c795 100644 --- a/Makefile +++ b/Makefile @@ -42,16 +42,16 @@ clean: ## Remove binary @rm -f $(APP_NAME) ~/bin/$(APP_NAME) build/darwin-arm64: - GOOS=darwin GOARCH=arm64 $(ALL_FLAGS) $(GO) build $(GO_FLAGS) -o release/darwin-arm64/devkit cmd/$(APP_NAME)/main.go + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(ALL_FLAGS) $(GO) build $(GO_FLAGS) -o release/darwin-arm64/devkit cmd/$(APP_NAME)/main.go build/darwin-amd64: - GOOS=darwin GOARCH=amd64 $(ALL_FLAGS) $(GO) build $(GO_FLAGS) -o release/darwin-amd64/devkit cmd/$(APP_NAME)/main.go + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(ALL_FLAGS) $(GO) build $(GO_FLAGS) -o release/darwin-amd64/devkit cmd/$(APP_NAME)/main.go build/linux-arm64: - GOOS=linux GOARCH=arm64 $(ALL_FLAGS) $(GO) build $(GO_FLAGS) -o release/linux-arm64/devkit cmd/$(APP_NAME)/main.go + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(ALL_FLAGS) $(GO) build $(GO_FLAGS) -o release/linux-arm64/devkit cmd/$(APP_NAME)/main.go build/linux-amd64: - GOOS=linux GOARCH=amd64 $(ALL_FLAGS) $(GO) build $(GO_FLAGS) -o release/linux-amd64/devkit cmd/$(APP_NAME)/main.go + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(ALL_FLAGS) $(GO) build $(GO_FLAGS) -o release/linux-amd64/devkit cmd/$(APP_NAME)/main.go .PHONY: release diff --git a/docker/anvil/docker-compose.yaml b/docker/anvil/docker-compose.yaml index 2ec0243b..d59f4471 100644 --- a/docker/anvil/docker-compose.yaml +++ b/docker/anvil/docker-compose.yaml @@ -6,3 +6,5 @@ services: command: "--host 0.0.0.0 --fork-url ${FORK_RPC_URL} --fork-block-number ${FORK_BLOCK_NUMBER} ${ANVIL_ARGS}" ports: - "${DEVNET_PORT}:8545" + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/docker/linux-test/Dockerfile b/docker/linux-test/Dockerfile new file mode 100644 index 00000000..692ac1ee --- /dev/null +++ b/docker/linux-test/Dockerfile @@ -0,0 +1,253 @@ +# Multi-stage Dockerfile for realistic Linux user testing +# Simulates a typical Linux desktop/server environment +FROM golang:1.24.2-bookworm AS base + +# Install what a typical Linux user would have +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + git \ + ca-certificates \ + sudo \ + docker.io \ + docker-compose \ + file \ + tree \ + vim \ + wget \ + unzip \ + python3-full \ + python3-pip \ + python3-venv \ + pipx \ + && rm -rf /var/lib/apt/lists/* + +# Create a non-root user (like a real Linux user) +RUN useradd -m -s /bin/bash devuser && \ + usermod -aG sudo devuser && \ + echo "devuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + +# Set environment variables for proper Docker networking +ENV CGO_ENABLED=0 + +# Configure Python to allow user installs and break system packages if needed in container +ENV PIP_BREAK_SYSTEM_PACKAGES=1 +ENV PIP_USER=1 + +# Copy go.mod and go.sum first for better caching and download as root +COPY go.mod go.sum ./ +RUN go mod download + +USER devuser +WORKDIR /home/devuser/workspace + +# Set Go environment for user +ENV GOPATH=/home/devuser/go +ENV GOCACHE=/home/devuser/.cache/go-build +RUN mkdir -p $GOPATH $GOCACHE + +# Add local bin directory to PATH for installed tools +ENV PATH="/home/devuser/.local/bin:/home/devuser/workspace/bin:${PATH}" + +# Initialize pipx for the user +RUN pipx ensurepath + +# Verify Go installation and show platform info +RUN go version +RUN uname -a +RUN go env GOOS GOARCH + +# Copy go.mod and go.sum again for the user workspace +COPY --chown=devuser:devuser go.mod go.sum ./ + +# Copy the entire project +COPY --chown=devuser:devuser . . + +# Stage 1: Test cross-compilation (this already works) +FROM base AS cross-compile-test +RUN echo "πŸ”¨ Testing cross-compilation..." +RUN make build/linux-amd64 +RUN file release/linux-amd64/devkit | grep "ELF 64-bit LSB executable" +RUN echo "βœ… Cross-compilation successful" + +# Stage 2: Use the cross-compiled binary for testing (realistic) +FROM cross-compile-test AS cli-test-setup +RUN echo "πŸ—οΈ Setting up CLI for testing..." +# Copy the working Linux binary to where tests expect it +RUN mkdir -p bin +RUN cp release/linux-amd64/devkit bin/devkit +RUN chmod +x bin/devkit +RUN devkit version +RUN echo "βœ… CLI setup successful" + +# Stage 3: Test CLI basic functionality +FROM cli-test-setup AS cli-basic-test +RUN echo "πŸ§ͺ Testing basic CLI functionality..." + +# Test version command +RUN echo "Testing version command..." +RUN devkit version + +# Test help command +RUN echo "Testing help command..." +RUN devkit --help >/dev/null + +# Test invalid command (should fail gracefully) +RUN echo "Testing invalid command handling..." +RUN devkit invalid-command || echo "Invalid command handled correctly" + +RUN echo "βœ… Basic CLI tests passed" + +# Stage 4: Test project creation +FROM cli-basic-test AS project-creation-test +RUN echo "πŸ“ Testing project creation..." + + +# Test create command with default settings +RUN echo "Testing create with defaults..." +RUN devkit avs create linux-test-basic + + +# Verify project structure was created (check both possible locations) +RUN test -d /home/devuser/workspace/linux-test-basic +RUN ls -la /home/devuser/workspace/linux-test-basic + +# Stage 5: Test build functionality in created projects +FROM project-creation-test AS project-build-test +RUN echo "πŸ”§ Testing build functionality..." + +# Test build in first project +RUN echo "Testing build in linux-test-basic..." +WORKDIR /home/devuser/workspace/linux-test-basic +RUN devkit avs build --context devnet || echo "Build test completed (may fail due to missing dependencies)" + +WORKDIR /home/devuser/workspace +RUN echo "βœ… Build functionality tests completed" + +# Stage 6: Test devnet functionality with fork URL +FROM project-build-test AS project-devnet-test +RUN echo "πŸ”§ Testing devnet functionality..." + +# Set environment variables for fork URL (correct way in Docker) +ENV L1_FORK_URL="https://ethereum-rpc.publicnode.com" +ENV L2_FORK_URL="https://ethereum-rpc.publicnode.com" + +WORKDIR /home/devuser/workspace/linux-test-basic +RUN cp .env.example .env || echo ".env.example not found, continuing..." +RUN echo "Testing devnet start..." +RUN timeout 30s devkit avs devnet start || echo "Devnet test completed (may timeout or fail due to network)" + +WORKDIR /home/devuser/workspace +RUN echo "βœ… Devnet functionality tests completed" + +# Stage 7: Test template functionality +FROM project-devnet-test AS template-test +RUN echo "πŸ“‹ Testing template functionality..." + +# Test template info command +WORKDIR /home/devuser/workspace/linux-test-basic +RUN devkit avs template info || echo "Template info test completed" + +# Test template commands from project root +WORKDIR /home/devuser/workspace +RUN echo "βœ… Template functionality tests completed" + +# Stage 8: Test file permissions and Linux-specific behavior +FROM template-test AS permissions-test +RUN echo "πŸ” Testing file permissions and Linux behavior..." + +# Check if binary has correct permissions +RUN test -x ./bin/devkit +RUN ls -la ./bin/devkit + +# Test with different GOOS/GOARCH (should not affect runtime) +ENV GOOS=linux +ENV GOARCH=amd64 +RUN devkit version + +# Test verbose mode +RUN devkit --verbose --help >/dev/null + +RUN echo "βœ… Environment tests passed" + +# Stage 9: Test Docker networking fixes (REGRESSION PROTECTION) +FROM permissions-test AS docker-networking-test +RUN echo "πŸ”Œ Testing Docker networking fixes (regression protection)..." + +# Test 1: Verify GetRPCURL returns localhost (not host.docker.internal) +RUN echo "Testing RPC URL generation..." +WORKDIR /home/devuser/workspace/linux-test-basic +RUN timeout 10s devkit avs devnet start --skip-deploy-contracts --skip-avs-run || true +RUN sleep 2 +# Check that devnet.yaml contains localhost:8545, not host.docker.internal:8545 +RUN if grep -q "host\.docker\.internal:8545" config/contexts/devnet.yaml; then \ + echo "❌ REGRESSION: RPC URL uses host.docker.internal instead of localhost!"; \ + echo "This means GetRPCURL() was reverted to old behavior"; \ + cat config/contexts/devnet.yaml; \ + exit 1; \ + fi +RUN if ! grep -q "localhost:8545" config/contexts/devnet.yaml; then \ + echo "❌ REGRESSION: RPC URL doesn't use localhost!"; \ + echo "Expected localhost:8545 in devnet.yaml"; \ + cat config/contexts/devnet.yaml; \ + exit 1; \ + fi +RUN echo "βœ… RPC URL correctly uses localhost" + +# Test 2: Verify docker-compose.yaml has host.docker.internal mapping +RUN echo "Testing docker-compose.yaml networking..." +RUN if ! grep -q "host.docker.internal:host-gateway" /tmp/devkit-compose/docker-compose.yaml; then \ + echo "❌ REGRESSION: docker-compose.yaml missing extra_hosts mapping!"; \ + echo "This means the docker networking fix was reverted"; \ + cat /tmp/devkit-compose/docker-compose.yaml; \ + exit 1; \ + fi +RUN echo "βœ… docker-compose.yaml has correct host mapping" + +# Test 3: Verify fork URL gets converted for container use (EnsureDockerHost) +RUN echo "Testing fork URL conversion..." +# Set a localhost fork URL and verify it gets converted +ENV L1_FORK_URL="http://localhost:8545/test" +RUN timeout 10s devkit avs devnet start --skip-deploy-contracts --skip-avs-run || true +# Check docker-compose environment shows converted URL (should be host.docker.internal on macOS runner) +# In Linux container, it should remain localhost, but the mechanism should work +RUN echo "βœ… Fork URL conversion mechanism works" + +# Test 4: Test OS detection logic +RUN echo "Testing OS detection in GetDockerHost..." +# Test that the binary behaves correctly on Linux (simpler approach) +RUN echo "Testing that devkit uses localhost URLs on Linux..." +# This test is implicitly covered by Test 1 above +RUN echo "βœ… OS detection logic works (verified by localhost URL test)" + +RUN devkit avs devnet stop || true +WORKDIR /home/devuser/workspace +RUN echo "βœ… Docker networking regression tests passed" + +# Final stage: Summary and interactive shell +FROM docker-networking-test AS final + +# Create a summary of what was tested +RUN echo "πŸŽ‰ All Linux compatibility tests passed!" > /home/devuser/workspace/test-results.txt +RUN echo "βœ… Cross-compilation: PASSED" >> /home/devuser/workspace/test-results.txt +RUN echo "βœ… CLI functionality (Linux binary): PASSED" >> /home/devuser/workspace/test-results.txt +RUN echo "βœ… Basic CLI functionality: PASSED" >> /home/devuser/workspace/test-results.txt +RUN echo "βœ… Project creation: PASSED" >> /home/devuser/workspace/test-results.txt +RUN echo "βœ… Build functionality: TESTED" >> /home/devuser/workspace/test-results.txt +RUN echo "βœ… Devnet functionality: TESTED" >> /home/devuser/workspace/test-results.txt +RUN echo "βœ… Template functionality: TESTED" >> /home/devuser/workspace/test-results.txt +RUN echo "βœ… File permissions: PASSED" >> /home/devuser/workspace/test-results.txt +RUN echo "βœ… Environment handling: PASSED" >> /home/devuser/workspace/test-results.txt +RUN echo "βœ… Docker networking fixes: PASSED (regression protection)" >> /home/devuser/workspace/test-results.txt + +# Show final summary +RUN cat /home/devuser/workspace/test-results.txt + +# Set up for interactive use +WORKDIR /home/devuser/workspace +ENV PS1="\[\033[01;32m\]devkit-linux-test\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$ " + +# Default command shows the summary and starts bash +CMD ["bash", "-c", "cat test-results.txt && echo '' && echo 'Linux testing environment ready! Use ./bin/devkit to test manually.' && bash"] + + diff --git a/docker/linux-test/docker-compose.yml b/docker/linux-test/docker-compose.yml new file mode 100644 index 00000000..27a48dfe --- /dev/null +++ b/docker/linux-test/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + devkit-linux-test: + build: + context: ../../ + dockerfile: docker/linux-test/Dockerfile + container_name: devkit-linux-test + volumes: + - ../../:/workspace + - /var/run/docker.sock:/var/run/docker.sock + working_dir: /workspace + environment: + - GOOS=linux + - GOARCH=amd64 + stdin_open: true + tty: true \ No newline at end of file diff --git a/pkg/commands/devnet_actions.go b/pkg/commands/devnet_actions.go index 3d802b3e..1d473f11 100644 --- a/pkg/commands/devnet_actions.go +++ b/pkg/commands/devnet_actions.go @@ -99,6 +99,9 @@ func StartDevnetAction(cCtx *cli.Context) error { return fmt.Errorf("fork-url not set; set fork-url in ./config/context/devnet.yaml or .env and consult README for guidance") } + // Ensure fork URL uses appropriate Docker host for container environments + dockerForkUrl := devnet.EnsureDockerHost(forkUrl) + // Get the block_time from env/config blockTime, err := devnet.GetDevnetBlockTimeOrDefault(config, devnet.L1) if err != nil { @@ -119,14 +122,14 @@ func StartDevnetAction(cCtx *cli.Context) error { "FOUNDRY_IMAGE="+chainImage, "ANVIL_ARGS="+chainArgs, fmt.Sprintf("DEVNET_PORT=%d", port), - "FORK_RPC_URL="+forkUrl, + "FORK_RPC_URL="+dockerForkUrl, fmt.Sprintf("FORK_BLOCK_NUMBER=%d", l1ChainConfig.Fork.Block), "AVS_CONTAINER_NAME="+containerName, ) if err := cmd.Run(); err != nil { return fmt.Errorf("❌ Failed to start devnet: %w", err) } - rpcUrl := fmt.Sprintf("http://localhost:%d", port) + rpcUrl := devnet.GetRPCURL(port) log.Info("Waiting for devnet to be ready...") // Set path for context yamls @@ -175,7 +178,7 @@ func StartDevnetAction(cCtx *cli.Context) error { // Sleep for 4 second to ensure the devnet is fully started time.Sleep(4 * time.Second) - + log.Info("Funding wallets... %s", rpcUrl) // Fund the wallets defined in config err = devnet.FundWalletsDevnet(config, rpcUrl) if err != nil { diff --git a/pkg/common/devnet/constants.go b/pkg/common/devnet/constants.go index 93124398..e3b1d6f4 100644 --- a/pkg/common/devnet/constants.go +++ b/pkg/common/devnet/constants.go @@ -1,13 +1,33 @@ package devnet +import ( + "fmt" + "os" + "runtime" +) + // Foundry Image Date : 21 April 2025 const FOUNDRY_IMAGE = "ghcr.io/foundry-rs/foundry:stable" const CHAIN_ARGS = "--chain-id 31337" const FUND_VALUE = "10000000000000000000" -const RPC_URL = "http://localhost:8545" const CONTEXT = "devnet" const L1 = "l1" // @TODO: Add core eigenlayer deployment addresses to context const ALLOCATION_MANAGER_ADDRESS = "0x948a420b8CC1d6BFd0B6087C2E7c344a2CD0bc39" const DELEGATION_MANAGER_ADDRESS = "0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A" + +// GetDefaultRPCURL returns the default RPC URL with platform-aware host +func GetDefaultRPCURL() string { + // Use same logic as GetDockerHost but inline to avoid circular import + host := "localhost" + if dockersHost := os.Getenv("DOCKERS_HOST"); dockersHost != "" { + host = dockersHost + } else if runtime.GOOS != "linux" { + host = "host.docker.internal" + } + return fmt.Sprintf("http://%s:8545", host) +} + +// Legacy constant for backward compatibility +const RPC_URL = "http://localhost:8545" diff --git a/pkg/common/devnet/utils.go b/pkg/common/devnet/utils.go index 3d23e2d8..6f82679c 100644 --- a/pkg/common/devnet/utils.go +++ b/pkg/common/devnet/utils.go @@ -4,7 +4,12 @@ import ( "fmt" "log" "net" + "net/url" + "os" "os/exec" + "regexp" + "runtime" + "strings" "time" "github.com/urfave/cli/v2" @@ -47,3 +52,78 @@ func GetDockerPsDevnetArgs() []string { "--format", "{{.Names}}: {{.Ports}}", } } + +// GetDockerHost returns the appropriate Docker host based on environment and platform. +// Uses DOCKERS_HOST environment variable if set, otherwise detects OS: +// - Linux: defaults to localhost (Docker containers can access host via localhost) +// - macOS/Windows: defaults to host.docker.internal (required for Docker Desktop) +func GetDockerHost() string { + if dockersHost := os.Getenv("DOCKERS_HOST"); dockersHost != "" { + return dockersHost + } + + // Detect OS and set appropriate default + if runtime.GOOS == "linux" { + return "localhost" + } else { + return "host.docker.internal" + } +} + +// EnsureDockerHost replaces localhost/127.0.0.1 in URLs with the appropriate Docker host. +// Only replaces when localhost/127.0.0.1 are the actual hostname, not substrings. +// This ensures URLs work correctly when passed to Docker containers across platforms. +func EnsureDockerHost(inputUrl string) string { + dockerHost := GetDockerHost() + + // Parse the URL to work with components safely + parsedUrl, err := url.Parse(inputUrl) + if err != nil { + // If URL parsing fails, fall back to regex-based replacement + return ensureDockerHostRegex(inputUrl, dockerHost) + } + + // Extract hostname (without port) + hostname := parsedUrl.Hostname() + + // Only replace if hostname is exactly localhost or 127.0.0.1 + if hostname == "localhost" || hostname == "127.0.0.1" { + // Replace just the hostname part + if parsedUrl.Port() != "" { + parsedUrl.Host = fmt.Sprintf("%s:%s", dockerHost, parsedUrl.Port()) + } else { + parsedUrl.Host = dockerHost + } + return parsedUrl.String() + } + + // Return original URL if hostname doesn't match + return inputUrl +} + +// ensureDockerHostRegex provides regex-based fallback for malformed URLs +func ensureDockerHostRegex(inputUrl string, dockerHost string) string { + // Pattern to match localhost or 127.0.0.1 as hostname (not substring) + // Matches: localhost:8545, localhost/, localhost, 127.0.0.1:8545, etc. + // Doesn't match: my-localhost.com, localhost.domain.com, etc. + localhostPattern := regexp.MustCompile(`\blocalhost(:[0-9]+)?(/|$|\?)`) + ipPattern := regexp.MustCompile(`\b127\.0\.0\.1(:[0-9]+)?(/|$|\?)`) + + // Replace localhost patterns + result := localhostPattern.ReplaceAllStringFunc(inputUrl, func(match string) string { + return strings.Replace(match, "localhost", dockerHost, 1) + }) + + // Replace 127.0.0.1 patterns + result = ipPattern.ReplaceAllStringFunc(result, func(match string) string { + return strings.Replace(match, "127.0.0.1", dockerHost, 1) + }) + + return result +} + +// GetRPCURL returns the RPC URL for accessing the devnet container from the host. +// Always uses localhost since Docker maps container ports to localhost on all platforms. +func GetRPCURL(port int) string { + return fmt.Sprintf("http://localhost:%d", port) +} diff --git a/scripts/linux-compatibility.sh b/scripts/linux-compatibility.sh new file mode 100755 index 00000000..5b7fbb0c --- /dev/null +++ b/scripts/linux-compatibility.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +#Linux testing script using Docker + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +echo "🐧 Running Linux compatibility tests..." +echo "This will test ALL aspects of devkit-cli in a Linux environment" +echo "" + +cd "$PROJECT_ROOT" + +# Function to cleanup +cleanup() { + echo "🧹 Cleaning up test containers..." + docker container prune -f >/dev/null 2>&1 || true + docker image prune -f >/dev/null 2>&1 || true +} + +# Trap cleanup on exit +trap cleanup EXIT + + +# Build the Docker image with all tests +# Each RUN command in the Dockerfile is a test - if any fail, the build stops +if docker build --add-host=host.docker.internal:host-gateway -f docker/linux-test/Dockerfile -t devkit-linux-test .; then + echo "" + echo "ALL LINUX COMPATIBILITY TESTS PASSED!" + echo "" + echo "devkit-cli works correctly on Linux!" + echo "" + echo "πŸ” To manually test in the Linux environment, run:" + echo " docker run -it --rm --add-host=host.docker.internal:host-gateway -v \$(pwd):/workspace -w /workspace devkit-linux-test" + echo "" + echo "πŸ“ To see detailed test results:" + echo " docker run --rm --add-host=host.docker.internal:host-gateway devkit-linux-test cat test-results.txt" + + # Show the test results + echo "" + echo "πŸ“‹ Test Summary:" + docker run --rm --add-host=host.docker.internal:host-gateway devkit-linux-test cat test-results.txt + +else + echo "" + echo "❌ LINUX COMPATIBILITY TESTS FAILED!" + echo "" + echo "The Docker build failed, which means there are Linux-specific issues." + echo "Check the error output above to see which test failed." + echo "" + echo "πŸ”§ To debug:" + echo "1. Look at the last successful RUN command in the output" + echo "2. Run intermediate stages manually:" + echo " docker build --target=cli-basic-test -f docker/linux-test/Dockerfile ." + echo "3. Start an interactive session:" + echo " docker run -it --rm -v \$(pwd):/workspace -w /workspace golang:1.24-bookworm bash" + + exit 1 +fi + +echo "" +echo "πŸš€ Testing complete!CLI is Linux-compatible." \ No newline at end of file diff --git a/scripts/test-networking-regression.sh b/scripts/test-networking-regression.sh new file mode 100755 index 00000000..01f64b32 --- /dev/null +++ b/scripts/test-networking-regression.sh @@ -0,0 +1,219 @@ +#!/bin/bash + +# Test script to verify Docker networking fixes are in place +# This script will FAIL if someone reverts our networking fixes + +set -e + +echo "πŸ”Œ Testing Docker networking regression protection..." + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$PROJECT_ROOT" + +# Cleanup function to run on exit +cleanup() { + echo "🧹 Cleaning up test artifacts..." + if [ -d "./test-networking-regression" ]; then + cd "./test-networking-regression" 2>/dev/null && { + "$PROJECT_ROOT/bin/devkit" avs devnet stop 2>/dev/null || true + cd "$PROJECT_ROOT" + } + rm -rf "./test-networking-regression" + echo "βœ… Removed test-networking-regression directory" + fi +} + +# Set trap to cleanup on exit (success, failure, or interruption) +trap cleanup EXIT + +# Build the CLI first +echo "Building CLI..." +make build + +# Create a test project +echo "Creating test project..." +./bin/devkit avs create test-networking-regression +cd ./test-networking-regression + +# Set environment for testing +export L1_FORK_URL="https://ethereum-rpc.publicnode.com" + +# Find an available port (start from 9545 to avoid conflicts) +find_available_port() { + local port=9545 + while netstat -an | grep -q ":$port "; do + port=$((port + 1)) + done + echo $port +} + +AVAILABLE_PORT=$(find_available_port) +echo "Using port $AVAILABLE_PORT for testing..." + +echo "Testing devnet start..." +# Use --skip-avs-run to start faster and use available port +timeout 60s "$PROJECT_ROOT/bin/devkit" avs devnet start --port $AVAILABLE_PORT --skip-deploy-contracts --skip-avs-run + +# Wait a moment for the YAML to be written +sleep 2 + +# Test 1: Check that devnet.yaml contains localhost, not host.docker.internal +echo "Checking RPC URL in devnet.yaml..." +if [ -f "config/contexts/devnet.yaml" ]; then + if grep -q "host\.docker\.internal:$AVAILABLE_PORT" config/contexts/devnet.yaml; then + echo "❌ REGRESSION DETECTED: RPC URL uses host.docker.internal instead of localhost!" + echo "This means GetRPCURL() was reverted to old behavior" + echo "Content of devnet.yaml:" + cat config/contexts/devnet.yaml + exit 1 + fi + + if ! grep -q "localhost:$AVAILABLE_PORT" config/contexts/devnet.yaml; then + echo "❌ REGRESSION DETECTED: RPC URL doesn't use localhost!" + echo "Expected localhost:$AVAILABLE_PORT in devnet.yaml but found:" + grep "rpc_url" config/contexts/devnet.yaml || echo "No rpc_url found" + echo "Full content of devnet.yaml:" + cat config/contexts/devnet.yaml + exit 1 + fi + + echo "βœ… RPC URL correctly uses localhost" +else + echo "❌ devnet.yaml not found!" + exit 1 +fi + +# Test 2: Check docker-compose.yaml has the extra_hosts mapping +echo "Checking docker-compose.yaml networking..." +if [ -f "/tmp/devkit-compose/docker-compose.yaml" ]; then + if ! grep -q "host.docker.internal:host-gateway" /tmp/devkit-compose/docker-compose.yaml; then + echo "❌ REGRESSION DETECTED: docker-compose.yaml missing extra_hosts mapping!" + echo "This means the docker networking fix was reverted" + echo "Content of docker-compose.yaml:" + cat /tmp/devkit-compose/docker-compose.yaml + exit 1 + fi + echo "βœ… docker-compose.yaml has correct host mapping" +else + echo "⚠️ docker-compose.yaml not found, skipping docker-compose test" +fi + +# Test 3: Verify EnsureDockerHost function handles edge cases correctly +echo "Testing EnsureDockerHost edge cases..." + +# Create a simple Go test to verify our EnsureDockerHost logic for BOTH platforms +cat > test_ensure_docker_host.go << 'EOF' +package main + +import ( + "fmt" + "net/url" + "os" + "regexp" + "runtime" + "strings" +) + +func GetDockerHost() string { + if dockersHost := os.Getenv("DOCKERS_HOST"); dockersHost != "" { + return dockersHost + } + if runtime.GOOS == "linux" { + return "localhost" + } else { + return "host.docker.internal" + } +} + +func EnsureDockerHost(inputUrl string) string { + dockerHost := GetDockerHost() + parsedUrl, err := url.Parse(inputUrl) + if err != nil { + return ensureDockerHostRegex(inputUrl, dockerHost) + } + hostname := parsedUrl.Hostname() + if hostname == "localhost" || hostname == "127.0.0.1" { + if parsedUrl.Port() != "" { + parsedUrl.Host = fmt.Sprintf("%s:%s", dockerHost, parsedUrl.Port()) + } else { + parsedUrl.Host = dockerHost + } + return parsedUrl.String() + } + return inputUrl +} + +func ensureDockerHostRegex(inputUrl string, dockerHost string) string { + localhostPattern := regexp.MustCompile(`\blocalhost(:[0-9]+)?(/|$|\?)`) + ipPattern := regexp.MustCompile(`\b127\.0\.0\.1(:[0-9]+)?(/|$|\?)`) + result := localhostPattern.ReplaceAllStringFunc(inputUrl, func(match string) string { + return strings.Replace(match, "localhost", dockerHost, 1) + }) + result = ipPattern.ReplaceAllStringFunc(result, func(match string) string { + return strings.Replace(match, "127.0.0.1", dockerHost, 1) + }) + return result +} + +func testPlatform(platformName, expectedDockerHost string) bool { + fmt.Printf("\nπŸ”§ Testing %s behavior (DOCKERS_HOST=%s)...\n", platformName, expectedDockerHost) + + // Override environment to simulate platform + os.Setenv("DOCKERS_HOST", expectedDockerHost) + defer os.Unsetenv("DOCKERS_HOST") + + testCases := []struct { + input string + expected string + desc string + }{ + {"http://localhost:8545", fmt.Sprintf("http://%s:8545", expectedDockerHost), "Should replace localhost"}, + {"https://127.0.0.1:3000", fmt.Sprintf("https://%s:3000", expectedDockerHost), "Should replace 127.0.0.1"}, + {"https://localhost.mycooldomain.com:8545", "https://localhost.mycooldomain.com:8545", "Should NOT replace localhost in domain"}, + {"https://api.localhost.network:3000", "https://api.localhost.network:3000", "Should NOT replace localhost in subdomain"}, + {"https://my-localhost-service.com:8080", "https://my-localhost-service.com:8080", "Should NOT replace localhost in service name"}, + {"http://mainnet.infura.io/v3/key", "http://mainnet.infura.io/v3/key", "Should not change external URLs"}, + } + + allPassed := true + for _, tc := range testCases { + result := EnsureDockerHost(tc.input) + if result != tc.expected { + fmt.Printf("❌ FAILED: %s\n", tc.desc) + fmt.Printf(" Input: %s\n", tc.input) + fmt.Printf(" Expected: %s\n", tc.expected) + fmt.Printf(" Got: %s\n", result) + allPassed = false + } else { + fmt.Printf("βœ… PASSED: %s\n", tc.desc) + } + } + + return allPassed +} + +func main() { + fmt.Println("πŸ” Testing cross-platform Docker host behavior...") + + // Test Linux behavior (localhost) + linuxPassed := testPlatform("Linux", "localhost") + + // Test macOS behavior (host.docker.internal) + macosPassed := testPlatform("macOS", "host.docker.internal") + + if !linuxPassed || !macosPassed { + fmt.Println("\n❌ Cross-platform EnsureDockerHost tests FAILED!") + os.Exit(1) + } else { + fmt.Println("\nβœ… All cross-platform EnsureDockerHost tests passed!") + fmt.Println("βœ… Linux behavior: localhost ← correct") + fmt.Println("βœ… macOS behavior: host.docker.internal ← correct") + } +} +EOF + +go run test_ensure_docker_host.go +rm test_ensure_docker_host.go + +echo "πŸŽ‰ All networking regression tests passed!" +echo "βœ… Docker networking fixes are in place and working correctly" \ No newline at end of file From ddbccd7e27ce2e2a6233f0c3f90c97888e3a48bd Mon Sep 17 00:00:00 2001 From: supernova Date: Fri, 23 May 2025 22:12:28 +0530 Subject: [PATCH 02/12] delete linux compatibility amd 64 --- .../linux-compatibility-linux-amd64.yml | 101 ------------------ 1 file changed, 101 deletions(-) delete mode 100644 .github/workflows/linux-compatibility-linux-amd64.yml diff --git a/.github/workflows/linux-compatibility-linux-amd64.yml b/.github/workflows/linux-compatibility-linux-amd64.yml deleted file mode 100644 index d551195d..00000000 --- a/.github/workflows/linux-compatibility-linux-amd64.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: linux-compatibility-linux-amd64 - -on: - push: - branches: - - main - pull_request: - branches: ["**"] - # Allow manual triggering - workflow_dispatch: - -jobs: - linux-amd64-compatibility: - name: Linux AMD64 Compatibility Testing - runs-on: ubuntu-latest - timeout-minutes: 25 - - strategy: - matrix: - # Test different Ubuntu versions to ensure broader compatibility - ubuntu-version: ["20.04", "22.04", "24.04"] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - platforms: linux/amd64 - - - name: Free up disk space - run: | - docker system prune -f - df -h - - - name: Build Linux AMD64 specific test image - run: | - docker build \ - --platform linux/amd64 \ - --add-host=host.docker.internal:host-gateway \ - -f docker/linux-test/Dockerfile \ - -t devkit-linux-amd64-test:${{ matrix.ubuntu-version }} \ - --build-arg BASE_IMAGE=ubuntu:${{ matrix.ubuntu-version }} \ - . - - - name: Make script executable - run: chmod +x ./scripts/linux-compatibility.sh - - - name: Run Linux AMD64 compatibility tests - env: - DOCKER_IMAGE_TAG: ${{ matrix.ubuntu-version }} - run: | - echo "πŸ”§ Testing on Ubuntu ${{ matrix.ubuntu-version }} (linux/amd64)" - # Modify the script to use our specific image - sed "s/devkit-linux-test/devkit-linux-amd64-test:${{ matrix.ubuntu-version }}/g" \ - scripts/linux-compatibility.sh > scripts/linux-compatibility-amd64.sh - chmod +x scripts/linux-compatibility-amd64.sh - ./scripts/linux-compatibility-amd64.sh - - - name: Test cross-compilation for Linux AMD64 - run: | - echo "πŸ”¨ Testing cross-compilation to linux/amd64..." - make build/linux-amd64 - file release/linux-amd64/devkit | grep "ELF 64-bit LSB executable, x86-64" - - - name: Save test results as artifact - if: always() - run: | - # Try to extract test results from the container if it exists - docker run --rm --add-host=host.docker.internal:host-gateway \ - devkit-linux-amd64-test:${{ matrix.ubuntu-version }} \ - cat test-results.txt > test-results-${{ matrix.ubuntu-version }}.txt 2>/dev/null || \ - echo "Could not extract test results for Ubuntu ${{ matrix.ubuntu-version }}" - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: linux-amd64-compatibility-results-ubuntu-${{ matrix.ubuntu-version }} - path: test-results-${{ matrix.ubuntu-version }}.txt - retention-days: 30 - - - name: Archive test results on failure - if: failure() - run: | - echo "πŸ“ Collecting debug information for Ubuntu ${{ matrix.ubuntu-version }}..." - docker images | grep devkit || echo "No devkit images found" - docker ps -a | grep devkit || echo "No devkit containers found" - docker system df - echo "Platform info:" - uname -a - echo "Docker version:" - docker version - - - name: Cleanup Docker resources - if: always() - run: | - docker container prune -f || true - docker image prune -f || true - docker rmi devkit-linux-amd64-test:${{ matrix.ubuntu-version }} || true \ No newline at end of file From c93b6b72922582513243d9ed9988f7f99930d8a1 Mon Sep 17 00:00:00 2001 From: supernova Date: Fri, 23 May 2025 22:17:05 +0530 Subject: [PATCH 03/12] add regression ci and remove regression from linux compatibility ci --- .github/workflows/networking-regression.yml | 57 ++++++++++++++++++++ docker/linux-test/Dockerfile | 58 +-------------------- 2 files changed, 59 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/networking-regression.yml diff --git a/.github/workflows/networking-regression.yml b/.github/workflows/networking-regression.yml new file mode 100644 index 00000000..273389ff --- /dev/null +++ b/.github/workflows/networking-regression.yml @@ -0,0 +1,57 @@ +name: Networking Regression Tests + +on: + push: + branches: + - main + pull_request: + branches: ["**"] + # Allow manual triggering + workflow_dispatch: + +jobs: + networking-regression: + name: Docker Networking Regression Protection + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Set up Docker + uses: docker/setup-buildx-action@v3 + + - name: Free up disk space + run: | + docker system prune -f + df -h + + - name: Make script executable + run: chmod +x ./scripts/test-networking-regression.sh + + - name: Run networking regression tests + run: ./scripts/test-networking-regression.sh + + - name: Archive test artifacts on failure + if: failure() + run: | + echo "πŸ“ Collecting debug information..." + docker images | grep devkit || echo "No devkit images found" + docker ps -a | grep devkit || echo "No devkit containers found" + docker system df + echo "πŸ“‚ Test project contents:" + ls -la test-networking-regression/ || echo "No test project found" + echo "πŸ“‹ Devnet.yaml contents:" + cat test-networking-regression/config/contexts/devnet.yaml || echo "No devnet.yaml found" + + - name: Cleanup Docker resources + if: always() + run: | + docker container prune -f || true + docker image prune -f || true \ No newline at end of file diff --git a/docker/linux-test/Dockerfile b/docker/linux-test/Dockerfile index 692ac1ee..91c0de60 100644 --- a/docker/linux-test/Dockerfile +++ b/docker/linux-test/Dockerfile @@ -170,62 +170,8 @@ RUN devkit --verbose --help >/dev/null RUN echo "βœ… Environment tests passed" -# Stage 9: Test Docker networking fixes (REGRESSION PROTECTION) -FROM permissions-test AS docker-networking-test -RUN echo "πŸ”Œ Testing Docker networking fixes (regression protection)..." - -# Test 1: Verify GetRPCURL returns localhost (not host.docker.internal) -RUN echo "Testing RPC URL generation..." -WORKDIR /home/devuser/workspace/linux-test-basic -RUN timeout 10s devkit avs devnet start --skip-deploy-contracts --skip-avs-run || true -RUN sleep 2 -# Check that devnet.yaml contains localhost:8545, not host.docker.internal:8545 -RUN if grep -q "host\.docker\.internal:8545" config/contexts/devnet.yaml; then \ - echo "❌ REGRESSION: RPC URL uses host.docker.internal instead of localhost!"; \ - echo "This means GetRPCURL() was reverted to old behavior"; \ - cat config/contexts/devnet.yaml; \ - exit 1; \ - fi -RUN if ! grep -q "localhost:8545" config/contexts/devnet.yaml; then \ - echo "❌ REGRESSION: RPC URL doesn't use localhost!"; \ - echo "Expected localhost:8545 in devnet.yaml"; \ - cat config/contexts/devnet.yaml; \ - exit 1; \ - fi -RUN echo "βœ… RPC URL correctly uses localhost" - -# Test 2: Verify docker-compose.yaml has host.docker.internal mapping -RUN echo "Testing docker-compose.yaml networking..." -RUN if ! grep -q "host.docker.internal:host-gateway" /tmp/devkit-compose/docker-compose.yaml; then \ - echo "❌ REGRESSION: docker-compose.yaml missing extra_hosts mapping!"; \ - echo "This means the docker networking fix was reverted"; \ - cat /tmp/devkit-compose/docker-compose.yaml; \ - exit 1; \ - fi -RUN echo "βœ… docker-compose.yaml has correct host mapping" - -# Test 3: Verify fork URL gets converted for container use (EnsureDockerHost) -RUN echo "Testing fork URL conversion..." -# Set a localhost fork URL and verify it gets converted -ENV L1_FORK_URL="http://localhost:8545/test" -RUN timeout 10s devkit avs devnet start --skip-deploy-contracts --skip-avs-run || true -# Check docker-compose environment shows converted URL (should be host.docker.internal on macOS runner) -# In Linux container, it should remain localhost, but the mechanism should work -RUN echo "βœ… Fork URL conversion mechanism works" - -# Test 4: Test OS detection logic -RUN echo "Testing OS detection in GetDockerHost..." -# Test that the binary behaves correctly on Linux (simpler approach) -RUN echo "Testing that devkit uses localhost URLs on Linux..." -# This test is implicitly covered by Test 1 above -RUN echo "βœ… OS detection logic works (verified by localhost URL test)" - -RUN devkit avs devnet stop || true -WORKDIR /home/devuser/workspace -RUN echo "βœ… Docker networking regression tests passed" - # Final stage: Summary and interactive shell -FROM docker-networking-test AS final +FROM permissions-test AS final # Create a summary of what was tested RUN echo "πŸŽ‰ All Linux compatibility tests passed!" > /home/devuser/workspace/test-results.txt @@ -238,7 +184,7 @@ RUN echo "βœ… Devnet functionality: TESTED" >> /home/devuser/workspace/test-resu RUN echo "βœ… Template functionality: TESTED" >> /home/devuser/workspace/test-results.txt RUN echo "βœ… File permissions: PASSED" >> /home/devuser/workspace/test-results.txt RUN echo "βœ… Environment handling: PASSED" >> /home/devuser/workspace/test-results.txt -RUN echo "βœ… Docker networking fixes: PASSED (regression protection)" >> /home/devuser/workspace/test-results.txt +RUN echo "ℹ️ Docker networking: Tested separately in networking-regression.yml" >> /home/devuser/workspace/test-results.txt # Show final summary RUN cat /home/devuser/workspace/test-results.txt From f33b0d024cb1de900f5e86d569b112e112d05bc6 Mon Sep 17 00:00:00 2001 From: supernova Date: Fri, 23 May 2025 22:23:04 +0530 Subject: [PATCH 04/12] add foundry toolchain to network regression ci --- .github/workflows/networking-regression.yml | 6 ++++++ pkg/common/devnet/utils.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/networking-regression.yml b/.github/workflows/networking-regression.yml index 273389ff..692112d9 100644 --- a/.github/workflows/networking-regression.yml +++ b/.github/workflows/networking-regression.yml @@ -9,6 +9,9 @@ on: # Allow manual triggering workflow_dispatch: +env: + FOUNDRY_PROFILE: ci + jobs: networking-regression: name: Docker Networking Regression Protection @@ -24,6 +27,9 @@ jobs: with: go-version: '1.24' + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1.4.0 + - name: Set up Docker uses: docker/setup-buildx-action@v3 diff --git a/pkg/common/devnet/utils.go b/pkg/common/devnet/utils.go index 6f82679c..62f4c805 100644 --- a/pkg/common/devnet/utils.go +++ b/pkg/common/devnet/utils.go @@ -123,7 +123,7 @@ func ensureDockerHostRegex(inputUrl string, dockerHost string) string { } // GetRPCURL returns the RPC URL for accessing the devnet container from the host. -// Always uses localhost since Docker maps container ports to localhost on all platforms. +// This should always use localhost since it's for hostβ†’container communication func GetRPCURL(port int) string { return fmt.Sprintf("http://localhost:%d", port) } From ea379a813b3f804c24726ff72926a4df62e4d534 Mon Sep 17 00:00:00 2001 From: supernova Date: Fri, 23 May 2025 22:32:02 +0530 Subject: [PATCH 05/12] timeout to 5m in linux compatiblity docker file --- docker/linux-test/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/linux-test/Dockerfile b/docker/linux-test/Dockerfile index 91c0de60..44a56272 100644 --- a/docker/linux-test/Dockerfile +++ b/docker/linux-test/Dockerfile @@ -135,7 +135,7 @@ ENV L2_FORK_URL="https://ethereum-rpc.publicnode.com" WORKDIR /home/devuser/workspace/linux-test-basic RUN cp .env.example .env || echo ".env.example not found, continuing..." RUN echo "Testing devnet start..." -RUN timeout 30s devkit avs devnet start || echo "Devnet test completed (may timeout or fail due to network)" +RUN timeout 5m devkit avs devnet start || echo "Devnet test completed (may timeout or fail due to network)" WORKDIR /home/devuser/workspace RUN echo "βœ… Devnet functionality tests completed" From e367bd25c65fbcb1046728f8e88b33e9abf76fdf Mon Sep 17 00:00:00 2001 From: supernova Date: Fri, 23 May 2025 23:24:43 +0530 Subject: [PATCH 06/12] remove linux compatiblity ci --- .github/workflows/linux-compatibility.yml | 62 ------- docker/linux-test/Dockerfile | 199 ---------------------- docker/linux-test/docker-compose.yml | 17 -- pkg/commands/devnet_actions.go | 1 - pkg/common/devnet/constants.go | 4 - scripts/linux-compatibility.sh | 64 ------- 6 files changed, 347 deletions(-) delete mode 100644 .github/workflows/linux-compatibility.yml delete mode 100644 docker/linux-test/Dockerfile delete mode 100644 docker/linux-test/docker-compose.yml delete mode 100755 scripts/linux-compatibility.sh diff --git a/.github/workflows/linux-compatibility.yml b/.github/workflows/linux-compatibility.yml deleted file mode 100644 index 9b756f72..00000000 --- a/.github/workflows/linux-compatibility.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Linux Compatibility Tests - -on: - push: - branches: - - main - pull_request: - branches: ["**"] - # Allow manual triggering - workflow_dispatch: - -jobs: - linux-compatibility: - name: Linux Compatibility Testing - runs-on: ubuntu-latest - timeout-minutes: 20 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Free up disk space - run: | - docker system prune -f - df -h - - - name: Make script executable - run: chmod +x ./scripts/linux-compatibility.sh - - - name: Run Linux compatibility tests - run: ./scripts/linux-compatibility.sh - - - name: Save test results as artifact - if: always() - run: | - # Try to extract test results from the container if it exists - docker run --rm --add-host=host.docker.internal:host-gateway devkit-linux-test cat test-results.txt > test-results.txt 2>/dev/null || echo "Could not extract test results" - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: linux-compatibility-results - path: test-results.txt - retention-days: 30 - - - name: Archive test results on failure - if: failure() - run: | - echo "πŸ“ Collecting debug information..." - docker images | grep devkit || echo "No devkit images found" - docker ps -a | grep devkit || echo "No devkit containers found" - docker system df - - - name: Cleanup Docker resources - if: always() - run: | - docker container prune -f || true - docker image prune -f || true \ No newline at end of file diff --git a/docker/linux-test/Dockerfile b/docker/linux-test/Dockerfile deleted file mode 100644 index 44a56272..00000000 --- a/docker/linux-test/Dockerfile +++ /dev/null @@ -1,199 +0,0 @@ -# Multi-stage Dockerfile for realistic Linux user testing -# Simulates a typical Linux desktop/server environment -FROM golang:1.24.2-bookworm AS base - -# Install what a typical Linux user would have -RUN apt-get update && apt-get install -y \ - build-essential \ - curl \ - git \ - ca-certificates \ - sudo \ - docker.io \ - docker-compose \ - file \ - tree \ - vim \ - wget \ - unzip \ - python3-full \ - python3-pip \ - python3-venv \ - pipx \ - && rm -rf /var/lib/apt/lists/* - -# Create a non-root user (like a real Linux user) -RUN useradd -m -s /bin/bash devuser && \ - usermod -aG sudo devuser && \ - echo "devuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers - -# Set environment variables for proper Docker networking -ENV CGO_ENABLED=0 - -# Configure Python to allow user installs and break system packages if needed in container -ENV PIP_BREAK_SYSTEM_PACKAGES=1 -ENV PIP_USER=1 - -# Copy go.mod and go.sum first for better caching and download as root -COPY go.mod go.sum ./ -RUN go mod download - -USER devuser -WORKDIR /home/devuser/workspace - -# Set Go environment for user -ENV GOPATH=/home/devuser/go -ENV GOCACHE=/home/devuser/.cache/go-build -RUN mkdir -p $GOPATH $GOCACHE - -# Add local bin directory to PATH for installed tools -ENV PATH="/home/devuser/.local/bin:/home/devuser/workspace/bin:${PATH}" - -# Initialize pipx for the user -RUN pipx ensurepath - -# Verify Go installation and show platform info -RUN go version -RUN uname -a -RUN go env GOOS GOARCH - -# Copy go.mod and go.sum again for the user workspace -COPY --chown=devuser:devuser go.mod go.sum ./ - -# Copy the entire project -COPY --chown=devuser:devuser . . - -# Stage 1: Test cross-compilation (this already works) -FROM base AS cross-compile-test -RUN echo "πŸ”¨ Testing cross-compilation..." -RUN make build/linux-amd64 -RUN file release/linux-amd64/devkit | grep "ELF 64-bit LSB executable" -RUN echo "βœ… Cross-compilation successful" - -# Stage 2: Use the cross-compiled binary for testing (realistic) -FROM cross-compile-test AS cli-test-setup -RUN echo "πŸ—οΈ Setting up CLI for testing..." -# Copy the working Linux binary to where tests expect it -RUN mkdir -p bin -RUN cp release/linux-amd64/devkit bin/devkit -RUN chmod +x bin/devkit -RUN devkit version -RUN echo "βœ… CLI setup successful" - -# Stage 3: Test CLI basic functionality -FROM cli-test-setup AS cli-basic-test -RUN echo "πŸ§ͺ Testing basic CLI functionality..." - -# Test version command -RUN echo "Testing version command..." -RUN devkit version - -# Test help command -RUN echo "Testing help command..." -RUN devkit --help >/dev/null - -# Test invalid command (should fail gracefully) -RUN echo "Testing invalid command handling..." -RUN devkit invalid-command || echo "Invalid command handled correctly" - -RUN echo "βœ… Basic CLI tests passed" - -# Stage 4: Test project creation -FROM cli-basic-test AS project-creation-test -RUN echo "πŸ“ Testing project creation..." - - -# Test create command with default settings -RUN echo "Testing create with defaults..." -RUN devkit avs create linux-test-basic - - -# Verify project structure was created (check both possible locations) -RUN test -d /home/devuser/workspace/linux-test-basic -RUN ls -la /home/devuser/workspace/linux-test-basic - -# Stage 5: Test build functionality in created projects -FROM project-creation-test AS project-build-test -RUN echo "πŸ”§ Testing build functionality..." - -# Test build in first project -RUN echo "Testing build in linux-test-basic..." -WORKDIR /home/devuser/workspace/linux-test-basic -RUN devkit avs build --context devnet || echo "Build test completed (may fail due to missing dependencies)" - -WORKDIR /home/devuser/workspace -RUN echo "βœ… Build functionality tests completed" - -# Stage 6: Test devnet functionality with fork URL -FROM project-build-test AS project-devnet-test -RUN echo "πŸ”§ Testing devnet functionality..." - -# Set environment variables for fork URL (correct way in Docker) -ENV L1_FORK_URL="https://ethereum-rpc.publicnode.com" -ENV L2_FORK_URL="https://ethereum-rpc.publicnode.com" - -WORKDIR /home/devuser/workspace/linux-test-basic -RUN cp .env.example .env || echo ".env.example not found, continuing..." -RUN echo "Testing devnet start..." -RUN timeout 5m devkit avs devnet start || echo "Devnet test completed (may timeout or fail due to network)" - -WORKDIR /home/devuser/workspace -RUN echo "βœ… Devnet functionality tests completed" - -# Stage 7: Test template functionality -FROM project-devnet-test AS template-test -RUN echo "πŸ“‹ Testing template functionality..." - -# Test template info command -WORKDIR /home/devuser/workspace/linux-test-basic -RUN devkit avs template info || echo "Template info test completed" - -# Test template commands from project root -WORKDIR /home/devuser/workspace -RUN echo "βœ… Template functionality tests completed" - -# Stage 8: Test file permissions and Linux-specific behavior -FROM template-test AS permissions-test -RUN echo "πŸ” Testing file permissions and Linux behavior..." - -# Check if binary has correct permissions -RUN test -x ./bin/devkit -RUN ls -la ./bin/devkit - -# Test with different GOOS/GOARCH (should not affect runtime) -ENV GOOS=linux -ENV GOARCH=amd64 -RUN devkit version - -# Test verbose mode -RUN devkit --verbose --help >/dev/null - -RUN echo "βœ… Environment tests passed" - -# Final stage: Summary and interactive shell -FROM permissions-test AS final - -# Create a summary of what was tested -RUN echo "πŸŽ‰ All Linux compatibility tests passed!" > /home/devuser/workspace/test-results.txt -RUN echo "βœ… Cross-compilation: PASSED" >> /home/devuser/workspace/test-results.txt -RUN echo "βœ… CLI functionality (Linux binary): PASSED" >> /home/devuser/workspace/test-results.txt -RUN echo "βœ… Basic CLI functionality: PASSED" >> /home/devuser/workspace/test-results.txt -RUN echo "βœ… Project creation: PASSED" >> /home/devuser/workspace/test-results.txt -RUN echo "βœ… Build functionality: TESTED" >> /home/devuser/workspace/test-results.txt -RUN echo "βœ… Devnet functionality: TESTED" >> /home/devuser/workspace/test-results.txt -RUN echo "βœ… Template functionality: TESTED" >> /home/devuser/workspace/test-results.txt -RUN echo "βœ… File permissions: PASSED" >> /home/devuser/workspace/test-results.txt -RUN echo "βœ… Environment handling: PASSED" >> /home/devuser/workspace/test-results.txt -RUN echo "ℹ️ Docker networking: Tested separately in networking-regression.yml" >> /home/devuser/workspace/test-results.txt - -# Show final summary -RUN cat /home/devuser/workspace/test-results.txt - -# Set up for interactive use -WORKDIR /home/devuser/workspace -ENV PS1="\[\033[01;32m\]devkit-linux-test\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$ " - -# Default command shows the summary and starts bash -CMD ["bash", "-c", "cat test-results.txt && echo '' && echo 'Linux testing environment ready! Use ./bin/devkit to test manually.' && bash"] - - diff --git a/docker/linux-test/docker-compose.yml b/docker/linux-test/docker-compose.yml deleted file mode 100644 index 27a48dfe..00000000 --- a/docker/linux-test/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: '3.8' - -services: - devkit-linux-test: - build: - context: ../../ - dockerfile: docker/linux-test/Dockerfile - container_name: devkit-linux-test - volumes: - - ../../:/workspace - - /var/run/docker.sock:/var/run/docker.sock - working_dir: /workspace - environment: - - GOOS=linux - - GOARCH=amd64 - stdin_open: true - tty: true \ No newline at end of file diff --git a/pkg/commands/devnet_actions.go b/pkg/commands/devnet_actions.go index 1d473f11..93525646 100644 --- a/pkg/commands/devnet_actions.go +++ b/pkg/commands/devnet_actions.go @@ -178,7 +178,6 @@ func StartDevnetAction(cCtx *cli.Context) error { // Sleep for 4 second to ensure the devnet is fully started time.Sleep(4 * time.Second) - log.Info("Funding wallets... %s", rpcUrl) // Fund the wallets defined in config err = devnet.FundWalletsDevnet(config, rpcUrl) if err != nil { diff --git a/pkg/common/devnet/constants.go b/pkg/common/devnet/constants.go index e3b1d6f4..6f966c91 100644 --- a/pkg/common/devnet/constants.go +++ b/pkg/common/devnet/constants.go @@ -19,7 +19,6 @@ const DELEGATION_MANAGER_ADDRESS = "0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A" // GetDefaultRPCURL returns the default RPC URL with platform-aware host func GetDefaultRPCURL() string { - // Use same logic as GetDockerHost but inline to avoid circular import host := "localhost" if dockersHost := os.Getenv("DOCKERS_HOST"); dockersHost != "" { host = dockersHost @@ -28,6 +27,3 @@ func GetDefaultRPCURL() string { } return fmt.Sprintf("http://%s:8545", host) } - -// Legacy constant for backward compatibility -const RPC_URL = "http://localhost:8545" diff --git a/scripts/linux-compatibility.sh b/scripts/linux-compatibility.sh deleted file mode 100755 index 5b7fbb0c..00000000 --- a/scripts/linux-compatibility.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash - -#Linux testing script using Docker - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -echo "🐧 Running Linux compatibility tests..." -echo "This will test ALL aspects of devkit-cli in a Linux environment" -echo "" - -cd "$PROJECT_ROOT" - -# Function to cleanup -cleanup() { - echo "🧹 Cleaning up test containers..." - docker container prune -f >/dev/null 2>&1 || true - docker image prune -f >/dev/null 2>&1 || true -} - -# Trap cleanup on exit -trap cleanup EXIT - - -# Build the Docker image with all tests -# Each RUN command in the Dockerfile is a test - if any fail, the build stops -if docker build --add-host=host.docker.internal:host-gateway -f docker/linux-test/Dockerfile -t devkit-linux-test .; then - echo "" - echo "ALL LINUX COMPATIBILITY TESTS PASSED!" - echo "" - echo "devkit-cli works correctly on Linux!" - echo "" - echo "πŸ” To manually test in the Linux environment, run:" - echo " docker run -it --rm --add-host=host.docker.internal:host-gateway -v \$(pwd):/workspace -w /workspace devkit-linux-test" - echo "" - echo "πŸ“ To see detailed test results:" - echo " docker run --rm --add-host=host.docker.internal:host-gateway devkit-linux-test cat test-results.txt" - - # Show the test results - echo "" - echo "πŸ“‹ Test Summary:" - docker run --rm --add-host=host.docker.internal:host-gateway devkit-linux-test cat test-results.txt - -else - echo "" - echo "❌ LINUX COMPATIBILITY TESTS FAILED!" - echo "" - echo "The Docker build failed, which means there are Linux-specific issues." - echo "Check the error output above to see which test failed." - echo "" - echo "πŸ”§ To debug:" - echo "1. Look at the last successful RUN command in the output" - echo "2. Run intermediate stages manually:" - echo " docker build --target=cli-basic-test -f docker/linux-test/Dockerfile ." - echo "3. Start an interactive session:" - echo " docker run -it --rm -v \$(pwd):/workspace -w /workspace golang:1.24-bookworm bash" - - exit 1 -fi - -echo "" -echo "πŸš€ Testing complete!CLI is Linux-compatible." \ No newline at end of file From cea30b33a794a3ffad10251ed809691d1bda74ee Mon Sep 17 00:00:00 2001 From: supernova Date: Sat, 24 May 2025 01:21:37 +0530 Subject: [PATCH 07/12] remove DOCKER_HOST in ci , add unit tests --- .github/workflows/e2e.yml | 2 +- pkg/common/devnet/utils.go | 9 +- pkg/common/devnet/utils_test.go | 493 ++++++++++++++++++++++++++++++++ 3 files changed, 499 insertions(+), 5 deletions(-) create mode 100644 pkg/common/devnet/utils_test.go diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 931cc853..b1d3a273 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -57,7 +57,7 @@ jobs: - name: Start devnet run: | cd ./my-awesome-avs/ - DOCKERS_HOST=localhost devkit avs devnet start --skip-avs-run + devkit avs devnet start sleep 10 - name: Check devnet RPC is live diff --git a/pkg/common/devnet/utils.go b/pkg/common/devnet/utils.go index 62f4c805..6109b2ed 100644 --- a/pkg/common/devnet/utils.go +++ b/pkg/common/devnet/utils.go @@ -104,10 +104,11 @@ func EnsureDockerHost(inputUrl string) string { // ensureDockerHostRegex provides regex-based fallback for malformed URLs func ensureDockerHostRegex(inputUrl string, dockerHost string) string { // Pattern to match localhost or 127.0.0.1 as hostname (not substring) - // Matches: localhost:8545, localhost/, localhost, 127.0.0.1:8545, etc. - // Doesn't match: my-localhost.com, localhost.domain.com, etc. - localhostPattern := regexp.MustCompile(`\blocalhost(:[0-9]+)?(/|$|\?)`) - ipPattern := regexp.MustCompile(`\b127\.0\.0\.1(:[0-9]+)?(/|$|\?)`) + // Matches localhost:port followed by safe separators, or standalone localhost + // Matches: localhost:8545, localhost/path, localhost?param, localhost (at end), localhost with space, etc. + // Doesn't match: localhost.domain.com, my-localhost-service.com, etc. + localhostPattern := regexp.MustCompile(`\blocalhost(:[0-9]+)?(?:[\s/=?#]|$)`) + ipPattern := regexp.MustCompile(`\b127\.0\.0\.1(:[0-9]+)?(?:[\s/=?#]|$)`) // Replace localhost patterns result := localhostPattern.ReplaceAllStringFunc(inputUrl, func(match string) string { diff --git a/pkg/common/devnet/utils_test.go b/pkg/common/devnet/utils_test.go new file mode 100644 index 00000000..ca4b0237 --- /dev/null +++ b/pkg/common/devnet/utils_test.go @@ -0,0 +1,493 @@ +package devnet + +import ( + "fmt" + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestGetDockerHost tests the GetDockerHost function for different platforms and environment variables +func TestGetDockerHost(t *testing.T) { + // Save original environment + originalDockerHost := os.Getenv("DOCKERS_HOST") + defer func() { + if originalDockerHost != "" { + os.Setenv("DOCKERS_HOST", originalDockerHost) + } else { + os.Unsetenv("DOCKERS_HOST") + } + }() + + tests := []struct { + name string + dockersHost string + expected string + }{ + { + name: "Custom DOCKERS_HOST environment variable", + dockersHost: "custom.docker.host", + expected: "custom.docker.host", + }, + { + name: "Empty DOCKERS_HOST should fallback to platform default", + dockersHost: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dockersHost != "" { + os.Setenv("DOCKERS_HOST", tt.dockersHost) + } else { + os.Unsetenv("DOCKERS_HOST") + } + + result := GetDockerHost() + + if tt.expected != "" { + assert.Equal(t, tt.expected, result) + } else { + // When DOCKERS_HOST is empty, should return platform-specific default + assert.Contains(t, []string{"localhost", "host.docker.internal"}, result) + } + }) + } +} + +// TestEnsureDockerHost tests the EnsureDockerHost function with various URL patterns +func TestEnsureDockerHost(t *testing.T) { + // Save original environment + originalDockerHost := os.Getenv("DOCKERS_HOST") + defer func() { + if originalDockerHost != "" { + os.Setenv("DOCKERS_HOST", originalDockerHost) + } else { + os.Unsetenv("DOCKERS_HOST") + } + }() + + tests := []struct { + name string + inputURL string + dockersHost string + expectedURL string + description string + }{ + { + name: "Replace localhost with custom host", + inputURL: "http://localhost:8545", + dockersHost: "custom.docker.host", + expectedURL: "http://custom.docker.host:8545", + description: "Should replace localhost with custom Docker host", + }, + { + name: "Replace 127.0.0.1 with custom host", + inputURL: "https://127.0.0.1:3000", + dockersHost: "custom.docker.host", + expectedURL: "https://custom.docker.host:3000", + description: "Should replace 127.0.0.1 with custom Docker host", + }, + { + name: "Do not replace localhost in subdomain", + inputURL: "https://localhost.mycooldomain.com:8545", + dockersHost: "custom.docker.host", + expectedURL: "https://localhost.mycooldomain.com:8545", + description: "Should NOT replace localhost when it's part of a domain name", + }, + { + name: "Do not replace localhost in API subdomain", + inputURL: "https://api.localhost.network:3000", + dockersHost: "custom.docker.host", + expectedURL: "https://api.localhost.network:3000", + description: "Should NOT replace localhost when it's part of a subdomain", + }, + { + name: "Do not replace localhost in service name", + inputURL: "https://my-localhost-service.com:8080", + dockersHost: "custom.docker.host", + expectedURL: "https://my-localhost-service.com:8080", + description: "Should NOT replace localhost when it's part of a service name", + }, + { + name: "Do not change external URLs", + inputURL: "http://mainnet.infura.io/v3/key", + dockersHost: "custom.docker.host", + expectedURL: "http://mainnet.infura.io/v3/key", + description: "Should not change external URLs", + }, + { + name: "Replace localhost without port", + inputURL: "http://localhost", + dockersHost: "custom.docker.host", + expectedURL: "http://custom.docker.host", + description: "Should replace localhost without port", + }, + { + name: "Replace localhost with path", + inputURL: "http://localhost/api/v1", + dockersHost: "custom.docker.host", + expectedURL: "http://custom.docker.host/api/v1", + description: "Should replace localhost and preserve path", + }, + { + name: "Replace localhost with query params", + inputURL: "http://localhost:8545?param=value", + dockersHost: "custom.docker.host", + expectedURL: "http://custom.docker.host:8545?param=value", + description: "Should replace localhost and preserve query parameters", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("DOCKERS_HOST", tt.dockersHost) + + result := EnsureDockerHost(tt.inputURL) + assert.Equal(t, tt.expectedURL, result, tt.description) + }) + } +} + +// TestEnsureDockerHostCrossPlatform tests cross-platform behavior +func TestEnsureDockerHostCrossPlatform(t *testing.T) { + // Save original environment + originalDockerHost := os.Getenv("DOCKERS_HOST") + defer func() { + if originalDockerHost != "" { + os.Setenv("DOCKERS_HOST", originalDockerHost) + } else { + os.Unsetenv("DOCKERS_HOST") + } + }() + + platforms := []struct { + name string + dockersHost string + description string + }{ + { + name: "Linux behavior", + dockersHost: "localhost", + description: "Linux should use localhost", + }, + { + name: "macOS/Windows behavior", + dockersHost: "host.docker.internal", + description: "macOS and Windows should use host.docker.internal", + }, + } + + testCases := []struct { + input string + description string + }{ + {"http://localhost:8545", "Should replace localhost"}, + {"https://127.0.0.1:3000", "Should replace 127.0.0.1"}, + {"https://localhost.mycooldomain.com:8545", "Should NOT replace localhost in domain"}, + {"https://api.localhost.network:3000", "Should NOT replace localhost in subdomain"}, + {"https://my-localhost-service.com:8080", "Should NOT replace localhost in service name"}, + {"http://mainnet.infura.io/v3/key", "Should not change external URLs"}, + } + + for _, platform := range platforms { + t.Run(platform.name, func(t *testing.T) { + os.Setenv("DOCKERS_HOST", platform.dockersHost) + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + result := EnsureDockerHost(tc.input) + + // Verify the transformation logic + if tc.input == "http://localhost:8545" { + expected := fmt.Sprintf("http://%s:8545", platform.dockersHost) + assert.Equal(t, expected, result) + } else if tc.input == "https://127.0.0.1:3000" { + expected := fmt.Sprintf("https://%s:3000", platform.dockersHost) + assert.Equal(t, expected, result) + } else { + // These URLs should not be modified + assert.Equal(t, tc.input, result) + } + }) + } + }) + } +} + +// TestEnsureDockerHostRegexFallback tests the regex fallback for malformed URLs +func TestEnsureDockerHostRegexFallback(t *testing.T) { + // Save original environment + originalDockerHost := os.Getenv("DOCKERS_HOST") + defer func() { + if originalDockerHost != "" { + os.Setenv("DOCKERS_HOST", originalDockerHost) + } else { + os.Unsetenv("DOCKERS_HOST") + } + }() + + os.Setenv("DOCKERS_HOST", "test.docker.host") + + tests := []struct { + name string + inputURL string + expectedURL string + description string + }{ + { + name: "URL with control characters and localhost", + inputURL: "ht\x00tp://localhost:8545", + expectedURL: "ht\x00tp://test.docker.host:8545", + description: "Should use regex fallback for URLs with control characters", + }, + { + name: "URL with invalid scheme and 127.0.0.1", + inputURL: "ht tp://127.0.0.1:3000/path", + expectedURL: "ht tp://test.docker.host:3000/path", + description: "Should use regex fallback for URLs with spaces in scheme", + }, + { + name: "Plain text with localhost port", + inputURL: "Connect to localhost:8545 for RPC", + expectedURL: "Connect to test.docker.host:8545 for RPC", + description: "Should replace localhost in plain text", + }, + { + name: "Configuration value with 127.0.0.1", + inputURL: "RPC_URL=127.0.0.1:3000", + expectedURL: "RPC_URL=test.docker.host:3000", + description: "Should replace 127.0.0.1 in configuration-style strings", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := EnsureDockerHost(tt.inputURL) + assert.Equal(t, tt.expectedURL, result, tt.description) + }) + } +} + + +// TestDockerNetworkingEdgeCases tests edge cases in URL parsing and transformation +func TestDockerNetworkingEdgeCases(t *testing.T) { + // Save original environment + originalDockerHost := os.Getenv("DOCKERS_HOST") + defer func() { + if originalDockerHost != "" { + os.Setenv("DOCKERS_HOST", originalDockerHost) + } else { + os.Unsetenv("DOCKERS_HOST") + } + }() + + os.Setenv("DOCKERS_HOST", "test.docker.host") + + tests := []struct { + name string + input string + expected string + description string + }{ + { + name: "Empty string", + input: "", + expected: "", + description: "Empty string should remain empty", + }, + { + name: "Just localhost", + input: "localhost", + expected: "test.docker.host", + description: "Bare localhost should be replaced", + }, + { + name: "Just 127.0.0.1", + input: "127.0.0.1", + expected: "test.docker.host", + description: "Bare 127.0.0.1 should be replaced", + }, + { + name: "URL with fragment", + input: "http://localhost:8545#section", + expected: "http://test.docker.host:8545#section", + description: "URL with fragment should preserve fragment", + }, + { + name: "URL with user info", + input: "http://user:pass@localhost:8545", + expected: "http://user:pass@test.docker.host:8545", + description: "URL with user info should preserve user info", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := EnsureDockerHost(tt.input) + assert.Equal(t, tt.expected, result, tt.description) + }) + } +} + +// TestEnsureDockerHostParsing tests URL parsing behavior +func TestEnsureDockerHostParsing(t *testing.T) { + // Save original environment + originalDockerHost := os.Getenv("DOCKERS_HOST") + defer func() { + if originalDockerHost != "" { + os.Setenv("DOCKERS_HOST", originalDockerHost) + } else { + os.Unsetenv("DOCKERS_HOST") + } + }() + + os.Setenv("DOCKERS_HOST", "docker.host") + + tests := []struct { + name string + input string + expected string + }{ + { + name: "Valid HTTP URL", + input: "http://localhost:8545/path?query=value", + expected: "http://docker.host:8545/path?query=value", + }, + { + name: "Valid HTTPS URL", + input: "https://127.0.0.1:443/secure", + expected: "https://docker.host:443/secure", + }, + { + name: "WebSocket URL", + input: "ws://localhost:8546", + expected: "ws://docker.host:8546", + }, + { + name: "URL without scheme", + input: "localhost:8545", + expected: "docker.host:8545", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := EnsureDockerHost(tt.input) + assert.Equal(t, tt.expected, result) + + // Verify the result is a valid URL (if it was valid to begin with) + if _, err := url.Parse(tt.input); err == nil { + _, err := url.Parse(result) + assert.NoError(t, err, "Result should be a valid URL") + } + }) + } +} + +func TestNetworkingRegression(t *testing.T) { + t.Log("πŸ” Running Docker networking regression protection tests...") + + // Test 1: Cross-platform Docker host behavior + t.Run("CrossPlatformBehavior", func(t *testing.T) { + testPlatformBehavior := func(t *testing.T, platformName, expectedDockerHost string) { + // Save original environment + originalDockerHost := os.Getenv("DOCKERS_HOST") + defer func() { + if originalDockerHost != "" { + os.Setenv("DOCKERS_HOST", originalDockerHost) + } else { + os.Unsetenv("DOCKERS_HOST") + } + }() + + t.Logf("πŸ”§ Testing %s behavior (DOCKERS_HOST=%s)...", platformName, expectedDockerHost) + os.Setenv("DOCKERS_HOST", expectedDockerHost) + + testCases := []struct { + input string + expected string + desc string + }{ + {"http://localhost:8545", fmt.Sprintf("http://%s:8545", expectedDockerHost), "Should replace localhost"}, + {"https://127.0.0.1:3000", fmt.Sprintf("https://%s:3000", expectedDockerHost), "Should replace 127.0.0.1"}, + {"https://localhost.mycooldomain.com:8545", "https://localhost.mycooldomain.com:8545", "Should NOT replace localhost in domain"}, + {"https://api.localhost.network:3000", "https://api.localhost.network:3000", "Should NOT replace localhost in subdomain"}, + {"https://my-localhost-service.com:8080", "https://my-localhost-service.com:8080", "Should NOT replace localhost in service name"}, + {"http://mainnet.infura.io/v3/key", "http://mainnet.infura.io/v3/key", "Should not change external URLs"}, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + result := EnsureDockerHost(tc.input) + assert.Equal(t, tc.expected, result, + "FAILED: %s\nInput: %s\nExpected: %s\nGot: %s", + tc.desc, tc.input, tc.expected, result) + t.Logf("βœ… PASSED: %s", tc.desc) + }) + } + } + + t.Run("Linux", func(t *testing.T) { + testPlatformBehavior(t, "Linux", "localhost") + }) + + t.Run("macOS", func(t *testing.T) { + testPlatformBehavior(t, "macOS", "host.docker.internal") + }) + }) + + // Test 2: Verify regression protection + t.Run("RegressionProtection", func(t *testing.T) { + // Save original environment + originalDockerHost := os.Getenv("DOCKERS_HOST") + defer func() { + if originalDockerHost != "" { + os.Setenv("DOCKERS_HOST", originalDockerHost) + } else { + os.Unsetenv("DOCKERS_HOST") + } + }() + + // Test that GetRPCURL always returns localhost + t.Run("GetRPCURLAlwaysUsesLocalhost", func(t *testing.T) { + testPorts := []int{8545, 9545, 3000} + dockerHosts := []string{"localhost", "host.docker.internal"} + + for _, dockerHost := range dockerHosts { + for _, port := range testPorts { + t.Run(fmt.Sprintf("DOCKERS_HOST=%s_port=%d", dockerHost, port), func(t *testing.T) { + os.Setenv("DOCKERS_HOST", dockerHost) + result := GetRPCURL(port) + expected := fmt.Sprintf("http://localhost:%d", port) + assert.Equal(t, expected, result, + "GetRPCURL should always use localhost, not %s", dockerHost) + }) + } + } + }) + + // Test that Docker containers can still access host services + t.Run("DockerHostConfiguration", func(t *testing.T) { + // Simulate what would happen in docker-compose.yaml generation + os.Setenv("DOCKERS_HOST", "host.docker.internal") + + // Fork URL should be transformed for container access + forkURL := "http://localhost:8545" + dockerForkURL := EnsureDockerHost(forkURL) + expected := "http://host.docker.internal:8545" + assert.Equal(t, expected, dockerForkURL, + "Fork URL should be transformed for Docker container access") + + // But RPC URL for host access should remain localhost + rpcURL := GetRPCURL(8545) + expectedRPC := "http://localhost:8545" + assert.Equal(t, expectedRPC, rpcURL, + "RPC URL for host access should always use localhost") + }) + }) + +} From e3baccfa7e1262c068fdd4cb89f243eb0c7b780d Mon Sep 17 00:00:00 2001 From: supernova Date: Sat, 24 May 2025 01:22:48 +0530 Subject: [PATCH 08/12] fmt --- pkg/common/devnet/utils_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/common/devnet/utils_test.go b/pkg/common/devnet/utils_test.go index ca4b0237..c6ef14a3 100644 --- a/pkg/common/devnet/utils_test.go +++ b/pkg/common/devnet/utils_test.go @@ -272,7 +272,6 @@ func TestEnsureDockerHostRegexFallback(t *testing.T) { } } - // TestDockerNetworkingEdgeCases tests edge cases in URL parsing and transformation func TestDockerNetworkingEdgeCases(t *testing.T) { // Save original environment From 57409f4f34c8d76c4399786d3dac56211dba7a46 Mon Sep 17 00:00:00 2001 From: supernova Date: Sat, 24 May 2025 01:29:24 +0530 Subject: [PATCH 09/12] remove regression ci --- .github/workflows/networking-regression.yml | 63 ------ scripts/test-networking-regression.sh | 219 -------------------- 2 files changed, 282 deletions(-) delete mode 100644 .github/workflows/networking-regression.yml delete mode 100755 scripts/test-networking-regression.sh diff --git a/.github/workflows/networking-regression.yml b/.github/workflows/networking-regression.yml deleted file mode 100644 index 692112d9..00000000 --- a/.github/workflows/networking-regression.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Networking Regression Tests - -on: - push: - branches: - - main - pull_request: - branches: ["**"] - # Allow manual triggering - workflow_dispatch: - -env: - FOUNDRY_PROFILE: ci - -jobs: - networking-regression: - name: Docker Networking Regression Protection - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.24' - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1.4.0 - - - name: Set up Docker - uses: docker/setup-buildx-action@v3 - - - name: Free up disk space - run: | - docker system prune -f - df -h - - - name: Make script executable - run: chmod +x ./scripts/test-networking-regression.sh - - - name: Run networking regression tests - run: ./scripts/test-networking-regression.sh - - - name: Archive test artifacts on failure - if: failure() - run: | - echo "πŸ“ Collecting debug information..." - docker images | grep devkit || echo "No devkit images found" - docker ps -a | grep devkit || echo "No devkit containers found" - docker system df - echo "πŸ“‚ Test project contents:" - ls -la test-networking-regression/ || echo "No test project found" - echo "πŸ“‹ Devnet.yaml contents:" - cat test-networking-regression/config/contexts/devnet.yaml || echo "No devnet.yaml found" - - - name: Cleanup Docker resources - if: always() - run: | - docker container prune -f || true - docker image prune -f || true \ No newline at end of file diff --git a/scripts/test-networking-regression.sh b/scripts/test-networking-regression.sh deleted file mode 100755 index 01f64b32..00000000 --- a/scripts/test-networking-regression.sh +++ /dev/null @@ -1,219 +0,0 @@ -#!/bin/bash - -# Test script to verify Docker networking fixes are in place -# This script will FAIL if someone reverts our networking fixes - -set -e - -echo "πŸ”Œ Testing Docker networking regression protection..." - -PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$PROJECT_ROOT" - -# Cleanup function to run on exit -cleanup() { - echo "🧹 Cleaning up test artifacts..." - if [ -d "./test-networking-regression" ]; then - cd "./test-networking-regression" 2>/dev/null && { - "$PROJECT_ROOT/bin/devkit" avs devnet stop 2>/dev/null || true - cd "$PROJECT_ROOT" - } - rm -rf "./test-networking-regression" - echo "βœ… Removed test-networking-regression directory" - fi -} - -# Set trap to cleanup on exit (success, failure, or interruption) -trap cleanup EXIT - -# Build the CLI first -echo "Building CLI..." -make build - -# Create a test project -echo "Creating test project..." -./bin/devkit avs create test-networking-regression -cd ./test-networking-regression - -# Set environment for testing -export L1_FORK_URL="https://ethereum-rpc.publicnode.com" - -# Find an available port (start from 9545 to avoid conflicts) -find_available_port() { - local port=9545 - while netstat -an | grep -q ":$port "; do - port=$((port + 1)) - done - echo $port -} - -AVAILABLE_PORT=$(find_available_port) -echo "Using port $AVAILABLE_PORT for testing..." - -echo "Testing devnet start..." -# Use --skip-avs-run to start faster and use available port -timeout 60s "$PROJECT_ROOT/bin/devkit" avs devnet start --port $AVAILABLE_PORT --skip-deploy-contracts --skip-avs-run - -# Wait a moment for the YAML to be written -sleep 2 - -# Test 1: Check that devnet.yaml contains localhost, not host.docker.internal -echo "Checking RPC URL in devnet.yaml..." -if [ -f "config/contexts/devnet.yaml" ]; then - if grep -q "host\.docker\.internal:$AVAILABLE_PORT" config/contexts/devnet.yaml; then - echo "❌ REGRESSION DETECTED: RPC URL uses host.docker.internal instead of localhost!" - echo "This means GetRPCURL() was reverted to old behavior" - echo "Content of devnet.yaml:" - cat config/contexts/devnet.yaml - exit 1 - fi - - if ! grep -q "localhost:$AVAILABLE_PORT" config/contexts/devnet.yaml; then - echo "❌ REGRESSION DETECTED: RPC URL doesn't use localhost!" - echo "Expected localhost:$AVAILABLE_PORT in devnet.yaml but found:" - grep "rpc_url" config/contexts/devnet.yaml || echo "No rpc_url found" - echo "Full content of devnet.yaml:" - cat config/contexts/devnet.yaml - exit 1 - fi - - echo "βœ… RPC URL correctly uses localhost" -else - echo "❌ devnet.yaml not found!" - exit 1 -fi - -# Test 2: Check docker-compose.yaml has the extra_hosts mapping -echo "Checking docker-compose.yaml networking..." -if [ -f "/tmp/devkit-compose/docker-compose.yaml" ]; then - if ! grep -q "host.docker.internal:host-gateway" /tmp/devkit-compose/docker-compose.yaml; then - echo "❌ REGRESSION DETECTED: docker-compose.yaml missing extra_hosts mapping!" - echo "This means the docker networking fix was reverted" - echo "Content of docker-compose.yaml:" - cat /tmp/devkit-compose/docker-compose.yaml - exit 1 - fi - echo "βœ… docker-compose.yaml has correct host mapping" -else - echo "⚠️ docker-compose.yaml not found, skipping docker-compose test" -fi - -# Test 3: Verify EnsureDockerHost function handles edge cases correctly -echo "Testing EnsureDockerHost edge cases..." - -# Create a simple Go test to verify our EnsureDockerHost logic for BOTH platforms -cat > test_ensure_docker_host.go << 'EOF' -package main - -import ( - "fmt" - "net/url" - "os" - "regexp" - "runtime" - "strings" -) - -func GetDockerHost() string { - if dockersHost := os.Getenv("DOCKERS_HOST"); dockersHost != "" { - return dockersHost - } - if runtime.GOOS == "linux" { - return "localhost" - } else { - return "host.docker.internal" - } -} - -func EnsureDockerHost(inputUrl string) string { - dockerHost := GetDockerHost() - parsedUrl, err := url.Parse(inputUrl) - if err != nil { - return ensureDockerHostRegex(inputUrl, dockerHost) - } - hostname := parsedUrl.Hostname() - if hostname == "localhost" || hostname == "127.0.0.1" { - if parsedUrl.Port() != "" { - parsedUrl.Host = fmt.Sprintf("%s:%s", dockerHost, parsedUrl.Port()) - } else { - parsedUrl.Host = dockerHost - } - return parsedUrl.String() - } - return inputUrl -} - -func ensureDockerHostRegex(inputUrl string, dockerHost string) string { - localhostPattern := regexp.MustCompile(`\blocalhost(:[0-9]+)?(/|$|\?)`) - ipPattern := regexp.MustCompile(`\b127\.0\.0\.1(:[0-9]+)?(/|$|\?)`) - result := localhostPattern.ReplaceAllStringFunc(inputUrl, func(match string) string { - return strings.Replace(match, "localhost", dockerHost, 1) - }) - result = ipPattern.ReplaceAllStringFunc(result, func(match string) string { - return strings.Replace(match, "127.0.0.1", dockerHost, 1) - }) - return result -} - -func testPlatform(platformName, expectedDockerHost string) bool { - fmt.Printf("\nπŸ”§ Testing %s behavior (DOCKERS_HOST=%s)...\n", platformName, expectedDockerHost) - - // Override environment to simulate platform - os.Setenv("DOCKERS_HOST", expectedDockerHost) - defer os.Unsetenv("DOCKERS_HOST") - - testCases := []struct { - input string - expected string - desc string - }{ - {"http://localhost:8545", fmt.Sprintf("http://%s:8545", expectedDockerHost), "Should replace localhost"}, - {"https://127.0.0.1:3000", fmt.Sprintf("https://%s:3000", expectedDockerHost), "Should replace 127.0.0.1"}, - {"https://localhost.mycooldomain.com:8545", "https://localhost.mycooldomain.com:8545", "Should NOT replace localhost in domain"}, - {"https://api.localhost.network:3000", "https://api.localhost.network:3000", "Should NOT replace localhost in subdomain"}, - {"https://my-localhost-service.com:8080", "https://my-localhost-service.com:8080", "Should NOT replace localhost in service name"}, - {"http://mainnet.infura.io/v3/key", "http://mainnet.infura.io/v3/key", "Should not change external URLs"}, - } - - allPassed := true - for _, tc := range testCases { - result := EnsureDockerHost(tc.input) - if result != tc.expected { - fmt.Printf("❌ FAILED: %s\n", tc.desc) - fmt.Printf(" Input: %s\n", tc.input) - fmt.Printf(" Expected: %s\n", tc.expected) - fmt.Printf(" Got: %s\n", result) - allPassed = false - } else { - fmt.Printf("βœ… PASSED: %s\n", tc.desc) - } - } - - return allPassed -} - -func main() { - fmt.Println("πŸ” Testing cross-platform Docker host behavior...") - - // Test Linux behavior (localhost) - linuxPassed := testPlatform("Linux", "localhost") - - // Test macOS behavior (host.docker.internal) - macosPassed := testPlatform("macOS", "host.docker.internal") - - if !linuxPassed || !macosPassed { - fmt.Println("\n❌ Cross-platform EnsureDockerHost tests FAILED!") - os.Exit(1) - } else { - fmt.Println("\nβœ… All cross-platform EnsureDockerHost tests passed!") - fmt.Println("βœ… Linux behavior: localhost ← correct") - fmt.Println("βœ… macOS behavior: host.docker.internal ← correct") - } -} -EOF - -go run test_ensure_docker_host.go -rm test_ensure_docker_host.go - -echo "πŸŽ‰ All networking regression tests passed!" -echo "βœ… Docker networking fixes are in place and working correctly" \ No newline at end of file From 11bc69c3ab4187a2d8016e4e83e92f8ae93e6791 Mon Sep 17 00:00:00 2001 From: supernova Date: Sat, 24 May 2025 01:59:01 +0530 Subject: [PATCH 10/12] fix edge case --- pkg/common/devnet/utils.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pkg/common/devnet/utils.go b/pkg/common/devnet/utils.go index 6109b2ed..8ad20ae2 100644 --- a/pkg/common/devnet/utils.go +++ b/pkg/common/devnet/utils.go @@ -76,6 +76,12 @@ func GetDockerHost() string { func EnsureDockerHost(inputUrl string) string { dockerHost := GetDockerHost() + // Handle edge cases first: bare localhost/127.0.0.1 strings + trimmed := strings.TrimSpace(inputUrl) + if trimmed == "localhost" || trimmed == "127.0.0.1" { + return dockerHost + } + // Parse the URL to work with components safely parsedUrl, err := url.Parse(inputUrl) if err != nil { @@ -86,6 +92,22 @@ func EnsureDockerHost(inputUrl string) string { // Extract hostname (without port) hostname := parsedUrl.Hostname() + // Handle the case where URL parsing succeeded but hostname is empty + // This happens with strings like "localhost:8545" (parsed as scheme:opaque) + if hostname == "" { + // Check if the scheme is localhost or 127.0.0.1 (meaning it was parsed as scheme:opaque) + if parsedUrl.Scheme == "localhost" || parsedUrl.Scheme == "127.0.0.1" { + // Reconstruct as host:port format + if parsedUrl.Opaque != "" { + return fmt.Sprintf("%s:%s", dockerHost, parsedUrl.Opaque) + } else { + return dockerHost + } + } + // If hostname is empty but it's not the scheme:opaque case, fall back to regex + return ensureDockerHostRegex(inputUrl, dockerHost) + } + // Only replace if hostname is exactly localhost or 127.0.0.1 if hostname == "localhost" || hostname == "127.0.0.1" { // Replace just the hostname part From f3f0eafe67ca7d09871f6e3142e6a5edd01a843f Mon Sep 17 00:00:00 2001 From: supernova Date: Tue, 27 May 2025 16:53:35 +0530 Subject: [PATCH 11/12] delete unused --- pkg/common/devnet/constants.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pkg/common/devnet/constants.go b/pkg/common/devnet/constants.go index 6f966c91..530f2229 100644 --- a/pkg/common/devnet/constants.go +++ b/pkg/common/devnet/constants.go @@ -1,11 +1,5 @@ package devnet -import ( - "fmt" - "os" - "runtime" -) - // Foundry Image Date : 21 April 2025 const FOUNDRY_IMAGE = "ghcr.io/foundry-rs/foundry:stable" const CHAIN_ARGS = "--chain-id 31337" @@ -16,14 +10,3 @@ const L1 = "l1" // @TODO: Add core eigenlayer deployment addresses to context const ALLOCATION_MANAGER_ADDRESS = "0x948a420b8CC1d6BFd0B6087C2E7c344a2CD0bc39" const DELEGATION_MANAGER_ADDRESS = "0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A" - -// GetDefaultRPCURL returns the default RPC URL with platform-aware host -func GetDefaultRPCURL() string { - host := "localhost" - if dockersHost := os.Getenv("DOCKERS_HOST"); dockersHost != "" { - host = dockersHost - } else if runtime.GOOS != "linux" { - host = "host.docker.internal" - } - return fmt.Sprintf("http://%s:8545", host) -} From 1b1cd8ac06ddf8e58e0ca9c4ede37cc5e71842e7 Mon Sep 17 00:00:00 2001 From: supernova Date: Wed, 28 May 2025 11:57:37 +0530 Subject: [PATCH 12/12] add scheme in docker host regex --- pkg/common/devnet/utils.go | 52 ++++++++++++++++++++++++++------- pkg/common/devnet/utils_test.go | 49 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/pkg/common/devnet/utils.go b/pkg/common/devnet/utils.go index 8ad20ae2..605a5fea 100644 --- a/pkg/common/devnet/utils.go +++ b/pkg/common/devnet/utils.go @@ -125,20 +125,50 @@ func EnsureDockerHost(inputUrl string) string { // ensureDockerHostRegex provides regex-based fallback for malformed URLs func ensureDockerHostRegex(inputUrl string, dockerHost string) string { - // Pattern to match localhost or 127.0.0.1 as hostname (not substring) - // Matches localhost:port followed by safe separators, or standalone localhost - // Matches: localhost:8545, localhost/path, localhost?param, localhost (at end), localhost with space, etc. - // Doesn't match: localhost.domain.com, my-localhost-service.com, etc. - localhostPattern := regexp.MustCompile(`\blocalhost(:[0-9]+)?(?:[\s/=?#]|$)`) - ipPattern := regexp.MustCompile(`\b127\.0\.0\.1(:[0-9]+)?(?:[\s/=?#]|$)`) - - // Replace localhost patterns - result := localhostPattern.ReplaceAllStringFunc(inputUrl, func(match string) string { + // Pattern to match URLs with schemes (http, https, ws, wss) followed by localhost + // This ensures we only rewrite actual localhost URLs, not subdomains like "api.localhost.company.com" + schemeLocalhostPattern := regexp.MustCompile(`(https?|wss?)://localhost(:[0-9]+)?(/\S*)?`) + schemeIPPattern := regexp.MustCompile(`(https?|wss?)://127\.0\.0\.1(:[0-9]+)?(/\S*)?`) + + // Pattern to match malformed scheme-like strings with localhost/127.0.0.1 + // This handles cases like "ht tp://localhost" or "ht\x00tp://localhost" + malformedSchemeLocalhostPattern := regexp.MustCompile(`\S*tp://localhost(:[0-9]+)?(/\S*)?`) + malformedSchemeIPPattern := regexp.MustCompile(`\S*tp://127\.0\.0\.1(:[0-9]+)?(/\S*)?`) + + // Pattern to match standalone localhost (no scheme) at start of string or after whitespace/equals + // This avoids matching localhost as part of a larger domain name + standaloneLocalhostPattern := regexp.MustCompile(`(?:^|[\s=])localhost(:[0-9]+)?(?:[\s/=?#]|$)`) + standaloneIPPattern := regexp.MustCompile(`(?:^|[\s=])127\.0\.0\.1(:[0-9]+)?(?:[\s/=?#]|$)`) + + result := inputUrl + + // Replace scheme-based localhost URLs + result = schemeLocalhostPattern.ReplaceAllStringFunc(result, func(match string) string { + return strings.Replace(match, "localhost", dockerHost, 1) + }) + + // Replace scheme-based 127.0.0.1 URLs + result = schemeIPPattern.ReplaceAllStringFunc(result, func(match string) string { + return strings.Replace(match, "127.0.0.1", dockerHost, 1) + }) + + // Replace malformed scheme localhost patterns + result = malformedSchemeLocalhostPattern.ReplaceAllStringFunc(result, func(match string) string { + return strings.Replace(match, "localhost", dockerHost, 1) + }) + + // Replace malformed scheme 127.0.0.1 patterns + result = malformedSchemeIPPattern.ReplaceAllStringFunc(result, func(match string) string { + return strings.Replace(match, "127.0.0.1", dockerHost, 1) + }) + + // Replace standalone localhost patterns + result = standaloneLocalhostPattern.ReplaceAllStringFunc(result, func(match string) string { return strings.Replace(match, "localhost", dockerHost, 1) }) - // Replace 127.0.0.1 patterns - result = ipPattern.ReplaceAllStringFunc(result, func(match string) string { + // Replace standalone 127.0.0.1 patterns + result = standaloneIPPattern.ReplaceAllStringFunc(result, func(match string) string { return strings.Replace(match, "127.0.0.1", dockerHost, 1) }) diff --git a/pkg/common/devnet/utils_test.go b/pkg/common/devnet/utils_test.go index c6ef14a3..905eda48 100644 --- a/pkg/common/devnet/utils_test.go +++ b/pkg/common/devnet/utils_test.go @@ -140,6 +140,55 @@ func TestEnsureDockerHost(t *testing.T) { expectedURL: "http://custom.docker.host:8545?param=value", description: "Should replace localhost and preserve query parameters", }, + { + name: "WebSocket localhost replacement", + inputURL: "ws://localhost:8546", + dockersHost: "custom.docker.host", + expectedURL: "ws://custom.docker.host:8546", + description: "Should replace localhost in WebSocket URLs", + }, + { + name: "Secure WebSocket localhost replacement", + inputURL: "wss://localhost:8546/ws", + dockersHost: "custom.docker.host", + expectedURL: "wss://custom.docker.host:8546/ws", + description: "Should replace localhost in secure WebSocket URLs", + }, + { + name: "Do not replace localhost in complex subdomain", + inputURL: "https://dev.localhost.internal.company.com:3000", + dockersHost: "custom.docker.host", + expectedURL: "https://dev.localhost.internal.company.com:3000", + description: "Should NOT replace localhost when it's part of a complex subdomain", + }, + { + name: "Do not replace localhost-like service names", + inputURL: "https://localhost-dev.myservice.com:8080", + dockersHost: "custom.docker.host", + expectedURL: "https://localhost-dev.myservice.com:8080", + description: "Should NOT replace localhost when it's part of a hyphenated service name", + }, + { + name: "Replace localhost in fragment", + inputURL: "http://localhost:8545/api#section", + dockersHost: "custom.docker.host", + expectedURL: "http://custom.docker.host:8545/api#section", + description: "Should replace localhost and preserve URL fragment", + }, + { + name: "Replace standalone localhost in complex string", + inputURL: "Connect to localhost:8545 for RPC", + dockersHost: "custom.docker.host", + expectedURL: "Connect to custom.docker.host:8545 for RPC", + description: "Should replace standalone localhost in descriptive text", + }, + { + name: "Do not replace when localhost is part of word", + inputURL: "Visit our-localhost-cluster.example.com", + dockersHost: "custom.docker.host", + expectedURL: "Visit our-localhost-cluster.example.com", + description: "Should NOT replace localhost when it's part of a hyphenated word", + }, } for _, tt := range tests {