second first commit
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# Stage 1: Build
|
||||
FROM eclipse-temurin:25-jdk-noble AS build
|
||||
WORKDIR /build
|
||||
|
||||
RUN apt-get update && apt-get install -y maven && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY pom.xml .
|
||||
RUN mvn dependency:go-offline -q
|
||||
|
||||
COPY src/ src/
|
||||
RUN mvn clean package -DskipTests -q
|
||||
|
||||
# Stage 2: Runtime
|
||||
FROM eclipse-temurin:25-jre-noble
|
||||
|
||||
ARG VERSION=latest
|
||||
ENV FLOCI_VERSION=${VERSION}
|
||||
ENV FLOCI_STORAGE_PERSISTENT_PATH=/app/data
|
||||
|
||||
ENV AWS_DEFAULT_REGION=us-east-1
|
||||
ENV AWS_ACCESS_KEY_ID=test
|
||||
ENV AWS_SECRET_ACCESS_KEY=test
|
||||
ENV AWS_CONFIG_FILE=/etc/floci/aws/config
|
||||
|
||||
# Privilege drop happens in the entrypoint through coreutils chroot, so no gosu
|
||||
# download; wget stays for the HEALTHCHECK below.
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends ca-certificates passwd python3 python3-pip wget; \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
pip3 install --no-cache-dir --break-system-packages awscli boto3; \
|
||||
aws_bin="$(command -v aws)"; \
|
||||
mv "$aws_bin" /usr/bin/aws.real; \
|
||||
printf '#!/bin/sh\nexec /usr/bin/aws.real --endpoint-url=http://localhost:4566 "$@"\n' > /usr/bin/aws; \
|
||||
chmod +x /usr/bin/aws; \
|
||||
useradd -r -u 1001 -g root -d /app -s /sbin/nologin floci
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# cli_follow_urlparam: the bundled CLI is v1 (pip ships no v2), where a parameter value that looks
|
||||
# like a URL is *fetched* and its body used as the value. An init hook putting an SSM parameter such
|
||||
# as MY_API_URL=https://example.com then fails with "Unable to retrieve https://…", and a hook that
|
||||
# backgrounds its calls loses that silently. AWS CLI v2 dropped the behaviour, so this only shows up
|
||||
# in the image, never when the same script is run from a host with v2.
|
||||
RUN mkdir -p /app/data /etc/floci/aws \
|
||||
&& printf '[default]\nendpoint_url = http://localhost:4566\ncli_follow_urlparam = false\n' > /etc/floci/aws/config \
|
||||
&& chown 1001:root /app \
|
||||
&& chmod "g+rwX" /app \
|
||||
&& chown 1001:root /app/data
|
||||
|
||||
COPY --from=build /build/target/quarkus-app/ quarkus-app/
|
||||
RUN chown -R 1001:root /app/quarkus-app
|
||||
|
||||
COPY --chmod=755 docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
COPY --chmod=755 docker/localstack-parity.sh /usr/local/bin/localstack-parity.sh
|
||||
|
||||
VOLUME /app/data
|
||||
|
||||
EXPOSE 4566
|
||||
|
||||
# Need to expose separately due to https://github.com/floci-io/floci/issues/2095.
|
||||
# This preseres compatibility with https://github.com/docker-java/docker-java/blob/002b6eb7aa2b892be729c7c2a5e95f782924db38/docker-java-api/src/main/java/com/github/dockerjava/api/model/ExposedPort.java#L119
|
||||
EXPOSE 6379
|
||||
EXPOSE 6380
|
||||
EXPOSE 6381
|
||||
EXPOSE 6382
|
||||
EXPOSE 6383
|
||||
EXPOSE 6384
|
||||
EXPOSE 6385
|
||||
EXPOSE 6386
|
||||
EXPOSE 6387
|
||||
EXPOSE 6388
|
||||
EXPOSE 6389
|
||||
EXPOSE 6390
|
||||
EXPOSE 6391
|
||||
EXPOSE 6392
|
||||
EXPOSE 6393
|
||||
EXPOSE 6394
|
||||
EXPOSE 6395
|
||||
EXPOSE 6396
|
||||
EXPOSE 6397
|
||||
EXPOSE 6398
|
||||
EXPOSE 6399
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=3s --retries=5 \
|
||||
CMD wget -q --spider http://localhost:4566/_floci/health || exit 1
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
# A published port needs 0.0.0.0 inside the container, which Floci only accepts with explicit consent.
|
||||
CMD ["java", "--enable-native-access=ALL-UNNAMED", "-Dquarkus.http.host=0.0.0.0", "-Dfloci.security.allow-unsafe-network-exposure=true", "-jar", "/app/quarkus-app/quarkus-run.jar"]
|
||||
@@ -0,0 +1,121 @@
|
||||
ARG VERSION=latest
|
||||
# BASE_IMAGE is the image produced by Dockerfile.native-package. Release and nightly
|
||||
# tags use VERSION; reusable builds override BASE_IMAGE with the digest-qualified
|
||||
# native image they just published.
|
||||
#
|
||||
# That image is ubi9-micro and has no package manager, so this image cannot extend it.
|
||||
ARG BASE_IMAGE=floci/floci:${VERSION}
|
||||
FROM ${BASE_IMAGE} AS native
|
||||
|
||||
# Everything that does not depend on the Floci version. CI builds this stage alone
|
||||
# (target: tools) to test the aws CLI shim and to warm the layer cache.
|
||||
FROM registry.access.redhat.com/ubi9-minimal:9.7 AS tools
|
||||
|
||||
LABEL org.opencontainers.image.title="Floci (compat)" \
|
||||
org.opencontainers.image.description="Floci compatibility image with AWS CLI and boto3 pre-installed" \
|
||||
org.opencontainers.image.url="https://floci.io" \
|
||||
org.opencontainers.image.source="https://github.com/floci-io/floci" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.vendor="Floci" \
|
||||
description="Floci compatibility image with AWS CLI and boto3 pre-installed" \
|
||||
io.k8s.description="Floci compatibility image with AWS CLI and boto3 pre-installed" \
|
||||
io.k8s.display-name="Floci (compat)" \
|
||||
io.openshift.expose-services="4566:tcp" \
|
||||
io.openshift.tags="floci aws-emulator" \
|
||||
maintainer="Floci <https://github.com/floci-io/floci>" \
|
||||
name="floci/floci" \
|
||||
summary="Floci compatibility image with AWS CLI and boto3 pre-installed" \
|
||||
url="https://floci.io" \
|
||||
vendor="Floci"
|
||||
|
||||
ENV AWS_DEFAULT_REGION=us-east-1
|
||||
ENV AWS_ACCESS_KEY_ID=test
|
||||
ENV AWS_SECRET_ACCESS_KEY=test
|
||||
ENV AWS_ENDPOINT_URL="http://localhost:4566"
|
||||
ENV AWS_CONFIG_FILE=/etc/floci/aws/config
|
||||
ENV AWS_PAGER=""
|
||||
|
||||
# AWS CLI v2. v1 (the only version pip3 install awscli can give you) resolves
|
||||
# a fixed set of services (SQS, RDS, DocDB, Neptune, EMR, Health) through
|
||||
# awscli/customizations/overridesslcommonname.py, which forces the real AWS
|
||||
# endpoint whenever no --endpoint-url flag is passed on the command line —
|
||||
# no environment variable, including AWS_ENDPOINT_URL and the per-service
|
||||
# AWS_ENDPOINT_URL_<SERVICE> overrides, can redirect it (aws/aws-cli#8737).
|
||||
# v2 has no equivalent override and resolves every service through the
|
||||
# normal endpoint chain, so AWS_ENDPOINT_URL alone is sufficient.
|
||||
#
|
||||
# v1 also entered maintenance mode 2026-07-15 (EOL 2027-07-15), so this is
|
||||
# needed regardless of the endpoint-resolution bug.
|
||||
#
|
||||
# cli_binary_format has no env var in v2 (its provider chain is config-file,
|
||||
# then a base64 constant), so blob params default to base64 here, same as
|
||||
# a v2 install against real AWS. A hook passing raw bytes (e.g.
|
||||
# aws lambda invoke --payload) needs --cli-binary-format raw-in-base64-out.
|
||||
ARG AWSCLI_VERSION=2.36.24
|
||||
RUN useradd -r -u 1001 -g root -d /app -s /sbin/nologin floci \
|
||||
&& microdnf install -y --nodocs python3 python3-pip unzip \
|
||||
&& pip3 install --no-cache-dir boto3 \
|
||||
&& case "$(uname -m)" in \
|
||||
x86_64) arch=x86_64 ;; \
|
||||
aarch64) arch=aarch64 ;; \
|
||||
*) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;; \
|
||||
esac \
|
||||
&& curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${arch}-${AWSCLI_VERSION}.zip" -o /tmp/awscliv2.zip \
|
||||
&& unzip -q /tmp/awscliv2.zip -d /tmp \
|
||||
&& /tmp/aws/install --bin-dir /usr/bin --install-dir /usr/local/aws-cli \
|
||||
&& rm -rf /tmp/awscliv2.zip /tmp/aws \
|
||||
# The CLI's `aws <cmd> help` example pages are 26 MB of text nobody reads inside a container;
|
||||
# the command reference itself (dist/awscli/data) stays. unzip exists only to unpack the CLI.
|
||||
# pip stays: the image documents it and init hooks use it to install packages.
|
||||
&& rm -rf /usr/local/aws-cli/v2/*/dist/awscli/examples \
|
||||
# Tab completion has no user inside a container: aws_completer and its 21 MB ac.index go.
|
||||
# The system Python keeps pip, setuptools and what boto3 imports; the optimised .pyc
|
||||
# variants, lib2to3 and ensurepip (pip is already installed) are build-time extras.
|
||||
&& rm -rf /usr/local/aws-cli/v2/*/dist/awscli/data/ac.index /usr/local/aws-cli/v2/*/dist/aws_completer /usr/bin/aws_completer \
|
||||
&& rm -rf /usr/lib64/python3.9/lib2to3 /usr/lib64/python3.9/ensurepip \
|
||||
&& python3 -c 'import pathlib; [p.unlink() for d in ("/usr/lib64/python3.9","/usr/lib/python3.9") for p in pathlib.Path(d).rglob("*.opt-[12].pyc")]' \
|
||||
&& microdnf remove -y unzip \
|
||||
&& microdnf clean all \
|
||||
&& rm -rf /root/.cache/pip
|
||||
|
||||
COPY --chmod=755 bin/awslocal /usr/local/bin/awslocal
|
||||
|
||||
# Pre-create the init-hook directories so archive-based copies (Testcontainers
|
||||
# withCopyFileToContainer / `docker cp`) can drop hooks straight into them.
|
||||
# Docker's archive extract unpacks into an existing dir and never mkdir -p's the
|
||||
# parents, so without these the copy fails. Mirrors LocalStack, which ships
|
||||
# /etc/localstack/init. Owned by the runtime user (uid 1001, group root).
|
||||
RUN mkdir -p /etc/localstack/init/boot.d /etc/localstack/init/start.d /etc/localstack/init/ready.d \
|
||||
/etc/floci/init/boot.d /etc/floci/init/start.d /etc/floci/init/ready.d \
|
||||
&& chown -R 1001:root /etc/localstack /etc/floci
|
||||
|
||||
WORKDIR /app
|
||||
# A chmod after the copy below would rewrite the 200 MB binary into a second layer.
|
||||
RUN chown 1001:root /app && chmod g+rwX /app
|
||||
|
||||
FROM tools
|
||||
|
||||
ARG VERSION
|
||||
ENV FLOCI_VERSION=${VERSION}
|
||||
ENV FLOCI_STORAGE_PERSISTENT_PATH=/app/data
|
||||
# The compatibility image can be built on top of an older native image during
|
||||
# CI. An environment variable is ignored by older binaries and enables the
|
||||
# network guard in images that support it.
|
||||
ENV FLOCI_SECURITY_ALLOW_UNSAFE_NETWORK_EXPOSURE=true
|
||||
|
||||
COPY --from=native --chown=1001:root /app/ /app/
|
||||
VOLUME /app/data
|
||||
|
||||
COPY --from=native /usr/local/bin/docker-entrypoint.sh /usr/local/bin/localstack-parity.sh /usr/local/bin/
|
||||
# From the build context, not the native image: a published native image may predate the script.
|
||||
COPY --chmod=755 docker/healthcheck.sh /usr/local/bin/
|
||||
|
||||
EXPOSE 4566
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=3s --retries=5 CMD ["/usr/local/bin/healthcheck.sh"]
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
# A published port needs 0.0.0.0 inside the container, which Floci only accepts with explicit consent.
|
||||
CMD ["/app/application", "-Dquarkus.http.host=0.0.0.0"]
|
||||
|
||||
USER 1001
|
||||
@@ -0,0 +1,89 @@
|
||||
# JVM image built from an artifact that already exists on the host:
|
||||
# ./mvnw package -DskipTests && docker build -f docker/Dockerfile.jvm-package -t floci/floci:dev .
|
||||
#
|
||||
# The counterpart to Dockerfile.native-package, for the JVM runtime: same idea, same reason. It
|
||||
# exists for the local loop: docker/Dockerfile runs Maven inside the build, which re-resolves every
|
||||
# dependency and wants a couple of gigabytes, so iterating on a change through it is slow and can
|
||||
# fall over on a machine with little headroom. Consuming target/quarkus-app takes seconds.
|
||||
#
|
||||
# Everything below the FROM is a copy of the runtime stage of docker/Dockerfile, which is the
|
||||
# canonical definition. Exactly two lines differ, both by design: the default VERSION, and where
|
||||
# the application directory is copied from. DockerfileRuntimeParityTest enforces that, so a change
|
||||
# to the canonical runtime stage fails the build here until it is mirrored.
|
||||
FROM eclipse-temurin:25-jre-noble
|
||||
|
||||
ARG VERSION=dev
|
||||
ENV FLOCI_VERSION=${VERSION}
|
||||
ENV FLOCI_STORAGE_PERSISTENT_PATH=/app/data
|
||||
|
||||
ENV AWS_DEFAULT_REGION=us-east-1
|
||||
ENV AWS_ACCESS_KEY_ID=test
|
||||
ENV AWS_SECRET_ACCESS_KEY=test
|
||||
ENV AWS_CONFIG_FILE=/etc/floci/aws/config
|
||||
|
||||
# Privilege drop happens in the entrypoint through coreutils chroot, so no gosu
|
||||
# download; wget stays for the HEALTHCHECK below.
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends ca-certificates passwd python3 python3-pip wget; \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
pip3 install --no-cache-dir --break-system-packages awscli boto3; \
|
||||
aws_bin="$(command -v aws)"; \
|
||||
mv "$aws_bin" /usr/bin/aws.real; \
|
||||
printf '#!/bin/sh\nexec /usr/bin/aws.real --endpoint-url=http://localhost:4566 "$@"\n' > /usr/bin/aws; \
|
||||
chmod +x /usr/bin/aws; \
|
||||
useradd -r -u 1001 -g root -d /app -s /sbin/nologin floci
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# cli_follow_urlparam: the bundled CLI is v1 (pip ships no v2), where a parameter value that looks
|
||||
# like a URL is *fetched* and its body used as the value. An init hook putting an SSM parameter such
|
||||
# as MY_API_URL=https://example.com then fails with "Unable to retrieve https://…", and a hook that
|
||||
# backgrounds its calls loses that silently. AWS CLI v2 dropped the behaviour, so this only shows up
|
||||
# in the image, never when the same script is run from a host with v2.
|
||||
RUN mkdir -p /app/data /etc/floci/aws \
|
||||
&& printf '[default]\nendpoint_url = http://localhost:4566\ncli_follow_urlparam = false\n' > /etc/floci/aws/config \
|
||||
&& chown 1001:root /app \
|
||||
&& chmod "g+rwX" /app \
|
||||
&& chown 1001:root /app/data
|
||||
|
||||
COPY target/quarkus-app/ quarkus-app/
|
||||
RUN chown -R 1001:root /app/quarkus-app
|
||||
|
||||
COPY --chmod=755 docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
COPY --chmod=755 docker/localstack-parity.sh /usr/local/bin/localstack-parity.sh
|
||||
|
||||
VOLUME /app/data
|
||||
|
||||
EXPOSE 4566
|
||||
|
||||
# Need to expose separately due to https://github.com/floci-io/floci/issues/2095.
|
||||
# This preseres compatibility with https://github.com/docker-java/docker-java/blob/002b6eb7aa2b892be729c7c2a5e95f782924db38/docker-java-api/src/main/java/com/github/dockerjava/api/model/ExposedPort.java#L119
|
||||
EXPOSE 6379
|
||||
EXPOSE 6380
|
||||
EXPOSE 6381
|
||||
EXPOSE 6382
|
||||
EXPOSE 6383
|
||||
EXPOSE 6384
|
||||
EXPOSE 6385
|
||||
EXPOSE 6386
|
||||
EXPOSE 6387
|
||||
EXPOSE 6388
|
||||
EXPOSE 6389
|
||||
EXPOSE 6390
|
||||
EXPOSE 6391
|
||||
EXPOSE 6392
|
||||
EXPOSE 6393
|
||||
EXPOSE 6394
|
||||
EXPOSE 6395
|
||||
EXPOSE 6396
|
||||
EXPOSE 6397
|
||||
EXPOSE 6398
|
||||
EXPOSE 6399
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=3s --retries=5 \
|
||||
CMD wget -q --spider http://localhost:4566/_floci/health || exit 1
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
# A published port needs 0.0.0.0 inside the container, which Floci only accepts with explicit consent.
|
||||
CMD ["java", "--enable-native-access=ALL-UNNAMED", "-Dquarkus.http.host=0.0.0.0", "-Dfloci.security.allow-unsafe-network-exposure=true", "-jar", "/app/quarkus-app/quarkus-run.jar"]
|
||||
@@ -0,0 +1,50 @@
|
||||
# Stage 1: Build native executable
|
||||
FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-25 AS build
|
||||
USER root
|
||||
WORKDIR /app
|
||||
|
||||
# Install Maven 3.9.x (system Maven is too old for Quarkus 3.31)
|
||||
RUN curl -fsSL https://archive.apache.org/dist/maven/maven-3/3.9.12/binaries/apache-maven-3.9.12-bin.tar.gz | tar xz -C /opt \
|
||||
&& ln -sf /opt/apache-maven-3.9.12/bin/mvn /usr/local/bin/mvn
|
||||
|
||||
COPY pom.xml .
|
||||
RUN mvn dependency:go-offline -B
|
||||
COPY src ./src
|
||||
|
||||
# Quarkus 3.39 switches on -H:+AOTSingleCallsiteInline for Mandrel 25.0.4+. The generated
|
||||
# REST invokers for AwsQueryController.dispatch and AwsJson11Controller.handle are each the
|
||||
# single call site of a very large method; inlined, the method outgrows the aarch64
|
||||
# conditional-branch range and the build dies with BranchTargetOutOfBoundsException.
|
||||
# The flag only exists on Mandrel, so it is passed here rather than in application.yml,
|
||||
# the same way the workflow native builds pass it.
|
||||
RUN mvn clean package -Dnative -DskipTests -B \
|
||||
-Dquarkus.native.additional-build-args-append=-H:-AOTSingleCallsiteInline
|
||||
|
||||
# Stage 2: Minimal runtime. Same base and user setup as Dockerfile.native-package.
|
||||
FROM registry.access.redhat.com/ubi9-micro:9.7
|
||||
|
||||
WORKDIR /app
|
||||
RUN echo 'floci:x:1001:0:Floci:/app:/sbin/nologin' >> /etc/passwd \
|
||||
&& mkdir -p /app/data \
|
||||
&& chown -R 1001:root /app \
|
||||
&& chmod -R g+rwX /app
|
||||
VOLUME /app/data
|
||||
|
||||
EXPOSE 4566
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=3s --retries=5 CMD ["/usr/local/bin/healthcheck.sh"]
|
||||
|
||||
ARG VERSION=latest
|
||||
ENV FLOCI_VERSION=${VERSION}
|
||||
ENV FLOCI_STORAGE_PERSISTENT_PATH=/app/data
|
||||
|
||||
# The builder image has no zlib-static, so native-image links zlib dynamically and the
|
||||
# runner needs libz.so.1, which ubi9-micro does not ship. Take it from the builder image.
|
||||
COPY --from=build /usr/lib64/libz.so.1 /usr/lib64/libz.so.1
|
||||
COPY --chmod=755 --from=build /app/target/*-runner /app/application
|
||||
COPY --chmod=755 docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
COPY --chmod=755 docker/localstack-parity.sh docker/healthcheck.sh /usr/local/bin/
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
# A published port needs 0.0.0.0 inside the container, which Floci only accepts with explicit consent.
|
||||
CMD ["/app/application", "-Dquarkus.http.host=0.0.0.0", "-Dfloci.security.allow-unsafe-network-exposure=true"]
|
||||
@@ -0,0 +1,47 @@
|
||||
FROM registry.access.redhat.com/ubi9-micro:9.7
|
||||
|
||||
# Automatically set by Docker BuildKit to "amd64" or "arm64"
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# ubi9-micro has no shadow-utils, so the runtime user is one /etc/passwd line.
|
||||
RUN echo 'floci:x:1001:0:Floci:/app:/sbin/nologin' >> /etc/passwd \
|
||||
&& mkdir -p /app/data \
|
||||
&& chown -R 1001:root /app \
|
||||
&& chmod -R g+rwX /app
|
||||
|
||||
VOLUME /app/data
|
||||
|
||||
COPY --chmod=755 docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
COPY --chmod=755 docker/localstack-parity.sh docker/healthcheck.sh /usr/local/bin/
|
||||
|
||||
ARG VERSION=latest
|
||||
ENV FLOCI_VERSION=${VERSION}
|
||||
ENV FLOCI_STORAGE_PERSISTENT_PATH=/app/data
|
||||
COPY --chmod=755 --chown=1001:root native/${TARGETARCH}/ /app/
|
||||
|
||||
LABEL org.opencontainers.image.title="Floci" \
|
||||
org.opencontainers.image.description="Local AWS emulator — open-source alternative to LocalStack Community" \
|
||||
org.opencontainers.image.url="https://floci.io" \
|
||||
org.opencontainers.image.source="https://github.com/floci-io/floci" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.vendor="Floci" \
|
||||
description="Local AWS emulator — open-source alternative to LocalStack Community" \
|
||||
io.k8s.description="Local AWS emulator — open-source alternative to LocalStack Community" \
|
||||
io.k8s.display-name="Floci" \
|
||||
io.openshift.expose-services="4566:tcp" \
|
||||
io.openshift.tags="floci aws-emulator" \
|
||||
maintainer="Floci <https://github.com/floci-io/floci>" \
|
||||
name="floci/floci" \
|
||||
summary="Local AWS emulator — open-source alternative to LocalStack Community" \
|
||||
url="https://floci.io" \
|
||||
vendor="Floci"
|
||||
|
||||
EXPOSE 4566
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=3s --retries=5 CMD ["/usr/local/bin/healthcheck.sh"]
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
# A published port needs 0.0.0.0 inside the container, which Floci only accepts with explicit consent.
|
||||
CMD ["/app/application", "-Dquarkus.http.host=0.0.0.0", "-Dfloci.security.allow-unsafe-network-exposure=true"]
|
||||
@@ -0,0 +1,30 @@
|
||||
releaseId: ubuntu-24.04
|
||||
images:
|
||||
- id: ubuntu-24.04-arm64
|
||||
catalogImageId: ami-ubuntu2404-cloud-arm64
|
||||
catalogAliases:
|
||||
- ami-ubuntu2404-cloud
|
||||
family: ubuntu
|
||||
version: "24.04"
|
||||
architecture: arm64
|
||||
aws:
|
||||
region: us-east-1
|
||||
ownerId: "099720109477"
|
||||
imageId: ami-ubuntu2404-arm64
|
||||
name: ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-arm64-server-20260515
|
||||
creationDate: "2026-05-15T00:00:00.000Z"
|
||||
virtualizationType: hvm
|
||||
rootDeviceType: ebs
|
||||
canonical:
|
||||
baseUrl: https://cloud-images.ubuntu.com/releases/noble/release
|
||||
rootfs: ubuntu-24.04-server-cloudimg-arm64-root.tar.xz
|
||||
rootfsSha256: 15188696da114a3ffd3d3554f5858a0c3ac257933656e85feb4e0e83ad542b4a
|
||||
manifest: ubuntu-24.04-server-cloudimg-arm64.manifest
|
||||
docker:
|
||||
image: floci/ami-ubuntu:24.04-arm64
|
||||
guest:
|
||||
runtime: systemd
|
||||
cloudInit: true
|
||||
smokePackages:
|
||||
- systemd
|
||||
- cloud-init
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/sh
|
||||
# Starts as root, reads the bind-mounted Docker socket's group so the
|
||||
# unprivileged `floci` user can reach it on any host, then re-executes this
|
||||
# script as `floci` via coreutils `chroot --userspec`, which also grants that
|
||||
# group as a supplementary group. The second invocation falls through to exec
|
||||
# the user's command. No gosu, no usermod: both would cost a package layer the
|
||||
# base image does not otherwise need.
|
||||
#
|
||||
# Why: on native Linux Docker, /var/run/docker.sock is owned by
|
||||
# root:docker with mode 660 and the docker GID varies by distro. On
|
||||
# Docker Desktop (macOS/Windows) the socket is typically root:root (GID
|
||||
# 0). Without the group fix-up below, floci (uid 1001, group 0) can open
|
||||
# the socket on Docker Desktop but not on native Linux — which breaks
|
||||
# ECR, Lambda, and RDS service emulation there. Discovering the GID at
|
||||
# runtime handles every host transparently.
|
||||
|
||||
set -eu
|
||||
|
||||
if [ "$(id -u)" = '0' ]; then
|
||||
groups='0'
|
||||
if [ -S /var/run/docker.sock ]; then
|
||||
sock_gid="$(stat -c '%g' /var/run/docker.sock)"
|
||||
if [ "$sock_gid" != '0' ]; then
|
||||
groups="0,$sock_gid"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Re-own state dir for the case where a host bind-mount arrives with
|
||||
# ownership the floci user cannot write to. Ignore errors (read-only
|
||||
# mounts, unusual filesystems) so the container still starts.
|
||||
if [ -d /app/data ]; then
|
||||
chown -R floci:root /app/data 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# `chroot /` changes nothing but the identity: uid 1001, primary gid 0, plus the socket's
|
||||
# group. Supplementary groups are set by number, so the group needs no /etc/group entry.
|
||||
# --skip-chdir keeps the working directory (/app, where relative data paths resolve); GNU
|
||||
# chroot would otherwise chdir to the new root.
|
||||
if [ "${FLOCI_RUN_AS_ROOT:-false}" != 'true' ]; then
|
||||
exec chroot --userspec=1001:0 --groups="$groups" --skip-chdir / "$0" "$@"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${LOCALSTACK_PARITY:-true}" != "false" ]; then
|
||||
. /usr/local/bin/localstack-parity.sh
|
||||
fi
|
||||
|
||||
# Probe the state dir as the unprivileged user and warn loudly when it is not
|
||||
# writable (read-only or root-owned bind mounts the chown above could not fix).
|
||||
# The container still starts: with the default in-memory storage the dir is
|
||||
# unused, and non-memory modes fail fast at boot with an actionable error.
|
||||
data_dir="${FLOCI_STORAGE_PERSISTENT_PATH:-/app/data}"
|
||||
if [ -d "$data_dir" ]; then
|
||||
probe="$data_dir/.floci-write-probe.$$"
|
||||
if ! ( touch "$probe" && rm -f "$probe" ) 2>/dev/null; then
|
||||
cat >&2 <<EOF
|
||||
**************************************************************************
|
||||
WARNING: $data_dir is not writable by $(id -un) (uid $(id -u)).
|
||||
Persisted state cannot be saved there. If FLOCI_STORAGE_MODE (or any
|
||||
per-service storage mode) is not 'memory', Floci will refuse to start.
|
||||
Fix the volume mount permissions (it may be read-only or root-owned) or
|
||||
set FLOCI_STORAGE_PERSISTENT_PATH to a writable directory.
|
||||
**************************************************************************
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fall back to the image's default command when invoked with no arguments.
|
||||
# Some tooling re-executes this entrypoint directly with an empty argv —
|
||||
# Testcontainers' LocalStackContainer does exactly that via its starter
|
||||
# script — and `exec "$@"` with no argv would make the script reach EOF
|
||||
# and exit 0 without ever starting the emulator. LocalStack's entrypoint
|
||||
# ignores its argv entirely, so this keeps drop-in parity.
|
||||
# The default matches the CMD of the image variant: native images ship
|
||||
# /app/application, JVM images ship /app/quarkus-app/quarkus-run.jar.
|
||||
# Both listen on 0.0.0.0 so a published port reaches Floci, which it only
|
||||
# accepts with explicit consent. The JVM reads -D options only before -jar.
|
||||
if [ $# -eq 0 ]; then
|
||||
if [ -x /app/application ]; then
|
||||
set -- /app/application -Dquarkus.http.host=0.0.0.0 -Dfloci.security.allow-unsafe-network-exposure=true
|
||||
else
|
||||
set -- java -Dquarkus.http.host=0.0.0.0 -Dfloci.security.allow-unsafe-network-exposure=true -jar /app/quarkus-app/quarkus-run.jar
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Container health probe. Bash only, because ubi9-micro has no curl.
|
||||
exec 3<>/dev/tcp/127.0.0.1/4566 || exit 1
|
||||
printf 'GET /_floci/health HTTP/1.0\r\nHost: localhost\r\n\r\n' >&3
|
||||
read -r _proto status _rest <&3
|
||||
[ "$status" = 200 ]
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/bin/sh
|
||||
# Maps LocalStack Community environment variables to their Floci equivalents.
|
||||
# Sourced by entrypoint.sh when LOCALSTACK_PARITY=true.
|
||||
# Floci vars always win: every mapping uses ${FLOCI_VAR:-<derived>} so an
|
||||
# explicitly-set Floci var is never overwritten.
|
||||
|
||||
# Storage mode — PERSISTENCE=1 / PERSIST_STATE=1 → persistent storage
|
||||
if [ -n "${PERSISTENCE:-}" ] || [ -n "${PERSIST_STATE:-}" ]; then
|
||||
_ls_persist="${PERSISTENCE:-${PERSIST_STATE:-}}"
|
||||
if [ "${_ls_persist}" = "1" ] || [ "${_ls_persist}" = "true" ]; then
|
||||
export FLOCI_STORAGE_MODE="${FLOCI_STORAGE_MODE:-persistent}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Bind port — EDGE_PORT → FLOCI_PORT
|
||||
[ -n "${EDGE_PORT:-}" ] && export FLOCI_PORT="${FLOCI_PORT:-${EDGE_PORT}}"
|
||||
|
||||
# Hostname returned in response URLs — LOCALSTACK_HOST / LOCALSTACK_HOSTNAME → FLOCI_HOSTNAME
|
||||
_ls_host="${LOCALSTACK_HOST:-${LOCALSTACK_HOSTNAME:-}}"
|
||||
[ -n "${_ls_host}" ] && export FLOCI_HOSTNAME="${FLOCI_HOSTNAME:-${_ls_host}}"
|
||||
|
||||
# Bind address — GATEWAY_LISTEN → QUARKUS_HTTP_HOST. LocalStack listens there with no further
|
||||
# opt-in, so GATEWAY_LISTEN also gives the consent Floci requires for a non-loopback address.
|
||||
if [ -n "${GATEWAY_LISTEN:-}" ]; then
|
||||
export QUARKUS_HTTP_HOST="${QUARKUS_HTTP_HOST:-${GATEWAY_LISTEN}}"
|
||||
export FLOCI_SECURITY_ALLOW_UNSAFE_NETWORK_EXPOSURE="${FLOCI_SECURITY_ALLOW_UNSAFE_NETWORK_EXPOSURE:-true}"
|
||||
fi
|
||||
|
||||
# Log level — LS_LOG / DEBUG=1 → QUARKUS_LOG_LEVEL
|
||||
if [ -n "${LS_LOG:-}" ]; then
|
||||
export QUARKUS_LOG_LEVEL="${QUARKUS_LOG_LEVEL:-${LS_LOG}}"
|
||||
elif [ "${DEBUG:-}" = "1" ]; then
|
||||
export QUARKUS_LOG_LEVEL="${QUARKUS_LOG_LEVEL:-DEBUG}"
|
||||
fi
|
||||
|
||||
# Lambda — LAMBDA_EXECUTOR is intentionally ignored; Floci always runs Lambda in Docker containers.
|
||||
|
||||
# Lambda Docker network
|
||||
[ -n "${LAMBDA_DOCKER_NETWORK:-}" ] && \
|
||||
export FLOCI_SERVICES_LAMBDA_DOCKER_NETWORK="${FLOCI_SERVICES_LAMBDA_DOCKER_NETWORK:-${LAMBDA_DOCKER_NETWORK}}"
|
||||
|
||||
# Lambda ephemeral containers
|
||||
if [ "${LAMBDA_REMOVE_CONTAINERS:-}" = "1" ] || [ "${LAMBDA_REMOVE_CONTAINERS:-}" = "true" ]; then
|
||||
export FLOCI_SERVICES_LAMBDA_EPHEMERAL="${FLOCI_SERVICES_LAMBDA_EPHEMERAL:-true}"
|
||||
fi
|
||||
|
||||
# LAMBDA_REMOTE_DOCKER — not fully supported.
|
||||
# Floci's hot-reload is per-function opt-in (S3Bucket=hot-reload), not a global bind-mount mode.
|
||||
if [ -n "${LAMBDA_REMOTE_DOCKER:-}" ]; then
|
||||
echo "[floci-parity] WARNING: LAMBDA_REMOTE_DOCKER is not fully supported by Floci." >&2
|
||||
echo "[floci-parity] Use S3Bucket=hot-reload per function instead. See https://floci.io/docs/services/lambda" >&2
|
||||
fi
|
||||
|
||||
# Docker host
|
||||
[ -n "${DOCKER_HOST:-}" ] && export FLOCI_DOCKER_DOCKER_HOST="${FLOCI_DOCKER_DOCKER_HOST:-${DOCKER_HOST}}"
|
||||
|
||||
# Docker network — shared across all container-based services (Lambda, RDS, ElastiCache, MSK, OpenSearch, EKS).
|
||||
# Per-service overrides (e.g. FLOCI_SERVICES_LAMBDA_DOCKER_NETWORK) take precedence when set.
|
||||
[ -n "${DOCKER_NETWORK:-}" ] && export FLOCI_SERVICES_DOCKER_NETWORK="${FLOCI_SERVICES_DOCKER_NETWORK:-${DOCKER_NETWORK}}"
|
||||
|
||||
# DNS suffixes — register LocalStack and Floci hostname suffixes so that container-to-container
|
||||
# hostname routing (Function URLs, presigned S3, SQS QueueUrl, etc.) works without manual config.
|
||||
_parity_suffixes="localhost.localstack.cloud,localhost.floci.io"
|
||||
if [ -n "${FLOCI_DNS_EXTRA_SUFFIXES:-}" ]; then
|
||||
export FLOCI_DNS_EXTRA_SUFFIXES="${FLOCI_DNS_EXTRA_SUFFIXES},${_parity_suffixes}"
|
||||
else
|
||||
export FLOCI_DNS_EXTRA_SUFFIXES="${_parity_suffixes}"
|
||||
fi
|
||||
|
||||
# TLS — USE_SSL=1 → FLOCI_TLS_ENABLED=true
|
||||
if [ "${USE_SSL:-0}" = "1" ]; then
|
||||
export FLOCI_TLS_ENABLED="${FLOCI_TLS_ENABLED:-true}"
|
||||
fi
|
||||
|
||||
# TLS — CUSTOM_SSL_CERT_PATH → FLOCI_TLS_CERT_PATH + FLOCI_TLS_KEY_PATH
|
||||
# LocalStack uses a single combined PEM (cert+key). Floci accepts it in both fields.
|
||||
if [ -n "${CUSTOM_SSL_CERT_PATH:-}" ]; then
|
||||
export FLOCI_TLS_CERT_PATH="${FLOCI_TLS_CERT_PATH:-${CUSTOM_SSL_CERT_PATH}}"
|
||||
export FLOCI_TLS_KEY_PATH="${FLOCI_TLS_KEY_PATH:-${CUSTOM_SSL_CERT_PATH}}"
|
||||
fi
|
||||
|
||||
# SERVICES: intentionally ignored; Floci starts every service in ~24ms.
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# 1. Start Floci
|
||||
echo "=== Starting Floci with docker-compose ==="
|
||||
docker compose up -d --build
|
||||
|
||||
# Wait for healthy
|
||||
echo "Waiting for Floci to be healthy..."
|
||||
# Portable wait without 'timeout' command
|
||||
MAX_RETRIES=60
|
||||
COUNT=0
|
||||
until curl -sf http://localhost:4566/_floci/health >/dev/null 2>&1; do
|
||||
if [ $COUNT -ge $MAX_RETRIES ]; then
|
||||
echo "Floci failed to become healthy in time"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
COUNT=$((COUNT + 1))
|
||||
echo -n "."
|
||||
done
|
||||
echo " Floci is up!"
|
||||
|
||||
# 2. Network setup (Floci uses floci_default from compose)
|
||||
NETWORK="floci_default"
|
||||
DOCKER_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || stat -f '%g' /var/run/docker.sock)
|
||||
|
||||
# Floci's embedded DNS server resolves *.floci → Floci's IP.
|
||||
# Passing --dns <floci-ip> to test containers lets the S3 virtual-host client
|
||||
# send to <bucket>.floci:4566 which Floci DNS resolves correctly. Without this,
|
||||
# Docker's built-in DNS only resolves the exact service name "floci", not
|
||||
# wildcard subdomains like my-bucket.floci.
|
||||
FLOCI_CONTAINER=$(docker compose ps -q floci 2>/dev/null | head -1)
|
||||
FLOCI_IP=$(docker inspect -f "{{.NetworkSettings.Networks.${NETWORK}.IPAddress}}" "$FLOCI_CONTAINER" 2>/dev/null || true)
|
||||
|
||||
# 3. Test suites
|
||||
SUITES=(
|
||||
"sdk-test-python"
|
||||
"sdk-test-node"
|
||||
"sdk-test-java"
|
||||
"sdk-test-go"
|
||||
"sdk-test-awscli"
|
||||
"compat-cdk"
|
||||
"compat-terraform"
|
||||
"compat-opentofu"
|
||||
)
|
||||
|
||||
# results dir
|
||||
mkdir -p test-results
|
||||
FAILED=0
|
||||
|
||||
for suite in "${SUITES[@]}"; do
|
||||
echo "=== Running $suite in Docker ==="
|
||||
|
||||
IMAGE_NAME="compat-$suite"
|
||||
|
||||
# Build
|
||||
if ! docker build -q -t "$IMAGE_NAME" "compatibility-tests/$suite"; then
|
||||
echo "Test suite $suite failed to build"
|
||||
FAILED=1
|
||||
continue
|
||||
fi
|
||||
|
||||
# Build DNS args: if we resolved Floci's IP, inject it as the DNS server so
|
||||
# wildcard subdomains like <bucket>.floci resolve inside test containers.
|
||||
DNS_ARGS=()
|
||||
if [ -n "$FLOCI_IP" ]; then
|
||||
DNS_ARGS=(--dns "$FLOCI_IP")
|
||||
fi
|
||||
|
||||
# Per-suite extra args, mirroring .github/workflows/compatibility.yml.
|
||||
# sdk-test-java's Lambda hot-reload test needs a host directory that the Docker
|
||||
# daemon can bind-mount into the hot-reload Lambda container.
|
||||
EXTRA_ARGS=()
|
||||
if [ "$suite" = "sdk-test-java" ]; then
|
||||
mkdir -p /tmp/floci-hot-reload
|
||||
EXTRA_ARGS=(-v /tmp/floci-hot-reload:/tmp/floci-hot-reload -e HOT_RELOAD_BASE_DIR=/tmp/floci-hot-reload)
|
||||
fi
|
||||
|
||||
# Run
|
||||
if ! docker run --rm --network "$NETWORK" \
|
||||
"${DNS_ARGS[@]}" \
|
||||
-e FLOCI_ENDPOINT=http://floci:4566 \
|
||||
-e FLOCI_S3_VHOST_ENDPOINT=http://floci:4566 \
|
||||
-v "$(pwd)/test-results:/results" \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--group-add "$DOCKER_GID" \
|
||||
"${EXTRA_ARGS[@]}" \
|
||||
"$IMAGE_NAME"; then
|
||||
echo "Test suite $suite failed"
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== All Docker tests completed ==="
|
||||
exit "$FAILED"
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/bin/sh
|
||||
# Unit tests for entrypoint.sh argument handling.
|
||||
# Run directly: sh docker/test-entrypoint.sh
|
||||
# Exit 0 on success, non-zero on first failure summary.
|
||||
#
|
||||
# These tests run as an unprivileged user with LOCALSTACK_PARITY=false.
|
||||
# Stub id and chroot to check the root path without changing privileges.
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT="$(cd "$(dirname "$0")" && pwd)/entrypoint.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_eq() {
|
||||
desc="$1"; expected="$2"; actual="$3"
|
||||
if [ "${actual}" = "${expected}" ]; then
|
||||
printf '[PASS] %s\n' "${desc}"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
printf '[FAIL] %s\n expected: %s\n actual: %s\n' "${desc}" "${expected}" "${actual}"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$(id -u)" = '0' ]; then
|
||||
echo "These tests must run as an unprivileged user (the root path re-execs via chroot)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "${WORK}"' EXIT INT TERM
|
||||
|
||||
# Stub java on PATH that prints the argv it was exec'd with.
|
||||
mkdir -p "${WORK}/bin"
|
||||
cat > "${WORK}/bin/java" <<'EOF'
|
||||
#!/bin/sh
|
||||
printf '%s\n' "java $*"
|
||||
EOF
|
||||
chmod +x "${WORK}/bin/java"
|
||||
|
||||
mkdir -p "${WORK}/root-bin"
|
||||
cat > "${WORK}/root-bin/id" <<'EOF'
|
||||
#!/bin/sh
|
||||
if [ "$1" = '-u' ]; then
|
||||
printf '0\n'
|
||||
else
|
||||
/usr/bin/id "$@"
|
||||
fi
|
||||
EOF
|
||||
cat > "${WORK}/root-bin/chroot" <<'EOF'
|
||||
#!/bin/sh
|
||||
printf 'dropped privileges\n'
|
||||
EOF
|
||||
chmod +x "${WORK}/root-bin/id" "${WORK}/root-bin/chroot"
|
||||
|
||||
assert_eq "root drops privileges by default" \
|
||||
"dropped privileges" \
|
||||
"$(PATH="${WORK}/root-bin:${PATH}" FLOCI_STORAGE_PERSISTENT_PATH="${WORK}/absent" LOCALSTACK_PARITY=false sh "${SCRIPT}" echo preserved)"
|
||||
|
||||
assert_eq "false keeps the unprivileged default" \
|
||||
"dropped privileges" \
|
||||
"$(PATH="${WORK}/root-bin:${PATH}" FLOCI_RUN_AS_ROOT=false FLOCI_STORAGE_PERSISTENT_PATH="${WORK}/absent" LOCALSTACK_PARITY=false sh "${SCRIPT}" echo preserved)"
|
||||
|
||||
assert_eq "other values keep the unprivileged default" \
|
||||
"dropped privileges" \
|
||||
"$(PATH="${WORK}/root-bin:${PATH}" FLOCI_RUN_AS_ROOT=TRUE FLOCI_STORAGE_PERSISTENT_PATH="${WORK}/absent" LOCALSTACK_PARITY=false sh "${SCRIPT}" echo preserved)"
|
||||
|
||||
assert_eq "explicit root option preserves command arguments" \
|
||||
"one two|three" \
|
||||
"$(PATH="${WORK}/root-bin:${PATH}" FLOCI_RUN_AS_ROOT=true FLOCI_STORAGE_PERSISTENT_PATH="${WORK}/absent" LOCALSTACK_PARITY=false sh "${SCRIPT}" sh -c 'printf "%s|%s\n" "$1" "$2"' _ "one two" three)"
|
||||
|
||||
assert_eq "non-root process cannot elevate with root option" \
|
||||
"one two" \
|
||||
"$(FLOCI_RUN_AS_ROOT=true LOCALSTACK_PARITY=false sh "${SCRIPT}" echo one two)"
|
||||
|
||||
# --- explicit arguments are exec'd unchanged ---
|
||||
assert_eq "explicit command is exec'd unchanged" \
|
||||
"one two" \
|
||||
"$(LOCALSTACK_PARITY=false sh "${SCRIPT}" echo one two)"
|
||||
|
||||
assert_eq "explicit java command bypasses the fallback" \
|
||||
"java -jar /custom/app.jar" \
|
||||
"$(PATH="${WORK}/bin:${PATH}" LOCALSTACK_PARITY=false sh "${SCRIPT}" java -jar /custom/app.jar)"
|
||||
|
||||
# --- empty argv falls back to the image default command ---
|
||||
# Without /app/application (JVM image layout), the fallback must exec the
|
||||
# Quarkus runner jar with the same arguments as the published image CMD.
|
||||
if [ ! -e /app/application ]; then
|
||||
assert_eq "empty argv falls back to the JVM default command" \
|
||||
"java -Dquarkus.http.host=0.0.0.0 -Dfloci.security.allow-unsafe-network-exposure=true -jar /app/quarkus-app/quarkus-run.jar" \
|
||||
"$(PATH="${WORK}/bin:${PATH}" LOCALSTACK_PARITY=false sh "${SCRIPT}")"
|
||||
else
|
||||
printf '[SKIP] empty argv falls back to the JVM default command (/app/application exists on this host)\n'
|
||||
fi
|
||||
|
||||
# With an executable /app/application (native image layout), the fallback
|
||||
# must prefer the native binary. Only runs where /app is writable or the
|
||||
# binary already exists (always true inside the published images).
|
||||
NATIVE_TESTABLE=false
|
||||
if [ -x /app/application ]; then
|
||||
NATIVE_TESTABLE=true
|
||||
elif mkdir -p /app 2>/dev/null && [ -w /app ]; then
|
||||
cat > /app/application <<'EOF'
|
||||
#!/bin/sh
|
||||
printf '%s\n' "/app/application $*"
|
||||
EOF
|
||||
chmod +x /app/application
|
||||
trap 'rm -f /app/application; rm -rf "${WORK}"' EXIT INT TERM
|
||||
NATIVE_TESTABLE=true
|
||||
fi
|
||||
if [ "${NATIVE_TESTABLE}" = 'true' ]; then
|
||||
assert_eq "empty argv prefers the native binary when present" \
|
||||
"/app/application -Dquarkus.http.host=0.0.0.0 -Dfloci.security.allow-unsafe-network-exposure=true" \
|
||||
"$(LOCALSTACK_PARITY=false sh "${SCRIPT}")"
|
||||
else
|
||||
printf '[SKIP] empty argv prefers the native binary when present (/app not writable)\n'
|
||||
fi
|
||||
|
||||
# --- unwritable state dir prints a warning but still execs the command ---
|
||||
RO_DIR="${WORK}/ro-data"
|
||||
mkdir -p "${RO_DIR}"
|
||||
chmod 555 "${RO_DIR}"
|
||||
RO_ERR="${WORK}/ro-err.txt"
|
||||
assert_eq "unwritable state dir still execs the command" \
|
||||
"ok" \
|
||||
"$(FLOCI_STORAGE_PERSISTENT_PATH="${RO_DIR}" LOCALSTACK_PARITY=false sh "${SCRIPT}" echo ok 2>"${RO_ERR}")"
|
||||
if grep -q 'not writable' "${RO_ERR}"; then
|
||||
printf '[PASS] unwritable state dir prints a loud warning\n'
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
printf '[FAIL] unwritable state dir prints a loud warning\n stderr was:\n%s\n' "$(cat "${RO_ERR}")"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
chmod 755 "${RO_DIR}"
|
||||
|
||||
# --- writable state dir stays quiet and leaves no probe behind ---
|
||||
RW_DIR="${WORK}/rw-data"
|
||||
mkdir -p "${RW_DIR}"
|
||||
RW_ERR="${WORK}/rw-err.txt"
|
||||
FLOCI_STORAGE_PERSISTENT_PATH="${RW_DIR}" LOCALSTACK_PARITY=false sh "${SCRIPT}" echo ok >/dev/null 2>"${RW_ERR}"
|
||||
if ! grep -q 'not writable' "${RW_ERR}" && [ -z "$(ls -A "${RW_DIR}")" ]; then
|
||||
printf '[PASS] writable state dir stays quiet and leaves no probe file\n'
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
printf '[FAIL] writable state dir stays quiet and leaves no probe file\n'
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
printf '\n%d passed, %d failed\n' "${PASS}" "${FAIL}"
|
||||
[ "${FAIL}" -eq 0 ]
|
||||
Executable
+153
@@ -0,0 +1,153 @@
|
||||
#!/bin/sh
|
||||
# Unit tests for localstack-parity.sh.
|
||||
# Run directly: sh docker/test-localstack-parity.sh
|
||||
# Exit 0 on success, non-zero on first failure.
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT="$(dirname "$0")/localstack-parity.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# Run the parity script in a subshell with a given environment and print the
|
||||
# value of a single variable. Arguments: VAR_NAME [ENV_KEY=VALUE ...]
|
||||
_run() {
|
||||
var="$1"; shift
|
||||
env -i "$@" sh -c ". '${SCRIPT}'; printf '%s' \"\${${var}:-}\""
|
||||
}
|
||||
|
||||
# Assert that _run produces an expected value.
|
||||
assert_eq() {
|
||||
desc="$1"; expected="$2"; actual="$3"
|
||||
if [ "${actual}" = "${expected}" ]; then
|
||||
printf '[PASS] %s\n' "${desc}"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
printf '[FAIL] %s\n expected: %s\n actual: %s\n' "${desc}" "${expected}" "${actual}"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# --- PERSISTENCE ---
|
||||
assert_eq "PERSISTENCE=1 sets FLOCI_STORAGE_MODE=persistent" \
|
||||
"persistent" \
|
||||
"$(_run FLOCI_STORAGE_MODE PERSISTENCE=1)"
|
||||
|
||||
assert_eq "PERSISTENCE=true sets FLOCI_STORAGE_MODE=persistent" \
|
||||
"persistent" \
|
||||
"$(_run FLOCI_STORAGE_MODE PERSISTENCE=true)"
|
||||
|
||||
assert_eq "PERSIST_STATE=1 sets FLOCI_STORAGE_MODE=persistent" \
|
||||
"persistent" \
|
||||
"$(_run FLOCI_STORAGE_MODE PERSIST_STATE=1)"
|
||||
|
||||
assert_eq "FLOCI_STORAGE_MODE wins over PERSISTENCE" \
|
||||
"hybrid" \
|
||||
"$(_run FLOCI_STORAGE_MODE PERSISTENCE=1 FLOCI_STORAGE_MODE=hybrid)"
|
||||
|
||||
# --- EDGE_PORT ---
|
||||
assert_eq "EDGE_PORT sets FLOCI_PORT" \
|
||||
"4567" \
|
||||
"$(_run FLOCI_PORT EDGE_PORT=4567)"
|
||||
|
||||
assert_eq "FLOCI_PORT wins over EDGE_PORT" \
|
||||
"4568" \
|
||||
"$(_run FLOCI_PORT EDGE_PORT=4567 FLOCI_PORT=4568)"
|
||||
|
||||
# --- LOCALSTACK_HOST / LOCALSTACK_HOSTNAME ---
|
||||
assert_eq "LOCALSTACK_HOST sets FLOCI_HOSTNAME" \
|
||||
"myhost" \
|
||||
"$(_run FLOCI_HOSTNAME LOCALSTACK_HOST=myhost)"
|
||||
|
||||
assert_eq "LOCALSTACK_HOSTNAME sets FLOCI_HOSTNAME when LOCALSTACK_HOST unset" \
|
||||
"myhost2" \
|
||||
"$(_run FLOCI_HOSTNAME LOCALSTACK_HOSTNAME=myhost2)"
|
||||
|
||||
assert_eq "LOCALSTACK_HOST takes priority over LOCALSTACK_HOSTNAME" \
|
||||
"primary" \
|
||||
"$(_run FLOCI_HOSTNAME LOCALSTACK_HOST=primary LOCALSTACK_HOSTNAME=secondary)"
|
||||
|
||||
assert_eq "FLOCI_HOSTNAME wins over LOCALSTACK_HOST" \
|
||||
"explicit" \
|
||||
"$(_run FLOCI_HOSTNAME LOCALSTACK_HOST=myhost FLOCI_HOSTNAME=explicit)"
|
||||
|
||||
# --- GATEWAY_LISTEN ---
|
||||
assert_eq "GATEWAY_LISTEN sets QUARKUS_HTTP_HOST" \
|
||||
"0.0.0.0" \
|
||||
"$(_run QUARKUS_HTTP_HOST GATEWAY_LISTEN=0.0.0.0)"
|
||||
|
||||
assert_eq "GATEWAY_LISTEN allows a non-loopback address" \
|
||||
"true" \
|
||||
"$(_run FLOCI_SECURITY_ALLOW_UNSAFE_NETWORK_EXPOSURE GATEWAY_LISTEN=0.0.0.0)"
|
||||
|
||||
assert_eq "FLOCI_SECURITY_ALLOW_UNSAFE_NETWORK_EXPOSURE wins over GATEWAY_LISTEN" \
|
||||
"false" \
|
||||
"$(_run FLOCI_SECURITY_ALLOW_UNSAFE_NETWORK_EXPOSURE GATEWAY_LISTEN=0.0.0.0 FLOCI_SECURITY_ALLOW_UNSAFE_NETWORK_EXPOSURE=false)"
|
||||
|
||||
assert_eq "no GATEWAY_LISTEN leaves network exposure unallowed" \
|
||||
"" \
|
||||
"$(_run FLOCI_SECURITY_ALLOW_UNSAFE_NETWORK_EXPOSURE)"
|
||||
|
||||
# --- LOG LEVEL ---
|
||||
assert_eq "LS_LOG sets QUARKUS_LOG_LEVEL" \
|
||||
"WARN" \
|
||||
"$(_run QUARKUS_LOG_LEVEL LS_LOG=WARN)"
|
||||
|
||||
assert_eq "DEBUG=1 sets QUARKUS_LOG_LEVEL=DEBUG" \
|
||||
"DEBUG" \
|
||||
"$(_run QUARKUS_LOG_LEVEL DEBUG=1)"
|
||||
|
||||
assert_eq "LS_LOG takes priority over DEBUG=1" \
|
||||
"TRACE" \
|
||||
"$(_run QUARKUS_LOG_LEVEL LS_LOG=TRACE DEBUG=1)"
|
||||
|
||||
assert_eq "QUARKUS_LOG_LEVEL wins over LS_LOG" \
|
||||
"INFO" \
|
||||
"$(_run QUARKUS_LOG_LEVEL LS_LOG=DEBUG QUARKUS_LOG_LEVEL=INFO)"
|
||||
|
||||
# --- LAMBDA ---
|
||||
assert_eq "LAMBDA_DOCKER_NETWORK sets FLOCI_SERVICES_LAMBDA_DOCKER_NETWORK" \
|
||||
"mynet" \
|
||||
"$(_run FLOCI_SERVICES_LAMBDA_DOCKER_NETWORK LAMBDA_DOCKER_NETWORK=mynet)"
|
||||
|
||||
assert_eq "FLOCI_SERVICES_LAMBDA_DOCKER_NETWORK wins over LAMBDA_DOCKER_NETWORK" \
|
||||
"floci-net" \
|
||||
"$(_run FLOCI_SERVICES_LAMBDA_DOCKER_NETWORK LAMBDA_DOCKER_NETWORK=mynet FLOCI_SERVICES_LAMBDA_DOCKER_NETWORK=floci-net)"
|
||||
|
||||
assert_eq "LAMBDA_REMOVE_CONTAINERS=1 sets FLOCI_SERVICES_LAMBDA_EPHEMERAL=true" \
|
||||
"true" \
|
||||
"$(_run FLOCI_SERVICES_LAMBDA_EPHEMERAL LAMBDA_REMOVE_CONTAINERS=1)"
|
||||
|
||||
assert_eq "LAMBDA_REMOVE_CONTAINERS=true sets FLOCI_SERVICES_LAMBDA_EPHEMERAL=true" \
|
||||
"true" \
|
||||
"$(_run FLOCI_SERVICES_LAMBDA_EPHEMERAL LAMBDA_REMOVE_CONTAINERS=true)"
|
||||
|
||||
assert_eq "FLOCI_SERVICES_LAMBDA_EPHEMERAL wins over LAMBDA_REMOVE_CONTAINERS" \
|
||||
"false" \
|
||||
"$(_run FLOCI_SERVICES_LAMBDA_EPHEMERAL LAMBDA_REMOVE_CONTAINERS=1 FLOCI_SERVICES_LAMBDA_EPHEMERAL=false)"
|
||||
|
||||
# --- DOCKER HOST / NETWORK ---
|
||||
assert_eq "DOCKER_HOST sets FLOCI_DOCKER_DOCKER_HOST" \
|
||||
"unix:///var/run/docker.sock" \
|
||||
"$(_run FLOCI_DOCKER_DOCKER_HOST DOCKER_HOST=unix:///var/run/docker.sock)"
|
||||
|
||||
assert_eq "DOCKER_NETWORK sets FLOCI_SERVICES_DOCKER_NETWORK" \
|
||||
"shared" \
|
||||
"$(_run FLOCI_SERVICES_DOCKER_NETWORK DOCKER_NETWORK=shared)"
|
||||
|
||||
assert_eq "FLOCI_SERVICES_DOCKER_NETWORK wins over DOCKER_NETWORK" \
|
||||
"override" \
|
||||
"$(_run FLOCI_SERVICES_DOCKER_NETWORK DOCKER_NETWORK=shared FLOCI_SERVICES_DOCKER_NETWORK=override)"
|
||||
|
||||
# --- DNS SUFFIXES ---
|
||||
assert_eq "DNS suffixes set when FLOCI_DNS_EXTRA_SUFFIXES unset" \
|
||||
"localhost.localstack.cloud,localhost.floci.io" \
|
||||
"$(_run FLOCI_DNS_EXTRA_SUFFIXES)"
|
||||
|
||||
assert_eq "DNS suffixes appended to existing FLOCI_DNS_EXTRA_SUFFIXES" \
|
||||
"custom.internal,localhost.localstack.cloud,localhost.floci.io" \
|
||||
"$(_run FLOCI_DNS_EXTRA_SUFFIXES FLOCI_DNS_EXTRA_SUFFIXES=custom.internal)"
|
||||
|
||||
# --- Summary ---
|
||||
printf '\nResults: %d passed, %d failed\n' "${PASS}" "${FAIL}"
|
||||
[ "${FAIL}" -eq 0 ]
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
||||
LOG_FILE=$(mktemp)
|
||||
trap 'rm -f "$LOG_FILE"' EXIT
|
||||
|
||||
export DOCKER_TEST_LOG="$LOG_FILE"
|
||||
export FAIL_RUN_SUITE=sdk-test-node
|
||||
export FAIL_BUILD_SUITE=
|
||||
|
||||
docker() {
|
||||
if [[ "$1" == compose && "$2" == up ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$1" == compose && "$2" == ps ]]; then
|
||||
printf 'floci-container\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$1" == inspect ]]; then
|
||||
printf '172.20.0.2\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$1" == build ]]; then
|
||||
suite="${!#}"
|
||||
suite="${suite#compatibility-tests/}"
|
||||
printf 'build:%s\n' "$suite" >> "$DOCKER_TEST_LOG"
|
||||
[[ "$suite" != "$FAIL_BUILD_SUITE" ]]
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$1" == run ]]; then
|
||||
suite="${!#}"
|
||||
suite="${suite#compat-}"
|
||||
printf 'run:%s\n' "$suite" >> "$DOCKER_TEST_LOG"
|
||||
[[ "$suite" != "$FAIL_RUN_SUITE" ]]
|
||||
return
|
||||
fi
|
||||
|
||||
printf 'unexpected docker command: %s\n' "$*" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
curl() {
|
||||
return 0
|
||||
}
|
||||
|
||||
export -f docker curl
|
||||
|
||||
if bash "$REPO_ROOT/docker/run-docker-tests.sh"; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
|
||||
if [[ "$status" -eq 0 ]]; then
|
||||
echo "expected a failed suite to produce a non-zero exit status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
expected_suites=(
|
||||
sdk-test-python
|
||||
sdk-test-node
|
||||
sdk-test-java
|
||||
sdk-test-go
|
||||
sdk-test-awscli
|
||||
compat-cdk
|
||||
compat-terraform
|
||||
compat-opentofu
|
||||
)
|
||||
|
||||
for suite in "${expected_suites[@]}"; do
|
||||
grep -qx "run:$suite" "$LOG_FILE" || {
|
||||
echo "expected suite to run after failure: $suite" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
: > "$LOG_FILE"
|
||||
export FAIL_RUN_SUITE=
|
||||
|
||||
bash "$REPO_ROOT/docker/run-docker-tests.sh" || {
|
||||
echo "expected all successful suites to produce a zero exit status" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
: > "$LOG_FILE"
|
||||
export FAIL_BUILD_SUITE=compat-cdk
|
||||
|
||||
if bash "$REPO_ROOT/docker/run-docker-tests.sh"; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
|
||||
if [[ "$status" -eq 0 ]]; then
|
||||
echo "expected a failed image build to produce a non-zero exit status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for suite in "${expected_suites[@]}"; do
|
||||
if [[ "$suite" == "compat-cdk" ]]; then
|
||||
grep -q "run:$suite" "$LOG_FILE" && {
|
||||
echo "failed image build should not run suite: $suite" >&2
|
||||
exit 1
|
||||
}
|
||||
continue
|
||||
fi
|
||||
grep -q "run:$suite" "$LOG_FILE" || {
|
||||
echo "expected suite to run after failed image build: $suite" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
Reference in New Issue
Block a user