From 7a1f229d3b07e8698124c92994ea1b033e83a8c6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:07:22 +0000 Subject: [PATCH 1/6] feat: add /_floci/health endpoint and Dockerfile.seed for persistence Co-Authored-By: Matej Snuderl --- Dockerfile.seed | 56 +++++++ docs/configuration/persistence.md | 141 ++++++++++++++++++ mkdocs.yml | 3 +- .../floci/lifecycle/HealthController.java | 54 +++++++ .../HealthControllerIntegrationTest.java | 31 ++++ 5 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 Dockerfile.seed create mode 100644 docs/configuration/persistence.md create mode 100644 src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java create mode 100644 src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java diff --git a/Dockerfile.seed b/Dockerfile.seed new file mode 100644 index 00000000..6f648d96 --- /dev/null +++ b/Dockerfile.seed @@ -0,0 +1,56 @@ +# Dockerfile.seed — Build a Floci image with pre-baked AWS resources. +# +# Usage: +# 1. Place shell scripts in a local directory (e.g. ./seed/). +# Each script should use the AWS CLI to create resources: +# #!/bin/bash +# aws s3 mb s3://my-bucket +# aws sqs create-queue --queue-name my-queue +# +# 2. Build the seeded image: +# docker build -f Dockerfile.seed \ +# --build-arg BASE_IMAGE=hectorvent/floci:latest-awscli \ +# -t my-floci-seeded . +# +# 3. Run it — resources are already there, no init overhead: +# docker run -p 4566:4566 my-floci-seeded +# +# The seed scripts run once at build time. The resulting persistent state +# is baked into the image so every container starts with those resources +# already available. + +ARG BASE_IMAGE=hectorvent/floci:latest-awscli + +# ---------- Stage 1: seed ---------- +FROM ${BASE_IMAGE} AS seed + +ENV FLOCI_STORAGE_MODE=persistent +ENV FLOCI_STORAGE_PERSISTENT_PATH=/app/data +ENV AWS_ENDPOINT_URL=http://localhost:4566 +ENV AWS_DEFAULT_REGION=us-east-1 +ENV AWS_ACCESS_KEY_ID=test +ENV AWS_SECRET_ACCESS_KEY=test + +COPY seed/ /etc/floci/seed/ + +# Start Floci, wait for it to become healthy, run every seed script, +# then shut down so the persistent data is flushed to /app/data. +RUN java -jar quarkus-app/quarkus-run.jar & \ + FLOCI_PID=$! && \ + echo "Waiting for Floci to become healthy..." && \ + timeout 60 bash -c \ + 'until curl -sf http://localhost:4566/_floci/health > /dev/null 2>&1; do sleep 0.5; done' && \ + echo "Floci is healthy — running seed scripts" && \ + for f in /etc/floci/seed/*.sh; do \ + [ -f "$f" ] && echo " → $f" && bash "$f"; \ + done && \ + echo "Seed complete — shutting down Floci" && \ + kill "$FLOCI_PID" && wait "$FLOCI_PID" 2>/dev/null || true + +# ---------- Stage 2: final image ---------- +FROM ${BASE_IMAGE} + +ENV FLOCI_STORAGE_MODE=persistent +ENV FLOCI_STORAGE_PERSISTENT_PATH=/app/data + +COPY --from=seed /app/data/ /app/data/ diff --git a/docs/configuration/persistence.md b/docs/configuration/persistence.md new file mode 100644 index 00000000..f56df486 --- /dev/null +++ b/docs/configuration/persistence.md @@ -0,0 +1,141 @@ +# Persistence + +Floci can bake AWS resources into a Docker image at build time so every container starts with those resources already available — no init-script overhead on each startup. + +This is useful when your project always needs the same baseline infrastructure (S3 buckets, SQS queues, SSM parameters, etc.) and you want instant availability without paying the setup cost on every `docker compose up`. + +## How It Works + +1. During `docker build`, Floci starts inside the build container with persistent storage enabled. +2. Your seed scripts run against the live Floci instance, creating resources via the AWS CLI. +3. Floci shuts down and flushes all state to `/app/data`. +4. A final image is produced with that data directory baked in. + +When you run the resulting image, Floci loads the persisted state on startup and all seeded resources are immediately available. + +## Quick Start + +### 1. Create Seed Scripts + +Create a `seed/` directory with shell scripts that set up your resources: + +```bash title="seed/01-buckets.sh" +#!/bin/bash +aws s3 mb s3://my-app-uploads +aws s3 mb s3://my-app-assets +``` + +```bash title="seed/02-queues.sh" +#!/bin/bash +aws sqs create-queue --queue-name order-events +aws sqs create-queue --queue-name order-events-dlq +``` + +```bash title="seed/03-params.sh" +#!/bin/bash +aws ssm put-parameter \ + --name /my-app/db-host \ + --value "localhost" \ + --type String +``` + +Scripts are executed in lexicographical order, so use numeric prefixes to control ordering. + +### 2. Build the Seeded Image + +```bash +docker build -f Dockerfile.seed \ + --build-arg BASE_IMAGE=hectorvent/floci:latest-awscli \ + -t my-floci-seeded . +``` + +!!! note + The base image must include the AWS CLI. Use `hectorvent/floci:latest-awscli` or build your own with `Dockerfile.awscli`. + +### 3. Run It + +```bash +docker run -p 4566:4566 my-floci-seeded +``` + +All seeded resources are available immediately: + +```bash +export AWS_ENDPOINT_URL=http://localhost:4566 +export AWS_DEFAULT_REGION=us-east-1 +export AWS_ACCESS_KEY_ID=test +export AWS_SECRET_ACCESS_KEY=test + +aws s3 ls # my-app-uploads, my-app-assets +aws sqs list-queues # order-events, order-events-dlq +aws ssm get-parameter --name /my-app/db-host +``` + +## Docker Compose + +Use the seeded image in your compose file: + +```yaml title="docker-compose.yml" +services: + floci: + image: my-floci-seeded + ports: + - "4566:4566" + my-app: + environment: + - AWS_ENDPOINT_URL=http://floci:4566 + depends_on: + - floci +``` + +## Health Endpoint + +Floci exposes `/_floci/health` which returns the version and status of all enabled services: + +```bash +curl http://localhost:4566/_floci/health +``` + +```json +{ + "version": "1.0.11", + "edition": "community", + "services": { + "ssm": "available", + "sqs": "available", + "s3": "available", + "dynamodb": "available", + ... + } +} +``` + +The seed Dockerfile uses this endpoint to wait for Floci to be ready before running seed scripts. You can also use it in your own readiness checks and CI pipelines. + +## How Dockerfile.seed Works + +The `Dockerfile.seed` uses a two-stage build: + +``` +┌─────────────────────────────────────────────┐ +│ Stage 1: seed │ +│ 1. Start Floci with persistent storage │ +│ 2. Wait for /_floci/health to respond │ +│ 3. Run seed/*.sh scripts (create resources)│ +│ 4. Stop Floci (state flushed to /app/data) │ +└─────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────┐ +│ Stage 2: final │ +│ Copy /app/data from stage 1 into a clean │ +│ Floci image with persistent storage enabled│ +└─────────────────────────────────────────────┘ +``` + +## Tips + +- **Idempotent scripts**: Write seed scripts so they can be re-run safely. Use `aws ... 2>/dev/null || true` for resources that might already exist. +- **Script ordering**: Scripts run in lexicographical order. Use numeric prefixes (`01-`, `02-`) to control the sequence. +- **Debugging**: If a seed script fails, the Docker build will fail with the script's error output. Fix the script and rebuild. +- **Runtime persistence**: The seeded image runs with `FLOCI_STORAGE_MODE=persistent` by default. Any resources created at runtime are also persisted. Override with `FLOCI_STORAGE_MODE=memory` if you want runtime changes to be ephemeral. diff --git a/mkdocs.yml b/mkdocs.yml index e4b6a9b0..511a841c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,7 @@ nav: - Ports Reference: configuration/ports.md - application.yml Reference: configuration/application-yml.md - Storage Modes: configuration/storage.md + - Persistence: configuration/persistence.md - Services: - Overview: services/index.md - SSM Parameter Store: services/ssm.md @@ -87,4 +88,4 @@ nav: - CloudWatch: services/cloudwatch.md - ACM: services/acm.md - OpenSearch: services/opensearch.md - - Contributing: contributing.md \ No newline at end of file + - Contributing: contributing.md diff --git a/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java b/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java new file mode 100644 index 00000000..c2a6297e --- /dev/null +++ b/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java @@ -0,0 +1,54 @@ +package io.github.hectorvent.floci.lifecycle; + +import io.github.hectorvent.floci.core.common.ServiceRegistry; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Internal health endpoint at /_floci/health. + * Returns the Floci version and the status of each enabled service. + * Compatible with the LocalStack /_localstack/health pattern. + */ +@Path("/_floci/health") +@Produces(MediaType.APPLICATION_JSON) +public class HealthController { + + private final ServiceRegistry serviceRegistry; + private final String version; + + @Inject + public HealthController(ServiceRegistry serviceRegistry) { + this.serviceRegistry = serviceRegistry; + this.version = resolveVersion(); + } + + @GET + public Response health() { + Map result = new LinkedHashMap<>(); + result.put("version", version); + result.put("edition", "community"); + + Map services = new LinkedHashMap<>(); + for (String service : serviceRegistry.getEnabledServices()) { + services.put(service, "available"); + } + result.put("services", services); + + return Response.ok(result).build(); + } + + private static String resolveVersion() { + String env = System.getenv("FLOCI_VERSION"); + if (env != null && !env.isBlank()) { + return env; + } + return "dev"; + } +} diff --git a/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java b/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java new file mode 100644 index 00000000..a4827234 --- /dev/null +++ b/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java @@ -0,0 +1,31 @@ +package io.github.hectorvent.floci.lifecycle; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@QuarkusTest +class HealthControllerIntegrationTest { + + @Test + void healthEndpoint_returnsVersionAndServices() { + given() + .when() + .get("/_floci/health") + .then() + .statusCode(200) + .contentType("application/json") + .body("version", notNullValue()) + .body("edition", equalTo("community")) + .body("services", notNullValue()) + .body("services.sqs", equalTo("available")) + .body("services.s3", equalTo("available")) + .body("services.dynamodb", equalTo("available")) + .body("services.ssm", equalTo("available")) + .body("services.sns", equalTo("available")) + .body("services.lambda", equalTo("available")) + .body("services.iam", equalTo("available")); + } +} From cfe0c03982e590a3db6cd578e8713de5c4adeb33 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:52:40 +0000 Subject: [PATCH 2/6] chore: remove Dockerfile.seed and persistence docs, keep health endpoint only Co-Authored-By: Matej Snuderl --- Dockerfile.seed | 56 ------------ docs/configuration/persistence.md | 141 ------------------------------ mkdocs.yml | 3 +- 3 files changed, 1 insertion(+), 199 deletions(-) delete mode 100644 Dockerfile.seed delete mode 100644 docs/configuration/persistence.md diff --git a/Dockerfile.seed b/Dockerfile.seed deleted file mode 100644 index 6f648d96..00000000 --- a/Dockerfile.seed +++ /dev/null @@ -1,56 +0,0 @@ -# Dockerfile.seed — Build a Floci image with pre-baked AWS resources. -# -# Usage: -# 1. Place shell scripts in a local directory (e.g. ./seed/). -# Each script should use the AWS CLI to create resources: -# #!/bin/bash -# aws s3 mb s3://my-bucket -# aws sqs create-queue --queue-name my-queue -# -# 2. Build the seeded image: -# docker build -f Dockerfile.seed \ -# --build-arg BASE_IMAGE=hectorvent/floci:latest-awscli \ -# -t my-floci-seeded . -# -# 3. Run it — resources are already there, no init overhead: -# docker run -p 4566:4566 my-floci-seeded -# -# The seed scripts run once at build time. The resulting persistent state -# is baked into the image so every container starts with those resources -# already available. - -ARG BASE_IMAGE=hectorvent/floci:latest-awscli - -# ---------- Stage 1: seed ---------- -FROM ${BASE_IMAGE} AS seed - -ENV FLOCI_STORAGE_MODE=persistent -ENV FLOCI_STORAGE_PERSISTENT_PATH=/app/data -ENV AWS_ENDPOINT_URL=http://localhost:4566 -ENV AWS_DEFAULT_REGION=us-east-1 -ENV AWS_ACCESS_KEY_ID=test -ENV AWS_SECRET_ACCESS_KEY=test - -COPY seed/ /etc/floci/seed/ - -# Start Floci, wait for it to become healthy, run every seed script, -# then shut down so the persistent data is flushed to /app/data. -RUN java -jar quarkus-app/quarkus-run.jar & \ - FLOCI_PID=$! && \ - echo "Waiting for Floci to become healthy..." && \ - timeout 60 bash -c \ - 'until curl -sf http://localhost:4566/_floci/health > /dev/null 2>&1; do sleep 0.5; done' && \ - echo "Floci is healthy — running seed scripts" && \ - for f in /etc/floci/seed/*.sh; do \ - [ -f "$f" ] && echo " → $f" && bash "$f"; \ - done && \ - echo "Seed complete — shutting down Floci" && \ - kill "$FLOCI_PID" && wait "$FLOCI_PID" 2>/dev/null || true - -# ---------- Stage 2: final image ---------- -FROM ${BASE_IMAGE} - -ENV FLOCI_STORAGE_MODE=persistent -ENV FLOCI_STORAGE_PERSISTENT_PATH=/app/data - -COPY --from=seed /app/data/ /app/data/ diff --git a/docs/configuration/persistence.md b/docs/configuration/persistence.md deleted file mode 100644 index f56df486..00000000 --- a/docs/configuration/persistence.md +++ /dev/null @@ -1,141 +0,0 @@ -# Persistence - -Floci can bake AWS resources into a Docker image at build time so every container starts with those resources already available — no init-script overhead on each startup. - -This is useful when your project always needs the same baseline infrastructure (S3 buckets, SQS queues, SSM parameters, etc.) and you want instant availability without paying the setup cost on every `docker compose up`. - -## How It Works - -1. During `docker build`, Floci starts inside the build container with persistent storage enabled. -2. Your seed scripts run against the live Floci instance, creating resources via the AWS CLI. -3. Floci shuts down and flushes all state to `/app/data`. -4. A final image is produced with that data directory baked in. - -When you run the resulting image, Floci loads the persisted state on startup and all seeded resources are immediately available. - -## Quick Start - -### 1. Create Seed Scripts - -Create a `seed/` directory with shell scripts that set up your resources: - -```bash title="seed/01-buckets.sh" -#!/bin/bash -aws s3 mb s3://my-app-uploads -aws s3 mb s3://my-app-assets -``` - -```bash title="seed/02-queues.sh" -#!/bin/bash -aws sqs create-queue --queue-name order-events -aws sqs create-queue --queue-name order-events-dlq -``` - -```bash title="seed/03-params.sh" -#!/bin/bash -aws ssm put-parameter \ - --name /my-app/db-host \ - --value "localhost" \ - --type String -``` - -Scripts are executed in lexicographical order, so use numeric prefixes to control ordering. - -### 2. Build the Seeded Image - -```bash -docker build -f Dockerfile.seed \ - --build-arg BASE_IMAGE=hectorvent/floci:latest-awscli \ - -t my-floci-seeded . -``` - -!!! note - The base image must include the AWS CLI. Use `hectorvent/floci:latest-awscli` or build your own with `Dockerfile.awscli`. - -### 3. Run It - -```bash -docker run -p 4566:4566 my-floci-seeded -``` - -All seeded resources are available immediately: - -```bash -export AWS_ENDPOINT_URL=http://localhost:4566 -export AWS_DEFAULT_REGION=us-east-1 -export AWS_ACCESS_KEY_ID=test -export AWS_SECRET_ACCESS_KEY=test - -aws s3 ls # my-app-uploads, my-app-assets -aws sqs list-queues # order-events, order-events-dlq -aws ssm get-parameter --name /my-app/db-host -``` - -## Docker Compose - -Use the seeded image in your compose file: - -```yaml title="docker-compose.yml" -services: - floci: - image: my-floci-seeded - ports: - - "4566:4566" - my-app: - environment: - - AWS_ENDPOINT_URL=http://floci:4566 - depends_on: - - floci -``` - -## Health Endpoint - -Floci exposes `/_floci/health` which returns the version and status of all enabled services: - -```bash -curl http://localhost:4566/_floci/health -``` - -```json -{ - "version": "1.0.11", - "edition": "community", - "services": { - "ssm": "available", - "sqs": "available", - "s3": "available", - "dynamodb": "available", - ... - } -} -``` - -The seed Dockerfile uses this endpoint to wait for Floci to be ready before running seed scripts. You can also use it in your own readiness checks and CI pipelines. - -## How Dockerfile.seed Works - -The `Dockerfile.seed` uses a two-stage build: - -``` -┌─────────────────────────────────────────────┐ -│ Stage 1: seed │ -│ 1. Start Floci with persistent storage │ -│ 2. Wait for /_floci/health to respond │ -│ 3. Run seed/*.sh scripts (create resources)│ -│ 4. Stop Floci (state flushed to /app/data) │ -└─────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────┐ -│ Stage 2: final │ -│ Copy /app/data from stage 1 into a clean │ -│ Floci image with persistent storage enabled│ -└─────────────────────────────────────────────┘ -``` - -## Tips - -- **Idempotent scripts**: Write seed scripts so they can be re-run safely. Use `aws ... 2>/dev/null || true` for resources that might already exist. -- **Script ordering**: Scripts run in lexicographical order. Use numeric prefixes (`01-`, `02-`) to control the sequence. -- **Debugging**: If a seed script fails, the Docker build will fail with the script's error output. Fix the script and rebuild. -- **Runtime persistence**: The seeded image runs with `FLOCI_STORAGE_MODE=persistent` by default. Any resources created at runtime are also persisted. Override with `FLOCI_STORAGE_MODE=memory` if you want runtime changes to be ephemeral. diff --git a/mkdocs.yml b/mkdocs.yml index 511a841c..e4b6a9b0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,7 +64,6 @@ nav: - Ports Reference: configuration/ports.md - application.yml Reference: configuration/application-yml.md - Storage Modes: configuration/storage.md - - Persistence: configuration/persistence.md - Services: - Overview: services/index.md - SSM Parameter Store: services/ssm.md @@ -88,4 +87,4 @@ nav: - CloudWatch: services/cloudwatch.md - ACM: services/acm.md - OpenSearch: services/opensearch.md - - Contributing: contributing.md + - Contributing: contributing.md \ No newline at end of file From 4b4d1262f0ffa1d985dfd1c6f41f2e9a203339bd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:18:45 +0000 Subject: [PATCH 3/6] fix: address PR review comments on health endpoint - Change edition from 'community' to 'floci-open-source' - Show all services in health response (enabled='running', disabled='available') - Support both /_floci/health and /_localstack/health paths for backward compatibility Co-Authored-By: Matej Snuderl --- .../floci/core/common/ServiceRegistry.java | 37 +++++++++++++++++++ .../floci/lifecycle/HealthController.java | 11 ++---- .../HealthControllerIntegrationTest.java | 33 +++++++++++++---- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/main/java/io/github/hectorvent/floci/core/common/ServiceRegistry.java b/src/main/java/io/github/hectorvent/floci/core/common/ServiceRegistry.java index 5da4e28f..a53334ad 100644 --- a/src/main/java/io/github/hectorvent/floci/core/common/ServiceRegistry.java +++ b/src/main/java/io/github/hectorvent/floci/core/common/ServiceRegistry.java @@ -6,7 +6,9 @@ import org.jboss.logging.Logger; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * Registry of enabled AWS services based on configuration. @@ -80,6 +82,41 @@ public List getEnabledServices() { return enabled; } + /** + * Returns all known services with their status: "running" if enabled, "available" if not. + */ + public Map getServices() { + Map services = new LinkedHashMap<>(); + services.put("ssm", status(config.services().ssm().enabled())); + services.put("sqs", status(config.services().sqs().enabled())); + services.put("s3", status(config.services().s3().enabled())); + services.put("dynamodb", status(config.services().dynamodb().enabled())); + services.put("sns", status(config.services().sns().enabled())); + services.put("lambda", status(config.services().lambda().enabled())); + services.put("apigateway", status(config.services().apigateway().enabled())); + services.put("iam", status(config.services().iam().enabled())); + services.put("elasticache", status(config.services().elasticache().enabled())); + services.put("rds", status(config.services().rds().enabled())); + services.put("events", status(config.services().eventbridge().enabled())); + services.put("logs", status(config.services().cloudwatchlogs().enabled())); + services.put("monitoring", status(config.services().cloudwatchmetrics().enabled())); + services.put("secretsmanager", status(config.services().secretsmanager().enabled())); + services.put("apigatewayv2", status(config.services().apigatewayv2().enabled())); + services.put("kinesis", status(config.services().kinesis().enabled())); + services.put("kms", status(config.services().kms().enabled())); + services.put("cognito-idp", status(config.services().cognito().enabled())); + services.put("states", status(config.services().stepfunctions().enabled())); + services.put("cloudformation", status(config.services().cloudformation().enabled())); + services.put("acm", status(config.services().acm().enabled())); + services.put("email", status(config.services().ses().enabled())); + services.put("es", status(config.services().opensearch().enabled())); + return services; + } + + private static String status(boolean enabled) { + return enabled ? "running" : "available"; + } + public void logEnabledServices() { LOG.infov("Enabled services: {0}", getEnabledServices()); } diff --git a/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java b/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java index c2a6297e..7db2a43f 100644 --- a/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java +++ b/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java @@ -16,7 +16,7 @@ * Returns the Floci version and the status of each enabled service. * Compatible with the LocalStack /_localstack/health pattern. */ -@Path("/_floci/health") +@Path("{path:(_floci|_localstack)/health}") @Produces(MediaType.APPLICATION_JSON) public class HealthController { @@ -32,14 +32,9 @@ public HealthController(ServiceRegistry serviceRegistry) { @GET public Response health() { Map result = new LinkedHashMap<>(); + result.put("services", serviceRegistry.getServices()); + result.put("edition", "floci-open-source"); result.put("version", version); - result.put("edition", "community"); - - Map services = new LinkedHashMap<>(); - for (String service : serviceRegistry.getEnabledServices()) { - services.put(service, "available"); - } - result.put("services", services); return Response.ok(result).build(); } diff --git a/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java b/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java index a4827234..d58d051a 100644 --- a/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java +++ b/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java @@ -18,14 +18,31 @@ void healthEndpoint_returnsVersionAndServices() { .statusCode(200) .contentType("application/json") .body("version", notNullValue()) - .body("edition", equalTo("community")) + .body("edition", equalTo("floci-open-source")) .body("services", notNullValue()) - .body("services.sqs", equalTo("available")) - .body("services.s3", equalTo("available")) - .body("services.dynamodb", equalTo("available")) - .body("services.ssm", equalTo("available")) - .body("services.sns", equalTo("available")) - .body("services.lambda", equalTo("available")) - .body("services.iam", equalTo("available")); + .body("services.sqs", anyOf(equalTo("running"), equalTo("available"))) + .body("services.s3", anyOf(equalTo("running"), equalTo("available"))) + .body("services.dynamodb", anyOf(equalTo("running"), equalTo("available"))) + .body("services.ssm", anyOf(equalTo("running"), equalTo("available"))) + .body("services.sns", anyOf(equalTo("running"), equalTo("available"))) + .body("services.lambda", anyOf(equalTo("running"), equalTo("available"))) + .body("services.iam", anyOf(equalTo("running"), equalTo("available"))) + .body("services.kms", anyOf(equalTo("running"), equalTo("available"))) + .body("services.secretsmanager", anyOf(equalTo("running"), equalTo("available"))) + .body("services.elasticache", anyOf(equalTo("running"), equalTo("available"))) + .body("services.rds", anyOf(equalTo("running"), equalTo("available"))); + } + + @Test + void healthEndpoint_localstackCompatPath() { + given() + .when() + .get("/_localstack/health") + .then() + .statusCode(200) + .contentType("application/json") + .body("version", notNullValue()) + .body("edition", equalTo("floci-open-source")) + .body("services", notNullValue()); } } From 3e2a90ddbe6e0c3a4c571c5451518d1038f4bd4c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:21:42 +0000 Subject: [PATCH 4/6] fix: use floci-always-free edition value per reviewer preference Co-Authored-By: Matej Snuderl --- .../github/hectorvent/floci/lifecycle/HealthController.java | 2 +- .../floci/lifecycle/HealthControllerIntegrationTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java b/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java index 7db2a43f..2eea07fc 100644 --- a/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java +++ b/src/main/java/io/github/hectorvent/floci/lifecycle/HealthController.java @@ -33,7 +33,7 @@ public HealthController(ServiceRegistry serviceRegistry) { public Response health() { Map result = new LinkedHashMap<>(); result.put("services", serviceRegistry.getServices()); - result.put("edition", "floci-open-source"); + result.put("edition", "floci-always-free"); result.put("version", version); return Response.ok(result).build(); diff --git a/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java b/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java index d58d051a..872ba207 100644 --- a/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java +++ b/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java @@ -18,7 +18,7 @@ void healthEndpoint_returnsVersionAndServices() { .statusCode(200) .contentType("application/json") .body("version", notNullValue()) - .body("edition", equalTo("floci-open-source")) + .body("edition", equalTo("floci-always-free")) .body("services", notNullValue()) .body("services.sqs", anyOf(equalTo("running"), equalTo("available"))) .body("services.s3", anyOf(equalTo("running"), equalTo("available"))) @@ -42,7 +42,7 @@ void healthEndpoint_localstackCompatPath() { .statusCode(200) .contentType("application/json") .body("version", notNullValue()) - .body("edition", equalTo("floci-open-source")) + .body("edition", equalTo("floci-always-free")) .body("services", notNullValue()); } } From 11377ad469299a223a038efbd4e216ff337e29fa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:25:31 +0000 Subject: [PATCH 5/6] test: assert full JSON response in health endpoint tests Co-Authored-By: Matej Snuderl --- .../HealthControllerIntegrationTest.java | 98 +++++++++++++------ 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java b/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java index 872ba207..226f25d6 100644 --- a/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java +++ b/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java @@ -1,48 +1,82 @@ package io.github.hectorvent.floci.lifecycle; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; +import java.util.LinkedHashMap; +import java.util.Map; + import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; @QuarkusTest class HealthControllerIntegrationTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); + @Test - void healthEndpoint_returnsVersionAndServices() { - given() - .when() - .get("/_floci/health") - .then() - .statusCode(200) - .contentType("application/json") - .body("version", notNullValue()) - .body("edition", equalTo("floci-always-free")) - .body("services", notNullValue()) - .body("services.sqs", anyOf(equalTo("running"), equalTo("available"))) - .body("services.s3", anyOf(equalTo("running"), equalTo("available"))) - .body("services.dynamodb", anyOf(equalTo("running"), equalTo("available"))) - .body("services.ssm", anyOf(equalTo("running"), equalTo("available"))) - .body("services.sns", anyOf(equalTo("running"), equalTo("available"))) - .body("services.lambda", anyOf(equalTo("running"), equalTo("available"))) - .body("services.iam", anyOf(equalTo("running"), equalTo("available"))) - .body("services.kms", anyOf(equalTo("running"), equalTo("available"))) - .body("services.secretsmanager", anyOf(equalTo("running"), equalTo("available"))) - .body("services.elasticache", anyOf(equalTo("running"), equalTo("available"))) - .body("services.rds", anyOf(equalTo("running"), equalTo("available"))); + void healthEndpoint_returnsExpectedJson() throws Exception { + String body = given() + .when() + .get("/_floci/health") + .then() + .statusCode(200) + .contentType("application/json") + .extract().body().asString(); + + JsonNode actual = MAPPER.readTree(body); + + Map expected = new LinkedHashMap<>(); + Map services = new LinkedHashMap<>(); + services.put("ssm", "running"); + services.put("sqs", "running"); + services.put("s3", "running"); + services.put("dynamodb", "running"); + services.put("sns", "running"); + services.put("lambda", "running"); + services.put("apigateway", "running"); + services.put("iam", "running"); + services.put("elasticache", "running"); + services.put("rds", "running"); + services.put("events", "running"); + services.put("logs", "running"); + services.put("monitoring", "running"); + services.put("secretsmanager", "running"); + services.put("apigatewayv2", "running"); + services.put("kinesis", "running"); + services.put("kms", "running"); + services.put("cognito-idp", "running"); + services.put("states", "running"); + services.put("cloudformation", "running"); + services.put("acm", "running"); + services.put("email", "running"); + services.put("es", "running"); + expected.put("services", services); + expected.put("edition", "floci-always-free"); + expected.put("version", "dev"); + + assertEquals(MAPPER.valueToTree(expected), actual); } @Test - void healthEndpoint_localstackCompatPath() { - given() - .when() - .get("/_localstack/health") - .then() - .statusCode(200) - .contentType("application/json") - .body("version", notNullValue()) - .body("edition", equalTo("floci-always-free")) - .body("services", notNullValue()); + void healthEndpoint_localstackCompatPath() throws Exception { + String body = given() + .when() + .get("/_localstack/health") + .then() + .statusCode(200) + .contentType("application/json") + .extract().body().asString(); + + JsonNode actual = MAPPER.readTree(body); + + assertNotNull(actual.get("services")); + assertEquals("floci-always-free", actual.get("edition").asText()); + assertNotNull(actual.get("version")); + // Verify same structure as /_floci/health + assertEquals(23, actual.get("services").size()); } } From 7b0445f0fb015e15b361b17b1211ffef828516ba Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:30:01 +0000 Subject: [PATCH 6/6] test: use raw JSON string for health endpoint assertion Co-Authored-By: Matej Snuderl --- .../HealthControllerIntegrationTest.java | 77 ++++++++----------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java b/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java index 226f25d6..a8db15e4 100644 --- a/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java +++ b/src/test/java/io/github/hectorvent/floci/lifecycle/HealthControllerIntegrationTest.java @@ -5,18 +5,46 @@ import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; -import java.util.LinkedHashMap; -import java.util.Map; - import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; @QuarkusTest class HealthControllerIntegrationTest { private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String EXPECTED_HEALTH_JSON = """ + { + "services": { + "ssm": "running", + "sqs": "running", + "s3": "running", + "dynamodb": "running", + "sns": "running", + "lambda": "running", + "apigateway": "running", + "iam": "running", + "elasticache": "running", + "rds": "running", + "events": "running", + "logs": "running", + "monitoring": "running", + "secretsmanager": "running", + "apigatewayv2": "running", + "kinesis": "running", + "kms": "running", + "cognito-idp": "running", + "states": "running", + "cloudformation": "running", + "acm": "running", + "email": "running", + "es": "running" + }, + "edition": "floci-always-free", + "version": "dev" + } + """; + @Test void healthEndpoint_returnsExpectedJson() throws Exception { String body = given() @@ -27,38 +55,7 @@ void healthEndpoint_returnsExpectedJson() throws Exception { .contentType("application/json") .extract().body().asString(); - JsonNode actual = MAPPER.readTree(body); - - Map expected = new LinkedHashMap<>(); - Map services = new LinkedHashMap<>(); - services.put("ssm", "running"); - services.put("sqs", "running"); - services.put("s3", "running"); - services.put("dynamodb", "running"); - services.put("sns", "running"); - services.put("lambda", "running"); - services.put("apigateway", "running"); - services.put("iam", "running"); - services.put("elasticache", "running"); - services.put("rds", "running"); - services.put("events", "running"); - services.put("logs", "running"); - services.put("monitoring", "running"); - services.put("secretsmanager", "running"); - services.put("apigatewayv2", "running"); - services.put("kinesis", "running"); - services.put("kms", "running"); - services.put("cognito-idp", "running"); - services.put("states", "running"); - services.put("cloudformation", "running"); - services.put("acm", "running"); - services.put("email", "running"); - services.put("es", "running"); - expected.put("services", services); - expected.put("edition", "floci-always-free"); - expected.put("version", "dev"); - - assertEquals(MAPPER.valueToTree(expected), actual); + assertEquals(MAPPER.readTree(EXPECTED_HEALTH_JSON), MAPPER.readTree(body)); } @Test @@ -71,12 +68,6 @@ void healthEndpoint_localstackCompatPath() throws Exception { .contentType("application/json") .extract().body().asString(); - JsonNode actual = MAPPER.readTree(body); - - assertNotNull(actual.get("services")); - assertEquals("floci-always-free", actual.get("edition").asText()); - assertNotNull(actual.get("version")); - // Verify same structure as /_floci/health - assertEquals(23, actual.get("services").size()); + assertEquals(MAPPER.readTree(EXPECTED_HEALTH_JSON), MAPPER.readTree(body)); } }