Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions acceptance/bin/free_port.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""
Print space-separated free TCP ports on the loopback interface.
Usage: free_port.py [count]
"""

import socket
import sys


def main():
count = int(sys.argv[1]) if len(sys.argv) > 1 else 1

# Keep every socket bound until all ports are picked, otherwise the kernel
# is free to hand out the same port twice.
sockets = [socket.socket() for _ in range(count)]
for s in sockets:
s.bind(("127.0.0.1", 0))

print(" ".join(str(s.getsockname()[1]) for s in sockets))

for s in sockets:
s.close()


if __name__ == "__main__":
main()
9 changes: 1 addition & 8 deletions acceptance/cmd/workspace/apps/run-local-node/script
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,7 @@ cd app
# so we don't need to start it in background. It will install the dependencies as part of the command
trace $CLI apps run-local --prepare-environment --entry-point test.yml 2>&1 | grep -w "Hello, world"

# Function to find a free port in safe range
get_free_port() {
python3 -c "import socket,random; port=random.randint(49152,65535); s=socket.socket(); s.bind(('',port)); print(port); s.close()" 2>/dev/null || python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()"
}

PORT=$(get_free_port)
DEBUG_PORT=$(get_free_port)
PROXY_PORT=$(get_free_port)
read -r PORT DEBUG_PORT PROXY_PORT <<< "$(free_port.py 3)"

cleanup() {
# Kill any still running processes on these ports when the script exits
Expand Down
39 changes: 28 additions & 11 deletions acceptance/cmd/workspace/apps/run-local/app/app.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
import http.server
import json
import os
import signal
from flask import Flask, request
import threading

app = Flask(__name__)
# Standard library only: acceptance tests run with the network disabled, so the
# app cannot install anything from PyPI.

app.logger.warning("Python Flask app has started with: " + os.environ.get("TEST"))

class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
shutdown = self.path == "/shutdown"
body = b"" if shutdown else json.dumps(dict(self.headers), sort_keys=True).encode() + b"\n"

@app.route("/")
def index():
return dict(request.headers)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)

# shutdown() blocks until serve_forever() returns, which cannot happen until this
# handler returns, so it has to run on another thread. Stopping the loop rather
# than exiting the process lets the connection close cleanly, otherwise the client
# can see a reset instead of the response we just wrote.
if shutdown:
threading.Thread(target=self.server.shutdown, daemon=True).start()

@app.route("/shutdown")
def shutdown():
os._exit(signal.SIGTERM)
return "Shutting down..."
def log_message(self, fmt, *args):
pass


# Bind before printing, so the message can never appear while the port is still closed.
server = http.server.HTTPServer(("127.0.0.1", int(os.environ["DATABRICKS_APP_PORT"])), Handler)
print("Python app has started with: " + os.environ["TEST"], flush=True)
server.serve_forever()
4 changes: 2 additions & 2 deletions acceptance/cmd/workspace/apps/run-local/app/app.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
command:
- flask
- run
- python
- app.py

env:
- name: TEST
Expand Down

This file was deleted.

4 changes: 2 additions & 2 deletions acceptance/cmd/workspace/apps/run-local/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 24 additions & 14 deletions acceptance/cmd/workspace/apps/run-local/output.txt
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@

=== Env var with valueFrom cannot be resolved locally

>>> errcode [CLI] apps run-local --entry-point value-from.yml
Error: VALUE_FROM defined in value-from.yml with valueFrom property and can't be resolved locally. Please set VALUE_FROM environment variable in your terminal or using --env flag

Exit code: 1
Running command: uv run python -c print('Hello, world')

=== Entry point overrides the command from app.yml

>>> grep -x Running command: python -c print('Hello, world') ../LOG.entry-point
Running command: python -c print('Hello, world')

>>> grep -x Hello, world ../LOG.entry-point
Hello, world

=== Starting the app in background...
=== Waiting
=== Checking app is running...
=== Checking the proxy is running...
>>> curl -s -o - http://127.0.0.1:$(port)
{"Accept":"*/*","Accept-Encoding":"gzip","Host":"127.0.0.1:$(port)","User-Agent":"curl/(version)","X-Forwarded-Email":"[USERNAME]","X-Forwarded-Host":"localhost","X-Forwarded-Preferred-Username":"","X-Forwarded-User":"[USERNAME]","X-Real-Ip":"127.0.0.1","X-Request-Id":"[UUID]"}
=== Proxy forwards to the app and injects the identity headers

>>> retry --until X-Forwarded-Email curl -sS http://127.0.0.1:[PROXY_PORT]
{"Accept": "*/*", "Accept-Encoding": "gzip", "Host": "127.0.0.1:[APP_PORT]", "User-Agent": "curl/(version)", "X-Forwarded-Email": "[USERNAME]", "X-Forwarded-Host": "localhost", "X-Forwarded-Preferred-Username": "", "X-Forwarded-User": "[USERNAME]", "X-Real-Ip": "127.0.0.1", "X-Request-Id": "[UUID]"}

=== Proxy URL is reported on the port that was asked for

>>> grep -x To access your app go to http://localhost:[PROXY_PORT] LOG.run
To access your app go to http://localhost:[PROXY_PORT]

=== App picked up the env var from app.yml

=== Sending shutdown request...
>>> curl -s -o /dev/null http://127.0.0.1:$(port)/shutdown
>>> grep -x Python app has started with: test LOG.run
Python app has started with: test

=== Checking CLI command output...
>>> grep To debug your app, attach a debugger to port ./out.run.txt
To debug your app, attach a debugger to port $(debug_port)
=== Shutting the app down

>>> grep -o Python Flask app has started with: test ./out.run.txt
Python Flask app has started with: test
>>> curl -sS -o /dev/null http://127.0.0.1:[PROXY_PORT]/shutdown
93 changes: 32 additions & 61 deletions acceptance/cmd/workspace/apps/run-local/script
Original file line number Diff line number Diff line change
@@ -1,75 +1,46 @@
cd app

trace errcode $CLI apps run-local --entry-point value-from.yml 2>&1
# Ports are allocated per run so that tests in parallel worktrees do not collide.
read -r APP_PORT PROXY_PORT ENTRY_POINT_PORT <<< "$(free_port.py 3)"
add_repl.py "$APP_PORT" APP_PORT
add_repl.py "$PROXY_PORT" PROXY_PORT

# The app and the proxy start asynchronously; allow up to 15s for them to come up.
export RETRY_MAX_ATTEMPTS=30

# We first run the command with different entry point which starts unblocking script
# so we don't need to start it in background. It will install the dependencies as part of the command
trace $CLI apps run-local --prepare-environment --entry-point test.yml 2>&1 | grep -w "Hello, world"
title "Env var with valueFrom cannot be resolved locally\n"
trace errcode $CLI apps run-local --entry-point value-from.yml 2>&1

PORT=8090
DEBUG_PORT=5252
PROXY_PORT=8091
title "Entry point overrides the command from app.yml\n"
trace $CLI apps run-local --entry-point test.yml --port "$ENTRY_POINT_PORT" &> ../LOG.entry-point
trace grep -x "Running command: python -c print('Hello, world')" ../LOG.entry-point
trace grep -x "Hello, world" ../LOG.entry-point

cleanup() {
# Kill any still running processes on these ports when the script exits
kill_port.py $PORT $DEBUG_PORT $PROXY_PORT
local status=$?
# Reap by port rather than by $PID: backgrounding a function puts a subshell between the
# script and the CLI, so $PID is that subshell and the CLI and the app it started are
# grandchildren that survive killing it. Windows reaches the same state for another
# reason: kill terminates the CLI outright rather than letting it stop the app. Only when
# the script failed: on success both have exited through /shutdown already and the ports
# may belong to another test by now.
[ "$status" -eq 0 ] || kill_port.py "$APP_PORT" "$PROXY_PORT"
}

trap cleanup EXIT

title "Starting the app in background..."
trace $CLI apps run-local --prepare-environment --debug --port "$PROXY_PORT" --debug-port "$DEBUG_PORT" --app-port "$PORT" > ../out.run.txt 2>&1 &
trace $CLI apps run-local --port "$PROXY_PORT" --app-port "$APP_PORT" &> ../LOG.run &
PID=$!
# Ensure background process is killed on script exit
trap 'kill $PID 2>/dev/null || true' EXIT
trap cleanup EXIT
cd ..

title Waiting for the app to start...
# Use a loop to check for the startup message instead of tail/sed which can be unreliable on Windows
# due to file locking, buffering issues, and different text processing behavior across Windows versions.
# A simple grep loop is more robust across platforms.
while [ -z "$(grep -o "Python Flask app has started with" out.run.txt 2>/dev/null)" ]; do
sleep 1
done

# Make sure the proxy is ready to serve requests
while [ -z "$(grep -o "To access your app go to " out.run.txt 2>/dev/null)" ]; do
sleep 1
done

title "Checking app is running..."
# Wait for the proxy to be ready to serve requests (with timeout)
timeout=5
counter=0
while ! curl -s -o /dev/null http://127.0.0.1:$PORT 2>/dev/null; do
sleep 1
counter=$((counter + 1))
if [ $counter -ge $timeout ]; then
echo "Timeout waiting for app to be ready after ${timeout}s"
exit 1
fi
done

title "Checking the proxy is running..."
# Wait for the proxy to be ready to serve requests (with timeout)
timeout=5
counter=0
while ! curl -s -o /dev/null http://127.0.0.1:$PROXY_PORT 2>/dev/null; do
sleep 1
counter=$((counter + 1))
if [ $counter -ge $timeout ]; then
echo "Timeout waiting for proxy to be ready after ${timeout}s"
exit 1
fi
done

trace curl -s -o - http://127.0.0.1:$PROXY_PORT
title "Proxy forwards to the app and injects the identity headers\n"
trace retry --until X-Forwarded-Email curl -sS "http://127.0.0.1:$PROXY_PORT"

title "Sending shutdown request..."
trace curl -s -o /dev/null http://127.0.0.1:$PROXY_PORT/shutdown || true
title "Proxy URL is reported on the port that was asked for\n"
trace grep -x "To access your app go to http://localhost:$PROXY_PORT" LOG.run

title "Checking CLI command output..."
title "App picked up the env var from app.yml\n"
trace grep -x "Python app has started with: test" LOG.run

trace grep "To debug your app, attach a debugger to port" ./out.run.txt
trace grep -o "Python Flask app has started with: test" ./out.run.txt
rm out.run.txt
title "Shutting the app down\n"
trace curl -sS -o /dev/null "http://127.0.0.1:$PROXY_PORT/shutdown"
wait $PID
25 changes: 5 additions & 20 deletions acceptance/cmd/workspace/apps/run-local/test.toml
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
Cloud = false
# This test uses fixed ports, so incompatible with parellel testing in different worktrees.
# It's also very slow when it does work.
Local = false
Local = true
RecordRequests = false
Timeout = '2m'
TimeoutWindows = '10m'

Ignore = [
'.venv',
'__pycache__'
]
# The command is unrelated to bundle deployment, so run it once rather than per engine.
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]

# The ports are registered as replacements by the script, which is where they are allocated.

[[Repls]]
Old='curl/[0-9]+\.[0-9]+\.[0-9]+'
New='curl/(version)'

[[Repls]]
Old='127.0.0.1:[0-9]+'
New='127.0.0.1:$(port)'

[[Repls]]
Old='To debug your app, attach a debugger to port [0-9]+'
New='To debug your app, attach a debugger to port $(debug_port)'

[EnvMatrix]
DATABRICKS_BUNDLE_ENGINE = ["terraform"]
Loading