diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000..9512dda --- /dev/null +++ b/.containerignore @@ -0,0 +1,7 @@ +* +!pyproject.toml +!uv.lock +!README.md +!LICENSE +!scim2_server +**/__pycache__ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f5a062..17b6abc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,3 +49,34 @@ jobs: - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + + container: + name: publish container image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: . + file: Containerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7185c74..879d770 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -53,3 +53,23 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + + container: + name: container image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Build image + run: docker build --file Containerfile --tag scim2-server:ci . + - name: Start container + run: docker run --detach --name scim2-server --publish 8080:8080 scim2-server:ci --bearer-token secret + - name: Check the server answers + run: | + curl --fail --silent --retry 30 --retry-delay 1 --retry-all-errors --output /dev/null http://127.0.0.1:8080/v2/ServiceProviderConfig + test "$(curl --silent --output /dev/null --write-out '%{http_code}' http://127.0.0.1:8080/v2/Users)" = 401 + curl --fail --silent --output /dev/null --header "Authorization: Bearer secret" http://127.0.0.1:8080/v2/Users + - name: Stop container + run: timeout 5 docker stop scim2-server + - name: Show logs + if: always() + run: docker logs scim2-server diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..0965994 --- /dev/null +++ b/Containerfile @@ -0,0 +1,23 @@ +FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS builder + +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PYTHON_DOWNLOADS=0 + +WORKDIR /app +COPY pyproject.toml uv.lock README.md LICENSE ./ +RUN uv sync --locked --no-dev --no-install-project +COPY scim2_server ./scim2_server +RUN uv sync --locked --no-dev --no-editable + +FROM python:3.14-slim-trixie + +COPY --from=builder /app/.venv /app/.venv +ENV PATH="/app/.venv/bin:$PATH" + +USER nobody +EXPOSE 8080 +# Werkzeug stops gracefully on KeyboardInterrupt, which lets --dump-resources be written. +STOPSIGNAL SIGINT +ENTRYPOINT ["scim2-server", "--hostname", "0.0.0.0"] +CMD ["--port", "8080"] diff --git a/README.md b/README.md index 7a7ce75..c0f2e92 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The only optional feature currently missing is support for Bulk operations ([RFC ## Usage ```shell -$ scim2-server [-h] [--schema SCHEMA] [--resource-type RESOURCE_TYPE] [--bearer-token BEARER_TOKEN] [--hostname HOSTNAME] [--port PORT] [--reverse-proxy] [--dump-resources DUMP_RESOURCES] +$ scim2-server [-h] [--schema SCHEMA] [--resource-type RESOURCE_TYPE] [--bearer-token BEARER_TOKEN] [--hostname HOSTNAME] [--port PORT] [--reverse-proxy] [--dump-resources DUMP_RESOURCES] [--debug] ``` - `-h`/`--help`: Show help message @@ -30,6 +30,18 @@ $ scim2-server [-h] [--schema SCHEMA] [--resource-type RESOURCE_TYPE] [--bearer- - `--hostname`: The hostname to listen on. Defaults to `127.0.0.1`. - `--port`: The port to listen on. Defaults to `8080`. - `--dump-resources`: Dump a JSON document containing all resources when the provider exits normally. +- `--debug`: Enable the interactive Werkzeug debugger, the reloader and the logging of the WSGI environment of each request. The debugger allows arbitrary code execution and the environment contains the bearer tokens, so never use this option on a server reachable by others. + +### Container + +A container image is published on the GitHub container registry for each release. +The server listens on `0.0.0.0:8080` inside the container, and the command line arguments are passed to `scim2-server`: + +```shell +$ docker run --publish 8080:8080 ghcr.io/python-scim/scim2-server --bearer-token secret +``` + +To build the image yourself, use `docker build --file Containerfile .` or `podman build .`. ## Notes diff --git a/scim2_server/cli.py b/scim2_server/cli.py index e5e9be4..75cbd75 100644 --- a/scim2_server/cli.py +++ b/scim2_server/cli.py @@ -44,9 +44,14 @@ def main(): type=argparse.FileType("w"), help="Dump resources to a JSON file on exit", ) + parser.add_argument( + "--debug", + action="store_true", + help="Enable the interactive debugger, the reloader and the logging of the WSGI environment", + ) args = parser.parse_args() - logging.basicConfig(level=logging.DEBUG) + logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) from werkzeug.serving import run_simple @@ -77,7 +82,8 @@ def main(): for bearer_token in args.bearer_token: app.register_bearer_token(bearer_token) - app = log_environ(app) + if args.debug: + app = log_environ(app) if args.reverse_proxy: app = ProxyFix(app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1) @@ -85,8 +91,8 @@ def main(): args.hostname, args.port, app, - use_debugger=True, - use_reloader=True, + use_debugger=args.debug, + use_reloader=args.debug, ) if args.dump_resources: