Skip to content

Commit 999017d

Browse files
pantherman594github-actions
andauthored
Create pgweb app (#329)
* Add pgweb app. * Add bookmarks for workspace database instances. * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * doc update * fixup * Clean up shellcheck disables * nounset --------- Co-authored-by: github-actions <github-actions@github.com>
1 parent 74cd654 commit 999017d

6 files changed

Lines changed: 384 additions & 0 deletions

File tree

src/pgweb/.devcontainer.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "pgweb",
3+
"dockerComposeFile": "docker-compose.yaml",
4+
"service": "app",
5+
"shutdownAction": "none",
6+
"workspaceFolder": "/workspace",
7+
"postCreateCommand": [
8+
"./startupscript/post-startup.sh",
9+
"root",
10+
"/root",
11+
"${templateOption:cloud}",
12+
"${templateOption:login}"
13+
],
14+
"postStartCommand": "/workspace/start-bookmark-refresh.sh",
15+
"features": {
16+
"ghcr.io/devcontainers/features/common-utils:2": {
17+
"installZsh": false,
18+
"installOhMyZsh": false,
19+
"upgradePackages": false
20+
},
21+
"ghcr.io/devcontainers/features/java:1": {
22+
"version": "17"
23+
},
24+
"ghcr.io/devcontainers/features/aws-cli:1": {}
25+
},
26+
"remoteUser": "root"
27+
}

src/pgweb/README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# pgweb
2+
3+
Custom Workbench application for querying PostgreSQL databases using pgweb - a lightweight, web-based database browser.
4+
5+
## Configuration
6+
7+
- **Image**: sosedoff/pgweb
8+
- **Port**: 8081
9+
- **User**: root
10+
- **Home Directory**: /root
11+
- **Sessions Mode**: Enabled (allows interactive login via web UI)
12+
13+
## Access
14+
15+
Once deployed in Workbench, access the pgweb UI at the app URL (port 8081).
16+
17+
## Automatic Database Discovery
18+
19+
The app automatically discovers all Aurora databases in your Workbench workspace and creates pre-configured connection bookmarks with fresh IAM authentication tokens.
20+
21+
### How It Works
22+
23+
1. **Auto-Discovery**: Every 10 minutes, the app queries `wb resource list` to find all Aurora databases
24+
2. **Access-Based Credentials**: For each database, attempts to get credentials based on your workspace permissions:
25+
- **Read-Only**: Always attempted first - if successful, creates a read-only bookmark
26+
- **Write-Read**: Only attempted if you have write access - creates a write-read bookmark if successful
27+
3. **IAM Token Generation**: Generates fresh IAM authentication tokens for each access level you have
28+
4. **Bookmark Creation**: Creates pgweb bookmarks only for the access levels you're granted
29+
5. **Always Fresh**: Tokens refresh every 10 minutes (they expire after 15), so connections never expire
30+
31+
**Note**: You'll only see bookmarks for databases you have access to. If you only have read-only access to a database, you'll only see the read-only bookmark. If a database is removed from the workspace or your access is revoked, its bookmarks will disappear on the next refresh.
32+
33+
### Using Bookmarks
34+
35+
When you open pgweb, you'll see bookmarks for databases you have access to. Examples:
36+
37+
- `aurora-demo-db-20260115 (Read-Only)` - Read-only connection
38+
- `aurora-demo-db-20260115 (Write-Read)` - Read-write connection (only if you have write access)
39+
- `dc-database (Read-Only)` - Read-only connection to referenced database
40+
- `dc-database (Write-Read)` - Read-write connection (only if you have write access)
41+
42+
Click any bookmark to connect instantly - no need to enter credentials!
43+
44+
### Manual Connections
45+
46+
You can also use the interactive login form to enter connection details manually:
47+
48+
- **Host**: Your Aurora cluster endpoint
49+
- **Port**: `5432`
50+
- **Username**: Your database username
51+
- **Password**: Your database password (works with IAM temporary passwords)
52+
- **Database**: Your database name
53+
- **SSL Mode**: `require`
54+
55+
## Aurora PostgreSQL with IAM Authentication
56+
57+
This app is optimized for Aurora PostgreSQL with IAM authentication. The automatic bookmark system handles token refresh transparently, and manual connections support entering temporary IAM passwords directly without URL encoding issues.
58+
59+
## Local Testing
60+
61+
For local testing of the bookmark refresh script:
62+
63+
```bash
64+
# Test with custom paths (useful for local development)
65+
WB_EXE="$(which wb)" PGWEB_BASE=/tmp/pgweb ./src/pgweb/refresh-bookmarks.sh
66+
```
67+
68+
Environment variables:
69+
70+
- `WB_EXE` - Path to wb executable (default: `/usr/bin/wb`)
71+
- `PGWEB_BASE` - Base directory for pgweb config (default: `/root/.pgweb`)
72+
73+
For full devcontainer testing:
74+
75+
1. Create Docker network: `docker network create app-network`
76+
2. Run the app: `devcontainer up --workspace-folder .`
77+
3. Access at: `http://localhost:8081`
78+
79+
## Customization
80+
81+
Edit the following files to customize your app:
82+
83+
- `.devcontainer.json` - Devcontainer configuration and features
84+
- `docker-compose.yaml` - Docker Compose configuration (change the `command` to customize pgweb options)
85+
- `devcontainer-template.json` - Template options and metadata
86+
87+
## Testing
88+
89+
To test this app template:
90+
91+
```bash
92+
cd test
93+
./test.sh pgweb
94+
```
95+
96+
## Usage
97+
98+
1. Fork the repository
99+
2. Modify the configuration files as needed
100+
3. In Workbench UI, create a custom app pointing to your forked repository
101+
4. Select this app template (pgweb)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"id": "pgweb",
3+
"version": "1.0.0",
4+
"name": "pgweb",
5+
"description": "Web-based PostgreSQL database browser for querying Aurora and other PostgreSQL databases",
6+
"options": {
7+
"cloud": {
8+
"type": "string",
9+
"enum": ["gcp", "aws"],
10+
"default": "gcp",
11+
"description": "Cloud provider (gcp or aws)"
12+
},
13+
"login": {
14+
"type": "string",
15+
"description": "Whether to log in to workbench CLI",
16+
"proposals": ["true", "false"],
17+
"default": "false"
18+
}
19+
}
20+
}

src/pgweb/docker-compose.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
services:
2+
app:
3+
# The container name must be "application-server"
4+
container_name: "application-server"
5+
# This can be either a pre-existing image or built from a Dockerfile
6+
image: "sosedoff/pgweb"
7+
# build:
8+
# context: .
9+
# Override the default entrypoint to use our custom script
10+
entrypoint: []
11+
command: ["pgweb", "--sessions", "--bind=0.0.0.0", "--listen=8081", "--bookmarks-dir=/root/.pgweb/bookmarks"]
12+
user: "root"
13+
restart: always
14+
volumes:
15+
- .:/workspace:cached
16+
- work:/root/work
17+
# The port specified here will be forwarded and accessible from the
18+
# Workbench UI.
19+
ports:
20+
- 8081:8081
21+
# The service must be connected to the "app-network" Docker network
22+
networks:
23+
- app-network
24+
# SYS_ADMIN and fuse are required to mount workspace resources into the
25+
# container.
26+
cap_add:
27+
- SYS_ADMIN
28+
devices:
29+
- /dev/fuse
30+
security_opt:
31+
- apparmor:unconfined
32+
33+
volumes:
34+
work:
35+
36+
networks:
37+
# The Docker network must be named "app-network". This is an external network
38+
# that is created outside of this docker-compose file.
39+
app-network:
40+
external: true

src/pgweb/refresh-bookmarks.sh

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/bin/bash
2+
set -o errexit
3+
set -o pipefail
4+
set -o nounset
5+
6+
# Allow overriding via environment for local testing
7+
readonly WB_EXE="${WB_EXE:-/usr/bin/wb}"
8+
readonly PGWEB_BASE="${PGWEB_BASE:-/root/.pgweb}"
9+
readonly BOOKMARK_DIR="${PGWEB_BASE}/bookmarks"
10+
11+
# Create base directory if it doesn't exist
12+
mkdir -p "${PGWEB_BASE}"
13+
14+
# Helper function to get credentials and generate IAM auth token
15+
generate_iam_token() {
16+
local resource_id="${1}"
17+
local scope="${2}"
18+
local endpoint="${3}"
19+
local port="${4}"
20+
local username="${5}"
21+
local region="${6}"
22+
23+
# Get credentials from Workbench
24+
local wb_creds
25+
wb_creds=$(${WB_EXE} resource credentials --id "${resource_id}" --scope "${scope}" --format json 2>/dev/null) || return 1
26+
readonly wb_creds
27+
28+
# Extract AWS credentials
29+
local access_key secret_key session_token
30+
access_key=$(echo "${wb_creds}" | jq -r '.AccessKeyId')
31+
secret_key=$(echo "${wb_creds}" | jq -r '.SecretAccessKey')
32+
session_token=$(echo "${wb_creds}" | jq -r '.SessionToken')
33+
readonly access_key secret_key session_token
34+
35+
# Generate IAM token
36+
AWS_ACCESS_KEY_ID="${access_key}" \
37+
AWS_SECRET_ACCESS_KEY="${secret_key}" \
38+
AWS_SESSION_TOKEN="${session_token}" \
39+
aws rds generate-db-auth-token \
40+
--hostname "${endpoint}" \
41+
--port "${port}" \
42+
--username "${username}" \
43+
--region "${region}"
44+
}
45+
46+
# Helper function to create bookmark TOML file
47+
create_bookmark() {
48+
local output_file="${1}"
49+
local endpoint="${2}"
50+
local port="${3}"
51+
local username="${4}"
52+
local password="${5}"
53+
local database="${6}"
54+
55+
cat > "${output_file}" <<EOF
56+
host = "${endpoint}"
57+
port = ${port}
58+
user = "${username}"
59+
password = "${password}"
60+
database = "${database}"
61+
sslmode = "require"
62+
EOF
63+
}
64+
65+
refresh_bookmarks() {
66+
echo "$(date): Refreshing pgweb bookmarks from Workbench resources..."
67+
68+
# Create temporary directory for new bookmarks (using PID for uniqueness)
69+
local TEMP_DIR="${PGWEB_BASE}/bookmarks.tmp.$$"
70+
readonly TEMP_DIR
71+
rm -rf "${TEMP_DIR}"
72+
mkdir -p "${TEMP_DIR}"
73+
74+
# Get list of Aurora databases from Workbench
75+
local RESOURCES
76+
RESOURCES=$(${WB_EXE} resource list --format json)
77+
readonly RESOURCES
78+
79+
# Process each resource
80+
echo "${RESOURCES}" | jq -c '.[]' | while read -r resource; do
81+
local RESOURCE_TYPE
82+
RESOURCE_TYPE=$(echo "${resource}" | jq -r '.resourceType')
83+
84+
# Skip non-Aurora resources
85+
if [[ ! "${RESOURCE_TYPE}" =~ AURORA_DATABASE ]]; then
86+
continue
87+
fi
88+
89+
local RESOURCE_ID
90+
RESOURCE_ID=$(echo "${resource}" | jq -r '.id')
91+
echo " Processing: ${RESOURCE_ID} (type: ${RESOURCE_TYPE})"
92+
93+
# Extract database details from top level (controlled) or referencedResource (reference)
94+
local DB_DATA
95+
if [[ "${RESOURCE_TYPE}" == "AWS_AURORA_DATABASE" ]]; then
96+
DB_DATA="${resource}"
97+
else
98+
DB_DATA=$(echo "${resource}" | jq -r '.referencedResource')
99+
fi
100+
101+
# Extract database connection info
102+
local DB_NAME RO_ENDPOINT RO_USER RW_ENDPOINT RW_USER PORT REGION
103+
DB_NAME=$(echo "${DB_DATA}" | jq -r '.databaseName')
104+
RO_ENDPOINT=$(echo "${DB_DATA}" | jq -r '.roEndpoint')
105+
RO_USER=$(echo "${DB_DATA}" | jq -r '.roUser')
106+
RW_ENDPOINT=$(echo "${DB_DATA}" | jq -r '.rwEndpoint')
107+
RW_USER=$(echo "${DB_DATA}" | jq -r '.rwUser')
108+
PORT=$(echo "${DB_DATA}" | jq -r '.port')
109+
REGION=$(echo "${DB_DATA}" | jq -r '.region // "us-east-1"')
110+
111+
# Validate all required fields are present
112+
if [[ -z "${DB_NAME}" || "${DB_NAME}" == "null" ]] || \
113+
[[ -z "${RO_ENDPOINT}" || "${RO_ENDPOINT}" == "null" ]] || \
114+
[[ -z "${RO_USER}" || "${RO_USER}" == "null" ]] || \
115+
[[ -z "${RW_ENDPOINT}" || "${RW_ENDPOINT}" == "null" ]] || \
116+
[[ -z "${RW_USER}" || "${RW_USER}" == "null" ]] || \
117+
[[ -z "${PORT}" || "${PORT}" == "null" ]]; then
118+
echo " Missing required database fields, skipping"
119+
continue
120+
fi
121+
122+
# Try to create READ_ONLY bookmark
123+
echo " Checking read access..."
124+
local RO_TOKEN
125+
if RO_TOKEN=$(generate_iam_token "${RESOURCE_ID}" "READ_ONLY" "${RO_ENDPOINT}" "${PORT}" "${RO_USER}" "${REGION}"); then
126+
echo " Read access confirmed"
127+
echo " Creating read-only bookmark..."
128+
create_bookmark "${TEMP_DIR}/${RESOURCE_ID} (Read-Only).toml" "${RO_ENDPOINT}" "${PORT}" "${RO_USER}" "${RO_TOKEN}" "${DB_NAME}"
129+
echo " Created bookmark: ${RESOURCE_ID} (Read-Only)"
130+
else
131+
echo " No read access to ${RESOURCE_ID}, skipping"
132+
continue
133+
fi
134+
135+
# Try to create WRITE_READ bookmark
136+
echo " Checking write access..."
137+
local RW_TOKEN
138+
if RW_TOKEN=$(generate_iam_token "${RESOURCE_ID}" "WRITE_READ" "${RW_ENDPOINT}" "${PORT}" "${RW_USER}" "${REGION}"); then
139+
echo " Write access confirmed"
140+
echo " Creating write-read bookmark..."
141+
create_bookmark "${TEMP_DIR}/${RESOURCE_ID} (Write-Read).toml" "${RW_ENDPOINT}" "${PORT}" "${RW_USER}" "${RW_TOKEN}" "${DB_NAME}"
142+
echo " Created bookmark: ${RESOURCE_ID} (Write-Read)"
143+
else
144+
echo " No write access, skipping write-read bookmark"
145+
fi
146+
done
147+
148+
# Count bookmarks - must use find since the while loop runs in a subshell (due to pipe),
149+
# so a counter variable incremented in the loop would not be visible here
150+
local BOOKMARK_COUNT
151+
BOOKMARK_COUNT=$(find "${TEMP_DIR}" -name "*.toml" -type f 2>/dev/null | wc -l)
152+
readonly BOOKMARK_COUNT
153+
echo "$(date): Refresh complete. Created ${BOOKMARK_COUNT} bookmarks."
154+
155+
# Atomically update symlink to point to new bookmark directory
156+
ln -sfn "$(basename "${TEMP_DIR}")" "${BOOKMARK_DIR}"
157+
158+
# Cleanup old bookmark directories (all except current)
159+
find "${PGWEB_BASE}" -maxdepth 1 -type d -name "bookmarks.tmp.*" ! -name "bookmarks.tmp.$$" -exec rm -rf {} \;
160+
}
161+
162+
# Run single refresh
163+
if ! refresh_bookmarks; then
164+
echo "$(date): ERROR: Bookmark refresh failed"
165+
exit 1
166+
fi
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
set -o errexit
3+
set -o pipefail
4+
set -o nounset
5+
6+
echo "Starting bookmark refresh for pgweb..."
7+
8+
# Create base directory (but not bookmarks subdirectory - that will be a symlink)
9+
mkdir -p /root/.pgweb
10+
11+
# Make sure refresh script is executable
12+
chmod +x /workspace/refresh-bookmarks.sh
13+
14+
# Run initial refresh (blocking) to populate bookmarks before app is marked ready
15+
echo "Running initial bookmark refresh..."
16+
/workspace/refresh-bookmarks.sh
17+
18+
# Start background loop for continuous refresh (detached from parent)
19+
echo "Starting background bookmark refresh service (every 10 minutes)..."
20+
# Single quotes intentional: $(date) should expand at runtime, not now
21+
# shellcheck disable=SC2016
22+
nohup bash -c '
23+
while true; do
24+
sleep 600 # 10 minutes
25+
/workspace/refresh-bookmarks.sh || echo "$(date): WARNING: Bookmark refresh failed"
26+
done
27+
' >> /root/.pgweb/refresh.log 2>&1 &
28+
29+
REFRESH_PID=$!
30+
echo "Bookmark refresh service configured (background PID: ${REFRESH_PID})"

0 commit comments

Comments
 (0)