diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index f24f6431..1fd1ea4d 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -25,10 +25,10 @@ jobs: - 'startupscript/**' - 'test/**' config: - - jupyter-template: - user: jupyter - jupyter: + - example: user: jovyan + jupyter-template: + user: jupyter r-analysis: user: rstudio workbench_tools: true @@ -73,6 +73,8 @@ jobs: filters: - 'src/aou-common/**' - 'src/workbench-jupyter-parabricks/**' + ubuntu-example: + user: vscode outputs: apps: ${{ steps.output.outputs.apps }} steps: diff --git a/.github/workflows/test-scripts.yaml b/.github/workflows/test-scripts.yaml new file mode 100644 index 00000000..49f66be8 --- /dev/null +++ b/.github/workflows/test-scripts.yaml @@ -0,0 +1,29 @@ +name: "CI - Test Scripts" +on: + pull_request: + paths: + - 'scripts/**' + - '.github/workflows/test-scripts.yaml' + push: + branches: + - master + paths: + - 'scripts/**' + - '.github/workflows/test-scripts.yaml' + +jobs: + test-create-custom-app: + name: "Test create-custom-app.sh" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install bats-core + run: | + sudo apt-get update + sudo apt-get install -y bats + + - name: Run bats tests + run: | + cd scripts/test + bats create-custom-app.bats diff --git a/README.md b/README.md index 06ab06d7..1a552012 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,17 @@ # workbench-app-devcontainer -Repo to store Verily Workbench-specific applications' devcontainer specifications. To develop your own custom app configuration, clone this repo. +Repo to store Verily Workbench-specific applications' devcontainer specifications. To develop your own custom app configuration, fork this repo. + +## Repository Structure + +- **`src/`**: Contains devcontainer app templates for various applications (Jupyter, R/RStudio, VSCode, etc.) + - Each subdirectory represents a complete app template with `.devcontainer.json`, `docker-compose.yaml`, and startup scripts + - Example: `src/example/` - A reference implementation showing the basic structure +- **`features/src/`**: Contains reusable devcontainer features that can be included in app templates + - `workbench-tools/` - Bioinformatics tools (plink, plink2, regenie, bcftools, samtools, etc.) + - `java/`, `jupyter/` - Language/framework-specific features +- **`startupscript/`**: VM provisioning scripts that run after container creation +- **`test/`**: Integration tests to verify app templates ## Workbench-specific application requirements @@ -12,11 +23,207 @@ Repo to store Verily Workbench-specific applications' devcontainer specification https://containers.dev/ +## Developing a New App + +### Quick Start (Recommended) + +The fastest way to create a custom app is using the `create-custom-app.sh` script: + +```bash +./scripts/create-custom-app.sh [username] [home-dir] +``` + +**Example** (this created the current example app): +```bash +./scripts/create-custom-app.sh example quay.io/jupyter/base-notebook 8888 jovyan /home/jovyan +``` + +This script generates a complete app structure in `src//` with: +- `.devcontainer.json` - Devcontainer configuration +- `docker-compose.yaml` - Docker Compose setup with ttyd terminal +- `devcontainer-template.json` - Template metadata +- `README.md` - App-specific documentation + +**Using a Dockerfile instead of a Docker image:** + +If you don't have a pre-built Docker image and only have a Dockerfile: + +1. Run the script with an empty image parameter: + ```bash + ./scripts/create-custom-app.sh my-app "" 8888 myuser /home/myuser + ``` + +2. In the generated `src/my-app/docker-compose.yaml`, uncomment the `build` section: + ```yaml + build: + context: . + ``` + +3. Add your `Dockerfile` to `src/my-app/` + +4. Remove the `image:` line from the `docker-compose.yaml` + +**Arguments:** +- `app-name`: Name of your custom app (e.g., `my-jupyter-app`) +- `docker-image`: Docker image to use (e.g., `jupyter/base-notebook`, `rocker/rstudio`) +- `port`: Port your app exposes (e.g., `8888` for Jupyter, `8787` for RStudio) +- `username`: (Optional) User inside container (default: `root`) +- `home-dir`: (Optional) Home directory (default: `/root` or `/home/`) + +After running the script: +1. Review and customize the generated files in `src//` +2. Test your app: `cd test && ./test.sh ` +3. Commit and push to your forked repository +4. Create a custom app in Workbench UI using your repository + +### Manual Setup (Advanced) + +If you need more control, you can manually create a custom app: + +1. **Fork this repository** to your own GitHub account or organization + +2. **Create a new directory** under `src/` for your app (e.g., `src/my-custom-app/`) + +3. **Add required files** to your app directory: + - `.devcontainer.json` - The devcontainer specification that defines your app configuration + - `docker-compose.yaml` - Docker Compose configuration (must follow Workbench requirements above) + - `startup.sh` - App-specific startup script (if needed) + +4. **Configure your `.devcontainer.json`**: + + At a bare minimum, you need to specify: + + - **Docker image**: The base container image your app runs on (e.g., `jupyter/base-notebook`, `rocker/rstudio`) + - **Port**: The port your application exposes (e.g., `8888` for Jupyter, `8787` for RStudio). This port is exposed on the bridge network so Workbench can reach your app + - **Default user**: The username that your application runs as inside the container (e.g., `jovyan` for Jupyter, `rstudio` for RStudio). If your app doesn't have a specific user, you can use `root` + - **Home directory**: The default working directory for the user. In most cases, this is `/home/$(whoami)` (e.g., `/home/jovyan`, `/home/rstudio`). If the default user is `root`, the home directory is typically `/root`. Note: VSCode is a unique case where the home directory is `/config` + + **Important**: The home directory is where Workbench mounts cloud storage buckets and clones GitHub repositories. These will be located at: + - Cloud storage buckets: `${homedir}/workspaces` + - GitHub repositories: `${homedir}/repos` + + Additional configuration: + - Set `postCreateCommand` to run `post-startup.sh` with parameters: `[username, home_dir, ${templateOption:cloud}]` + - Include any needed features from `features/src/` (e.g., `workbench-tools`) + - Use template option `${templateOption:cloud}` to specify the cloud provider (GCP or AWS) + +5. **Test your app**: + - Run the test script: `cd test && ./test.sh ` + - Create a custom app in Workbench UI pointing to your forked repo and branch + +6. **Reference the example app** at [src/example](https://github.com/verily-src/workbench-app-devcontainer/tree/main/src/example) to see a basic implementation + +For detailed guidance, visit https://support.workbench.verily.com/docs/guides/cloud_apps/create_custom_apps/ + +## Running Linux Distros on Workbench + +Linux distributions (Ubuntu, Debian, RHEL, etc.) typically don't have a web UI or exposed port by default. Since Workbench apps must be accessible via a browser, you need to add a web-based interface to your Linux distro container. + +### Recommended Approaches + +#### Option 1: JupyterLab (Recommended for Data Science & General Use) + +JupyterLab provides a full-featured web interface with built-in terminal access, file browser, text editor, and notebook support. + +**Examples**: See the NeMo and Parabricks apps (`src/nemo_jupyter/` and `src/workbench-jupyter-parabricks/`) which use JupyterLab to provide web access to specialized NVIDIA CUDA Linux environments. + +To add JupyterLab to your Linux distro, use the `features/src/jupyter` feature with `installJupyterlab: true` and configure the container command: + +```yaml +# docker-compose.yaml +services: + app: + container_name: "application-server" + image: "ubuntu:22.04" + command: ["jupyter", "lab", "--ip=0.0.0.0", "--port=8888", "--no-browser", "--LabApp.token=''"] + ports: + - 8888:8888 + # ... rest of configuration +``` + +#### Option 2: ttyd (Lightweight Terminal-Only Access) + +If you only need terminal access without the full JupyterLab interface, use [ttyd](https://github.com/tsl0922/ttyd) - a lightweight web-based terminal. + +To add ttyd to your Linux distro, add the [ttyd feature](https://github.com/ar90n/devcontainer-features/tree/main/src/ttyd) to your `.devcontainer.json`: + +```json +// .devcontainer.json +{ + "features": { + "ghcr.io/ar90n/devcontainer-features/ttyd:1": {} + } +} +``` + +Then configure the container command in your `docker-compose.yaml`: + +```yaml +# docker-compose.yaml +services: + app: + container_name: "application-server" + image: "mcr.microsoft.com/devcontainers/base:ubuntu" + user: vscode + command: ["ttyd", "-W", "-p", "7681", "bash"] + ports: + - 7681:7681 + cap_add: + - SYS_ADMIN + devices: + - /dev/fuse + security_opt: + - apparmor:unconfined + # ... rest of configuration +``` + +**Important**: +- The `-W` flag makes the terminal writable (interactive). Without it, the terminal will be read-only. + +#### Option 3: VS Code Server (Full IDE Experience) + +For a full IDE experience, use the [vscode-server feature](https://github.com/devcontainers-extra/features/tree/main/src/vscode-server) which provides VS Code in the browser with built-in terminal access. + +## Debugging and Local Development + +To run and debug your app locally: + +1. **Install the devcontainer CLI**: Follow the installation instructions at https://code.visualstudio.com/docs/devcontainers/devcontainer-cli + +2. **Create the Docker network**: Workbench apps require an external Docker network named `app-network` + ```bash + docker network create app-network + ``` + +3. **Comment out Workbench-specific commands**: For local testing, you should comment out the `postCreateCommand` and `postStartCommand` in your `.devcontainer.json` since these scripts are designed to run in the Workbench environment and may fail locally: + ```json + { + // "postCreateCommand": [ + // "./startupscript/post-startup.sh", + // "username", + // "/home/username", + // "gcp" + // ], + // "postStartCommand": [ + // "./startupscript/remount-on-restart.sh", + // "username", + // "/home/username", + // "gcp" + // ] + } + ``` + +4. **Run your app**: + ```bash + cd src/ + devcontainer up --workspace-folder . + ``` + +5. **Access your app**: Once the container is running, you can access it at `localhost:` where `` is the port you specified in your configuration (e.g., `localhost:8888` for Jupyter, `localhost:7681` for ttyd) + ## How to use The `.devcontainer.json` file in the custom app folder (e.g. r-analysis/) contains the custom app configuration. `post-startup.sh` contains workbench specific set up. Please visit https://support.workbench.verily.com/docs/guides/cloud_apps/create_custom_apps/ for details about using a dev container specification to create a custom app in Workbench. - -For an example app, see [src/jupyter](https://github.com/verily-src/workbench-app-devcontainer/tree/main/src/jupyter). diff --git a/scripts/create-custom-app.sh b/scripts/create-custom-app.sh new file mode 100755 index 00000000..0ea97314 --- /dev/null +++ b/scripts/create-custom-app.sh @@ -0,0 +1,205 @@ +#!/bin/bash +# Script to create a custom Workbench app devcontainer structure +# Usage: ./create-custom-app.sh [username] [home-dir] + +set -o errexit -o nounset -o pipefail -o xtrace + +# Parse arguments +if [ $# -lt 3 ]; then + echo "Usage: $0 [username] [home-dir]" + echo "" + echo "Arguments:" + echo " app-name - Name of your custom app (e.g., my-jupyter-app)" + echo " docker-image - Docker image to use (e.g., jupyter/base-notebook)" + echo " port - Port your app exposes (e.g., 8888)" + echo " username - (Optional) User inside container (default: root)" + echo " home-dir - (Optional) Home directory (default: /root or /home/)" + echo "" + echo "Example:" + echo " $0 my-jupyter jupyter/base-notebook 8888 jovyan /home/jovyan" + exit 1 +fi + +readonly APP_NAME="$1" +readonly DOCKER_IMAGE="$2" +readonly PORT="$3" +readonly USERNAME="${4:-root}" + +# Calculate home directory if not provided +if [ $# -ge 5 ]; then + readonly HOME_DIR="$5" +else + if [ "$USERNAME" = "root" ]; then + readonly HOME_DIR="/root" + else + readonly HOME_DIR="/home/$USERNAME" + fi +fi +readonly -f + +readonly APP_DIR="src/${APP_NAME}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +readonly REPO_ROOT + +# Create app directory +echo "Creating app directory: ${APP_DIR}" +mkdir -p "${REPO_ROOT}/${APP_DIR}" + +# Generate .devcontainer.json +echo "Generating .devcontainer.json" +cat > "${REPO_ROOT}/${APP_DIR}/.devcontainer.json" < "${REPO_ROOT}/${APP_DIR}/docker-compose.yaml" < "${REPO_ROOT}/${APP_DIR}/devcontainer-template.json" < "${REPO_ROOT}/${APP_DIR}/README.md" </dev/null 2>&1 && pwd )" + REPO_ROOT="$(cd "${DIR}/../.." && pwd)" + SCRIPT="${REPO_ROOT}/scripts/create-custom-app.sh" + + export REPO_ROOT + export SCRIPT + + # Work from repo root so script can create files correctly + cd "${REPO_ROOT}" +} + +teardown() { + # Clean up any test apps created in src/ + if [ -d "${REPO_ROOT}/src/test-app" ]; then + rm -rf "${REPO_ROOT}/src/test-app" + fi + if [ -d "${REPO_ROOT}/src/my-jupyter" ]; then + rm -rf "${REPO_ROOT}/src/my-jupyter" + fi +} + +@test "create-custom-app.sh: shows usage when no arguments provided" { + run bash "${SCRIPT}" + [ "$status" -eq 1 ] + [[ "$output" == *"Usage:"* ]] + [[ "$output" == *"app-name"* ]] + [[ "$output" == *"docker-image"* ]] + [[ "$output" == *"port"* ]] +} + +@test "create-custom-app.sh: shows usage when insufficient arguments provided" { + run bash "${SCRIPT}" my-app + [ "$status" -eq 1 ] + [[ "$output" == *"Usage:"* ]] +} + +@test "create-custom-app.sh: creates app with minimal arguments (defaults to root user)" { + run bash "${SCRIPT}" test-app python:3.11 8080 + [ "$status" -eq 0 ] + [ -d "src/test-app" ] + [ -f "src/test-app/.devcontainer.json" ] + [ -f "src/test-app/docker-compose.yaml" ] + [ -f "src/test-app/devcontainer-template.json" ] + [ -f "src/test-app/README.md" ] +} + +@test "create-custom-app.sh: creates app with custom username and home directory" { + run bash "${SCRIPT}" my-jupyter jupyter/base-notebook 8888 jovyan /home/jovyan + [ "$status" -eq 0 ] + [ -d "src/my-jupyter" ] +} + +@test "create-custom-app.sh: .devcontainer.json contains correct values" { + bash "${SCRIPT}" test-app python:3.11 8080 testuser /home/testuser + + # Check that .devcontainer.json has the correct structure + [ -f "src/test-app/.devcontainer.json" ] + + # Verify it contains direct values (not template options) + grep -q '"testuser"' "src/test-app/.devcontainer.json" + grep -q '"/home/testuser"' "src/test-app/.devcontainer.json" + + # Verify it still contains cloud template option (supported by parser) + grep -q '${templateOption:cloud}' "src/test-app/.devcontainer.json" + + # Verify postCreateCommand exists + grep -q 'postCreateCommand' "src/test-app/.devcontainer.json" + + # Verify postStartCommand exists + grep -q 'postStartCommand' "src/test-app/.devcontainer.json" +} + +@test "create-custom-app.sh: docker-compose.yaml contains correct direct values" { + bash "${SCRIPT}" test-app python:3.11 8080 testuser /home/testuser + + [ -f "src/test-app/docker-compose.yaml" ] + + # Check for direct values (not template variables) + grep -q 'image: "python:3.11"' "src/test-app/docker-compose.yaml" + grep -q '8080:8080' "src/test-app/docker-compose.yaml" + grep -q 'work:/home/testuser/work' "src/test-app/docker-compose.yaml" + + # Check for required workbench settings + grep -q 'container_name: "application-server"' "src/test-app/docker-compose.yaml" + grep -q 'app-network' "src/test-app/docker-compose.yaml" +} + +@test "create-custom-app.sh: devcontainer-template.json has correct structure" { + bash "${SCRIPT}" my-jupyter jupyter/base-notebook 8888 jovyan /home/jovyan + + [ -f "src/my-jupyter/devcontainer-template.json" ] + + # Check for the app id + grep -q '"id": "my-jupyter"' "src/my-jupyter/devcontainer-template.json" + + # Check that description includes image, port, and user info + grep -q 'jupyter/base-notebook' "src/my-jupyter/devcontainer-template.json" + grep -q '8888' "src/my-jupyter/devcontainer-template.json" + grep -q 'jovyan' "src/my-jupyter/devcontainer-template.json" + + # Check for cloud option (the only remaining template option) + grep -q '"cloud"' "src/my-jupyter/devcontainer-template.json" + grep -q '"default": "gcp"' "src/my-jupyter/devcontainer-template.json" +} + +@test "create-custom-app.sh: README.md is generated with correct content" { + bash "${SCRIPT}" test-app python:3.11 8080 testuser /home/testuser + + [ -f "src/test-app/README.md" ] + + # Check for app name in README + grep -q 'test-app' "src/test-app/README.md" + + # Check for image reference + grep -q 'python:3.11' "src/test-app/README.md" + + # Check for port reference + grep -q '8080' "src/test-app/README.md" + + # Check for username reference + grep -q 'testuser' "src/test-app/README.md" + + # Check for home directory reference + grep -q '/home/testuser' "src/test-app/README.md" +} + +@test "create-custom-app.sh: uses /root as home dir when user is root" { + bash "${SCRIPT}" test-app python:3.11 8080 root + + [ -f "src/test-app/.devcontainer.json" ] + [ -f "src/test-app/docker-compose.yaml" ] + + # Check that homeDir is /root for root user in generated files + grep -q '"/root"' "src/test-app/.devcontainer.json" + grep -q 'work:/root/work' "src/test-app/docker-compose.yaml" +} + +@test "create-custom-app.sh: uses /home/username as home dir when user is not root" { + bash "${SCRIPT}" test-app python:3.11 8080 myuser + + [ -f "src/test-app/.devcontainer.json" ] + [ -f "src/test-app/docker-compose.yaml" ] + + # Check that homeDir is /home/myuser in generated files + grep -q '"/home/myuser"' "src/test-app/.devcontainer.json" + grep -q 'work:/home/myuser/work' "src/test-app/docker-compose.yaml" +} + +@test "create-custom-app.sh: all generated files are valid JSON" { + bash "${SCRIPT}" test-app python:3.11 8080 + + # Validate .devcontainer.json + run python3 -m json.tool "src/test-app/.devcontainer.json" + [ "$status" -eq 0 ] + + # Validate devcontainer-template.json + run python3 -m json.tool "src/test-app/devcontainer-template.json" + [ "$status" -eq 0 ] +} + +@test "create-custom-app.sh: output message confirms creation" { + run bash "${SCRIPT}" test-app python:3.11 8080 + [ "$status" -eq 0 ] + [[ "$output" == *"Custom app created successfully"* ]] + [[ "$output" == *"src/test-app"* ]] +} diff --git a/src/example/.devcontainer.json b/src/example/.devcontainer.json new file mode 100644 index 00000000..1c03f85c --- /dev/null +++ b/src/example/.devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "example", + "dockerComposeFile": "docker-compose.yaml", + "service": "app", + "shutdownAction": "none", + "workspaceFolder": "/workspace", + "postCreateCommand": [ + "./startupscript/post-startup.sh", + "jovyan", + "/home/jovyan", + "${templateOption:cloud}", + "${templateOption:login}" + ], + "postStartCommand": [ + "./startupscript/remount-on-restart.sh", + "jovyan", + "/home/jovyan", + "${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/example/README.md b/src/example/README.md new file mode 100644 index 00000000..429d4844 --- /dev/null +++ b/src/example/README.md @@ -0,0 +1,43 @@ +# example + +Custom Workbench application based on quay.io/jupyter/base-notebook. + +## Configuration + +- **Image**: quay.io/jupyter/base-notebook +- **Port**: 8888 +- **User**: jovyan +- **Home Directory**: /home/jovyan + +## Access + +Once deployed in Workbench, access your terminal at the app URL (port 8888). + +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:8888` + +## 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 ttyd options) +- `devcontainer-template.json` - Template options and metadata + +## Testing + +To test this app template: + +```bash +cd test +./test.sh example +``` + +## 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 (example) diff --git a/src/example/devcontainer-template.json b/src/example/devcontainer-template.json new file mode 100644 index 00000000..027ce340 --- /dev/null +++ b/src/example/devcontainer-template.json @@ -0,0 +1,20 @@ +{ + "id": "example", + "version": "1.0.0", + "name": "example", + "description": "Custom Workbench app: example (Image: quay.io/jupyter/base-notebook, Port: 8888, User: jovyan)", + "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/jupyter/docker-compose.yaml b/src/example/docker-compose.yaml similarity index 86% rename from src/jupyter/docker-compose.yaml rename to src/example/docker-compose.yaml index c6f03276..d5975834 100644 --- a/src/jupyter/docker-compose.yaml +++ b/src/example/docker-compose.yaml @@ -6,8 +6,6 @@ services: image: "quay.io/jupyter/base-notebook" # build: # context: . - environment: - NOTEBOOK_ARGS: "--ServerApp.token='' --ServerApp.password='' --ServerApp.root_dir=/home/jovyan/work --ServerApp.allow_origin='*'" restart: always volumes: - .:/workspace:cached diff --git a/src/jupyter/.devcontainer.json b/src/jupyter/.devcontainer.json deleted file mode 100644 index b55e15a0..00000000 --- a/src/jupyter/.devcontainer.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "Workbench Example Jupyter App", - "dockerComposeFile": "docker-compose.yaml", - "service": "app", - "shutdownAction": "none", - "workspaceFolder": "/workspace", - // This post create command is required to set up the user environment with - // the Workbench CLI and mount workspace resources. To add additional setup - // commands, specify a string shell command instead. - // "postCreateCommand": "./startupscript/post-startup.sh 'jovyan' '/home/jovyan' '${templateOption:cloud}' '${templateOption:login}' && echo 'Additional setup commands can go here.'", - "postCreateCommand": [ - "./startupscript/post-startup.sh", - "jovyan", // This must be the user inside the container and their home directory. - "/home/jovyan", - "${templateOption:cloud}", - "${templateOption:login}" - ], - // Re-mount workspace resources on container start up - "postStartCommand": [ - "./startupscript/remount-on-restart.sh", - "jovyan", - "/home/jovyan", - "${templateOption:cloud}", - "${templateOption:login}" - ], - "features": { - // The below features are required for the Workbench CLI. If the image - // already comes with these features, they can be omitted. Feel free to add - // other devcontainer features as desired (see - // https://containers.dev/features). - "ghcr.io/devcontainers/features/java:1": { - "version": "17" - }, - "ghcr.io/devcontainers/features/aws-cli:1": {}, - "ghcr.io/dhoeric/features/google-cloud-cli:1": {} - }, - // This is the user that will run the post create and post start commands. - "remoteUser": "root", - "customizations": { - "workbench": { - "opens": { - "extensions": [ - // Source - ".ipynb", - ".R", - ".py", - // Documents - ".md", - ".html", - ".latex", - ".pdf", - // Images - ".bmp", - ".gif", - ".jpeg", - ".jpg", - ".png", - ".svg", - // Data - ".csv", - ".tsv", - ".json", - ".vl" - ], - "fileUrlSuffix": "/lab/tree/{path}", - "folderUrlSuffix": "/lab/tree/{path}" - } - } - } -} diff --git a/src/jupyter/README.md b/src/jupyter/README.md deleted file mode 100644 index f5792aa0..00000000 --- a/src/jupyter/README.md +++ /dev/null @@ -1,17 +0,0 @@ - -# Workbench Example Jupyter App (jupyter) - -An example app containing a JupyterLab image - -## Options - -| Options Id | Description | Type | Default Value | -|-----|-----|-----|-----| -| cloud | VM cloud environment | string | gcp | -| login | Whether to log in to workbench CLI | string | false | - - - ---- - -_Note: This file was auto-generated from the [devcontainer-template.json](devcontainer-template.json). Add additional notes to a `NOTES.md`._ diff --git a/src/jupyter/devcontainer-template.json b/src/jupyter/devcontainer-template.json deleted file mode 100644 index 1efcce78..00000000 --- a/src/jupyter/devcontainer-template.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "id": "jupyter", - "description": "An example app containing a JupyterLab image", - "version": "0.0.1", - "name": "Workbench Example Jupyter App", - "documentationURL": "https://github.com/verily-src/workbench-app-devcontainers/tree/master/src/jupyter", - "licenseURL": "https://github.com/verily-src/workbench-app-devcontainers/blob/master/LICENSE", - "options": { - "cloud": { - "type": "string", - "description": "VM cloud environment", - "proposals": ["gcp", "aws"], - "default": "gcp" - }, - "login": { - "type": "string", - "description": "Whether to log in to workbench CLI", - "proposals": ["true", "false"], - "default": "false" - } - }, - "platforms": ["Any"] -} diff --git a/src/ubuntu-example/.devcontainer.json b/src/ubuntu-example/.devcontainer.json new file mode 100644 index 00000000..ef8819b1 --- /dev/null +++ b/src/ubuntu-example/.devcontainer.json @@ -0,0 +1,30 @@ +{ + "name": "ubuntu-example", + "dockerComposeFile": "docker-compose.yaml", + "service": "app", + "shutdownAction": "none", + "workspaceFolder": "/workspace", + "postCreateCommand": [ + "./startupscript/post-startup.sh", + "vscode", + "/home/vscode", + "${templateOption:cloud}", + "${templateOption:login}" + ], + "postStartCommand": [ + "./startupscript/remount-on-restart.sh", + "vscode", + "/home/vscode", + "${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": {}, + "ghcr.io/ar90n/devcontainer-features/ttyd:1": {} + }, + "remoteUser": "root" +} diff --git a/src/ubuntu-example/README.md b/src/ubuntu-example/README.md new file mode 100644 index 00000000..e4b5aaa8 --- /dev/null +++ b/src/ubuntu-example/README.md @@ -0,0 +1,67 @@ +# ubuntu-example + +Custom Workbench application based on mcr.microsoft.com/devcontainers/base:ubuntu. + +## Configuration + +- **Image**: mcr.microsoft.com/devcontainers/base:ubuntu +- **Port**: 7681 +- **User**: vscode +- **Home Directory**: /home/vscode + +## Access + +This app uses [ttyd](https://github.com/tsl0922/ttyd) to provide web-based terminal access. + +Once deployed in Workbench, access your terminal at the app URL (port 7681). + +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:7681` + +## Development + +This app was created as a reference implementation for building custom distro image apps: + +1. **Initial scaffold** - Created using the `scripts/create-custom-app.sh` script: + ```bash + ./scripts/create-custom-app.sh ubuntu-example mcr.microsoft.com/devcontainers/base:ubuntu 7681 vscode /home/vscode + ``` + +2. **Added ttyd feature** - Modified `.devcontainer.json` to include the ttyd devcontainer feature: + ```json + "ghcr.io/ar90n/devcontainer-features/ttyd:1": {} + ``` + +3. **Configured user** - Added `user: vscode` in `docker-compose.yaml` (line 13) because the default user is root, but we want to run as the vscode user for better permissions handling. + +4. **Added ttyd command** - Added the ttyd startup command in `docker-compose.yaml` (line 14): + ```yaml + command: ["ttyd", "-W", "-p", "7681", "bash"] + ``` + This starts ttyd with web terminal access on port 7681. Since we're already running as the vscode user (via `user: vscode` on line 13), we can start bash directly. + +## 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 ttyd options) +- `devcontainer-template.json` - Template options and metadata + +## Testing + +To test this app template: + +```bash +cd test +./test.sh ubuntu-example +``` + +## 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 (ubuntu-example) diff --git a/src/ubuntu-example/devcontainer-template.json b/src/ubuntu-example/devcontainer-template.json new file mode 100644 index 00000000..3c68d269 --- /dev/null +++ b/src/ubuntu-example/devcontainer-template.json @@ -0,0 +1,20 @@ +{ + "id": "ubuntu-example", + "version": "1.0.0", + "name": "ubuntu-example", + "description": "Custom Workbench app: ubuntu-example (Image: mcr.microsoft.com/devcontainers/base:ubuntu, Port: 7681, User: vscode)", + "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/ubuntu-example/docker-compose.yaml b/src/ubuntu-example/docker-compose.yaml new file mode 100644 index 00000000..10d7ecab --- /dev/null +++ b/src/ubuntu-example/docker-compose.yaml @@ -0,0 +1,38 @@ +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: "mcr.microsoft.com/devcontainers/base:ubuntu" + # build: + # context: . + restart: always + volumes: + - .:/workspace:cached + - work:/home/vscode/work + user: vscode + command: ["ttyd", "-W", "-p", "7681", "bash"] + # The port specified here will be forwarded and accessible from the + # Workbench UI. + ports: + - 7681:7681 + # 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