diff --git a/pkg/container/templates/go.tmpl b/pkg/container/templates/go.tmpl index a5c600222..f2c9d1c53 100644 --- a/pkg/container/templates/go.tmpl +++ b/pkg/container/templates/go.tmpl @@ -1,4 +1,4 @@ -FROM golang:1.25-alpine +FROM golang:1.25-alpine AS builder {{if .CACertContent}} # Add custom CA certificate BEFORE any network operations @@ -8,19 +8,8 @@ RUN cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt && \ rm /tmp/custom-ca.crt {{end}} -# Install CA certificates -RUN apk add --no-cache ca-certificates - -# Set working directory -WORKDIR /app - -# Create a non-root user to run the application and set proper permissions -RUN addgroup -S appgroup && \ - adduser -S appuser -G appgroup && \ - mkdir -p /app && \ - chown -R appuser:appgroup /app && \ - mkdir -p /home/appuser/.cache && \ - chown -R appuser:appgroup /home/appuser +# Install CA certificates and git (needed for go modules) +RUN apk add --no-cache ca-certificates git {{if .CACertContent}} # Properly install the custom CA certificate using standard tools @@ -37,18 +26,78 @@ ENV CGO_ENABLED=0 \ GOARCH=amd64 \ GO111MODULE=on +# Set working directory +WORKDIR /build + {{if .IsLocalPath}} # Copy the local source code -COPY . /app/ +COPY . /build/ + +# Build the application +RUN go build -o /app/mcp-server {{.MCPPackage}} +{{else}} +# Pre-install the Go package at build time +# This downloads all dependencies and builds the binary +# Check if the package already has a version specifier +RUN package="{{.MCPPackage}}"; \ + # If package doesn't have @ version specifier, add @latest + if ! echo "$package" | grep -q '@'; then \ + package="${package}@latest"; \ + fi; \ + # Create the app directory first + mkdir -p /app && \ + # Install the package + go install "$package" && \ + # Move the installed binary to a known location + mv $GOPATH/bin/* /app/mcp-server 2>/dev/null || \ + # If the package name differs from binary name, try to find it + find $GOPATH/bin -type f -executable -exec mv {} /app/mcp-server \; 2>/dev/null || \ + # As a fallback, build it directly (strip version for go build) + (base_package=$(echo "$package" | sed 's/@.*//'); \ + go get "$package" && go build -o /app/mcp-server "$base_package") +{{end}} + +# Final stage - minimal runtime image +FROM alpine:3.20 + +{{if .CACertContent}} +# Add custom CA certificate for runtime +COPY ca-cert.crt /tmp/custom-ca.crt +RUN cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt && \ + rm /tmp/custom-ca.crt +{{end}} -# Change ownership of copied files to appuser -USER root -RUN chown -R appuser:appgroup /app +# Install only runtime dependencies +RUN apk add --no-cache ca-certificates + +# Set working directory +WORKDIR /app + +# Create a non-root user to run the application +RUN addgroup -S appgroup && \ + adduser -S appuser -G appgroup && \ + mkdir -p /app && \ + chown -R appuser:appgroup /app + +{{if .CACertContent}} +# Install CA certificate for runtime +RUN mkdir -p /usr/local/share/ca-certificates && \ + cp /tmp/custom-ca.crt /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || \ + echo "CA cert already added to bundle" && \ + chmod 644 /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || true && \ + update-ca-certificates +{{end}} + +# Copy the pre-built binary from builder stage +COPY --from=builder --chown=appuser:appgroup /app/mcp-server /app/mcp-server + +{{if .IsLocalPath}} +# Copy any additional files that might be needed at runtime +COPY --from=builder --chown=appuser:appgroup /build/ /app/ {{end}} # Switch to non-root user USER appuser -# Run the MCP server using go -# The entrypoint will be constructed dynamically based on the package and arguments -ENTRYPOINT ["go", "run", "{{.MCPPackage}}"{{range .MCPArgs}}, "{{.}}"{{end}}] \ No newline at end of file +# Run the pre-built MCP server binary +ENTRYPOINT ["/app/mcp-server"{{range .MCPArgs}}, "{{.}}"{{end}}] \ No newline at end of file diff --git a/pkg/container/templates/npx.tmpl b/pkg/container/templates/npx.tmpl index 659791c69..285d83f24 100644 --- a/pkg/container/templates/npx.tmpl +++ b/pkg/container/templates/npx.tmpl @@ -1,4 +1,4 @@ -FROM node:22-alpine +FROM node:22-alpine AS builder {{if .CACertContent}} # Add custom CA certificate BEFORE any network operations @@ -11,14 +11,14 @@ RUN cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt && \ # Install git for package installation support RUN apk add --no-cache git ca-certificates -# Set working directory -WORKDIR /app - -# Create a non-root user to run the application and set proper permissions -RUN addgroup -S appgroup && \ - adduser -S appuser -G appgroup && \ - mkdir -p /app && \ - chown -R appuser:appgroup /app +{{if .CACertContent}} +# Properly install the custom CA certificate using standard tools +RUN mkdir -p /usr/local/share/ca-certificates && \ + cp /tmp/custom-ca.crt /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || \ + echo "CA cert already added to bundle" && \ + chmod 644 /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || true && \ + update-ca-certificates +{{end}} # Configure npm for faster installations in containerized environments ENV NODE_ENV=production \ @@ -28,8 +28,47 @@ ENV NODE_ENV=production \ NPM_CONFIG_UPDATE_NOTIFIER=false \ NPM_CONFIG_PROGRESS=false +# Set working directory for package installation +WORKDIR /build + +{{if .IsLocalPath}} +# Copy the local source code +COPY . /build/ +# Install dependencies if package.json exists +RUN if [ -f package.json ]; then npm ci --only=production || npm install --production; fi +{{else}} +# Create a package.json to install the MCP package +RUN echo '{"name":"mcp-container","version":"1.0.0"}' > package.json + +# Install the MCP package and its dependencies at build time +# This ensures all dependencies are downloaded during the build phase +RUN npm install --save {{.MCPPackage}} +{{end}} + +# Final stage - runtime image with pre-installed packages +FROM node:22-alpine + {{if .CACertContent}} -# Properly install the custom CA certificate using standard tools +# Add custom CA certificate for runtime +COPY ca-cert.crt /tmp/custom-ca.crt +RUN cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt && \ + rm /tmp/custom-ca.crt +{{end}} + +# Install runtime dependencies +RUN apk add --no-cache ca-certificates + +# Set working directory +WORKDIR /app + +# Create a non-root user to run the application +RUN addgroup -S appgroup && \ + adduser -S appuser -G appgroup && \ + mkdir -p /app && \ + chown -R appuser:appgroup /app + +{{if .CACertContent}} +# Install CA certificate for runtime RUN mkdir -p /usr/local/share/ca-certificates && \ cp /tmp/custom-ca.crt /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || \ echo "CA cert already added to bundle" && \ @@ -37,12 +76,24 @@ RUN mkdir -p /usr/local/share/ca-certificates && \ update-ca-certificates {{end}} -# Run the MCP server using npx -# The entrypoint will be constructed dynamically based on the package and arguments -# Using the form: npx -- [@] [args...] -# The -- separates npx options from the package name and arguments +# Copy the installed node_modules from builder stage +COPY --from=builder --chown=appuser:appgroup /build/node_modules /app/node_modules + +{{if .IsLocalPath}} +# Copy the local application files +COPY --from=builder --chown=appuser:appgroup /build/ /app/ +{{else}} +# Copy package.json to maintain the dependency tree +COPY --from=builder --chown=appuser:appgroup /build/package.json /app/package.json +{{end}} + +# Set NODE_PATH to find the pre-installed modules +ENV NODE_PATH=/app/node_modules \ + PATH=/app/node_modules/.bin:$PATH # Switch to non-root user USER appuser -ENTRYPOINT ["npx", "--yes", "--", "{{.MCPPackage}}"{{range .MCPArgs}}, "{{.}}"{{end}}] \ No newline at end of file +# Run the pre-installed MCP package directly using npx +# The package is already installed, so npx will use the local version without downloading +ENTRYPOINT ["npx", "--no-install", "{{.MCPPackage}}"{{range .MCPArgs}}, "{{.}}"{{end}}] \ No newline at end of file diff --git a/pkg/container/templates/templates_test.go b/pkg/container/templates/templates_test.go index f0528b257..53332d78e 100644 --- a/pkg/container/templates/templates_test.go +++ b/pkg/container/templates/templates_test.go @@ -25,12 +25,17 @@ func TestGetDockerfileTemplate(t *testing.T) { MCPArgs: []string{"--arg1", "--arg2", "value"}, }, wantContains: []string{ - "apt-get install -y --no-install-recommends ca-certificates", + "FROM python:3.13-slim AS builder", + "apt-get install -y --no-install-recommends", "pip install --no-cache-dir uv", - "ENTRYPOINT [\"uvx\", \"example-package\", \"--arg1\", \"--arg2\", \"value\"]", + "package_spec=$(echo \"$package\" | sed 's/@/==/')", + "uv tool install \"$package_spec\"", + "COPY --from=builder --chown=appuser:appgroup /opt/uv-tools /opt/uv-tools", + "ENTRYPOINT [\"sh\", \"-c\", \"package='example-package'; exec \\\"${package%%@*}\\\" \\\"--arg1\\\" \\\"--arg2\\\" \\\"value\\\" \\\"$@\\\"\", \"--\"]", }, wantMatches: []string{ - `FROM python:\d+\.\d+-slim`, // Match any Python version + `FROM python:\d+\.\d+-slim AS builder`, // Match builder stage + `FROM python:\d+\.\d+-slim`, // Match runtime stage }, wantNotContains: []string{ "Add custom CA certificate", @@ -47,16 +52,21 @@ func TestGetDockerfileTemplate(t *testing.T) { CACertContent: "-----BEGIN CERTIFICATE-----\nMIICertificateContent\n-----END CERTIFICATE-----", }, wantContains: []string{ - "apt-get install -y --no-install-recommends ca-certificates", + "FROM python:3.13-slim AS builder", + "apt-get install -y --no-install-recommends", "pip install --no-cache-dir uv", - "ENTRYPOINT [\"uvx\", \"example-package\", \"--arg1\", \"--arg2\", \"value\"]", + "package_spec=$(echo \"$package\" | sed 's/@/==/')", + "uv tool install \"$package_spec\"", + "COPY --from=builder --chown=appuser:appgroup /opt/uv-tools /opt/uv-tools", + "ENTRYPOINT [\"sh\", \"-c\", \"package='example-package'; exec \\\"${package%%@*}\\\" \\\"--arg1\\\" \\\"--arg2\\\" \\\"value\\\" \\\"$@\\\"\", \"--\"]", "Add custom CA certificate BEFORE any network operations", "COPY ca-cert.crt /tmp/custom-ca.crt", "cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt", "update-ca-certificates", }, wantMatches: []string{ - `FROM python:\d+\.\d+-slim`, // Match any Python version + `FROM python:\d+\.\d+-slim AS builder`, // Match builder stage + `FROM python:\d+\.\d+-slim`, // Match runtime stage }, wantNotContains: []string{}, wantErr: false, @@ -69,10 +79,14 @@ func TestGetDockerfileTemplate(t *testing.T) { MCPArgs: []string{"--arg1", "--arg2", "value"}, }, wantContains: []string{ - "ENTRYPOINT [\"npx\", \"--yes\", \"--\", \"example-package\", \"--arg1\", \"--arg2\", \"value\"]", + "FROM node:22-alpine AS builder", + "npm install --save example-package", + "COPY --from=builder --chown=appuser:appgroup /build/node_modules /app/node_modules", + "ENTRYPOINT [\"npx\", \"--no-install\", \"example-package\", \"--arg1\", \"--arg2\", \"value\"]", }, wantMatches: []string{ - `FROM node:\d+-alpine`, // Match any Node version + `FROM node:\d+-alpine AS builder`, // Match builder stage + `FROM node:\d+-alpine`, // Match runtime stage }, wantNotContains: []string{ "Add custom CA certificate", @@ -89,14 +103,17 @@ func TestGetDockerfileTemplate(t *testing.T) { CACertContent: "-----BEGIN CERTIFICATE-----\nMIICertificateContent\n-----END CERTIFICATE-----", }, wantContains: []string{ - "ENTRYPOINT [\"npx\", \"--yes\", \"--\", \"example-package\", \"--arg1\", \"--arg2\", \"value\"]", + "FROM node:22-alpine AS builder", + "npm install --save example-package", + "ENTRYPOINT [\"npx\", \"--no-install\", \"example-package\", \"--arg1\", \"--arg2\", \"value\"]", "Add custom CA certificate BEFORE any network operations", "COPY ca-cert.crt /tmp/custom-ca.crt", "cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt", "update-ca-certificates", }, wantMatches: []string{ - `FROM node:\d+-alpine`, // Match any Node version + `FROM node:\d+-alpine AS builder`, // Match builder stage + `FROM node:\d+-alpine`, // Match runtime stage }, wantNotContains: []string{}, wantErr: false, @@ -109,10 +126,17 @@ func TestGetDockerfileTemplate(t *testing.T) { MCPArgs: []string{"--arg1", "--arg2", "value"}, }, wantContains: []string{ - "ENTRYPOINT [\"go\", \"run\", \"example-package\", \"--arg1\", \"--arg2\", \"value\"]", + "FROM golang:1.25-alpine AS builder", + "if ! echo \"$package\" | grep -q '@'; then", + "package=\"${package}@latest\"", + "go install \"$package\"", + "FROM alpine:3.20", + "COPY --from=builder --chown=appuser:appgroup /app/mcp-server /app/mcp-server", + "ENTRYPOINT [\"/app/mcp-server\", \"--arg1\", \"--arg2\", \"value\"]", }, wantMatches: []string{ - `FROM golang:\d+\.\d+-alpine`, // Match any Go version + `FROM golang:\d+\.\d+-alpine AS builder`, // Match builder stage + `FROM alpine:\d+\.\d+`, // Match runtime stage }, wantNotContains: []string{ "Add custom CA certificate", @@ -129,14 +153,20 @@ func TestGetDockerfileTemplate(t *testing.T) { CACertContent: "-----BEGIN CERTIFICATE-----\nMIICertificateContent\n-----END CERTIFICATE-----", }, wantContains: []string{ - "ENTRYPOINT [\"go\", \"run\", \"example-package\", \"--arg1\", \"--arg2\", \"value\"]", + "FROM golang:1.25-alpine AS builder", + "if ! echo \"$package\" | grep -q '@'; then", + "package=\"${package}@latest\"", + "go install \"$package\"", + "FROM alpine:3.20", + "ENTRYPOINT [\"/app/mcp-server\", \"--arg1\", \"--arg2\", \"value\"]", "Add custom CA certificate BEFORE any network operations", "COPY ca-cert.crt /tmp/custom-ca.crt", "cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt", "update-ca-certificates", }, wantMatches: []string{ - `FROM golang:\d+\.\d+-alpine`, // Match any Go version + `FROM golang:\d+\.\d+-alpine AS builder`, // Match builder stage + `FROM alpine:\d+\.\d+`, // Match runtime stage }, wantNotContains: []string{}, wantErr: false, @@ -150,11 +180,17 @@ func TestGetDockerfileTemplate(t *testing.T) { IsLocalPath: true, }, wantContains: []string{ - "COPY . /app/", - "ENTRYPOINT [\"go\", \"run\", \"./cmd/server\", \"--arg1\", \"value\"]", + "FROM golang:1.25-alpine AS builder", + "COPY . /build/", + "go build -o /app/mcp-server ./cmd/server", + "FROM alpine:3.20", + "COPY --from=builder --chown=appuser:appgroup /app/mcp-server /app/mcp-server", + "COPY --from=builder --chown=appuser:appgroup /build/ /app/", + "ENTRYPOINT [\"/app/mcp-server\", \"--arg1\", \"value\"]", }, wantMatches: []string{ - `FROM golang:\d+\.\d+-alpine`, // Match any Go version + `FROM golang:\d+\.\d+-alpine AS builder`, // Match builder stage + `FROM alpine:\d+\.\d+`, // Match runtime stage }, wantNotContains: []string{ "Add custom CA certificate", @@ -170,11 +206,16 @@ func TestGetDockerfileTemplate(t *testing.T) { IsLocalPath: true, }, wantContains: []string{ - "COPY . /app/", - "ENTRYPOINT [\"go\", \"run\", \".\"]", + "FROM golang:1.25-alpine AS builder", + "COPY . /build/", + "go build -o /app/mcp-server .", + "FROM alpine:3.20", + "COPY --from=builder --chown=appuser:appgroup /app/mcp-server /app/mcp-server", + "ENTRYPOINT [\"/app/mcp-server\"]", }, wantMatches: []string{ - `FROM golang:\d+\.\d+-alpine`, // Match any Go version + `FROM golang:\d+\.\d+-alpine AS builder`, // Match builder stage + `FROM alpine:\d+\.\d+`, // Match runtime stage }, wantNotContains: []string{ "Add custom CA certificate", diff --git a/pkg/container/templates/uvx.tmpl b/pkg/container/templates/uvx.tmpl index cf905d066..05ceebdab 100644 --- a/pkg/container/templates/uvx.tmpl +++ b/pkg/container/templates/uvx.tmpl @@ -1,4 +1,4 @@ -FROM python:3.13-slim +FROM python:3.13-slim AS builder {{if .CACertContent}} # Add custom CA certificate BEFORE any network operations @@ -8,16 +8,72 @@ RUN cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt && \ rm /tmp/custom-ca.crt {{end}} -# Install uv package manager and CA certificates +# Install build dependencies and uv package manager +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + && pip install --no-cache-dir uv \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +{{if .CACertContent}} +# Properly install the custom CA certificate using standard tools +RUN mkdir -p /usr/local/share/ca-certificates && \ + cp /tmp/custom-ca.crt /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || \ + echo "CA cert already added to bundle" && \ + chmod 644 /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || true && \ + update-ca-certificates +{{end}} + +# Set environment variables for build +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + UV_SYSTEM_PYTHON=1 + +# Set working directory for package installation +WORKDIR /build + +{{if .IsLocalPath}} +# Copy the local source code +COPY . /build/ +# Install the local package and its dependencies +RUN uv pip install --system /build/ +{{else}} +# Install the tool using uv tool install +# This properly handles package-to-executable mapping and dependencies +# We set UV_TOOL_DIR to a custom location so we can copy it to the runtime stage +ENV UV_TOOL_DIR=/opt/uv-tools \ + UV_TOOL_BIN_DIR=/opt/uv-tools/bin +# Convert @ version separator to == for Python package specification +RUN package="{{.MCPPackage}}"; \ + # Replace @ with == for uv tool install (Python uses == for version pinning) + package_spec=$(echo "$package" | sed 's/@/==/'); \ + uv tool install "$package_spec" && \ + # List installed executables for debugging + ls -la /opt/uv-tools/bin/ +{{end}} + +# Final stage - runtime image with pre-installed packages +FROM python:3.13-slim + +{{if .CACertContent}} +# Add custom CA certificate for runtime +COPY ca-cert.crt /tmp/custom-ca.crt +RUN cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt && \ + rm /tmp/custom-ca.crt +{{end}} + +# Install only runtime dependencies RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \ - pip install --no-cache-dir uv && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # Set working directory WORKDIR /app -# Create a non-root user to run the application and set proper permissions +# Create a non-root user to run the application RUN groupadd -r appgroup && \ useradd -r -g appgroup -m appuser && \ mkdir -p /app && \ @@ -26,7 +82,7 @@ RUN groupadd -r appgroup && \ chown -R appuser:appgroup /home/appuser {{if .CACertContent}} -# Properly install the custom CA certificate using standard tools +# Install CA certificate for runtime RUN mkdir -p /usr/local/share/ca-certificates && \ cp /tmp/custom-ca.crt /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || \ echo "CA cert already added to bundle" && \ @@ -34,16 +90,32 @@ RUN mkdir -p /usr/local/share/ca-certificates && \ update-ca-certificates {{end}} -# Set environment variables for better performance in containers +{{if .IsLocalPath}} +# Copy the system Python packages if local installation +COPY --from=builder --chown=appuser:appgroup /usr/local/lib/python3.13 /usr/local/lib/python3.13 +{{else}} +# Copy the uv tool installation from builder +COPY --from=builder --chown=appuser:appgroup /opt/uv-tools /opt/uv-tools +{{end}} + +{{if .IsLocalPath}} +# Copy the application files if local +COPY --from=builder --chown=appuser:appgroup /build/ /app/ +{{end}} + +# Set environment variables for runtime ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 \ - PIP_DISABLE_PIP_VERSION_CHECK=1 \ - UV_SYSTEM_PYTHON=1 + UV_TOOL_DIR=/opt/uv-tools \ + UV_TOOL_BIN_DIR=/opt/uv-tools/bin \ + PATH="/opt/uv-tools/bin:$PATH" # Switch to non-root user USER appuser -# Run the MCP server using uvx (alias for uv tool run) -# The entrypoint will be constructed dynamically based on the package and arguments -ENTRYPOINT ["uvx", "{{.MCPPackage}}"{{range .MCPArgs}}, "{{.}}"{{end}}] \ No newline at end of file +# Run the pre-installed MCP package +# uv tool install puts the correct executable in the bin directory +# We use sh -c to allow the package name to be resolved from PATH +# Strip version specifier (if present) from package name for execution +# Handles format like package@version +ENTRYPOINT ["sh", "-c", "package='{{.MCPPackage}}'; exec \"${package%%@*}\" {{range .MCPArgs}}\"{{.}}\" {{end}}\"$@\"", "--"] \ No newline at end of file diff --git a/test/e2e/inspector_test.go b/test/e2e/inspector_test.go index 2d6175451..0f34aef2e 100644 --- a/test/e2e/inspector_test.go +++ b/test/e2e/inspector_test.go @@ -173,10 +173,10 @@ var _ = Describe("Inspector", func() { Context("when testing inspector connectivity", func() { It("should make inspector UI accessible when running", func() { By("Starting inspector in background using goroutine") - done := helper.startInspectorInBackground(30 * time.Second) + done := helper.startInspectorInBackground(60 * time.Second) By("Waiting for inspector UI to be ready") - helper.waitForInspectorUI(20 * time.Second) + helper.waitForInspectorUI(45 * time.Second) By("Verifying inspector UI responds with valid content") helper.verifyInspectorUIAccessible() diff --git a/test/e2e/protocol_builds_e2e_test.go b/test/e2e/protocol_builds_e2e_test.go index fa23473a9..6a26bf5b6 100644 --- a/test/e2e/protocol_builds_e2e_test.go +++ b/test/e2e/protocol_builds_e2e_test.go @@ -164,8 +164,6 @@ var _ = Describe("Protocol Builds E2E", Serial, func() { }) It("should fail gracefully with non-existent uvx package [Serial]", func() { - Skip("Skipping uvx error test - uvx builds succeed even with non-existent packages") - By("Trying to run with non-existent uvx package") _, stderr, err := e2e.NewTHVCommand(config, "run", "--name", serverName,