From 65ddac991e6244506be614dc78a743eab214ed34 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 2 Feb 2026 18:12:12 -0500 Subject: [PATCH 01/17] Add pgweb app. --- src/pgweb/.devcontainer.json | 29 ++++++++++++++ src/pgweb/README.md | 56 ++++++++++++++++++++++++++++ src/pgweb/devcontainer-template.json | 20 ++++++++++ src/pgweb/docker-compose.yaml | 37 ++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 src/pgweb/.devcontainer.json create mode 100644 src/pgweb/README.md create mode 100644 src/pgweb/devcontainer-template.json create mode 100644 src/pgweb/docker-compose.yaml diff --git a/src/pgweb/.devcontainer.json b/src/pgweb/.devcontainer.json new file mode 100644 index 00000000..d741327c --- /dev/null +++ b/src/pgweb/.devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "pgweb", + "dockerComposeFile": "docker-compose.yaml", + "service": "app", + "shutdownAction": "none", + "workspaceFolder": "/workspace", + "postCreateCommand": [ + "./startupscript/post-startup.sh", + "root", + "/root", + "${templateOption:cloud}", + "${templateOption:login}" + ], + "postStartCommand": [ + "./startupscript/remount-on-restart.sh", + "root", + "/root", + "${templateOption:cloud}", + "${templateOption:login}" + ], + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "17" + }, + "ghcr.io/devcontainers/features/aws-cli:1": {}, + "ghcr.io/dhoeric/features/google-cloud-cli:1": {} + }, + "remoteUser": "root" +} diff --git a/src/pgweb/README.md b/src/pgweb/README.md new file mode 100644 index 00000000..1ce60379 --- /dev/null +++ b/src/pgweb/README.md @@ -0,0 +1,56 @@ +# pgweb + +Custom Workbench application for querying PostgreSQL databases using pgweb - a lightweight, web-based database browser. + +## Configuration + +- **Image**: sosedoff/pgweb +- **Port**: 8081 +- **User**: root +- **Home Directory**: /root +- **Sessions Mode**: Enabled (allows interactive login via web UI) + +## Access + +Once deployed in Workbench, access the pgweb UI at the app URL (port 8081). + +You'll see an interactive login form where you can enter your database connection details: +- **Host**: Your Aurora cluster endpoint (e.g., `mycluster.cluster-xxx.us-east-1.rds.amazonaws.com`) +- **Port**: `5432` (default PostgreSQL port) +- **Username**: Your database username +- **Password**: Your database password (works with IAM temporary passwords) +- **Database**: Your database name +- **SSL Mode**: `require` (recommended for Aurora) + +## Aurora PostgreSQL with IAM Authentication + +This app works well with Aurora PostgreSQL IAM authentication. The sessions mode allows you to enter temporary IAM passwords directly in the web form, avoiding URL encoding issues. + +For local testing: +1. Create Docker network: `docker network create app-network` +2. Run the app: `devcontainer up --workspace-folder .` +3. Access at: `http://localhost:8081` + +## Customization + +Edit the following files to customize your app: + +- `.devcontainer.json` - Devcontainer configuration and features +- `docker-compose.yaml` - Docker Compose configuration (change the `command` to customize pgweb options) +- `devcontainer-template.json` - Template options and metadata + +## Testing + +To test this app template: + +```bash +cd test +./test.sh pgweb +``` + +## Usage + +1. Fork the repository +2. Modify the configuration files as needed +3. In Workbench UI, create a custom app pointing to your forked repository +4. Select this app template (pgweb) diff --git a/src/pgweb/devcontainer-template.json b/src/pgweb/devcontainer-template.json new file mode 100644 index 00000000..9b02a2ce --- /dev/null +++ b/src/pgweb/devcontainer-template.json @@ -0,0 +1,20 @@ +{ + "id": "pgweb", + "version": "1.0.0", + "name": "pgweb", + "description": "Web-based PostgreSQL database browser for querying Aurora and other PostgreSQL databases", + "options": { + "cloud": { + "type": "string", + "enum": ["gcp", "aws"], + "default": "gcp", + "description": "Cloud provider (gcp or aws)" + }, + "login": { + "type": "string", + "description": "Whether to log in to workbench CLI", + "proposals": ["true", "false"], + "default": "false" + } + } +} diff --git a/src/pgweb/docker-compose.yaml b/src/pgweb/docker-compose.yaml new file mode 100644 index 00000000..bd91b9e3 --- /dev/null +++ b/src/pgweb/docker-compose.yaml @@ -0,0 +1,37 @@ +services: + app: + # The container name must be "application-server" + container_name: "application-server" + # This can be either a pre-existing image or built from a Dockerfile + image: "sosedoff/pgweb" + # build: + # context: . + command: ["pgweb", "--sessions", "--bind=0.0.0.0", "--listen=8081"] + restart: always + volumes: + - .:/workspace:cached + - work:/root/work + # The port specified here will be forwarded and accessible from the + # Workbench UI. + ports: + - 8081:8081 + # The service must be connected to the "app-network" Docker network + networks: + - app-network + # SYS_ADMIN and fuse are required to mount workspace resources into the + # container. + cap_add: + - SYS_ADMIN + devices: + - /dev/fuse + security_opt: + - apparmor:unconfined + +volumes: + work: + +networks: + # The Docker network must be named "app-network". This is an external network + # that is created outside of this docker-compose file. + app-network: + external: true From e4e87608282f1820cdd33f79e4e547f261760b29 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Feb 2026 13:59:01 -0500 Subject: [PATCH 02/17] Add bookmarks for workspace database instances. --- src/pgweb/README.md | 33 ++++- src/pgweb/docker-compose.yaml | 4 +- src/pgweb/refresh-bookmarks.sh | 232 +++++++++++++++++++++++++++++++++ src/pgweb/start-pgweb.sh | 22 ++++ 4 files changed, 285 insertions(+), 6 deletions(-) create mode 100755 src/pgweb/refresh-bookmarks.sh create mode 100755 src/pgweb/start-pgweb.sh diff --git a/src/pgweb/README.md b/src/pgweb/README.md index 1ce60379..869559ef 100644 --- a/src/pgweb/README.md +++ b/src/pgweb/README.md @@ -14,17 +14,40 @@ Custom Workbench application for querying PostgreSQL databases using pgweb - a l Once deployed in Workbench, access the pgweb UI at the app URL (port 8081). -You'll see an interactive login form where you can enter your database connection details: -- **Host**: Your Aurora cluster endpoint (e.g., `mycluster.cluster-xxx.us-east-1.rds.amazonaws.com`) -- **Port**: `5432` (default PostgreSQL port) +## Automatic Database Discovery + +The app automatically discovers all Aurora databases in your Workbench workspace and creates pre-configured connection bookmarks with fresh IAM authentication tokens. + +### How It Works + +1. **Auto-Discovery**: Every 10 minutes, the app queries `wb resource list` to find all Aurora databases +2. **IAM Token Generation**: For each database, generates fresh IAM authentication tokens for both read-write (RW) and read-only (RO) users +3. **Bookmark Creation**: Creates pgweb bookmarks for each database connection +4. **Always Fresh**: Tokens refresh every 10 minutes (they expire after 15), so connections never expire + +### Using Bookmarks + +When you open pgweb, you'll see bookmarks for each database in your workspace: +- `aurora-demo-db-20260115 (Write-Read)` - Read-write connection +- `aurora-demo-db-20260115 (Read-Only)` - Read-only connection +- `dc-database (Write-Read)` - Read-write connection (referenced database) +- `dc-database (Read-Only)` - Read-only connection (referenced database) + +Click any bookmark to connect instantly - no need to enter credentials! + +### Manual Connections + +You can also use the interactive login form to enter connection details manually: +- **Host**: Your Aurora cluster endpoint +- **Port**: `5432` - **Username**: Your database username - **Password**: Your database password (works with IAM temporary passwords) - **Database**: Your database name -- **SSL Mode**: `require` (recommended for Aurora) +- **SSL Mode**: `require` ## Aurora PostgreSQL with IAM Authentication -This app works well with Aurora PostgreSQL IAM authentication. The sessions mode allows you to enter temporary IAM passwords directly in the web form, avoiding URL encoding issues. +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. For local testing: 1. Create Docker network: `docker network create app-network` diff --git a/src/pgweb/docker-compose.yaml b/src/pgweb/docker-compose.yaml index bd91b9e3..8e8f6835 100644 --- a/src/pgweb/docker-compose.yaml +++ b/src/pgweb/docker-compose.yaml @@ -6,7 +6,9 @@ services: image: "sosedoff/pgweb" # build: # context: . - command: ["pgweb", "--sessions", "--bind=0.0.0.0", "--listen=8081"] + # Override the default entrypoint to use our custom script + entrypoint: [] + command: ["/bin/bash", "/workspace/start-pgweb.sh"] restart: always volumes: - .:/workspace:cached diff --git a/src/pgweb/refresh-bookmarks.sh b/src/pgweb/refresh-bookmarks.sh new file mode 100755 index 00000000..adbc119f --- /dev/null +++ b/src/pgweb/refresh-bookmarks.sh @@ -0,0 +1,232 @@ +#!/bin/bash +set -e + +BOOKMARK_DIR="/home/pgweb/.pgweb/bookmarks" +REFRESH_INTERVAL=600 # 10 minutes (IAM tokens last 15 min) + +# Create bookmark directory if it doesn't exist +mkdir -p "$BOOKMARK_DIR" + +# Helper function to find workspace ID from UUID +get_workspace_id_from_uuid() { + local uuid="$1" + echo "$ALL_WORKSPACES" | jq -r --arg uuid "$uuid" '.[] | select(.uuid == $uuid) | .id' +} + +# Recursively follow references using embedded referencedResource data +find_controlled_resource() { + local resource_json="$1" + local source_workspace_uuid="$2" + + local resource_type=$(echo "$resource_json" | jq -r '.resourceType') + + # If it's a controlled resource, return it and the workspace ID + if [[ "$resource_type" == "AWS_AURORA_DATABASE" ]]; then + # If we have a source workspace UUID, look up its ID + if [[ -n "$source_workspace_uuid" && "$source_workspace_uuid" != "null" ]]; then + local workspace_id=$(get_workspace_id_from_uuid "$source_workspace_uuid") + if [[ -z "$workspace_id" ]]; then + echo "ERROR: Could not find workspace ID for UUID: $source_workspace_uuid" >&2 + return 1 + fi + echo "$workspace_id|$resource_json" + else + # Use current workspace + echo "$CURRENT_WORKSPACE|$resource_json" + fi + return + fi + + # If it's a reference, use the embedded referencedResource data + if [[ "$resource_type" == "AWS_AURORA_DATABASE_REFERENCE" ]]; then + local referenced_resource=$(echo "$resource_json" | jq -r '.referencedResource') + + if [[ -z "$referenced_resource" || "$referenced_resource" == "null" ]]; then + echo "ERROR: Reference has no embedded referencedResource data" >&2 + return 1 + fi + + # Get the source workspace UUID - this is where the controlled resource lives + local next_workspace_uuid=$(echo "$resource_json" | jq -r '.sourceWorkspaceId') + + # Recursively check the referenced resource, passing the workspace UUID + find_controlled_resource "$referenced_resource" "$next_workspace_uuid" + return + fi + + # Unknown type + echo "ERROR: Unknown resource type: $resource_type" >&2 + return 1 +} + +refresh_bookmarks() { + echo "$(date): Refreshing pgweb bookmarks from Workbench resources..." + + # Get current workspace ID + CURRENT_WORKSPACE=$(wb workspace describe --format json | jq -r '.id') + + # Get list of all accessible workspaces for UUID->ID lookup + ALL_WORKSPACES=$(wb workspace list --format json) + + # Get list of Aurora databases from Workbench + RESOURCES=$(wb resource list --format json) + + # Clear old bookmarks + rm -f "$BOOKMARK_DIR"/*.toml + + # Process each resource + echo "$RESOURCES" | jq -c '.[]' | while read -r resource; do + RESOURCE_TYPE=$(echo "$resource" | jq -r '.resourceType') + + # Skip non-Aurora resources + if [[ ! "$RESOURCE_TYPE" =~ AURORA_DATABASE ]]; then + continue + fi + + RESOURCE_ID=$(echo "$resource" | jq -r '.id') + echo " Processing: $RESOURCE_ID (type: $RESOURCE_TYPE)" + + # Find the controlled resource by following reference chain + if [[ "$RESOURCE_TYPE" == "AWS_AURORA_DATABASE_REFERENCE" ]]; then + echo " Following reference chain..." + + # Use embedded referencedResource data + CONTROLLED_INFO=$(find_controlled_resource "$resource" "") + CONTROLLED_WORKSPACE=$(echo "$CONTROLLED_INFO" | cut -d'|' -f1) + DB_DATA=$(echo "$CONTROLLED_INFO" | cut -d'|' -f2-) + else + # Already a controlled resource + CONTROLLED_WORKSPACE="$CURRENT_WORKSPACE" + DB_DATA="$resource" + fi + + echo " Controlled resource workspace: $CONTROLLED_WORKSPACE" + + # For permissions, use credentials from the resource in the CURRENT workspace + # Don't try to access the controlled workspace if it's a reference (may not have access) + CAN_WRITE=false + if [[ "$RESOURCE_TYPE" == "AWS_AURORA_DATABASE" ]]; then + # Controlled resource - check workspace role + WORKSPACE_INFO=$(wb workspace describe --workspace "$CONTROLLED_WORKSPACE" --format json) + HIGHEST_ROLE=$(echo "$WORKSPACE_INFO" | jq -r '.highestRole') + echo " User role in controlled workspace: $HIGHEST_ROLE" + if [[ "$HIGHEST_ROLE" == "OWNER" || "$HIGHEST_ROLE" == "WRITER" ]]; then + CAN_WRITE=true + fi + else + # Referenced resource - try to get WRITE_READ credentials to check access + echo " Checking write access for referenced resource..." + if wb resource credentials --id "$RESOURCE_ID" --scope WRITE_READ --format json >/dev/null 2>&1; then + CAN_WRITE=true + echo " User has write access to referenced resource" + else + echo " User has read-only access to referenced resource" + fi + fi + + # Extract database info + DB_NAME=$(echo "$DB_DATA" | jq -r '.databaseName') + RW_ENDPOINT=$(echo "$DB_DATA" | jq -r '.rwEndpoint') + RW_USER=$(echo "$DB_DATA" | jq -r '.rwUser') + RO_ENDPOINT=$(echo "$DB_DATA" | jq -r '.roEndpoint') + RO_USER=$(echo "$DB_DATA" | jq -r '.roUser') + PORT=$(echo "$DB_DATA" | jq -r '.port') + REGION=$(echo "$DB_DATA" | jq -r '.region // "us-east-1"') + + # Generate IAM token for RW user (only if user has write permissions) + if [[ "$CAN_WRITE" == "true" && -n "$RW_ENDPOINT" && "$RW_ENDPOINT" != "null" ]]; then + echo " Getting Workbench credentials for RW access..." + + # Get temporary AWS credentials from Workbench + WB_CREDS=$(wb resource credentials --id "$RESOURCE_ID" --scope WRITE_READ --format json) + AWS_ACCESS_KEY_ID=$(echo "$WB_CREDS" | jq -r '.AccessKeyId') + AWS_SECRET_ACCESS_KEY=$(echo "$WB_CREDS" | jq -r '.SecretAccessKey') + AWS_SESSION_TOKEN=$(echo "$WB_CREDS" | jq -r '.SessionToken') + + echo " Generating RW token for $RW_USER@$RW_ENDPOINT..." + # Generate RDS IAM auth token using those credentials + RW_TOKEN=$(AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \ + AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \ + AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" \ + aws rds generate-db-auth-token \ + --hostname "$RW_ENDPOINT" \ + --port "$PORT" \ + --username "$RW_USER" \ + --region "$REGION") + + # Create RW bookmark + cat > "$BOOKMARK_DIR/${RESOURCE_ID} (Write-Read).toml" < "$BOOKMARK_DIR/${RESOURCE_ID} (Read-Only).toml" </dev/null | wc -l) + echo "$(date): Refresh complete. Created $BOOKMARK_COUNT bookmarks." +} + +# Main loop +echo "Starting pgweb bookmark refresh service..." +echo "Bookmark directory: $BOOKMARK_DIR" +echo "Refresh interval: $REFRESH_INTERVAL seconds" + +while true; do + START_TIME=$(date +%s) + + refresh_bookmarks || echo "$(date): ERROR: Bookmark refresh failed" + + # Calculate elapsed time and adjust sleep to maintain 10-minute cycle + END_TIME=$(date +%s) + ELAPSED=$((END_TIME - START_TIME)) + SLEEP_TIME=$((REFRESH_INTERVAL - ELAPSED)) + + if [ $SLEEP_TIME -gt 0 ]; then + echo "$(date): Refresh took ${ELAPSED}s. Sleeping for ${SLEEP_TIME}s to maintain ${REFRESH_INTERVAL}s cycle..." + sleep "$SLEEP_TIME" + else + echo "$(date): WARNING: Refresh took ${ELAPSED}s, longer than ${REFRESH_INTERVAL}s interval!" + fi +done diff --git a/src/pgweb/start-pgweb.sh b/src/pgweb/start-pgweb.sh new file mode 100755 index 00000000..ba568a7f --- /dev/null +++ b/src/pgweb/start-pgweb.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +echo "Starting pgweb with auto-refreshing IAM bookmarks..." + +# Create bookmarks directory with proper permissions +mkdir -p /home/pgweb/.pgweb/bookmarks +chown -R pgweb:pgweb /home/pgweb/.pgweb + +# Make sure refresh script is executable +chmod +x /workspace/refresh-bookmarks.sh + +# Start bookmark refresher in background +echo "Starting bookmark refresh service..." +/workspace/refresh-bookmarks.sh & + +# Give it a moment to create initial bookmarks +sleep 2 + +# Start pgweb in foreground (keeps container alive) +echo "Starting pgweb server on port 8081..." +exec pgweb --sessions --bind=0.0.0.0 --listen=8081 From 0478afca4899829e3060f5fe8ca664a481ca3c17 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Feb 2026 14:21:56 -0500 Subject: [PATCH 03/17] fixup --- src/pgweb/refresh-bookmarks.sh | 2 +- src/pgweb/start-pgweb.sh | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pgweb/refresh-bookmarks.sh b/src/pgweb/refresh-bookmarks.sh index adbc119f..f66da0b0 100755 --- a/src/pgweb/refresh-bookmarks.sh +++ b/src/pgweb/refresh-bookmarks.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -BOOKMARK_DIR="/home/pgweb/.pgweb/bookmarks" +BOOKMARK_DIR="/root/.pgweb/bookmarks" REFRESH_INTERVAL=600 # 10 minutes (IAM tokens last 15 min) # Create bookmark directory if it doesn't exist diff --git a/src/pgweb/start-pgweb.sh b/src/pgweb/start-pgweb.sh index ba568a7f..5b6ceba3 100755 --- a/src/pgweb/start-pgweb.sh +++ b/src/pgweb/start-pgweb.sh @@ -3,9 +3,8 @@ set -e echo "Starting pgweb with auto-refreshing IAM bookmarks..." -# Create bookmarks directory with proper permissions -mkdir -p /home/pgweb/.pgweb/bookmarks -chown -R pgweb:pgweb /home/pgweb/.pgweb +# Create bookmarks directory +mkdir -p /root/.pgweb/bookmarks # Make sure refresh script is executable chmod +x /workspace/refresh-bookmarks.sh @@ -19,4 +18,4 @@ sleep 2 # Start pgweb in foreground (keeps container alive) echo "Starting pgweb server on port 8081..." -exec pgweb --sessions --bind=0.0.0.0 --listen=8081 +exec pgweb --sessions --bind=0.0.0.0 --listen=8081 --bookmarks-dir=/root/.pgweb/bookmarks From d8fae8009a48c0d141e3044a4c50c3e510e577e4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Feb 2026 14:48:19 -0500 Subject: [PATCH 04/17] fixup --- src/pgweb/docker-compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pgweb/docker-compose.yaml b/src/pgweb/docker-compose.yaml index 8e8f6835..14e5d941 100644 --- a/src/pgweb/docker-compose.yaml +++ b/src/pgweb/docker-compose.yaml @@ -9,6 +9,7 @@ services: # Override the default entrypoint to use our custom script entrypoint: [] command: ["/bin/bash", "/workspace/start-pgweb.sh"] + user: "root" restart: always volumes: - .:/workspace:cached From 325ca7f4510a760d224abebb2cf1aa70c089f473 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Feb 2026 15:05:37 -0500 Subject: [PATCH 05/17] fixup --- src/pgweb/refresh-bookmarks.sh | 17 ++++++++++++++++- src/pgweb/start-pgweb.sh | 25 +++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/pgweb/refresh-bookmarks.sh b/src/pgweb/refresh-bookmarks.sh index f66da0b0..f9903d87 100755 --- a/src/pgweb/refresh-bookmarks.sh +++ b/src/pgweb/refresh-bookmarks.sh @@ -206,6 +206,13 @@ EOF BOOKMARK_COUNT=$(ls -1 "$BOOKMARK_DIR"/*.toml 2>/dev/null | wc -l) echo "$(date): Refresh complete. Created $BOOKMARK_COUNT bookmarks." + + # Write touchfile with refresh status + cat > "$BOOKMARK_DIR/.last_refresh" < "$BOOKMARK_DIR/.last_refresh" < Date: Tue, 3 Feb 2026 15:38:41 -0500 Subject: [PATCH 06/17] fixup --- src/pgweb/.devcontainer.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pgweb/.devcontainer.json b/src/pgweb/.devcontainer.json index d741327c..3000d882 100644 --- a/src/pgweb/.devcontainer.json +++ b/src/pgweb/.devcontainer.json @@ -19,11 +19,15 @@ "${templateOption:login}" ], "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": false, + "installOhMyZsh": false, + "upgradePackages": false + }, "ghcr.io/devcontainers/features/java:1": { "version": "17" }, - "ghcr.io/devcontainers/features/aws-cli:1": {}, - "ghcr.io/dhoeric/features/google-cloud-cli:1": {} + "ghcr.io/devcontainers/features/aws-cli:1": {} }, "remoteUser": "root" } From 93a2cdf54f03e12966eea97a8b431577cedc7a28 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Feb 2026 15:51:14 -0500 Subject: [PATCH 07/17] fixup --- src/pgweb/refresh-bookmarks.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pgweb/refresh-bookmarks.sh b/src/pgweb/refresh-bookmarks.sh index f9903d87..ff64b099 100755 --- a/src/pgweb/refresh-bookmarks.sh +++ b/src/pgweb/refresh-bookmarks.sh @@ -63,13 +63,13 @@ refresh_bookmarks() { echo "$(date): Refreshing pgweb bookmarks from Workbench resources..." # Get current workspace ID - CURRENT_WORKSPACE=$(wb workspace describe --format json | jq -r '.id') + CURRENT_WORKSPACE=$(/usr/bin/wb workspace describe --format json | jq -r '.id') # Get list of all accessible workspaces for UUID->ID lookup - ALL_WORKSPACES=$(wb workspace list --format json) + ALL_WORKSPACES=$(/usr/bin/wb workspace list --format json) # Get list of Aurora databases from Workbench - RESOURCES=$(wb resource list --format json) + RESOURCES=$(/usr/bin/wb resource list --format json) # Clear old bookmarks rm -f "$BOOKMARK_DIR"/*.toml @@ -107,7 +107,7 @@ refresh_bookmarks() { CAN_WRITE=false if [[ "$RESOURCE_TYPE" == "AWS_AURORA_DATABASE" ]]; then # Controlled resource - check workspace role - WORKSPACE_INFO=$(wb workspace describe --workspace "$CONTROLLED_WORKSPACE" --format json) + WORKSPACE_INFO=$(/usr/bin/wb workspace describe --workspace "$CONTROLLED_WORKSPACE" --format json) HIGHEST_ROLE=$(echo "$WORKSPACE_INFO" | jq -r '.highestRole') echo " User role in controlled workspace: $HIGHEST_ROLE" if [[ "$HIGHEST_ROLE" == "OWNER" || "$HIGHEST_ROLE" == "WRITER" ]]; then @@ -116,7 +116,7 @@ refresh_bookmarks() { else # Referenced resource - try to get WRITE_READ credentials to check access echo " Checking write access for referenced resource..." - if wb resource credentials --id "$RESOURCE_ID" --scope WRITE_READ --format json >/dev/null 2>&1; then + if /usr/bin/wb resource credentials --id "$RESOURCE_ID" --scope WRITE_READ --format json >/dev/null 2>&1; then CAN_WRITE=true echo " User has write access to referenced resource" else @@ -138,7 +138,7 @@ refresh_bookmarks() { echo " Getting Workbench credentials for RW access..." # Get temporary AWS credentials from Workbench - WB_CREDS=$(wb resource credentials --id "$RESOURCE_ID" --scope WRITE_READ --format json) + WB_CREDS=$(/usr/bin/wb resource credentials --id "$RESOURCE_ID" --scope WRITE_READ --format json) AWS_ACCESS_KEY_ID=$(echo "$WB_CREDS" | jq -r '.AccessKeyId') AWS_SECRET_ACCESS_KEY=$(echo "$WB_CREDS" | jq -r '.SecretAccessKey') AWS_SESSION_TOKEN=$(echo "$WB_CREDS" | jq -r '.SessionToken') @@ -174,7 +174,7 @@ EOF echo " Getting Workbench credentials for RO access..." # Get temporary AWS credentials from Workbench - WB_CREDS=$(wb resource credentials --id "$RESOURCE_ID" --scope READ_ONLY --format json) + WB_CREDS=$(/usr/bin/wb resource credentials --id "$RESOURCE_ID" --scope READ_ONLY --format json) AWS_ACCESS_KEY_ID=$(echo "$WB_CREDS" | jq -r '.AccessKeyId') AWS_SECRET_ACCESS_KEY=$(echo "$WB_CREDS" | jq -r '.SecretAccessKey') AWS_SESSION_TOKEN=$(echo "$WB_CREDS" | jq -r '.SessionToken') From 9ae82e4a8ce4f9a1d1b52e20e7e9cc2ea5b68bdb Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Feb 2026 16:02:06 -0500 Subject: [PATCH 08/17] fixup --- src/pgweb/start-pgweb.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pgweb/start-pgweb.sh b/src/pgweb/start-pgweb.sh index 771b4690..50042407 100755 --- a/src/pgweb/start-pgweb.sh +++ b/src/pgweb/start-pgweb.sh @@ -6,10 +6,10 @@ echo "Starting pgweb with auto-refreshing IAM bookmarks..." # Create bookmarks directory mkdir -p /root/.pgweb/bookmarks -# Wait for Workbench CLI to be initialized (postCreateCommand completes) +# Wait for Workbench CLI to be installed and authenticated (postCreateCommand completes) # This ensures wb is installed, configured, and authenticated echo "Waiting for Workbench CLI initialization..." -while [ ! -f /root/.workbench/context.json ]; do +while ! /usr/bin/wb auth status >/dev/null 2>&1; do sleep 2 done echo "Workbench CLI is ready!" From f3cc88f9a139ebee3b5851f5d656b034b71f5c63 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Feb 2026 16:12:26 -0500 Subject: [PATCH 09/17] fixup --- src/pgweb/start-pgweb.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pgweb/start-pgweb.sh b/src/pgweb/start-pgweb.sh index 50042407..8c428dd1 100755 --- a/src/pgweb/start-pgweb.sh +++ b/src/pgweb/start-pgweb.sh @@ -6,12 +6,16 @@ echo "Starting pgweb with auto-refreshing IAM bookmarks..." # Create bookmarks directory mkdir -p /root/.pgweb/bookmarks -# Wait for Workbench CLI to be installed and authenticated (postCreateCommand completes) -# This ensures wb is installed, configured, and authenticated +# Wait for Workbench CLI to be installed, authenticated, and workspace set (postCreateCommand completes) +# This ensures wb is installed, configured, authenticated, and workspace is set echo "Waiting for Workbench CLI initialization..." while ! /usr/bin/wb auth status >/dev/null 2>&1; do sleep 2 done +echo "Authenticated, waiting for workspace to be set..." +while ! /usr/bin/wb workspace describe --format json >/dev/null 2>&1; do + sleep 2 +done echo "Workbench CLI is ready!" # Make sure refresh script is executable From 1e128110adc168d60309d2c7ee09cda65df0dfdd Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Feb 2026 17:04:54 -0500 Subject: [PATCH 10/17] fixup --- src/pgweb/refresh-bookmarks.sh | 6 ++---- src/pgweb/start-pgweb.sh | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pgweb/refresh-bookmarks.sh b/src/pgweb/refresh-bookmarks.sh index ff64b099..81d514b4 100755 --- a/src/pgweb/refresh-bookmarks.sh +++ b/src/pgweb/refresh-bookmarks.sh @@ -156,13 +156,12 @@ refresh_bookmarks() { # Create RW bookmark cat > "$BOOKMARK_DIR/${RESOURCE_ID} (Write-Read).toml" < "$BOOKMARK_DIR/${RESOURCE_ID} (Read-Only).toml" < Date: Wed, 4 Feb 2026 10:41:22 -0500 Subject: [PATCH 11/17] fixup --- src/pgweb/.devcontainer.json | 8 +--- src/pgweb/docker-compose.yaml | 2 +- src/pgweb/refresh-bookmarks.sh | 62 ++++++++++++----------------- src/pgweb/start-bookmark-refresh.sh | 25 ++++++++++++ src/pgweb/start-pgweb.sh | 46 --------------------- 5 files changed, 53 insertions(+), 90 deletions(-) create mode 100755 src/pgweb/start-bookmark-refresh.sh delete mode 100755 src/pgweb/start-pgweb.sh diff --git a/src/pgweb/.devcontainer.json b/src/pgweb/.devcontainer.json index 3000d882..09687e8b 100644 --- a/src/pgweb/.devcontainer.json +++ b/src/pgweb/.devcontainer.json @@ -11,13 +11,7 @@ "${templateOption:cloud}", "${templateOption:login}" ], - "postStartCommand": [ - "./startupscript/remount-on-restart.sh", - "root", - "/root", - "${templateOption:cloud}", - "${templateOption:login}" - ], + "postStartCommand": "/workspace/start-bookmark-refresh.sh", "features": { "ghcr.io/devcontainers/features/common-utils:2": { "installZsh": false, diff --git a/src/pgweb/docker-compose.yaml b/src/pgweb/docker-compose.yaml index 14e5d941..acd25d4d 100644 --- a/src/pgweb/docker-compose.yaml +++ b/src/pgweb/docker-compose.yaml @@ -8,7 +8,7 @@ services: # context: . # Override the default entrypoint to use our custom script entrypoint: [] - command: ["/bin/bash", "/workspace/start-pgweb.sh"] + command: ["pgweb", "--sessions", "--bind=0.0.0.0", "--listen=8081", "--bookmarks-dir=/root/.pgweb/bookmarks", "--debug"] user: "root" restart: always volumes: diff --git a/src/pgweb/refresh-bookmarks.sh b/src/pgweb/refresh-bookmarks.sh index 81d514b4..2eeabb62 100755 --- a/src/pgweb/refresh-bookmarks.sh +++ b/src/pgweb/refresh-bookmarks.sh @@ -2,10 +2,11 @@ set -e BOOKMARK_DIR="/root/.pgweb/bookmarks" +BOOKMARK_BASE="/root/.pgweb" REFRESH_INTERVAL=600 # 10 minutes (IAM tokens last 15 min) -# Create bookmark directory if it doesn't exist -mkdir -p "$BOOKMARK_DIR" +# Create base directory if it doesn't exist +mkdir -p "$BOOKMARK_BASE" # Helper function to find workspace ID from UUID get_workspace_id_from_uuid() { @@ -62,6 +63,11 @@ find_controlled_resource() { refresh_bookmarks() { echo "$(date): Refreshing pgweb bookmarks from Workbench resources..." + # Create temporary directory for new bookmarks (using PID for uniqueness) + TEMP_DIR="/root/.pgweb/bookmarks.tmp.$$" + rm -rf "$TEMP_DIR" + mkdir -p "$TEMP_DIR" + # Get current workspace ID CURRENT_WORKSPACE=$(/usr/bin/wb workspace describe --format json | jq -r '.id') @@ -71,9 +77,6 @@ refresh_bookmarks() { # Get list of Aurora databases from Workbench RESOURCES=$(/usr/bin/wb resource list --format json) - # Clear old bookmarks - rm -f "$BOOKMARK_DIR"/*.toml - # Process each resource echo "$RESOURCES" | jq -c '.[]' | while read -r resource; do RESOURCE_TYPE=$(echo "$resource" | jq -r '.resourceType') @@ -154,8 +157,8 @@ refresh_bookmarks() { --username "$RW_USER" \ --region "$REGION") - # Create RW bookmark - cat > "$BOOKMARK_DIR/${RESOURCE_ID} (Write-Read).toml" < "$TEMP_DIR/${RESOURCE_ID} (Write-Read).toml" < "$BOOKMARK_DIR/${RESOURCE_ID} (Read-Only).toml" < "$TEMP_DIR/${RESOURCE_ID} (Read-Only).toml" </dev/null | wc -l) + BOOKMARK_COUNT=$(ls -1 "$TEMP_DIR"/*.toml 2>/dev/null | wc -l) echo "$(date): Refresh complete. Created $BOOKMARK_COUNT bookmarks." # Write touchfile with refresh status - cat > "$BOOKMARK_DIR/.last_refresh" < "$TEMP_DIR/.last_refresh" < "$BOOKMARK_DIR/.last_refresh" < "$BOOKMARK_DIR/.last_refresh" </dev/null 2>&1; do - sleep 2 -done -echo "Authenticated, waiting for workspace to be set..." -while ! /usr/bin/wb workspace describe --format json >/dev/null 2>&1; do - sleep 2 -done -echo "Workbench CLI is ready!" - -# Make sure refresh script is executable -chmod +x /workspace/refresh-bookmarks.sh - -# Start bookmark refresher in background -echo "Starting bookmark refresh service..." -/workspace/refresh-bookmarks.sh & - -# Wait for initial refresh to complete (up to 5 minutes) -echo "Waiting for initial bookmark discovery..." -WAIT_COUNT=0 -while [ ! -f /root/.pgweb/bookmarks/.last_refresh ] && [ $WAIT_COUNT -lt 300 ]; do - sleep 2 - WAIT_COUNT=$((WAIT_COUNT + 2)) -done - -if [ -f /root/.pgweb/bookmarks/.last_refresh ]; then - # Show refresh status - source /root/.pgweb/bookmarks/.last_refresh - echo "Initial refresh complete: $bookmark_count bookmark(s) created at $timestamp" -else - echo "WARNING: Initial refresh did not complete within 5 minutes" -fi - -# Start pgweb in foreground (keeps container alive) -echo "Starting pgweb server on port 8081..." -exec pgweb --sessions --bind=0.0.0.0 --listen=8081 --bookmarks-dir=/root/.pgweb/bookmarks --debug From ad73cc8de9e9550db7cbac73788f0ee9050ec60d Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 4 Feb 2026 11:06:30 -0500 Subject: [PATCH 12/17] fixup --- src/pgweb/start-bookmark-refresh.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pgweb/start-bookmark-refresh.sh b/src/pgweb/start-bookmark-refresh.sh index d6c17eea..1340a560 100755 --- a/src/pgweb/start-bookmark-refresh.sh +++ b/src/pgweb/start-bookmark-refresh.sh @@ -3,8 +3,8 @@ set -e echo "Starting bookmark refresh for pgweb..." -# Create bookmarks directory -mkdir -p /root/.pgweb/bookmarks +# Create base directory (but not bookmarks subdirectory - that will be a symlink) +mkdir -p /root/.pgweb # Make sure refresh script is executable chmod +x /workspace/refresh-bookmarks.sh @@ -13,13 +13,14 @@ chmod +x /workspace/refresh-bookmarks.sh echo "Running initial bookmark refresh..." /workspace/refresh-bookmarks.sh -# Start background loop for continuous refresh +# Start background loop for continuous refresh (detached from parent) echo "Starting background bookmark refresh service (every 10 minutes)..." -( +nohup bash -c ' while true; do sleep 600 # 10 minutes /workspace/refresh-bookmarks.sh || echo "$(date): WARNING: Bookmark refresh failed" done -) & +' >> /root/.pgweb/refresh.log 2>&1 & -echo "Bookmark refresh service configured (background PID: $!)" +REFRESH_PID=$! +echo "Bookmark refresh service configured (background PID: $REFRESH_PID)" From 45a75a8e5f7d2fbd53238c0677f317b53c4941f8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Feb 2026 10:39:06 -0500 Subject: [PATCH 13/17] fixup --- src/pgweb/refresh-bookmarks.sh | 335 ++++++++++++---------------- src/pgweb/start-bookmark-refresh.sh | 3 +- 2 files changed, 146 insertions(+), 192 deletions(-) diff --git a/src/pgweb/refresh-bookmarks.sh b/src/pgweb/refresh-bookmarks.sh index 2eeabb62..373c1303 100755 --- a/src/pgweb/refresh-bookmarks.sh +++ b/src/pgweb/refresh-bookmarks.sh @@ -1,235 +1,188 @@ #!/bin/bash set -e -BOOKMARK_DIR="/root/.pgweb/bookmarks" -BOOKMARK_BASE="/root/.pgweb" -REFRESH_INTERVAL=600 # 10 minutes (IAM tokens last 15 min) +# Allow overriding via environment for local testing +readonly WB_EXE="${WB_EXE:-/usr/bin/wb}" +readonly PGWEB_BASE="${PGWEB_BASE:-/root/.pgweb}" +readonly BOOKMARK_DIR="${PGWEB_BASE}/bookmarks" # Create base directory if it doesn't exist -mkdir -p "$BOOKMARK_BASE" - -# Helper function to find workspace ID from UUID -get_workspace_id_from_uuid() { - local uuid="$1" - echo "$ALL_WORKSPACES" | jq -r --arg uuid "$uuid" '.[] | select(.uuid == $uuid) | .id' +mkdir -p "${PGWEB_BASE}" + +# Helper function to get credentials and generate IAM auth token +generate_iam_token() { + local resource_id="${1}" + local scope="${2}" + local endpoint="${3}" + local port="${4}" + local username="${5}" + local region="${6}" + + # Get credentials from Workbench + local wb_creds + # shellcheck disable=SC2312 + wb_creds=$(${WB_EXE} resource credentials --id "${resource_id}" --scope "${scope}" --format json 2>/dev/null) || return 1 + readonly wb_creds + + # Extract AWS credentials + local access_key secret_key session_token + # shellcheck disable=SC2312 + access_key=$(echo "${wb_creds}" | jq -r '.AccessKeyId') + # shellcheck disable=SC2312 + secret_key=$(echo "${wb_creds}" | jq -r '.SecretAccessKey') + # shellcheck disable=SC2312 + session_token=$(echo "${wb_creds}" | jq -r '.SessionToken') + readonly access_key secret_key session_token + + # Generate IAM token + AWS_ACCESS_KEY_ID="${access_key}" \ + AWS_SECRET_ACCESS_KEY="${secret_key}" \ + AWS_SESSION_TOKEN="${session_token}" \ + aws rds generate-db-auth-token \ + --hostname "${endpoint}" \ + --port "${port}" \ + --username "${username}" \ + --region "${region}" } -# Recursively follow references using embedded referencedResource data -find_controlled_resource() { - local resource_json="$1" - local source_workspace_uuid="$2" - - local resource_type=$(echo "$resource_json" | jq -r '.resourceType') - - # If it's a controlled resource, return it and the workspace ID - if [[ "$resource_type" == "AWS_AURORA_DATABASE" ]]; then - # If we have a source workspace UUID, look up its ID - if [[ -n "$source_workspace_uuid" && "$source_workspace_uuid" != "null" ]]; then - local workspace_id=$(get_workspace_id_from_uuid "$source_workspace_uuid") - if [[ -z "$workspace_id" ]]; then - echo "ERROR: Could not find workspace ID for UUID: $source_workspace_uuid" >&2 - return 1 - fi - echo "$workspace_id|$resource_json" - else - # Use current workspace - echo "$CURRENT_WORKSPACE|$resource_json" - fi - return - fi - - # If it's a reference, use the embedded referencedResource data - if [[ "$resource_type" == "AWS_AURORA_DATABASE_REFERENCE" ]]; then - local referenced_resource=$(echo "$resource_json" | jq -r '.referencedResource') - - if [[ -z "$referenced_resource" || "$referenced_resource" == "null" ]]; then - echo "ERROR: Reference has no embedded referencedResource data" >&2 - return 1 - fi - - # Get the source workspace UUID - this is where the controlled resource lives - local next_workspace_uuid=$(echo "$resource_json" | jq -r '.sourceWorkspaceId') - - # Recursively check the referenced resource, passing the workspace UUID - find_controlled_resource "$referenced_resource" "$next_workspace_uuid" - return - fi - - # Unknown type - echo "ERROR: Unknown resource type: $resource_type" >&2 - return 1 +# Helper function to create bookmark TOML file +create_bookmark() { + local output_file="${1}" + local endpoint="${2}" + local port="${3}" + local username="${4}" + local password="${5}" + local database="${6}" + + cat > "${output_file}" <ID lookup - ALL_WORKSPACES=$(/usr/bin/wb workspace list --format json) + local TEMP_DIR="${PGWEB_BASE}/bookmarks.tmp.$$" + readonly TEMP_DIR + rm -rf "${TEMP_DIR}" + mkdir -p "${TEMP_DIR}" # Get list of Aurora databases from Workbench - RESOURCES=$(/usr/bin/wb resource list --format json) + local RESOURCES + # shellcheck disable=SC2312 + RESOURCES=$(${WB_EXE} resource list --format json) + readonly RESOURCES # Process each resource - echo "$RESOURCES" | jq -c '.[]' | while read -r resource; do - RESOURCE_TYPE=$(echo "$resource" | jq -r '.resourceType') + # shellcheck disable=SC2312 + echo "${RESOURCES}" | jq -c '.[]' | while read -r resource; do + local RESOURCE_TYPE + # shellcheck disable=SC2312 + RESOURCE_TYPE=$(echo "${resource}" | jq -r '.resourceType') # Skip non-Aurora resources - if [[ ! "$RESOURCE_TYPE" =~ AURORA_DATABASE ]]; then + if [[ ! "${RESOURCE_TYPE}" =~ AURORA_DATABASE ]]; then continue fi - RESOURCE_ID=$(echo "$resource" | jq -r '.id') - echo " Processing: $RESOURCE_ID (type: $RESOURCE_TYPE)" - - # Find the controlled resource by following reference chain - if [[ "$RESOURCE_TYPE" == "AWS_AURORA_DATABASE_REFERENCE" ]]; then - echo " Following reference chain..." + local RESOURCE_ID + # shellcheck disable=SC2312 + RESOURCE_ID=$(echo "${resource}" | jq -r '.id') + echo " Processing: ${RESOURCE_ID} (type: ${RESOURCE_TYPE})" - # Use embedded referencedResource data - CONTROLLED_INFO=$(find_controlled_resource "$resource" "") - CONTROLLED_WORKSPACE=$(echo "$CONTROLLED_INFO" | cut -d'|' -f1) - DB_DATA=$(echo "$CONTROLLED_INFO" | cut -d'|' -f2-) + # Extract database details from top level (controlled) or referencedResource (reference) + local DB_DATA + if [[ "${RESOURCE_TYPE}" == "AWS_AURORA_DATABASE" ]]; then + DB_DATA="${resource}" else - # Already a controlled resource - CONTROLLED_WORKSPACE="$CURRENT_WORKSPACE" - DB_DATA="$resource" + # shellcheck disable=SC2312 + DB_DATA=$(echo "${resource}" | jq -r '.referencedResource') fi - echo " Controlled resource workspace: $CONTROLLED_WORKSPACE" - - # For permissions, use credentials from the resource in the CURRENT workspace - # Don't try to access the controlled workspace if it's a reference (may not have access) - CAN_WRITE=false - if [[ "$RESOURCE_TYPE" == "AWS_AURORA_DATABASE" ]]; then - # Controlled resource - check workspace role - WORKSPACE_INFO=$(/usr/bin/wb workspace describe --workspace "$CONTROLLED_WORKSPACE" --format json) - HIGHEST_ROLE=$(echo "$WORKSPACE_INFO" | jq -r '.highestRole') - echo " User role in controlled workspace: $HIGHEST_ROLE" - if [[ "$HIGHEST_ROLE" == "OWNER" || "$HIGHEST_ROLE" == "WRITER" ]]; then - CAN_WRITE=true - fi - else - # Referenced resource - try to get WRITE_READ credentials to check access - echo " Checking write access for referenced resource..." - if /usr/bin/wb resource credentials --id "$RESOURCE_ID" --scope WRITE_READ --format json >/dev/null 2>&1; then - CAN_WRITE=true - echo " User has write access to referenced resource" - else - echo " User has read-only access to referenced resource" - fi + # Extract database connection info + local DB_NAME RO_ENDPOINT RO_USER RW_ENDPOINT RW_USER PORT REGION + # shellcheck disable=SC2312 + DB_NAME=$(echo "${DB_DATA}" | jq -r '.databaseName') + # shellcheck disable=SC2312 + RO_ENDPOINT=$(echo "${DB_DATA}" | jq -r '.roEndpoint') + # shellcheck disable=SC2312 + RO_USER=$(echo "${DB_DATA}" | jq -r '.roUser') + # shellcheck disable=SC2312 + RW_ENDPOINT=$(echo "${DB_DATA}" | jq -r '.rwEndpoint') + # shellcheck disable=SC2312 + RW_USER=$(echo "${DB_DATA}" | jq -r '.rwUser') + # shellcheck disable=SC2312 + PORT=$(echo "${DB_DATA}" | jq -r '.port') + # shellcheck disable=SC2312 + REGION=$(echo "${DB_DATA}" | jq -r '.region // "us-east-1"') + + # Validate all required fields are present + if [[ -z "${DB_NAME}" || "${DB_NAME}" == "null" ]] || \ + [[ -z "${RO_ENDPOINT}" || "${RO_ENDPOINT}" == "null" ]] || \ + [[ -z "${RO_USER}" || "${RO_USER}" == "null" ]] || \ + [[ -z "${RW_ENDPOINT}" || "${RW_ENDPOINT}" == "null" ]] || \ + [[ -z "${RW_USER}" || "${RW_USER}" == "null" ]] || \ + [[ -z "${PORT}" || "${PORT}" == "null" ]]; then + echo " Missing required database fields, skipping" + continue fi - # Extract database info - DB_NAME=$(echo "$DB_DATA" | jq -r '.databaseName') - RW_ENDPOINT=$(echo "$DB_DATA" | jq -r '.rwEndpoint') - RW_USER=$(echo "$DB_DATA" | jq -r '.rwUser') - RO_ENDPOINT=$(echo "$DB_DATA" | jq -r '.roEndpoint') - RO_USER=$(echo "$DB_DATA" | jq -r '.roUser') - PORT=$(echo "$DB_DATA" | jq -r '.port') - REGION=$(echo "$DB_DATA" | jq -r '.region // "us-east-1"') - - # Generate IAM token for RW user (only if user has write permissions) - if [[ "$CAN_WRITE" == "true" && -n "$RW_ENDPOINT" && "$RW_ENDPOINT" != "null" ]]; then - echo " Getting Workbench credentials for RW access..." - - # Get temporary AWS credentials from Workbench - WB_CREDS=$(/usr/bin/wb resource credentials --id "$RESOURCE_ID" --scope WRITE_READ --format json) - AWS_ACCESS_KEY_ID=$(echo "$WB_CREDS" | jq -r '.AccessKeyId') - AWS_SECRET_ACCESS_KEY=$(echo "$WB_CREDS" | jq -r '.SecretAccessKey') - AWS_SESSION_TOKEN=$(echo "$WB_CREDS" | jq -r '.SessionToken') - - echo " Generating RW token for $RW_USER@$RW_ENDPOINT..." - # Generate RDS IAM auth token using those credentials - RW_TOKEN=$(AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \ - AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \ - AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" \ - aws rds generate-db-auth-token \ - --hostname "$RW_ENDPOINT" \ - --port "$PORT" \ - --username "$RW_USER" \ - --region "$REGION") - - # Create RW bookmark in temp directory - cat > "$TEMP_DIR/${RESOURCE_ID} (Write-Read).toml" < "$TEMP_DIR/${RESOURCE_ID} (Read-Only).toml" </dev/null | wc -l) - echo "$(date): Refresh complete. Created $BOOKMARK_COUNT bookmarks." - - # Write touchfile with refresh status - cat > "$TEMP_DIR/.last_refresh" </dev/null | wc -l) + readonly BOOKMARK_COUNT + # shellcheck disable=SC2312 + echo "$(date): Refresh complete. Created ${BOOKMARK_COUNT} bookmarks." # Atomically update symlink to point to new bookmark directory - ln -sfn "$(basename "$TEMP_DIR")" "$BOOKMARK_DIR" + # shellcheck disable=SC2312 + ln -sfn "$(basename "${TEMP_DIR}")" "${BOOKMARK_DIR}" # Cleanup old bookmark directories (all except current) - find "$BOOKMARK_BASE" -maxdepth 1 -type d -name "bookmarks.tmp.*" ! -name "bookmarks.tmp.$$" -exec rm -rf {} \; + find "${PGWEB_BASE}" -maxdepth 1 -type d -name "bookmarks.tmp.*" ! -name "bookmarks.tmp.$$" -exec rm -rf {} \; } # Run single refresh +# shellcheck disable=SC2310 if ! refresh_bookmarks; then + # shellcheck disable=SC2312 echo "$(date): ERROR: Bookmark refresh failed" - # Write error status to touchfile - cat > "$BOOKMARK_DIR/.last_refresh" <> /root/.pgweb/refresh.log 2>&1 & REFRESH_PID=$! -echo "Bookmark refresh service configured (background PID: $REFRESH_PID)" +echo "Bookmark refresh service configured (background PID: ${REFRESH_PID})" From cf3067ea86805a93774df933bd295a7b6b798e75 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Feb 2026 11:33:13 -0500 Subject: [PATCH 14/17] doc update --- src/pgweb/README.md | 38 +++++++++++++++++++++++++++-------- src/pgweb/docker-compose.yaml | 2 +- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/pgweb/README.md b/src/pgweb/README.md index 869559ef..31a3426a 100644 --- a/src/pgweb/README.md +++ b/src/pgweb/README.md @@ -21,23 +21,30 @@ The app automatically discovers all Aurora databases in your Workbench workspace ### How It Works 1. **Auto-Discovery**: Every 10 minutes, the app queries `wb resource list` to find all Aurora databases -2. **IAM Token Generation**: For each database, generates fresh IAM authentication tokens for both read-write (RW) and read-only (RO) users -3. **Bookmark Creation**: Creates pgweb bookmarks for each database connection -4. **Always Fresh**: Tokens refresh every 10 minutes (they expire after 15), so connections never expire +2. **Access-Based Credentials**: For each database, attempts to get credentials based on your workspace permissions: + - **Read-Only**: Always attempted first - if successful, creates a read-only bookmark + - **Write-Read**: Only attempted if you have write access - creates a write-read bookmark if successful +3. **IAM Token Generation**: Generates fresh IAM authentication tokens for each access level you have +4. **Bookmark Creation**: Creates pgweb bookmarks only for the access levels you're granted +5. **Always Fresh**: Tokens refresh every 10 minutes (they expire after 15), so connections never expire + +**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. ### Using Bookmarks -When you open pgweb, you'll see bookmarks for each database in your workspace: -- `aurora-demo-db-20260115 (Write-Read)` - Read-write connection +When you open pgweb, you'll see bookmarks for databases you have access to. Examples: + - `aurora-demo-db-20260115 (Read-Only)` - Read-only connection -- `dc-database (Write-Read)` - Read-write connection (referenced database) -- `dc-database (Read-Only)` - Read-only connection (referenced database) +- `aurora-demo-db-20260115 (Write-Read)` - Read-write connection (only if you have write access) +- `dc-database (Read-Only)` - Read-only connection to referenced database +- `dc-database (Write-Read)` - Read-write connection (only if you have write access) Click any bookmark to connect instantly - no need to enter credentials! ### Manual Connections You can also use the interactive login form to enter connection details manually: + - **Host**: Your Aurora cluster endpoint - **Port**: `5432` - **Username**: Your database username @@ -49,7 +56,22 @@ You can also use the interactive login form to enter connection details manually 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. -For local testing: +## Local Testing + +For local testing of the bookmark refresh script: + +```bash +# Test with custom paths (useful for local development) +WB_EXE="$(which wb)" PGWEB_BASE=/tmp/pgweb ./src/pgweb/refresh-bookmarks.sh +``` + +Environment variables: + +- `WB_EXE` - Path to wb executable (default: `/usr/bin/wb`) +- `PGWEB_BASE` - Base directory for pgweb config (default: `/root/.pgweb`) + +For full devcontainer testing: + 1. Create Docker network: `docker network create app-network` 2. Run the app: `devcontainer up --workspace-folder .` 3. Access at: `http://localhost:8081` diff --git a/src/pgweb/docker-compose.yaml b/src/pgweb/docker-compose.yaml index acd25d4d..d8030f04 100644 --- a/src/pgweb/docker-compose.yaml +++ b/src/pgweb/docker-compose.yaml @@ -8,7 +8,7 @@ services: # context: . # Override the default entrypoint to use our custom script entrypoint: [] - command: ["pgweb", "--sessions", "--bind=0.0.0.0", "--listen=8081", "--bookmarks-dir=/root/.pgweb/bookmarks", "--debug"] + command: ["pgweb", "--sessions", "--bind=0.0.0.0", "--listen=8081", "--bookmarks-dir=/root/.pgweb/bookmarks", "--lock-session"] user: "root" restart: always volumes: From 1ef5afdcc1471387bc33c4f9eb0cfff086f63bee Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Feb 2026 11:45:50 -0500 Subject: [PATCH 15/17] fixup --- src/pgweb/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pgweb/docker-compose.yaml b/src/pgweb/docker-compose.yaml index d8030f04..834b6d36 100644 --- a/src/pgweb/docker-compose.yaml +++ b/src/pgweb/docker-compose.yaml @@ -8,7 +8,7 @@ services: # context: . # Override the default entrypoint to use our custom script entrypoint: [] - command: ["pgweb", "--sessions", "--bind=0.0.0.0", "--listen=8081", "--bookmarks-dir=/root/.pgweb/bookmarks", "--lock-session"] + command: ["pgweb", "--sessions", "--bind=0.0.0.0", "--listen=8081", "--bookmarks-dir=/root/.pgweb/bookmarks"] user: "root" restart: always volumes: From ce5314580695d18a6fb739858ecb8de0fac6eb7a Mon Sep 17 00:00:00 2001 From: David Shen Date: Fri, 27 Feb 2026 13:49:25 -0500 Subject: [PATCH 16/17] Clean up shellcheck disables --- src/pgweb/refresh-bookmarks.sh | 27 ++------------------------- src/pgweb/start-bookmark-refresh.sh | 4 +++- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/pgweb/refresh-bookmarks.sh b/src/pgweb/refresh-bookmarks.sh index 373c1303..11c0eb82 100755 --- a/src/pgweb/refresh-bookmarks.sh +++ b/src/pgweb/refresh-bookmarks.sh @@ -1,5 +1,6 @@ #!/bin/bash -set -e +set -o errexit +set -o pipefail # Allow overriding via environment for local testing readonly WB_EXE="${WB_EXE:-/usr/bin/wb}" @@ -20,17 +21,13 @@ generate_iam_token() { # Get credentials from Workbench local wb_creds - # shellcheck disable=SC2312 wb_creds=$(${WB_EXE} resource credentials --id "${resource_id}" --scope "${scope}" --format json 2>/dev/null) || return 1 readonly wb_creds # Extract AWS credentials local access_key secret_key session_token - # shellcheck disable=SC2312 access_key=$(echo "${wb_creds}" | jq -r '.AccessKeyId') - # shellcheck disable=SC2312 secret_key=$(echo "${wb_creds}" | jq -r '.SecretAccessKey') - # shellcheck disable=SC2312 session_token=$(echo "${wb_creds}" | jq -r '.SessionToken') readonly access_key secret_key session_token @@ -65,7 +62,6 @@ EOF } refresh_bookmarks() { - # shellcheck disable=SC2312 echo "$(date): Refreshing pgweb bookmarks from Workbench resources..." # Create temporary directory for new bookmarks (using PID for uniqueness) @@ -76,15 +72,12 @@ refresh_bookmarks() { # Get list of Aurora databases from Workbench local RESOURCES - # shellcheck disable=SC2312 RESOURCES=$(${WB_EXE} resource list --format json) readonly RESOURCES # Process each resource - # shellcheck disable=SC2312 echo "${RESOURCES}" | jq -c '.[]' | while read -r resource; do local RESOURCE_TYPE - # shellcheck disable=SC2312 RESOURCE_TYPE=$(echo "${resource}" | jq -r '.resourceType') # Skip non-Aurora resources @@ -93,7 +86,6 @@ refresh_bookmarks() { fi local RESOURCE_ID - # shellcheck disable=SC2312 RESOURCE_ID=$(echo "${resource}" | jq -r '.id') echo " Processing: ${RESOURCE_ID} (type: ${RESOURCE_TYPE})" @@ -102,25 +94,17 @@ refresh_bookmarks() { if [[ "${RESOURCE_TYPE}" == "AWS_AURORA_DATABASE" ]]; then DB_DATA="${resource}" else - # shellcheck disable=SC2312 DB_DATA=$(echo "${resource}" | jq -r '.referencedResource') fi # Extract database connection info local DB_NAME RO_ENDPOINT RO_USER RW_ENDPOINT RW_USER PORT REGION - # shellcheck disable=SC2312 DB_NAME=$(echo "${DB_DATA}" | jq -r '.databaseName') - # shellcheck disable=SC2312 RO_ENDPOINT=$(echo "${DB_DATA}" | jq -r '.roEndpoint') - # shellcheck disable=SC2312 RO_USER=$(echo "${DB_DATA}" | jq -r '.roUser') - # shellcheck disable=SC2312 RW_ENDPOINT=$(echo "${DB_DATA}" | jq -r '.rwEndpoint') - # shellcheck disable=SC2312 RW_USER=$(echo "${DB_DATA}" | jq -r '.rwUser') - # shellcheck disable=SC2312 PORT=$(echo "${DB_DATA}" | jq -r '.port') - # shellcheck disable=SC2312 REGION=$(echo "${DB_DATA}" | jq -r '.region // "us-east-1"') # Validate all required fields are present @@ -137,7 +121,6 @@ refresh_bookmarks() { # Try to create READ_ONLY bookmark echo " Checking read access..." local RO_TOKEN - # shellcheck disable=SC2310,SC2311,SC2312 if RO_TOKEN=$(generate_iam_token "${RESOURCE_ID}" "READ_ONLY" "${RO_ENDPOINT}" "${PORT}" "${RO_USER}" "${REGION}"); then echo " Read access confirmed" echo " Creating read-only bookmark..." @@ -151,7 +134,6 @@ refresh_bookmarks() { # Try to create WRITE_READ bookmark echo " Checking write access..." local RW_TOKEN - # shellcheck disable=SC2310,SC2311,SC2312 if RW_TOKEN=$(generate_iam_token "${RESOURCE_ID}" "WRITE_READ" "${RW_ENDPOINT}" "${PORT}" "${RW_USER}" "${REGION}"); then echo " Write access confirmed" echo " Creating write-read bookmark..." @@ -165,14 +147,11 @@ refresh_bookmarks() { # Count bookmarks - must use find since the while loop runs in a subshell (due to pipe), # so a counter variable incremented in the loop would not be visible here local BOOKMARK_COUNT - # shellcheck disable=SC2312 BOOKMARK_COUNT=$(find "${TEMP_DIR}" -name "*.toml" -type f 2>/dev/null | wc -l) readonly BOOKMARK_COUNT - # shellcheck disable=SC2312 echo "$(date): Refresh complete. Created ${BOOKMARK_COUNT} bookmarks." # Atomically update symlink to point to new bookmark directory - # shellcheck disable=SC2312 ln -sfn "$(basename "${TEMP_DIR}")" "${BOOKMARK_DIR}" # Cleanup old bookmark directories (all except current) @@ -180,9 +159,7 @@ refresh_bookmarks() { } # Run single refresh -# shellcheck disable=SC2310 if ! refresh_bookmarks; then - # shellcheck disable=SC2312 echo "$(date): ERROR: Bookmark refresh failed" exit 1 fi diff --git a/src/pgweb/start-bookmark-refresh.sh b/src/pgweb/start-bookmark-refresh.sh index 06d6aec1..2772a29c 100755 --- a/src/pgweb/start-bookmark-refresh.sh +++ b/src/pgweb/start-bookmark-refresh.sh @@ -1,5 +1,6 @@ #!/bin/bash -set -e +set -o errexit +set -o pipefail echo "Starting bookmark refresh for pgweb..." @@ -15,6 +16,7 @@ echo "Running initial bookmark refresh..." # Start background loop for continuous refresh (detached from parent) echo "Starting background bookmark refresh service (every 10 minutes)..." +# Single quotes intentional: $(date) should expand at runtime, not now # shellcheck disable=SC2016 nohup bash -c ' while true; do From 1032db41c1fa9e659771dc14dc3b8a473ae603db Mon Sep 17 00:00:00 2001 From: David Shen Date: Fri, 27 Feb 2026 17:24:37 -0500 Subject: [PATCH 17/17] nounset --- src/pgweb/refresh-bookmarks.sh | 1 + src/pgweb/start-bookmark-refresh.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pgweb/refresh-bookmarks.sh b/src/pgweb/refresh-bookmarks.sh index 11c0eb82..9b8c7ec2 100755 --- a/src/pgweb/refresh-bookmarks.sh +++ b/src/pgweb/refresh-bookmarks.sh @@ -1,6 +1,7 @@ #!/bin/bash set -o errexit set -o pipefail +set -o nounset # Allow overriding via environment for local testing readonly WB_EXE="${WB_EXE:-/usr/bin/wb}" diff --git a/src/pgweb/start-bookmark-refresh.sh b/src/pgweb/start-bookmark-refresh.sh index 2772a29c..1a4e523f 100755 --- a/src/pgweb/start-bookmark-refresh.sh +++ b/src/pgweb/start-bookmark-refresh.sh @@ -1,6 +1,7 @@ #!/bin/bash set -o errexit set -o pipefail +set -o nounset echo "Starting bookmark refresh for pgweb..."