diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..b5fcf79 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,192 @@ +# ShinyProxy Operator - Copilot Instructions + +## Project Overview + +ShinyProxy Operator is a Kubernetes/Docker operator for managing ShinyProxy deployments. It's written in Kotlin and uses coroutines for asynchronous operations. + +## Technology Stack + +- **Language**: Kotlin 2.1.10 with language version 2.1 +- **JDK**: Java 21 (required) +- **Build Tool**: Maven 3.x +- **Container Orchestration**: Kubernetes (via Fabric8 client) and Docker +- **Logging**: kotlin-logging with Log4j 2 +- **Testing**: JUnit 5 (Jupiter) for unit and integration tests +- **Async**: Kotlin Coroutines + +## Build and Test Commands + +### Building the Project +```bash +devbox run mvn -U clean install -DskipTests +``` + +The build produces: `target/shinyproxy-operator-jar-with-dependencies.jar` + +### Running Tests +```bash +devbox run mvn test +``` + +Tests require: +- Docker network: `sp-shared-network` +- Docker plugin: `grafana/loki-docker-driver:3.2.1` +- Environment variable: `SPO_DOCKER_GID` (group ID for docker group) + +### License Header Management +```bash +devbox run mvn validate license:format +``` + +Automatically updates copyright headers. Year updates are handled automatically - don't change year manually. + +## Code Style and Conventions + +### File Structure +- Source code: `src/main/kotlin/` +- Test code: `src/test/kotlin/` +- Package: `eu.openanalytics.shinyproxyoperator` + +### Formatting Guidelines +- **Indentation**: 4 spaces (not tabs) +- **Max line length**: 120 characters +- **Charset**: UTF-8 +- **Line endings**: LF (Unix-style) +- **Trailing whitespace**: Remove +- **Final newline**: Always insert + +### Kotlin Conventions +- Use `object` for singletons (e.g., `LabelFactory`) +- Use `const val` for compile-time constants +- Prefer `when` expressions over if-else chains +- Use `kotlinx.coroutines` for async operations +- Use `suspend` functions appropriately +- Use kotlin-logging's KotlinLogging for logging: `private val logger = KotlinLogging.logger {}` + +### Copyright Headers +Every Kotlin source file must include the Apache License 2.0 header. Use the template in `LICENSE_HEADER`. The license plugin handles year updates automatically. + +Example header structure: +```kotlin +/* + * ShinyProxy-Operator + * + * Copyright (C) 2021-2025 Open Analytics + * + * =========================================================================== + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License as published by + * The Apache Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Apache License for more details. + * + * You should have received a copy of the Apache License + * along with this program. If not, see + */ +``` + +## Architecture + +### Core Components +- **Operator Interface**: `IOperator` - main entry point +- **Orchestrators**: Platform-specific implementations + - `KubernetesOperator`: For Kubernetes deployments + - `DockerOperator`: For Docker deployments +- **Event System**: `ShinyProxyEvent`, `ShinyProxyEventType`, `IEventController` +- **Model**: `ShinyProxy`, `ShinyProxyInstance`, `ShinyProxyStatus` + +### Configuration +- Configuration via environment variables (see `Config` class) +- Key variable: `SPO_ORCHESTRATOR` - determines platform (kubernetes/docker) + +### Implementations +- `src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/kubernetes/` - Kubernetes-specific code +- `src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/` - Docker-specific code + +## Testing + +### Test Structure +- Integration tests: `MainIntegrationTest.kt` in both impl/kubernetes and impl/docker +- Helper classes in `src/test/kotlin/eu/openanalytics/shinyproxyoperator/helpers/` +- Test base classes provide setup/teardown functionality + +### Testing Patterns +- Use `@Test` annotation from JUnit 5 +- Use Kotlin test assertions: `assertEquals`, `assertTrue`, `assertFalse`, `assertNotNull`, `assertNull` +- Integration tests use `setup()` helper with closures +- Tests use coroutines with `withTimeout` for async operations +- Helper extensions in `helpers/extensions.kt` + +### Test Requirements +- Tests run in a Minikube environment (for Kubernetes tests) +- Docker daemon required (for Docker tests) +- Specific images pre-pulled before tests + +## Dependencies + +### Key Libraries +- **Fabric8 Kubernetes Client** (7.1.0): Kubernetes API interactions +- **Docker Client** (7.0.8-OA-5): Docker API interactions (custom OpenAnalytics version) +- **Jackson** (2.18.3): JSON processing +- **Log4j** (2.24.3): Logging backend +- **Kotlin Coroutines** (1.10.1): Async programming +- **Kotlin** (2.1.10): Kotlin language and standard library +- **JUnit Jupiter** (5.11.4): Testing framework + +### Repository Configuration +The project uses OpenAnalytics Nexus repositories for the custom Docker client version: +- Releases: https://nexus.openanalytics.eu/repository/releases +- Snapshots: https://nexus.openanalytics.eu/repository/snapshots + +## Common Patterns + +### Logging +```kotlin +private val logger = KotlinLogging.logger {} +logger.info { "Message with lazy evaluation" } +logger.warn { "Warning: ${variable}" } +``` + +### Error Handling +- Use `InternalException` for operator-specific errors +- Catch and log exceptions appropriately +- Use `exitProcess(1)` for fatal errors in main + +### Labels +Use `LabelFactory` for consistent label creation: +- `labelsForShinyProxyInstance()` - for instances +- `labelsForShinyProxy()` - for resources + +### Configuration Reading +```kotlin +val value = config.readConfigValue(default, "ENV_VAR_NAME") { it.lowercase() } +``` + +## Documentation + +Main documentation is hosted at: https://shinyproxy.io/documentation/shinyproxy-operator/ + +Additional docs in `docs/` directory: +- Deployment configurations +- Prometheus integration + +## CI/CD + +GitHub Actions workflow in `.github/workflows/workflows.yaml`: +- Runs on Java 21 +- Tests against multiple Kubernetes versions (1.30.11, 1.31.7, 1.32.3) +- Uses Minikube for integration tests +- Caches Maven dependencies + +## Important Notes + +- This is a critical infrastructure component for ShinyProxy deployments +- Changes should maintain backward compatibility +- Integration tests are comprehensive but can be time-consuming +- The operator manages both Kubernetes and Docker environments +- Resource cleanup is important in tests to prevent test pollution diff --git a/.github/workflows/build-and-push-docker.yaml b/.github/workflows/build-and-push-docker.yaml new file mode 100644 index 0000000..af2b926 --- /dev/null +++ b/.github/workflows/build-and-push-docker.yaml @@ -0,0 +1,100 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - master + - dev + tags: + - 'v*' + pull_request: + branches: + - master + - dev + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout shinyproxy-operator + uses: actions/checkout@v4 + with: + path: shinyproxy-operator + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + cache: 'maven' + cache-dependency-path: 'shinyproxy-operator/pom.xml' + + - name: Build shinyproxy-operator JAR + run: | + cd shinyproxy-operator + mvn -U clean install -DskipTests + + - name: Extract version from JAR + id: version + run: | + VERSION=$(unzip -p shinyproxy-operator/target/shinyproxy-operator-jar-with-dependencies.jar META-INF/MANIFEST.MF | grep Implementation-Version | cut -d' ' -f2 | tr -d '\r') + echo "full=$VERSION" >> $GITHUB_OUTPUT + MAJOR=$(echo $VERSION | cut -d. -f1) + echo "major=$MAJOR" >> $GITHUB_OUTPUT + MINOR=$(echo $VERSION | cut -d. -f2) + echo "minor=$MINOR" >> $GITHUB_OUTPUT + echo "Version: $VERSION (Major: $MAJOR, Minor: $MINOR)" + + - name: Checkout shinyproxy-docker + uses: actions/checkout@v4 + with: + repository: openanalytics/shinyproxy-docker + path: shinyproxy-docker + + - name: Copy JAR to Docker build context + run: | + cp shinyproxy-operator/target/shinyproxy-operator-jar-with-dependencies.jar shinyproxy-docker/Operator/ + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ secrets.CR_REGISTRY }} + username: ${{ secrets.CR_USERNAME }} + password: ${{ secrets.CR_PASSWORD }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.CR_REGISTRY }}/shinyproxy-operator + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=dev-${{ steps.version.outputs.full }},enable=${{ github.ref == 'refs/heads/dev' }} + type=raw,value=dev-${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }},enable=${{ github.ref == 'refs/heads/dev' }} + type=raw,value=dev-${{ steps.version.outputs.major }},enable=${{ github.ref == 'refs/heads/dev' }} + type=sha,format=short + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: shinyproxy-docker/Operator + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + JAR_LOCATION=shinyproxy-operator-jar-with-dependencies.jar + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..041404e --- /dev/null +++ b/BUILD.md @@ -0,0 +1,145 @@ +# Building ShinyProxy Operator + +This guide provides multiple options for building the ShinyProxy Operator without needing to install build tools locally. + +## Prerequisites + +Choose one of the following options: +- **Option 1**: Docker (recommended for most users) +- **Option 2**: Devbox (for development environments) +- **Option 3**: Local build (requires JDK 21 and Maven) + +--- + +## Option 1: Build with Docker (Recommended) + +This is the easiest option if you have Docker installed. + +```bash +docker build -f Dockerfile.build --target artifacts -t shinyproxy-operator-builder -o ./. . + +# the jar will be at ./shinyproxy-operator-jar-with-dependencies.jar +``` + +## Option 2: Build with Devbox + +[Devbox](https://www.jetify.com/devbox) provides an isolated development environment without Docker. + +### Install Devbox + +```bash +curl -fsSL https://get.jetify.com/devbox | bash +``` + +### Build the Project + +```bash +# Enter the devbox shell (installs JDK 21 and Maven automatically) +devbox shell + +# Install the needed dependencies +devbox run install-deps + +# Build the project +devbox run build + +# Or build without tests (faster) +devbox run build-fast + +# Or to use previous build steps (faster still) +devbox run package-fast + +# The JAR will be at ./target/shinyproxy-operator-jar-with-dependencies.jar +``` + +## Verifying the Build + +After building, verify the JAR exists: + +```bash +# if using docker +ls -lh shinyproxy-operator-jar-with-dependencies.jar + +# if using devbox +ls -lh target/shinyproxy-operator-jar-with-dependencies.jar +``` + +You should see a file of approximately 50-100 MB. + +## Building a Docker Image + +This needs to be copied into the image at https://github.com/openanalytics/shinyproxy-docker/. + +First clonse the repo + +```bash +# clone the repo +git clone git@github.com:openanalytics/shinyproxy-docker.git +cd shinyproxy-docker +``` + +Copy the jar and build the image + +```bash +# copy the jar into the build context +cp ../shinyproxy-operator/shinyproxy-operator-jar-with-dependencies.jar Operator/. + +# build the docker image +docker build -t shinyproxy-operator-dev:2.3.1 --build-arg JAR_LOCATION=shinyproxy-operator-jar-with-dependencies.jar Operator +``` + +## Troubleshooting + +### Docker build fails with network errors + +If you experience network issues during the Maven build: + +```bash +# Build with host network (Linux only) +docker build --network=host -f Dockerfile.build --target artifacts -t shinyproxy-operator-builder -o ./. . +``` + +### Permission issues with target directory + +```bash +# On Linux/Mac, you might need to fix permissions +sudo chown -R $USER:$USER target/ +``` + +### Out of memory during build + +```bash +# Increase Docker memory limit or add Maven options +docker build -f Dockerfile.build --target artifacts --build-arg MAVEN_OPTS="-Xmx2048m" -t shinyproxy-operator-builder -o ./. . +``` + +--- + +## Development Workflow + +For ongoing development: + +1. **Use Devbox** for the best development experience with automatic dependency management +1. **Use Docker** for one-off builds without installing tools + +--- + +## Testing Your Changes + +After building with your environment variable changes: + +1. Create a test docker-compose.yml with the operator +2. Set `SHINYPROXY_ENV_*` environment variables +3. Verify they are passed to the ShinyProxy container + +Example: +```yaml +services: + shinyproxy-operator: + image: shinyproxy-operator-dev:v2.3.1 + environment: + - SPO_DOCKER_GID=999 + - SHINYPROXY_ENV_TEST_VAR=test_value + volumes: + - /var/run/docker.sock:/var/run/docker.sock +``` diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 0000000..eadce36 --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,23 @@ +# Dockerfile for building the ShinyProxy Operator +# This provides a containerized build environment with all necessary tools + +FROM maven:3.9.9-eclipse-temurin-21 AS builder + +# Set working directory +WORKDIR /build + +# Copy project files +COPY pom.xml . + +# Download dependencies (this layer will be cached if pom.xml doesn't change) +RUN mvn dependency:go-offline -B + +COPY src ./src +COPY LICENSE_HEADER . + +# Build the project +RUN mvn clean package -DskipTests + +# Export stage - only contains the JAR artifact +FROM scratch AS artifacts +COPY --from=builder /build/target/shinyproxy-operator-jar-with-dependencies.jar /shinyproxy-operator-jar-with-dependencies.jar diff --git a/README.md b/README.md index a222754..85ad571 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ page in the documentation to understand why this is so great. See the [website](https://shinyproxy.io/documentation/shinyproxy-operator/kubernetes/) for all documentation. +### Docker Mode: Passing Environment Variables + +When using the operator in Docker mode, you can pass environment variables from the operator's environment to ShinyProxy containers. This is useful for injecting secrets without storing them in plain text. See [Environment Variables for Docker Mode](docs/environment-variables-docker.md) for details. + ## Support See the [website](https://shinyproxy.io/support/) on how to get support. @@ -35,3 +39,17 @@ The build will result in a single `.jar` file: ## Java Version This project requires JDK 21. + +## CI/CD + +This repository includes automated GitHub Actions workflows for building and deploying the ShinyProxy Operator. + +### Docker Image Build and Push + +A workflow automatically builds the operator JAR, creates a Docker image, and pushes it to a Container Registry. See [docs/CI-CD.md](docs/CI-CD.md) for detailed documentation on: + +- Workflow configuration +- Required secrets setup +- Docker image tags +- Manual workflow dispatch +- Local development replication diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..e2d62e4 --- /dev/null +++ b/devbox.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json", + "packages": [ + "javaPackages.compiler.openjdk21@latest", + "binutils@latest", + "maven@latest" + ], + "shell": { + "init_hook": [ + "echo 'Welcome to ShinyProxy Operator development environment!'", + "echo 'Java version:'", + "java -version", + "echo ''", + "echo 'Maven version:'", + "mvn -version", + "echo ''", + "echo 'To build the project, run: mvn clean package -DskipTests'", + "echo 'The JAR will be at: target/shinyproxy-operator-jar-with-dependencies.jar'" + ], + "scripts": { + "install-deps": [ + "mvn dependency:go-offline -B" + ], + "build": [ + "mvn clean package" + ], + "build-fast": [ + "mvn clean package -DskipTests" + ], + "package": [ + "mvn package" + ], + "package-fast": [ + "mvn package -DskipTests" + ], + "clean-compile": [ + "mvn clean compile" + ], + "clean-compile-fast": [ + "mvn clean compile -DskipTests" + ], + "test": [ + "mvn test" + ] + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..29c703b --- /dev/null +++ b/devbox.lock @@ -0,0 +1,177 @@ +{ + "lockfile_version": "1", + "packages": { + "binutils@latest": { + "last_modified": "2025-11-23T21:50:36Z", + "resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#binutils", + "source": "devbox-search", + "version": "2.44", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/0hz32dza8cmfq4i823mcz4kqpk54bils-binutils-wrapper-2.44", + "default": true + }, + { + "name": "man", + "path": "/nix/store/16cprzg9s7bw501rppw2bcmf3wpwsxw8-binutils-wrapper-2.44-man", + "default": true + }, + { + "name": "info", + "path": "/nix/store/y9kanlp4jzfzwjriga6jplw8jk9hx5dk-binutils-wrapper-2.44-info" + } + ], + "store_path": "/nix/store/0hz32dza8cmfq4i823mcz4kqpk54bils-binutils-wrapper-2.44" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/fhmb82a3q7nbsz3w9yj28nyscykrj87p-binutils-wrapper-2.44", + "default": true + }, + { + "name": "man", + "path": "/nix/store/136rg4hbbzzgfk9fzpih43wnsb7k04q5-binutils-wrapper-2.44-man", + "default": true + }, + { + "name": "info", + "path": "/nix/store/glmqwm69w9b7wdil3iqncr1ach6py0zh-binutils-wrapper-2.44-info" + } + ], + "store_path": "/nix/store/fhmb82a3q7nbsz3w9yj28nyscykrj87p-binutils-wrapper-2.44" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jprgd6iai892g6dhajv9k9izxwin5g74-binutils-wrapper-2.44", + "default": true + }, + { + "name": "man", + "path": "/nix/store/g4xhkh8nh9c61xfd428a96c5j97gzyd1-binutils-wrapper-2.44-man", + "default": true + }, + { + "name": "info", + "path": "/nix/store/75plj78svg9ala98s5cvk8dma4bd7syn-binutils-wrapper-2.44-info" + } + ], + "store_path": "/nix/store/jprgd6iai892g6dhajv9k9izxwin5g74-binutils-wrapper-2.44" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/xwydcyvlsa3cvssk0y5llgdhlhjvmqdm-binutils-wrapper-2.44", + "default": true + }, + { + "name": "man", + "path": "/nix/store/2gjcdfkx83vgaszic2vn2cnsfl5n1mzk-binutils-wrapper-2.44-man", + "default": true + }, + { + "name": "info", + "path": "/nix/store/5c4dylia9lsg6qbp3a9rdvv9nr0xp3jz-binutils-wrapper-2.44-info" + } + ], + "store_path": "/nix/store/xwydcyvlsa3cvssk0y5llgdhlhjvmqdm-binutils-wrapper-2.44" + } + } + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2025-12-03T20:43:00Z", + "resolved": "github:NixOS/nixpkgs/ebc94f855ef25347c314258c10393a92794e7ab9?lastModified=1764794580&narHash=sha256-UMVihg0OQ980YqmOAPz%2BzkuCEb9hpE5Xj2v%2BZGNjQ%2BM%3D" + }, + "javaPackages.compiler.openjdk21@latest": { + "last_modified": "2025-11-23T21:50:36Z", + "resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#javaPackages.compiler.openjdk21", + "source": "devbox-search", + "version": "21.0.9+10", + "systems": { + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jlxgl3k14x3zhfa00rsygfw3fgp6xgg8-openjdk-21.0.9+10", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/qmczwsgqyfq49baj86rwza8nc49ag282-openjdk-21.0.9+10-debug" + } + ], + "store_path": "/nix/store/jlxgl3k14x3zhfa00rsygfw3fgp6xgg8-openjdk-21.0.9+10" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/65qpdkc33j5wqzxvz8c23zhgms8hl35y-openjdk-21.0.9+10", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/iqrpym2396ppr5498xci88zf6kc37yxl-openjdk-21.0.9+10-debug" + } + ], + "store_path": "/nix/store/65qpdkc33j5wqzxvz8c23zhgms8hl35y-openjdk-21.0.9+10" + } + } + }, + "maven@latest": { + "last_modified": "2025-11-23T21:50:36Z", + "resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#maven", + "source": "devbox-search", + "version": "3.9.11", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/xqja439g5mrggz87a4f4hp9rdwjmkwj4-maven-3.9.11", + "default": true + } + ], + "store_path": "/nix/store/xqja439g5mrggz87a4f4hp9rdwjmkwj4-maven-3.9.11" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/sjhc4f9kvcr9vz3skyl3dvhir2dby2f1-maven-3.9.11", + "default": true + } + ], + "store_path": "/nix/store/sjhc4f9kvcr9vz3skyl3dvhir2dby2f1-maven-3.9.11" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/90qdbpmykzmgxm2df56axf8lxh1bdv5k-maven-3.9.11", + "default": true + } + ], + "store_path": "/nix/store/90qdbpmykzmgxm2df56axf8lxh1bdv5k-maven-3.9.11" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/4djac4688b16msvhkmyp217ad9kc1m5s-maven-3.9.11", + "default": true + } + ], + "store_path": "/nix/store/4djac4688b16msvhkmyp217ad9kc1m5s-maven-3.9.11" + } + } + } + } +} diff --git a/docs/CI-CD.md b/docs/CI-CD.md new file mode 100644 index 0000000..d787fc3 --- /dev/null +++ b/docs/CI-CD.md @@ -0,0 +1,154 @@ +# CI/CD Documentation + +## Build and Push Docker Image Workflow + +This repository includes a GitHub Actions workflow that automatically builds the shinyproxy-operator JAR and creates a Docker image that is pushed to a Container Registry. + +### Workflow File + +The workflow is defined in `.github/workflows/build-and-push-docker.yaml`. + +### Triggers + +The workflow is triggered on: +- Pushes to `main` or `dev` branches +- Pull requests to `main` or `dev` branches +- Tag pushes matching `v*` pattern (e.g., `v1.0.0`) +- Manual workflow dispatch + +### Workflow Steps + +1. **Checkout shinyproxy-operator**: Clones this repository +2. **Set up JDK 21**: Installs Java 21 (Temurin distribution) +3. **Cache Maven packages**: Caches Maven dependencies for faster builds +4. **Build shinyproxy-operator JAR**: Runs `mvn -U clean install -DskipTests` to build the JAR +5. **Checkout shinyproxy-docker**: Clones the [openanalytics/shinyproxy-docker](https://github.com/openanalytics/shinyproxy-docker) repository +6. **Copy JAR to Docker build context**: Copies the built JAR to the Operator directory in shinyproxy-docker +7. **Set up Docker Buildx**: Configures Docker Buildx for advanced build features +8. **Login to Container Registry**: Authenticates with the Container Registry +9. **Extract metadata for Docker**: Generates Docker tags and labels based on Git context +10. **Build and push Docker image**: Builds the Docker image and pushes it to the registry + +### Required Secrets + +The workflow requires the following secrets to be configured in the GitHub repository: + +#### `CR_REGISTRY` +- **Description**: Container Registry URL +- **Type**: Repository Secret +- **Usage**: The registry URL (e.g., `ghcr.io`, etc.) +- **Example**: `ghcr.io` + +#### `CR_USERNAME` +- **Description**: Username for Container Registry authentication +- **Type**: Repository Secret +- **Usage**: Used to authenticate with the Container Registry + +#### `CR_PASSWORD` +- **Description**: Password for Container Registry authentication +- **Type**: Repository Secret +- **Usage**: Used to authenticate with the Container Registry + +### Setting Up Secrets + +To configure these secrets: + +1. Go to the GitHub repository +2. Navigate to **Settings** > **Secrets and variables** > **Actions** +3. Click **New repository secret** +4. Add each secret with the name and value: + - Name: `CR_REGISTRY`, Value: Your Container Registry URL (e.g., `ghcr.io`) + - Name: `CR_USERNAME`, Value: Your Container Registry username + - Name: `CR_PASSWORD`, Value: Your Container Registry password + +### Docker Image Tags + +The workflow automatically generates tags based on the Git context: + +- **Branch pushes**: `` (e.g., `main`, `dev`) +- **Pull requests**: `pr-` (e.g., `pr-123`) +- **Version tags**: Multiple formats for semantic versioning + - `` (e.g., `1.2.3`) + - `.` (e.g., `1.2`) + - `` (e.g., `1`) +- **Dev branch version tags**: When triggered by the `dev` branch with a version tag + - `dev-` (e.g., `dev-1.2.3`) + - `dev-.` (e.g., `dev-1.2`) + - `dev-` (e.g., `dev-1`) +- **SHA-based**: `` (e.g., `abc1234`) + +### Docker Registry + +Images are pushed to the Container Registry specified by the `CR_REGISTRY` secret, under the repository name `shinyproxy-operator`. + +### Build Arguments + +The Docker build uses the following build argument: +- `JAR_LOCATION=shinyproxy-operator-jar-with-dependencies.jar`: Specifies the local JAR file to use instead of downloading from Nexus + +### Caching + +The workflow uses GitHub Actions cache for: +- Maven dependencies (`.m2` directory) +- Docker layer caching (GitHub Actions cache backend) + +This improves build times for subsequent runs. + +### Manual Workflow Dispatch + +You can manually trigger the workflow: + +1. Go to the **Actions** tab in the GitHub repository +2. Select the "Build and Push Docker Image" workflow +3. Click **Run workflow** +4. Select the branch to run from +5. Click **Run workflow** + +### Troubleshooting + +#### Build Failures + +If the Maven build fails: +- Check that the POM file is valid +- Verify that all dependencies are accessible +- Review the build logs for specific error messages + +#### Docker Push Failures + +If pushing to the registry fails: +- Verify that `CR_REGISTRY`, `CR_USERNAME`, and `CR_PASSWORD` secrets are correctly set +- Ensure the credentials have push permissions to the registry +- Check that the registry URL in `CR_REGISTRY` is correct and accessible + +#### JAR Not Found + +If the JAR file is not found during the copy step: +- Verify the Maven build completed successfully +- Check that the JAR name matches: `shinyproxy-operator-jar-with-dependencies.jar` +- Review the build output for the correct target directory + +### Local Development + +To replicate the workflow locally: + +```bash +# Step 1: Build the operator +git clone https://github.com/jaredlander/shinyproxy-operator.git +cd shinyproxy-operator +mvn -U clean install -DskipTests + +# Step 2: Clone shinyproxy-docker +cd .. +git clone https://github.com/openanalytics/shinyproxy-docker.git +cd shinyproxy-docker/Operator + +# Step 3: Copy the JAR +cp ../../shinyproxy-operator/target/shinyproxy-operator-jar-with-dependencies.jar . + +# Step 4: Build the Docker image +docker build -t shinyproxy-operator-dev --build-arg JAR_LOCATION=shinyproxy-operator-jar-with-dependencies.jar . + +# Step 5: Tag and push (optional) +docker tag shinyproxy-operator-dev /shinyproxy-operator:dev +docker push /shinyproxy-operator:dev +``` diff --git a/docs/environment-variables-docker.md b/docs/environment-variables-docker.md new file mode 100644 index 0000000..578b01b --- /dev/null +++ b/docs/environment-variables-docker.md @@ -0,0 +1,226 @@ +# Environment Variables for ShinyProxy Container (Docker Mode) + +## Overview + +When running the ShinyProxy Operator in Docker mode, you can pass environment variables from the operator's environment to the ShinyProxy container. This is useful for injecting sensitive configuration values (like database passwords, API keys, etc.) without storing them in plain text in configuration files. + +## How It Works + +Any environment variable set in the operator's environment that starts with the prefix `SHINYPROXY_ENV_` will be automatically passed to the ShinyProxy container with the prefix stripped. + +**Example:** +- Operator environment: `SHINYPROXY_ENV_DATABASE_URL=jdbc:postgresql://db:5432/myapp` +- ShinyProxy container receives: `DATABASE_URL=jdbc:postgresql://db:5432/myapp` + +## Operator Configuration Variables + +The following environment variables control the ShinyProxy Operator's behavior in Docker mode: + +### SPO_REDIS_CONTAINER_NAME + +**Default:** `sp-redis` + +**Description:** Specifies the name of the Redis container that will be created and used by ShinyProxy for session storage. This allows you to customize the container name if needed (e.g., to avoid naming conflicts in multi-operator setups). + +**Example:** +```bash +SPO_REDIS_CONTAINER_NAME=my-custom-redis +``` + +**Note:** If you change this value, the operator will create a new Redis container with the custom name and a corresponding data directory. Changing this after initial deployment will result in a new Redis instance; the old one will need to be manually cleaned up. + +**Important:** The value of this variable is automatically used by ShinyProxy and Crane services through their Spring Boot configuration, so they will correctly reference the custom Redis container name. + +### SPO_CADDY_CONTAINER_NAME + +**Default:** `sp-caddy` + +**Description:** Specifies the name of the Caddy reverse proxy container that will be created to manage ingress routing for ShinyProxy instances. This allows you to customize the container name if needed (e.g., to avoid naming conflicts in multi-operator setups). + +**Example:** +```bash +SPO_CADDY_CONTAINER_NAME=my-custom-caddy +``` + +**Note:** If you change this value, the operator will create a new Caddy container with the custom name and a corresponding data directory. Changing this after initial deployment will result in a new Caddy instance; the old one will need to be manually cleaned up. + +## Usage with Docker Compose + +This feature is designed to work seamlessly with Docker Compose and environment files: + +### Example docker-compose.yml + +```yaml +version: '3.8' + +services: + shinyproxy-operator: + image: openanalytics/shinyproxy-operator:latest + environment: + # Operator configuration + - SPO_DOCKER_GID=999 + - SPO_REDIS_CONTAINER_NAME=sp-redis + - SPO_CADDY_CONTAINER_NAME=sp-caddy + + # Environment variables to pass to ShinyProxy container + - SHINYPROXY_ENV_DATABASE_URL=jdbc:postgresql://db:5432/myapp + - SHINYPROXY_ENV_DATABASE_USER=shinyproxy + - SHINYPROXY_ENV_DATABASE_PASSWORD=secret123 + - SHINYPROXY_ENV_SPRING_PROFILES_ACTIVE=production + - SHINYPROXY_ENV_LOG_LEVEL=INFO + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./config:/opt/shinyproxy-operator/config + - ./data:/opt/shinyproxy-operator/data +``` + +### Using Environment Files + +For better security, store sensitive values in an environment file: + +**shinyproxy.env:** +```bash +SHINYPROXY_ENV_DATABASE_PASSWORD=super_secret_password +SHINYPROXY_ENV_API_KEY=my_api_key_12345 +SHINYPROXY_ENV_REDIS_PASSWORD=redis_secret +``` + +**docker-compose.yml:** +```yaml +version: '3.8' + +services: + shinyproxy-operator: + image: openanalytics/shinyproxy-operator:latest + env_file: + - shinyproxy.env + environment: + - SPO_DOCKER_GID=999 + - SHINYPROXY_ENV_SPRING_PROFILES_ACTIVE=production + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./config:/opt/shinyproxy-operator/config + - ./data:/opt/shinyproxy-operator/data +``` + +**Important:** Make sure to add `shinyproxy.env` to your `.gitignore` file to avoid committing secrets to version control. + +## Use Cases + +### 1. Database Credentials + +```yaml +environment: + - SHINYPROXY_ENV_SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/myapp + - SHINYPROXY_ENV_SPRING_DATASOURCE_USERNAME=dbuser + - SHINYPROXY_ENV_SPRING_DATASOURCE_PASSWORD=dbpass +``` + +### 2. External Service API Keys + +```yaml +environment: + - SHINYPROXY_ENV_EXTERNAL_API_KEY=your_api_key_here + - SHINYPROXY_ENV_EXTERNAL_API_ENDPOINT=https://api.example.com +``` + +### 3. Spring Boot Configuration + +```yaml +environment: + - SHINYPROXY_ENV_SPRING_PROFILES_ACTIVE=production + - SHINYPROXY_ENV_LOGGING_LEVEL_EU_OPENANALYTICS=DEBUG + - SHINYPROXY_ENV_SERVER_PORT=8080 +``` + +### 4. Redis Configuration (when using Redis for session storage) + +```yaml +environment: + - SHINYPROXY_ENV_SPRING_REDIS_PASSWORD=${REDIS_PASSWORD} + - SHINYPROXY_ENV_SPRING_REDIS_HOST=redis + - SHINYPROXY_ENV_SPRING_REDIS_PORT=6379 +``` + +## Important Notes + +- **Prefix Required:** Only environment variables starting with `SHINYPROXY_ENV_` are passed through +- **Prefix Stripped:** The `SHINYPROXY_ENV_` prefix is automatically removed when setting the variable in the ShinyProxy container +- **Variable Name Validation:** The variable name after stripping the prefix must not be empty (i.e., don't use `SHINYPROXY_ENV_` as the exact variable name) +- **Value Validation:** Environment variable values cannot contain newline characters (`\n` or `\r`) for security reasons +- **Invalid Variables:** Environment variables that don't pass validation are logged with a warning and skipped +- **Logging:** Each passed environment variable is logged (variable name only, not the value) when the container is created +- **Docker Only:** This feature is only available for Docker backend deployments +- **Kubernetes:** For Kubernetes deployments, use `kubernetesPodTemplateSpecPatches` to inject environment variables or reference secrets + +## Security Best Practices + +1. **Use Environment Files:** Store secrets in `.env` files that are excluded from version control +2. **File Permissions:** Restrict permissions on environment files (e.g., `chmod 600 shinyproxy.env`) +3. **Docker Secrets:** Consider using Docker Swarm secrets for production deployments +4. **Rotate Credentials:** Regularly rotate sensitive credentials +5. **Minimal Access:** Only pass environment variables that are actually needed by ShinyProxy + +## Troubleshooting + +### Environment variables not appearing in ShinyProxy + +1. Verify the prefix is correct: `SHINYPROXY_ENV_` (note the underscore at the end) +2. Check the operator logs for messages about passed environment variables +3. Ensure the environment variable is set in the operator's environment, not in the ShinyProxy config + +### Conflicts with existing variables + +If you set an environment variable that conflicts with system variables (like `PROXY_VERSION`, `PROXY_REALM_ID`, or `SPRING_CONFIG_IMPORT`), the system variables will take precedence. The operator will log a warning if this occurs. + +## Example: Full Docker Compose Setup + +```yaml +version: '3.8' + +services: + redis: + image: redis:7-alpine + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - redis-data:/data + + database: + image: postgres:15-alpine + environment: + - POSTGRES_DB=shinyproxy + - POSTGRES_USER=shinyproxy + - POSTGRES_PASSWORD=${DB_PASSWORD} + volumes: + - db-data:/var/lib/postgresql/data + + shinyproxy-operator: + image: openanalytics/shinyproxy-operator:latest + environment: + # Operator configuration + - SPO_DOCKER_GID=999 + + # ShinyProxy environment variables + - SHINYPROXY_ENV_SPRING_REDIS_PASSWORD=${REDIS_PASSWORD} + - SHINYPROXY_ENV_SPRING_DATASOURCE_URL=jdbc:postgresql://database:5432/shinyproxy + - SHINYPROXY_ENV_SPRING_DATASOURCE_USERNAME=shinyproxy + - SHINYPROXY_ENV_SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD} + - SHINYPROXY_ENV_SPRING_PROFILES_ACTIVE=production + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./config:/opt/shinyproxy-operator/config + - ./data:/opt/shinyproxy-operator/data + depends_on: + - redis + - database + +volumes: + redis-data: + db-data: +``` + +**.env file:** +```bash +REDIS_PASSWORD=secure_redis_password +DB_PASSWORD=secure_database_password +``` diff --git a/pom.xml b/pom.xml index 6c72a54..7fbbb94 100644 --- a/pom.xml +++ b/pom.xml @@ -366,6 +366,7 @@ .gitignore .editorconfig JenkinsfileSCM + .devbox/** diff --git a/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/CaddyConfig.kt b/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/CaddyConfig.kt index fb5a5aa..3e87ae8 100644 --- a/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/CaddyConfig.kt +++ b/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/CaddyConfig.kt @@ -48,7 +48,7 @@ import java.util.concurrent.TimeUnit class CaddyConfig(private val dockerClient: DockerClient, mainDataDir: Path, config: Config) { - private val containerName = "sp-caddy" + private val containerName: String = config.readConfigValue("sp-caddy", "SPO_CADDY_CONTAINER_NAME") { it } private val dataDir: Path = mainDataDir.resolve(containerName) private val shinyProxies = mutableMapOf>() private val craneServers = hashMapOf() @@ -59,6 +59,8 @@ class CaddyConfig(private val dockerClient: DockerClient, mainDataDir: Path, con private val fileManager = FileManager() private val caddyImage: String = config.readConfigValue("docker.io/library/caddy:2.8", "SPO_CADDY_IMAGE") { it } private val enableTls = config.readConfigValue(false, "SPO_CADDY_ENABLE_TLS") { it.toBoolean() } + private val caddyPortHttp: Int = config.readConfigValue(80, "SPO_CADDY_PORT_HTTP") { it.toInt() } + private val caddyPortHttps: Int = config.readConfigValue(443, "SPO_CADDY_PORT_HTTPS") { it.toInt() } private val client: OkHttpClient = OkHttpClient.Builder() .connectTimeout(3, TimeUnit.SECONDS) .readTimeout(3, TimeUnit.SECONDS) @@ -77,6 +79,10 @@ class CaddyConfig(private val dockerClient: DockerClient, mainDataDir: Path, con fileManager.createDirectories(dataDir.resolve("certs")) } + fun getContainerName(): String { + return containerName + } + suspend fun removeRealm(realmId: String) { shinyProxies.remove(realmId) reconcile() @@ -277,7 +283,17 @@ class CaddyConfig(private val dockerClient: DockerClient, mainDataDir: Path, con logger.info { "[Caddy] Pulling image" } dockerActions.pullImage(caddyImage) + val portBindings = if (enableTls) { + mapOf( + "80" to listOf(PortBinding.of("0.0.0.0", caddyPortHttp.toString())), + "443" to listOf(PortBinding.of("0.0.0.0", caddyPortHttps.toString())) + ) + } else { + mapOf("80" to listOf(PortBinding.of("0.0.0.0", caddyPortHttp.toString()))) + } + val ports = if (enableTls) listOf("80", "443") else listOf("80") + val hostConfig = HostConfig.builder() .networkMode(DockerOrchestrator.SHARED_NETWORK_NAME) .binds(HostConfig.Bind.builder() @@ -296,7 +312,7 @@ class CaddyConfig(private val dockerClient: DockerClient, mainDataDir: Path, con .from(dataDir.resolve("certs").toString()) .to("/certs") .build() - ).portBindings(ports.associateWith { listOf(PortBinding.of("0.0.0.0", it)) }) + ).portBindings(portBindings) .restartPolicy(HostConfig.RestartPolicy.always()) .build() diff --git a/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/CraneConfig.kt b/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/CraneConfig.kt index 6ede8b9..d73a50a 100644 --- a/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/CraneConfig.kt +++ b/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/CraneConfig.kt @@ -259,7 +259,7 @@ class CraneConfig(private val dockerClient: DockerClient, "data" to mapOf( "redis" to mapOf( "password" to redisConfig.getRedisPassword(), - "host" to "sp-redis" + "host" to redisConfig.getContainerName() ) ) ), diff --git a/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/DockerOrchestrator.kt b/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/DockerOrchestrator.kt index d99ef94..d7ec23c 100644 --- a/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/DockerOrchestrator.kt +++ b/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/DockerOrchestrator.kt @@ -95,6 +95,8 @@ class DockerOrchestrator(channel: Channel, private val logFilesCleaner: LogFilesCleaner init { + initSharedNetworkName(config) + if (!Files.exists(dataDir)) { throw InternalException("The data directory doesn't exist: '$dataDir'!") } @@ -134,7 +136,20 @@ class DockerOrchestrator(channel: Channel, } companion object { - const val SHARED_NETWORK_NAME = "sp-shared-network" + lateinit var SHARED_NETWORK_NAME: String + private set + + fun initSharedNetworkName(config: Config) { + SHARED_NETWORK_NAME = config.readConfigValue("sp-shared-network", "SPO_SHARED_NETWORK_NAME") { it } + } + } + + fun getRedisConfig(): RedisConfig { + return redisConfig + } + + fun getCaddyConfig(): CaddyConfig { + return caddyConfig } override fun getShinyProxyStatus(shinyProxy: ShinyProxy): ShinyProxyStatus { @@ -191,7 +206,7 @@ class DockerOrchestrator(channel: Channel, } val containers = dockerActions.getContainers(shinyProxyInstance) if (containers.size < shinyProxy.replicas) { - val networkName = "sp-network-${shinyProxy.realmId}" + val networkName = "${SHARED_NETWORK_NAME}-internal-${shinyProxy.realmId}" if (!dockerActions.networkExists(networkName)) { logger.info { "${logPrefix(shinyProxyInstance)} [Docker] Creating network" } dockerActions.createNetwork(networkName, disableICC) @@ -297,11 +312,40 @@ class DockerOrchestrator(channel: Channel, .build()) } + // Build environment variables list: default ones + operator environment variables + val envVars = mutableListOf( + "PROXY_VERSION=${version}", + "PROXY_REALM_ID=${shinyProxy.realmId}", + "SPRING_CONFIG_IMPORT_0=/opt/shinyproxy/generated.yml" + ) + + // Add environment variables from the operator's environment + // Any environment variable with prefix SHINYPROXY_ENV_ will be passed to the container + // with the prefix stripped (e.g., SHINYPROXY_ENV_DATABASE_URL -> DATABASE_URL) + System.getenv().forEach { (key, value) -> + if (key.startsWith("SHINYPROXY_ENV_")) { + val targetKey = key.removePrefix("SHINYPROXY_ENV_") + // Validate that the target key is not empty and value doesn't contain newlines + when { + targetKey.isEmpty() -> { + logger.warn { "${logPrefix(shinyProxyInstance)} [Docker] Ignoring invalid environment variable with empty key: $key" } + } + value.contains('\n') || value.contains('\r') -> { + logger.warn { "${logPrefix(shinyProxyInstance)} [Docker] Skipping environment variable '$targetKey' due to invalid value containing newline characters" } + } + else -> { + envVars.add("${targetKey}=${value}") + logger.info { "${logPrefix(shinyProxyInstance)} [Docker] Passing environment variable '$targetKey' to ShinyProxy container" } + } + } + } + } + val containerConfig = ContainerConfig.builder() .image(shinyProxy.image) .hostConfig(hostConfigBuilder.build()) .labels(shinyProxy.labels + LabelFactory.labelsForShinyProxyInstance(shinyProxyInstance, version)) - .env("PROXY_VERSION=${version}", "PROXY_REALM_ID=${shinyProxy.realmId}", "SPRING_CONFIG_IMPORT=/opt/shinyproxy/generated.yml") + .env(envVars) .user(dataDirUid.toString()) .build() @@ -490,7 +534,7 @@ class DockerOrchestrator(channel: Channel, "data" to mapOf( "redis" to mapOf( "password" to redisConfig.getRedisPassword(), - "host" to "sp-redis" + "host" to redisConfig.getContainerName() ) ) )) diff --git a/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/RedisConfig.kt b/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/RedisConfig.kt index 85fe396..e4d8bde 100644 --- a/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/RedisConfig.kt +++ b/src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/RedisConfig.kt @@ -39,8 +39,8 @@ class RedisConfig(private val dockerClient: DockerClient, private val dataDirUid: Int, config: Config) { - private val containerName = "sp-redis" - private val dataDir: Path = mainDataDir.resolve(containerName) + private val containerName: String = config.readConfigValue("sp-redis", "SPO_REDIS_CONTAINER_NAME") { it } + private val dataDir: Path = mainDataDir.resolve("sp-redis") private lateinit var redisPassword: String private val logger = KotlinLogging.logger {} private val redisImage: String = config.readConfigValue("docker.io/library/redis:8.2.2", "SPO_REDIS_IMAGE") { it } @@ -68,6 +68,10 @@ class RedisConfig(private val dockerClient: DockerClient, return redisPassword } + fun getContainerName(): String { + return containerName + } + suspend fun reconcile() { dockerActions.stopAndRemoveNotRunningContainer(containerName) if (dockerActions.isContainerRunning(containerName, redisImage)) { diff --git a/src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/helpers/DockerAssertions.kt b/src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/helpers/DockerAssertions.kt index 879540f..d302e12 100644 --- a/src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/helpers/DockerAssertions.kt +++ b/src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/helpers/DockerAssertions.kt @@ -25,6 +25,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.registerKotlinModule import eu.openanalytics.shinyproxyoperator.Config +import eu.openanalytics.shinyproxyoperator.impl.docker.DockerOrchestrator import eu.openanalytics.shinyproxyoperator.model.ShinyProxyInstance import org.mandas.docker.client.messages.Container import org.mandas.docker.client.messages.PortBinding @@ -36,14 +37,17 @@ import kotlin.test.assertTrue class DockerAssertions(private val base: IntegrationTestBase, private val dataDir: Path, - private val inputDir: Path) { + private val inputDir: Path, + private val orchestrator: DockerOrchestrator) { private val objectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule() private val dockerGID = Config().readConfigValue(null, "SPO_DOCKER_GID") { it } private val dockerSocket = Config().readConfigValue("/var/run/docker.sock", "SPO_DOCKER_SOCKET") { it } + private val redisContainerName = orchestrator.getRedisConfig().getContainerName() + private val caddyContainerName = orchestrator.getCaddyConfig().getContainerName() fun assertRedisContainer() { - val redisContainer = base.inspectContainer(this.base.getContainerByName("sp-redis")) + val redisContainer = base.inspectContainer(this.base.getContainerByName(redisContainerName)) assertNotNull(redisContainer) assertEquals(true, redisContainer.state().running()) assertEquals("sp-shared-network", redisContainer.hostConfig().networkMode()) @@ -62,7 +66,7 @@ class DockerAssertions(private val base: IntegrationTestBase, } fun assertCaddyContainer(expectedName: String, replacements: Map, tls: Boolean = false) { - val caddyContainer = base.inspectContainer(this.base.getContainerByName("sp-caddy")) + val caddyContainer = base.inspectContainer(this.base.getContainerByName(caddyContainerName)) assertNotNull(caddyContainer) assertEquals(true, caddyContainer.state().running()) assertEquals("sp-shared-network", caddyContainer.hostConfig().networkMode()) diff --git a/src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/helpers/IntegrationTestBase.kt b/src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/helpers/IntegrationTestBase.kt index 3bb9846..cabda55 100644 --- a/src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/helpers/IntegrationTestBase.kt +++ b/src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/helpers/IntegrationTestBase.kt @@ -106,7 +106,7 @@ abstract class IntegrationTestBase { val operator = DockerOperator(mockConfig, eventController, mockRecyclableChecker) eventController.setDelegate(EventController(operator.orchestrator)) - val dockerAssertions = DockerAssertions(this@IntegrationTestBase, dataDir, inputDir) + val dockerAssertions = DockerAssertions(this@IntegrationTestBase, dataDir, inputDir, operator.orchestrator) try { // 3. run test @@ -116,15 +116,15 @@ abstract class IntegrationTestBase { // 4. stop operator operator.stop() // 4. cleanup docker containers - deleteContainers() + deleteContainers(operator.orchestrator.getRedisConfig().getContainerName(), operator.orchestrator.getCaddyConfig().getContainerName()) } } } - private fun deleteContainers() { - stopAndRemoveContainer(getContainerByName("sp-redis")) - stopAndRemoveContainer(getContainerByName("sp-caddy")) + private fun deleteContainers(redisContainerName: String = "sp-redis", caddyContainerName: String = "sp-caddy") { + stopAndRemoveContainer(getContainerByName(redisContainerName)) + stopAndRemoveContainer(getContainerByName(caddyContainerName)) val containers = dockerClient .listContainers(DockerClient.ListContainersParam.allContainers())