Skip to content

Commit dc9a17b

Browse files
committed
feat: Added new e2e tests. Able to ping remote service.
1 parent 1aade0e commit dc9a17b

21 files changed

Lines changed: 529 additions & 79 deletions

File tree

.devcontainer/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bridge-*/

.devcontainer/bridge-test-api-server/devcontainer.json

Lines changed: 0 additions & 16 deletions
This file was deleted.

.devcontainer/devcontainer.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
{
22
"name": "bridge",
3-
"image": "mcr.microsoft.com/devcontainers/base:ubuntu"
3+
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
4+
"mounts": [
5+
"source=bridge-bashhistory,target=/commandhistory,type=volume"
6+
],
7+
"postCreateCommand": "sudo chown $(id -u) /commandhistory",
8+
"remoteEnv": {
9+
"HISTFILE": "/commandhistory/.shell_history"
10+
}
411
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"id": "bridge",
3+
"version": "1.4.0",
4+
"name": "Bridge Tunnel Client",
5+
"description": "Connects to a bridge proxy server to intercept outbound traffic via iptables and tunnel it over gRPC.",
6+
"entrypoint": "/usr/local/bin/bridge-entrypoint.sh",
7+
"options": {
8+
"bridgeServerAddr": {
9+
"type": "string",
10+
"description": "Address of the bridge proxy server (e.g. k8spf:///pod.ns:9090 or localhost:9090)",
11+
"default": ""
12+
},
13+
"appPort": {
14+
"type": "string",
15+
"description": "Local app port to forward inbound requests to",
16+
"default": "3000"
17+
},
18+
"bridgeVersion": {
19+
"type": "string",
20+
"description": "Version of bridge CLI to install (e.g., 'v1.0.0' or 'edge')",
21+
"default": "edge"
22+
},
23+
"workspacePath": {
24+
"type": "string",
25+
"description": "Container workspace folder path — pass ${containerWorkspaceFolder} from devcontainer.json",
26+
"default": ""
27+
},
28+
"forwardDomains": {
29+
"type": "string",
30+
"description": "Comma-separated list of domain patterns to intercept and resolve through the tunnel (e.g., '*.vercel.app,example.com')",
31+
"default": ""
32+
}
33+
},
34+
"capAdd": [
35+
"NET_ADMIN"
36+
],
37+
"installsAfter": [
38+
"ghcr.io/devcontainers/features/common-utils"
39+
]
40+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# GitHub repository for bridge releases
5+
REPO="vercel-eddie/bridge"
6+
7+
# Install required tools
8+
install_dependencies() {
9+
if ! command -v curl &> /dev/null; then
10+
if command -v apt-get &> /dev/null; then
11+
apt-get update && apt-get install -y curl ca-certificates
12+
elif command -v apk &> /dev/null; then
13+
apk add --no-cache curl ca-certificates
14+
elif command -v yum &> /dev/null; then
15+
yum install -y curl ca-certificates
16+
fi
17+
fi
18+
19+
# Install iptables if not present (needed for traffic interception)
20+
if ! command -v iptables &> /dev/null; then
21+
if command -v apt-get &> /dev/null; then
22+
apt-get update && apt-get install -y iptables
23+
elif command -v apk &> /dev/null; then
24+
apk add --no-cache iptables
25+
elif command -v yum &> /dev/null; then
26+
yum install -y iptables
27+
fi
28+
fi
29+
}
30+
31+
# Detect architecture
32+
get_arch() {
33+
local arch=$(uname -m)
34+
case "$arch" in
35+
x86_64) echo "amd64" ;;
36+
aarch64|arm64) echo "arm64" ;;
37+
*) echo "Unsupported architecture: $arch" >&2; exit 1 ;;
38+
esac
39+
}
40+
41+
# Get the latest release version from GitHub
42+
get_latest_version() {
43+
curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | \
44+
grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
45+
}
46+
47+
# Download and install bridge binary
48+
install_bridge() {
49+
if [ -x /usr/local/bin/bridge ]; then
50+
echo "Bridge binary already installed, skipping download"
51+
return 0
52+
fi
53+
54+
local version="${BRIDGEVERSION:-edge}"
55+
56+
# In dev mode the binary is expected to be provided via bind mount at runtime.
57+
if [ "$version" = "dev" ]; then
58+
echo "Dev mode: skipping binary download (expecting bind mount at runtime)"
59+
return 0
60+
fi
61+
62+
local arch=$(get_arch)
63+
local os="linux"
64+
65+
# Resolve 'latest' to actual version
66+
if [ "$version" = "latest" ]; then
67+
version=$(get_latest_version)
68+
if [ -z "$version" ]; then
69+
echo "Failed to fetch latest version" >&2
70+
exit 1
71+
fi
72+
fi
73+
74+
echo "Installing bridge ${version} for ${os}-${arch}..."
75+
76+
local binary_name="bridge-${os}-${arch}"
77+
local download_url="https://github.com/${REPO}/releases/download/${version}/${binary_name}"
78+
79+
echo "Downloading from: ${download_url}"
80+
81+
if ! curl -fsSL -o /usr/local/bin/bridge "${download_url}"; then
82+
echo "Failed to download bridge binary" >&2
83+
exit 1
84+
fi
85+
86+
chmod +x /usr/local/bin/bridge
87+
echo "Bridge ${version} installed successfully"
88+
}
89+
90+
# Write environment configuration to /etc/profile.d (sourced by entrypoint)
91+
write_env_config() {
92+
cat > /etc/profile.d/bridge.sh << EOF
93+
export BRIDGE_SERVER_ADDR="${BRIDGESERVERADDR:-}"
94+
export APP_PORT="${APPPORT:-3000}"
95+
export FORWARD_DOMAINS="${FORWARDDOMAINS:-$FORWARD_DOMAINS}"
96+
EOF
97+
}
98+
99+
# Create entrypoint script
100+
create_entrypoint() {
101+
# First part: bake the workspace path at install time (unquoted heredoc
102+
# so ${WORKSPACEPATH} is expanded now, not at runtime)
103+
cat > /usr/local/bin/bridge-entrypoint.sh << EOF
104+
#!/bin/bash
105+
# Baked at install time from feature option "workspacePath"
106+
BRIDGE_WORKSPACE_PATH="${WORKSPACEPATH:-}"
107+
EOF
108+
109+
# Second part: runtime logic (quoted heredoc — no expansion at install time)
110+
cat >> /usr/local/bin/bridge-entrypoint.sh << 'RUNTIME'
111+
112+
# Source profile in case env vars aren't inherited
113+
[ -f /etc/profile.d/bridge.sh ] && source /etc/profile.d/bridge.sh
114+
115+
# Run bridge intercept as root (required for iptables)
116+
if [ -n "$BRIDGE_SERVER_ADDR" ]; then
117+
sudo BRIDGE_SERVER_ADDR="$BRIDGE_SERVER_ADDR" \
118+
FORWARD_DOMAINS="$FORWARD_DOMAINS" \
119+
KUBECONFIG="${KUBECONFIG:-}" \
120+
/usr/local/bin/bridge intercept > /tmp/bridge-intercept.log 2>&1 &
121+
fi
122+
123+
exec "$@"
124+
RUNTIME
125+
chmod +x /usr/local/bin/bridge-entrypoint.sh
126+
}
127+
128+
# Main installation
129+
main() {
130+
echo "Installing Bridge Tunnel Client..."
131+
132+
install_dependencies
133+
install_bridge
134+
write_env_config
135+
create_entrypoint
136+
137+
echo "Bridge Tunnel Client installation complete"
138+
}
139+
140+
main

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,23 @@ make
2424
- [k3d](https://k3d.io/) (`brew install k3d`)
2525
- Go 1.25+
2626

27-
### 1. Create a k3d cluster
27+
### 1. Create a k3d cluster with a registry
2828

2929
```bash
30-
k3d cluster create bridge
30+
k3d cluster create bridge --registry-create bridge-registry:0.0.0.0:5111
3131
```
3232

33-
This creates a lightweight k3s cluster running in Docker. Your kubeconfig context is automatically switched to `k3d-bridge`.
33+
This creates a lightweight k3s cluster with a local container registry at `k3d-bridge-registry.localhost:5111`. Your kubeconfig context is automatically switched to `k3d-bridge`.
3434

3535
### 2. Seed the cluster
3636

37-
Build images, import them into k3d, and apply the Kubernetes manifests:
37+
Build images, push to the registry, and apply the Kubernetes manifests:
3838

3939
```bash
40-
go run deploy/main.go -cluster bridge
40+
go run deploy/main.go
4141
```
4242

43-
This deploys the bridge administrator (namespace `bridge`) and a test HTTP server (namespace `test-workloads`).
43+
This deploys the bridge administrator (namespace `bridge`) and a test HTTP server (namespace `test-workloads`). Re-run to pick up code changes — images are pushed and deployments are restarted automatically.
4444

4545
### 3. Build the CLI
4646

@@ -62,6 +62,20 @@ Create a bridge to the test server:
6262
./bridge create test-api-server -n test-workloads --connect
6363
```
6464

65+
To test local changes to the devcontainer feature (intercept, DNS, etc.), first set up hardlinks to the feature files:
66+
67+
```bash
68+
mkdir -p .devcontainer/local-features/bridge
69+
ln features/bridge/install.sh .devcontainer/local-features/bridge/install.sh
70+
ln features/bridge/devcontainer-feature.json .devcontainer/local-features/bridge/devcontainer-feature.json
71+
```
72+
73+
Then run with the local feature ref:
74+
75+
```bash
76+
./bridge create test-api-server -n test-workloads --feature-ref ../local-features/bridge -f .devcontainer/devcontainer.json --force --connect
77+
```
78+
6579
Or start just the administrator port-forward to verify connectivity:
6680

6781
```bash
@@ -71,7 +85,7 @@ kubectl port-forward -n bridge svc/administrator 9090:9090
7185
### Teardown
7286

7387
```bash
74-
k3d cluster delete bridge
88+
k3d cluster delete bridge --registry-delete k3d-bridge-registry
7589
```
7690

7791
## Architecture

api/go/bridge/v1/administrator.pb.go

Lines changed: 15 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/ts/bridge/v1/administrator_pb.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ export declare type CreateBridgeResponse = Message<"bridge.v1.CreateBridgeRespon
7979
* @generated from field: int32 port = 3;
8080
*/
8181
port: number;
82+
83+
/**
84+
* The name of the Deployment that owns the bridged pod.
85+
*
86+
* @generated from field: string deployment_name = 4 [json_name = "deployment_name"];
87+
*/
88+
deploymentName: string;
8289
};
8390

8491
/**

api/ts/bridge/v1/administrator_pb.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2
88
* Describes the file bridge/v1/administrator.proto.
99
*/
1010
export const file_bridge_v1_administrator = /*@__PURE__*/
11-
fileDesc("Ch1icmlkZ2UvdjEvYWRtaW5pc3RyYXRvci5wcm90bxIJYnJpZGdlLnYxIpwBChNDcmVhdGVCcmlkZ2VSZXF1ZXN0EhwKCWRldmljZV9pZBgBIAEoCVIJZGV2aWNlX2lkEiwKEXNvdXJjZV9kZXBsb3ltZW50GAIgASgJUhFzb3VyY2VfZGVwbG95bWVudBIqChBzb3VyY2VfbmFtZXNwYWNlGAMgASgJUhBzb3VyY2VfbmFtZXNwYWNlEg0KBWZvcmNlGAQgASgIIlMKFENyZWF0ZUJyaWRnZVJlc3BvbnNlEhEKCW5hbWVzcGFjZRgBIAEoCRIaCghwb2RfbmFtZRgCIAEoCVIIcG9kX25hbWUSDAoEcG9ydBgDIAEoBSIyChJMaXN0QnJpZGdlc1JlcXVlc3QSHAoJZGV2aWNlX2lkGAEgASgJUglkZXZpY2VfaWQixwEKCkJyaWRnZUluZm8SHAoJZGV2aWNlX2lkGAEgASgJUglkZXZpY2VfaWQSLAoRc291cmNlX2RlcGxveW1lbnQYAiABKAlSEXNvdXJjZV9kZXBsb3ltZW50EioKEHNvdXJjZV9uYW1lc3BhY2UYAyABKAlSEHNvdXJjZV9uYW1lc3BhY2USEQoJbmFtZXNwYWNlGAQgASgJEh4KCmNyZWF0ZWRfYXQYBSABKAlSCmNyZWF0ZWRfYXQSDgoGc3RhdHVzGAYgASgJIj0KE0xpc3RCcmlkZ2VzUmVzcG9uc2USJgoHYnJpZGdlcxgBIAMoCzIVLmJyaWRnZS52MS5CcmlkZ2VJbmZvImEKE0RlbGV0ZUJyaWRnZVJlcXVlc3QSHAoJZGV2aWNlX2lkGAEgASgJUglkZXZpY2VfaWQSLAoRc291cmNlX2RlcGxveW1lbnQYAiABKAlSEXNvdXJjZV9kZXBsb3ltZW50IhYKFERlbGV0ZUJyaWRnZVJlc3BvbnNlMoYCChRBZG1pbmlzdHJhdG9yU2VydmljZRJPCgxDcmVhdGVCcmlkZ2USHi5icmlkZ2UudjEuQ3JlYXRlQnJpZGdlUmVxdWVzdBofLmJyaWRnZS52MS5DcmVhdGVCcmlkZ2VSZXNwb25zZRJMCgtMaXN0QnJpZGdlcxIdLmJyaWRnZS52MS5MaXN0QnJpZGdlc1JlcXVlc3QaHi5icmlkZ2UudjEuTGlzdEJyaWRnZXNSZXNwb25zZRJPCgxEZWxldGVCcmlkZ2USHi5icmlkZ2UudjEuRGVsZXRlQnJpZGdlUmVxdWVzdBofLmJyaWRnZS52MS5EZWxldGVCcmlkZ2VSZXNwb25zZUKiAQoNY29tLmJyaWRnZS52MUISQWRtaW5pc3RyYXRvclByb3RvUAFaOGdpdGh1Yi5jb20vdmVyY2VsLWVkZGllL2JyaWRnZS9hcGkvZ28vYnJpZGdlL3YxO2JyaWRnZXYxogIDQlhYqgIJQnJpZGdlLlYxygIJQnJpZGdlXFYx4gIVQnJpZGdlXFYxXEdQQk1ldGFkYXRh6gIKQnJpZGdlOjpWMWIGcHJvdG8z");
11+
fileDesc("Ch1icmlkZ2UvdjEvYWRtaW5pc3RyYXRvci5wcm90bxIJYnJpZGdlLnYxIpwBChNDcmVhdGVCcmlkZ2VSZXF1ZXN0EhwKCWRldmljZV9pZBgBIAEoCVIJZGV2aWNlX2lkEiwKEXNvdXJjZV9kZXBsb3ltZW50GAIgASgJUhFzb3VyY2VfZGVwbG95bWVudBIqChBzb3VyY2VfbmFtZXNwYWNlGAMgASgJUhBzb3VyY2VfbmFtZXNwYWNlEg0KBWZvcmNlGAQgASgIIn0KFENyZWF0ZUJyaWRnZVJlc3BvbnNlEhEKCW5hbWVzcGFjZRgBIAEoCRIaCghwb2RfbmFtZRgCIAEoCVIIcG9kX25hbWUSDAoEcG9ydBgDIAEoBRIoCg9kZXBsb3ltZW50X25hbWUYBCABKAlSD2RlcGxveW1lbnRfbmFtZSIyChJMaXN0QnJpZGdlc1JlcXVlc3QSHAoJZGV2aWNlX2lkGAEgASgJUglkZXZpY2VfaWQixwEKCkJyaWRnZUluZm8SHAoJZGV2aWNlX2lkGAEgASgJUglkZXZpY2VfaWQSLAoRc291cmNlX2RlcGxveW1lbnQYAiABKAlSEXNvdXJjZV9kZXBsb3ltZW50EioKEHNvdXJjZV9uYW1lc3BhY2UYAyABKAlSEHNvdXJjZV9uYW1lc3BhY2USEQoJbmFtZXNwYWNlGAQgASgJEh4KCmNyZWF0ZWRfYXQYBSABKAlSCmNyZWF0ZWRfYXQSDgoGc3RhdHVzGAYgASgJIj0KE0xpc3RCcmlkZ2VzUmVzcG9uc2USJgoHYnJpZGdlcxgBIAMoCzIVLmJyaWRnZS52MS5CcmlkZ2VJbmZvImEKE0RlbGV0ZUJyaWRnZVJlcXVlc3QSHAoJZGV2aWNlX2lkGAEgASgJUglkZXZpY2VfaWQSLAoRc291cmNlX2RlcGxveW1lbnQYAiABKAlSEXNvdXJjZV9kZXBsb3ltZW50IhYKFERlbGV0ZUJyaWRnZVJlc3BvbnNlMoYCChRBZG1pbmlzdHJhdG9yU2VydmljZRJPCgxDcmVhdGVCcmlkZ2USHi5icmlkZ2UudjEuQ3JlYXRlQnJpZGdlUmVxdWVzdBofLmJyaWRnZS52MS5DcmVhdGVCcmlkZ2VSZXNwb25zZRJMCgtMaXN0QnJpZGdlcxIdLmJyaWRnZS52MS5MaXN0QnJpZGdlc1JlcXVlc3QaHi5icmlkZ2UudjEuTGlzdEJyaWRnZXNSZXNwb25zZRJPCgxEZWxldGVCcmlkZ2USHi5icmlkZ2UudjEuRGVsZXRlQnJpZGdlUmVxdWVzdBofLmJyaWRnZS52MS5EZWxldGVCcmlkZ2VSZXNwb25zZUKiAQoNY29tLmJyaWRnZS52MUISQWRtaW5pc3RyYXRvclByb3RvUAFaOGdpdGh1Yi5jb20vdmVyY2VsLWVkZGllL2JyaWRnZS9hcGkvZ28vYnJpZGdlL3YxO2JyaWRnZXYxogIDQlhYqgIJQnJpZGdlLlYxygIJQnJpZGdlXFYx4gIVQnJpZGdlXFYxXEdQQk1ldGFkYXRh6gIKQnJpZGdlOjpWMWIGcHJvdG8z");
1212

1313
/**
1414
* Describes the message bridge.v1.CreateBridgeRequest.

deploy/k8s/administrator.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ spec:
4343
containers:
4444
- name: administrator
4545
image: "{{ADMINISTRATOR_IMAGE}}"
46+
imagePullPolicy: Always
4647
command: ["bridge", "administrator"]
4748
env:
4849
- name: POD_NAMESPACE

0 commit comments

Comments
 (0)