diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c77e8c..738fe65 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,10 +37,22 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build run: cargo build --release - - name: Build op-portal OCI image - run: oci/portal/build.sh --release + - name: Build op-bridge OCI image run: oci/bridge/build.sh --release + - name: Build op-cluster OCI image + run: oci/cluster/build.sh --release + - name: Build op-clusters OCI image + run: oci/clusters/build.sh --release + - name: Build op-filesystem OCI image + run: oci/filesystem/build.sh --release + - name: Build op-freeipa OCI image + run: oci/freeipa/build.sh --release + - name: Build op-portal OCI image + run: oci/portal/build.sh --release + - name: Build op-provider OCI image + run: oci/provider/build.sh --release + - name: Get version id: get_version run: | @@ -48,17 +60,34 @@ jobs: - name: Get version for Helm id: get_helm_version run: | - if [[ "${{ github.ref_name }}" == "master" ]]; then + if [[ "${{ github.ref_name }}" == "main" ]]; then echo version="${{ steps.get_version.outputs.version }}" >> "${GITHUB_OUTPUT}" else echo version="${{ steps.get_version.outputs.version }}.${{ github.ref_name }}" >> "${GITHUB_OUTPUT}" fi + + - name: package op-bridge helm chart + run: | + helm package helm/bridge --version "${{ steps.get_helm_version.outputs.version }}" --app-version "${{ steps.get_version.outputs.version }}" + - name: package op-cluster helm chart + run: | + helm package helm/cluster --version "${{ steps.get_helm_version.outputs.version }}" --app-version "${{ steps.get_version.outputs.version }}" + - name: package op-clusters helm chart + run: | + helm package helm/clusters --version "${{ steps.get_helm_version.outputs.version }}" --app-version "${{ steps.get_version.outputs.version }}" + - name: package op-filesystem helm chart + run: | + helm package helm/filesystem --version "${{ steps.get_helm_version.outputs.version }}" --app-version "${{ steps.get_version.outputs.version }}" + - name: package op-freeipa helm chart + run: | + helm package helm/freeipa --version "${{ steps.get_helm_version.outputs.version }}" --app-version "${{ steps.get_version.outputs.version }}" - name: package op-portal helm chart run: | helm package helm/portal --version "${{ steps.get_helm_version.outputs.version }}" --app-version "${{ steps.get_version.outputs.version }}" - - name: package op-bridge helm chart + - name: package op-provider helm chart run: | - helm package helm/bridge --version "${{ steps.get_helm_version.outputs.version }}" --app-version "${{ steps.get_version.outputs.version }}" + helm package helm/provider --version "${{ steps.get_helm_version.outputs.version }}" --app-version "${{ steps.get_version.outputs.version }}" + - name: Log in to GHCR uses: redhat-actions/podman-login@v1 with: @@ -69,6 +98,42 @@ jobs: run: echo $GITHUB_TOKEN | helm registry login "ghcr.io/${{ github.repository_owner }}" --username "${{ github.actor }}" --password-stdin env: GITHUB_TOKEN: "${{ github.token }}" + + - name: Publish op-bridge OCI image + id: push-bridge-to-ghcr + uses: redhat-actions/push-to-registry@v2 + with: + image: op-bridge + tags: ${{ steps.get_version.outputs.version }} + registry: ghcr.io/${{ github.repository_owner }} + - name: Publish op-cluster OCI image + id: push-cluster-to-ghcr + uses: redhat-actions/push-to-registry@v2 + with: + image: op-cluster + tags: ${{ steps.get_version.outputs.version }} + registry: ghcr.io/${{ github.repository_owner }} + - name: Publish op-clusters OCI image + id: push-clusters-to-ghcr + uses: redhat-actions/push-to-registry@v2 + with: + image: op-clusters + tags: ${{ steps.get_version.outputs.version }} + registry: ghcr.io/${{ github.repository_owner }} + - name: Publish op-filesystem OCI image + id: push-filesystem-to-ghcr + uses: redhat-actions/push-to-registry@v2 + with: + image: op-filesystem + tags: ${{ steps.get_version.outputs.version }} + registry: ghcr.io/${{ github.repository_owner }} + - name: Publish op-freeipa OCI image + id: push-freeipa-to-ghcr + uses: redhat-actions/push-to-registry@v2 + with: + image: op-freeipa + tags: ${{ steps.get_version.outputs.version }} + registry: ghcr.io/${{ github.repository_owner }} - name: Publish op-portal OCI image id: push-portal-to-ghcr uses: redhat-actions/push-to-registry@v2 @@ -76,41 +141,117 @@ jobs: image: op-portal tags: ${{ steps.get_version.outputs.version }} registry: ghcr.io/${{ github.repository_owner }} - - name: Publish op-bridge OCI image - id: push-bridge-to-ghcr + - name: Publish op-provider OCI image + id: push-provider-to-ghcr uses: redhat-actions/push-to-registry@v2 with: - image: op-bridge + image: op-provider tags: ${{ steps.get_version.outputs.version }} registry: ghcr.io/${{ github.repository_owner }} + + - name: Attest op-bridge image + uses: actions/attest-build-provenance@v1 + id: attest-bridge + with: + subject-name: ghcr.io/${{ github.repository_owner }}/op-bridge + subject-digest: ${{ steps.push-bridge-to-ghcr.outputs.digest }} + push-to-registry: true + - name: Attest op-cluster image + uses: actions/attest-build-provenance@v1 + id: attest-cluster + with: + subject-name: ghcr.io/${{ github.repository_owner }}/op-cluster + subject-digest: ${{ steps.push-cluster-to-ghcr.outputs.digest }} + push-to-registry: true + - name: Attest op-clusters image + uses: actions/attest-build-provenance@v1 + id: attest-clusters + with: + subject-name: ghcr.io/${{ github.repository_owner }}/op-clusters + subject-digest: ${{ steps.push-clusters-to-ghcr.outputs.digest }} + push-to-registry: true + - name: Attest op-filesystem image + uses: actions/attest-build-provenance@v1 + id: attest-filesystem + with: + subject-name: ghcr.io/${{ github.repository_owner }}/op-filesystem + subject-digest: ${{ steps.push-filesystem-to-ghcr.outputs.digest }} + push-to-registry: true + - name: Attest op-freeipa image + uses: actions/attest-build-provenance@v1 + id: attest-freeipa + with: + subject-name: ghcr.io/${{ github.repository_owner }}/op-freeipa + subject-digest: ${{ steps.push-freeipa-to-ghcr.outputs.digest }} + push-to-registry: true - name: Attest op-portal image uses: actions/attest-build-provenance@v1 id: attest-portal with: - subject-name: ghcr.io/${{ github.repository }} + subject-name: ghcr.io/${{ github.repository_owner }}/op-portal subject-digest: ${{ steps.push-portal-to-ghcr.outputs.digest }} push-to-registry: true - - name: Attest op-bridge image + - name: Attest op-provider image uses: actions/attest-build-provenance@v1 - id: attest-bridge + id: attest-provider with: - subject-name: ghcr.io/${{ github.repository }} - subject-digest: ${{ steps.push-bridge-to-ghcr.outputs.digest }} + subject-name: ghcr.io/${{ github.repository_owner }}/op-provider + subject-digest: ${{ steps.push-provider-to-ghcr.outputs.digest }} push-to-registry: true - - name: Push op-portal Helm chart - run: helm push "./op-portal-${{ steps.get_helm_version.outputs.version }}.tgz" "oci://ghcr.io/${{ github.repository_owner }}/charts" - name: Push op-bridge Helm chart run: helm push "./op-bridge-${{ steps.get_helm_version.outputs.version }}.tgz" "oci://ghcr.io/${{ github.repository_owner }}/charts" - - name: Store build artefacts + - name: Push op-cluster Helm chart + run: helm push "./op-cluster-${{ steps.get_helm_version.outputs.version }}.tgz" "oci://ghcr.io/${{ github.repository_owner }}/charts" + - name: Push op-clusters Helm chart + run: helm push "./op-clusters-${{ steps.get_helm_version.outputs.version }}.tgz" "oci://ghcr.io/${{ github.repository_owner }}/charts" + - name: Push op-filesystem Helm chart + run: helm push "./op-filesystem-${{ steps.get_helm_version.outputs.version }}.tgz" "oci://ghcr.io/${{ github.repository_owner }}/charts" + - name: Push op-freeipa Helm chart + run: helm push "./op-freeipa-${{ steps.get_helm_version.outputs.version }}.tgz" "oci://ghcr.io/${{ github.repository_owner }}/charts" + - name: Push op-portal Helm chart + run: helm push "./op-portal-${{ steps.get_helm_version.outputs.version }}.tgz" "oci://ghcr.io/${{ github.repository_owner }}/charts" + - name: Push op-provider Helm chart + run: helm push "./op-provider-${{ steps.get_helm_version.outputs.version }}.tgz" "oci://ghcr.io/${{ github.repository_owner }}/charts" + + - name: Store bridge artefact uses: actions/upload-artifact@v4 with: - name: openportal-agents + name: op-bridge path: | - target/release/op-portal target/release/op-bridge - target/release/op-provider + - name: Store cluster artefact + uses: actions/upload-artifact@v4 + with: + name: op-cluster + path: | target/release/op-cluster - target/release/op-slurm + - name: Store clusters artefact + uses: actions/upload-artifact@v4 + with: + name: op-clusters + path: | + target/release/op-clusters + - name: Store filesystem artefact + uses: actions/upload-artifact@v4 + with: + name: op-filesystem + path: | target/release/op-filesystem + - name: Store freeipa artefact + uses: actions/upload-artifact@v4 + with: + name: op-freeipa + path: | target/release/op-freeipa - + - name: Store portal artefact + uses: actions/upload-artifact@v4 + with: + name: op-portal + path: | + target/release/op-portal + - name: Store provider artefact + uses: actions/upload-artifact@v4 + with: + name: op-provider + path: | + target/release/op-provider diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..3a5651a --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT + +name: Python Module + +on: + workflow_dispatch: + workflow_call: + inputs: + ref: + type: string + required: true + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-latest + target: x86_64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: auto + working-directory: python + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: python/dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-latest + target: x86_64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: musllinux_1_2 + working-directory: python + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-musllinux-${{ matrix.platform.target }} + path: python/dist + + release: + name: Release + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} + needs: [linux, musllinux] + permissions: + # Use to sign the release artifacts + id-token: write + # Used to upload release artifacts + contents: write + # Used to generate artifact attestation + attestations: write + steps: + - uses: actions/download-artifact@v4 + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-path: 'wheels-*/*' + - name: Publish to PyPI + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --non-interactive --skip-existing wheels-*/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a92032..dbb9082 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,7 +82,7 @@ jobs: echo 'Updating based on explicit version' cargo set-version ${{ inputs.version }} fi - git add Cargo.toml Cargo.lock + git add Cargo.toml - name: Save the version id: get_version run: echo version="$(cargo metadata --format-version 1 --no-deps | jq --raw-output '.packages[0].version')" >> "${GITHUB_OUTPUT}" @@ -116,8 +116,8 @@ jobs: attestations: write id-token: write - attest: - name: Attest + attest-bridge: + name: Attest Bridge needs: build-release runs-on: ubuntu-latest permissions: @@ -133,26 +133,232 @@ jobs: with: tool: cargo-sbom - name: Generate SBOM - run: cargo sbom --output-format=spdx_json_2_3 > sbom.spdx.json + run: cargo sbom --cargo-package op-bridge --output-format=spdx_json_2_3 > sbom-bridge.spdx.json - name: Fetch release artefacts uses: actions/download-artifact@v4 with: - pattern: op-* + pattern: op-bridge + merge-multiple: true + - name: Attest SBOM + uses: actions/attest-sbom@v1 + with: + subject-path: op-bridge + sbom-path: sbom-bridge.spdx.json + - name: Store SBOM + uses: actions/upload-artifact@v4 + with: + name: sbom-bridge.spdx.json + path: sbom-bridge.spdx.json + + attest-cluster: + name: Attest Cluster + needs: build-release + runs-on: ubuntu-latest + permissions: + contents: read + attestations: write + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + - name: Install cargo-sbom + uses: taiki-e/install-action@v2 + with: + tool: cargo-sbom + - name: Generate SBOM + run: cargo sbom --cargo-package op-cluster --output-format=spdx_json_2_3 > sbom-cluster.spdx.json + - name: Fetch release artefacts + uses: actions/download-artifact@v4 + with: + pattern: op-cluster + merge-multiple: true + - name: Attest SBOM + uses: actions/attest-sbom@v1 + with: + subject-path: op-cluster + sbom-path: sbom-cluster.spdx.json + - name: Store SBOM + uses: actions/upload-artifact@v4 + with: + name: sbom-cluster.spdx.json + path: sbom-cluster.spdx.json + + attest-clusters: + name: Attest Clusters + needs: build-release + runs-on: ubuntu-latest + permissions: + contents: read + attestations: write + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + - name: Install cargo-sbom + uses: taiki-e/install-action@v2 + with: + tool: cargo-sbom + - name: Generate SBOM + run: cargo sbom --cargo-package op-clusters --output-format=spdx_json_2_3 > sbom-clusters.spdx.json + - name: Fetch release artefacts + uses: actions/download-artifact@v4 + with: + pattern: op-clusters + merge-multiple: true + - name: Attest SBOM + uses: actions/attest-sbom@v1 + with: + subject-path: op-clusters + sbom-path: sbom-clusters.spdx.json + - name: Store SBOM + uses: actions/upload-artifact@v4 + with: + name: sbom-clusters.spdx.json + path: sbom-clusters.spdx.json + + attest-filesystem: + name: Attest Filesystem + needs: build-release + runs-on: ubuntu-latest + permissions: + contents: read + attestations: write + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + - name: Install cargo-sbom + uses: taiki-e/install-action@v2 + with: + tool: cargo-sbom + - name: Generate SBOM + run: cargo sbom --cargo-package op-filesystem --output-format=spdx_json_2_3 > sbom-filesystem.spdx.json + - name: Fetch release artefacts + uses: actions/download-artifact@v4 + with: + pattern: op-filesystem + merge-multiple: true + - name: Attest SBOM + uses: actions/attest-sbom@v1 + with: + subject-path: op-filesystem + sbom-path: sbom-filesystem.spdx.json + - name: Store SBOM + uses: actions/upload-artifact@v4 + with: + name: sbom-filesystem.spdx.json + path: sbom-filesystem.spdx.json + + attest-freeipa: + name: Attest FreeIPA + needs: build-release + runs-on: ubuntu-latest + permissions: + contents: read + attestations: write + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + - name: Install cargo-sbom + uses: taiki-e/install-action@v2 + with: + tool: cargo-sbom + - name: Generate SBOM + run: cargo sbom --cargo-package op-freeipa --output-format=spdx_json_2_3 > sbom-freeipa.spdx.json + - name: Fetch release artefacts + uses: actions/download-artifact@v4 + with: + pattern: op-freeipa + merge-multiple: true + - name: Attest SBOM + uses: actions/attest-sbom@v1 + with: + subject-path: op-freeipa + sbom-path: sbom-freeipa.spdx.json + - name: Store SBOM + uses: actions/upload-artifact@v4 + with: + name: sbom-freeipa.spdx.json + path: sbom-freeipa.spdx.json + + attest-portal: + name: Attest Portal + needs: build-release + runs-on: ubuntu-latest + permissions: + contents: read + attestations: write + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + - name: Install cargo-sbom + uses: taiki-e/install-action@v2 + with: + tool: cargo-sbom + - name: Generate SBOM + run: cargo sbom --cargo-package op-portal --output-format=spdx_json_2_3 > sbom-portal.spdx.json + - name: Fetch release artefacts + uses: actions/download-artifact@v4 + with: + pattern: op-portal + merge-multiple: true + - name: Attest SBOM + uses: actions/attest-sbom@v1 + with: + subject-path: op-portal + sbom-path: sbom-portal.spdx.json + - name: Store SBOM + uses: actions/upload-artifact@v4 + with: + name: sbom-portal.spdx.json + path: sbom-portal.spdx.json + + attest-provider: + name: Attest Provider + needs: build-release + runs-on: ubuntu-latest + permissions: + contents: read + attestations: write + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + - name: Install cargo-sbom + uses: taiki-e/install-action@v2 + with: + tool: cargo-sbom + - name: Generate SBOM + run: cargo sbom --cargo-package op-provider --output-format=spdx_json_2_3 > sbom-provider.spdx.json + - name: Fetch release artefacts + uses: actions/download-artifact@v4 + with: + pattern: op-provider merge-multiple: true - name: Attest SBOM uses: actions/attest-sbom@v1 with: - subject-path: openportal - sbom-path: sbom.spdx.json + subject-path: op-provider + sbom-path: sbom-provider.spdx.json - name: Store SBOM uses: actions/upload-artifact@v4 with: - name: sbom.spdx.json - path: sbom.spdx.json + name: sbom-provider.spdx.json + path: sbom-provider.spdx.json make-release: name: Make release ${{ needs.tag-release.outputs.ref }} - needs: [build-release, tag-release, attest] + needs: [build-release, tag-release, attest-bridge, attest-cluster, + attest-clusters, attest-filesystem, attest-freeipa, + attest-portal, attest-provider] runs-on: ubuntu-latest permissions: contents: write diff --git a/.gitignore b/.gitignore index 0d6ec47..8e198ed 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ Cargo.lock invitation.toml invite*.toml *_config.toml +*-config.toml example*.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 222c13b..2292087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,109 @@ - - # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.0.1] - 2024-10-22 +## Unreleased + +## [0.0.19] - 2024-11-07 +### Fixed +- Made the code more robust to freeipa being cleared / having groups removed + behind our back. Also better way to handle errors. + +## [0.0.18] - 2024-11-05 +### Fixed +- Specified default TLS provider so that containerised services can run without + panicing. + +## [0.0.17] - 2024-11-01 +### Fixed +- Fixed issues with attestations that depended on releases. Need to release + each agent separately, which this release now does. + +## [0.0.16] - 2024-11-01 +### Fixed +- Fixed issue with attestation of OCI images + +## [0.0.15] - 2024-11-01 +### Fixed +- Fixed issues with the helm charts and OCI images (removed `op-platform` as it + doesn't exist!) + +## [0.0.14] - 2024-11-01 +### Added +- Changed the names of the cluster instance and platform agents to `cluster` and `clusters`, + as they don't need to be named after slurm (and would cause confusion with the slurm agent). +- Added OCI images and helm charts for all agents +- Added instructions on how to configure the freeipa agent + +## [0.0.12] - 2024-10-28 +### Added +- Added support for keepalive messages so that connections are kept open + +## [0.0.11] - 2024-10-28 +### Added +- Fixed bug in handling of client proxy IP - need to use IP not port ;-) + +## [0.0.10] - 2024-10-25 +### Added +- Fixed bug in parsing header proxy IP address + +## [0.0.9] - 2024-10-25 ### Added +- Fixed bug in parsing command line options for bridge +- Added support for getting the client IP address from a proxy header (e.g. `X-Forwarded-For`) +- Cleaned up port handling, so URLs with default ports don't have the ports specified + +## [0.0.8] - 2024-10-24 +### Added +- Added names for the ports in the helm charts + +## [0.0.7] - 2024-10-24 +### Added +- Added a healthcheck server to simplify pod healthchecks +- Updated helm charts to use the healthcheck server, plus expose the bridge server port + +## [0.0.6] - 2024-10-23 +### Added +- Separated out build artefacts so that they can be picked up by the rest of the build + +## [0.0.5] - 2024-10-23 +### Added +- Fixing generation and attestation of SBOMs for container images (finally!) + +## [0.0.4] - 2024-10-23 +### Added +- Fixing release issues, and beginning work on the workflow for the Python module + +## [0.0.3] - 2024-10-23 +### Added +- Fixing the attestations so that SBOMs are correctly generated for container images. + +## [0.0.2] - 2024-10-23 +### Added +- Fixing the helm charts so that they version numbers are correctly set. + +## [0.0.1] - 2024-10-23 +### Changed - Initial release + This is an initial alpha release of the OpenPortal project. It is not yet feature complete and is not recommended for production use. +[0.0.19]: https://github.com/isambard-sc/openportal/releases/tag/0.0.19 +[0.0.18]: https://github.com/isambard-sc/openportal/releases/tag/0.0.18 +[0.0.17]: https://github.com/isambard-sc/openportal/releases/tag/0.0.17 +[0.0.16]: https://github.com/isambard-sc/openportal/releases/tag/0.0.16 +[0.0.15]: https://github.com/isambard-sc/openportal/releases/tag/0.0.15 +[0.0.14]: https://github.com/isambard-sc/openportal/releases/tag/0.0.14 +[0.0.12]: https://github.com/isambard-sc/openportal/releases/tag/0.0.12 +[0.0.11]: https://github.com/isambard-sc/openportal/releases/tag/0.0.11 +[0.0.10]: https://github.com/isambard-sc/openportal/releases/tag/0.0.10 +[0.0.9]: https://github.com/isambard-sc/openportal/releases/tag/0.0.9 +[0.0.8]: https://github.com/isambard-sc/openportal/releases/tag/0.0.8 +[0.0.7]: https://github.com/isambard-sc/openportal/releases/tag/0.0.7 +[0.0.6]: https://github.com/isambard-sc/openportal/releases/tag/0.0.6 +[0.0.5]: https://github.com/isambard-sc/openportal/releases/tag/0.0.5 +[0.0.4]: https://github.com/isambard-sc/openportal/releases/tag/0.0.4 +[0.0.3]: https://github.com/isambard-sc/openportal/releases/tag/0.0.3 +[0.0.2]: https://github.com/isambard-sc/openportal/releases/tag/0.0.2 [0.0.1]: https://github.com/isambard-sc/openportal/releases/tag/0.0.1 diff --git a/Cargo.toml b/Cargo.toml index f7c5a24..268e9ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,9 @@ [workspace] members = [ - "bridge", "cluster", + "bridge", "cluster", "clusters", "filesystem", "freeipa", "paddington", "portal", - "provider", "python", "slurm", "templemeads", + "provider", "python", "templemeads", "docs/echo", "docs/job", "docs/cmdline/portal", "docs/cmdline/cluster" ] diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..42c6706 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = "CHANGELOG.md" +SPDX-FileCopyrightText = "© 2024 Christopher Woods , Matt Williams " +SPDX-License-Identifier = "CC-BY-SA-4.0" diff --git a/bridge/Cargo.toml b/bridge/Cargo.toml index 299fa7e..c7b81ad 100644 --- a/bridge/Cargo.toml +++ b/bridge/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-bridge" -version = "0.0.1" +version = "0.0.19" description = "An example of an OpenPortal user portal to OpenPortal bridge" edition = "2021" license = "MIT" diff --git a/bridge/src/main.rs b/bridge/src/main.rs index bd812b8..5c3c18d 100644 --- a/bridge/src/main.rs +++ b/bridge/src/main.rs @@ -39,6 +39,8 @@ async fn main() -> Result<()> { Some("ws://localhost:8044".to_owned()), Some("127.0.0.1".to_owned()), Some(8044), + None, + None, Some("http://localhost:3000".to_owned()), Some("127.0.0.1".to_owned()), Some(3000), diff --git a/cluster/Cargo.toml b/cluster/Cargo.toml index c436c77..1c13992 100644 --- a/cluster/Cargo.toml +++ b/cluster/Cargo.toml @@ -3,8 +3,8 @@ [package] name = "op-cluster" -version = "0.0.1" -description = "An example of an OpenPortal HPC cluster platform agent" +version = "0.0.19" +description = "An example of an OpenPortal cluster instance agent" edition = "2021" license = "MIT" homepage = "https://github.com/chryswoods/openportal/" diff --git a/cluster/src/main.rs b/cluster/src/main.rs index 5c8fa22..3d06e63 100644 --- a/cluster/src/main.rs +++ b/cluster/src/main.rs @@ -3,15 +3,21 @@ use anyhow::Result; -use templemeads::agent::platform::{process_args, run, Defaults}; +use templemeads::agent; +use templemeads::agent::instance::{process_args, run, Defaults}; use templemeads::agent::Type as AgentType; +use templemeads::async_runnable; +use templemeads::grammar::Instruction::{AddUser, RemoveUser}; +use templemeads::grammar::{UserIdentifier, UserMapping}; +use templemeads::job::{Envelope, Job}; +use templemeads::Error; /// -/// Main function for the cluster platform agent +/// Main function for the cluster instance agent /// -/// This purpose of this agent is to manage clusters, defined -/// as HPC batch clusters. It will manage the lifecycle of -/// the cluster, including creating and deleting the cluster +/// This purpose of this agent is to manage an individual instance +/// of a batch cluster. It will manage the lifecycle of +/// users and projects on the cluster. /// #[tokio::main] async fn main() -> Result<()> { @@ -31,10 +37,12 @@ async fn main() -> Result<()> { .join("openportal") .join("cluster-config.toml"), ), - Some("ws://localhost:8045".to_owned()), + Some("ws://localhost:8046".to_owned()), Some("127.0.0.1".to_owned()), - Some(8045), - Some(AgentType::Platform), + Some(8046), + None, + None, + Some(AgentType::Instance), ); // now parse the command line arguments to get the service configuration @@ -46,8 +54,161 @@ async fn main() -> Result<()> { } }; + async_runnable! { + /// + /// Runnable function that will be called when a job is received + /// by the agent + /// + pub async fn cluster_runner(envelope: Envelope) -> Result + { + tracing::info!("Using the cluster runner"); + + let me = envelope.recipient(); + let sender = envelope.sender(); + let mut job = envelope.job(); + + match job.instruction() { + AddUser(user) => { + // add the user to the cluster + tracing::info!("Adding user to cluster: {}", user); + let mapping = create_account(&me, &user).await?; + + job = job.running(Some("Step 1/3: Account created".to_string()))?; + job = job.update(&sender).await?; + + let homedir = create_directories(&me, &mapping).await?; + + job = job.running(Some("Step 2/3: Directories created".to_string()))?; + job = job.update(&sender).await?; + + let _ = update_homedir(&me, &user, &homedir).await?; + + job = job.completed(mapping)?; + } + RemoveUser(user) => { + // remove the user from the cluster + tracing::info!("Removing user from cluster: {}", user); + job = job.completed("User removed")?; + } + _ => { + tracing::error!("Unknown instruction: {:?}", job.instruction()); + return Err(Error::UnknownInstruction( + format!("Unknown instruction: {:?}", job.instruction()).to_string(), + )); + } + } + + Ok(job) + } + } + // run the agent - run(config).await?; + run(config, cluster_runner).await?; Ok(()) } + +async fn create_account(me: &str, user: &UserIdentifier) -> Result { + // find the Account agent + match agent::account().await { + Some(account) => { + // send the add_job to the account agent + let job = Job::parse(&format!("{}.{} add_user {}", me, account, user))? + .put(&account) + .await?; + + // Wait for the add_job to complete + let result = job.wait().await?.result::()?; + + match result { + Some(mapping) => { + tracing::info!("User added to account agent: {:?}", mapping); + Ok(mapping) + } + None => { + tracing::error!("Error creating the user's account: {:?}", job); + Err(Error::Call( + format!("Error creating the user's account: {:?}", job).to_string(), + )) + } + } + } + None => { + tracing::error!("No account agent found"); + Err(Error::MissingAgent( + "Cannot run the job because there is no account agent".to_string(), + )) + } + } +} + +async fn create_directories(me: &str, mapping: &UserMapping) -> Result { + // find the Filesystem agent + match agent::filesystem().await { + Some(filesystem) => { + // send the add_job to the filesystem agent + let job = Job::parse(&format!("{}.{} add_local_user {}", me, filesystem, mapping))? + .put(&filesystem) + .await?; + + // Wait for the add_job to complete + let result = job.wait().await?.result::()?; + + match result { + Some(homedir) => { + tracing::info!("Directories created for user: {:?}", mapping); + Ok(homedir) + } + None => { + tracing::error!("Error creating the user's directories: {:?}", job); + Err(Error::Call( + format!("Error creating the user's directories: {:?}", job).to_string(), + )) + } + } + } + None => { + tracing::error!("No filesystem agent found"); + Err(Error::MissingAgent( + "Cannot run the job because there is no filesystem agent".to_string(), + )) + } + } +} + +async fn update_homedir(me: &str, user: &UserIdentifier, homedir: &str) -> Result { + // find the Account agent + match agent::account().await { + Some(account) => { + // send the add_job to the account agent + let job = Job::parse(&format!( + "{}.{} update_homedir {} {}", + me, account, user, homedir + ))? + .put(&account) + .await?; + + // Wait for the add_job to complete + let result = job.wait().await?.result::()?; + + match result { + Some(homedir) => { + tracing::info!("User {} homedir updated: {:?}", user, homedir); + Ok(homedir) + } + None => { + tracing::error!("Error updating the user's homedir: {:?}", job); + Err(Error::Call( + format!("Error updating the user's homedir: {:?}", job).to_string(), + )) + } + } + } + None => { + tracing::error!("No account agent found"); + Err(Error::MissingAgent( + "Cannot run the job because there is no account agent".to_string(), + )) + } + } +} diff --git a/slurm/Cargo.toml b/clusters/Cargo.toml similarity index 86% rename from slurm/Cargo.toml rename to clusters/Cargo.toml index 69e634f..8848bad 100644 --- a/slurm/Cargo.toml +++ b/clusters/Cargo.toml @@ -2,9 +2,9 @@ # SPDX-License-Identifier: CC0-1.0 [package] -name = "op-slurm" -version = "0.0.1" -description = "An example of an OpenPortal Slurm cluster instance agent" +name = "op-clusters" +version = "0.0.19" +description = "An example of an OpenPortal cluster platform agent" edition = "2021" license = "MIT" homepage = "https://github.com/chryswoods/openportal/" diff --git a/clusters/src/main.rs b/clusters/src/main.rs new file mode 100644 index 0000000..fd46df2 --- /dev/null +++ b/clusters/src/main.rs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: © 2024 Christopher Woods +// SPDX-License-Identifier: MIT + +use anyhow::Result; + +use templemeads::agent::platform::{process_args, run, Defaults}; +use templemeads::agent::Type as AgentType; + +/// +/// Main function for the cluster platform agent +/// +/// This purpose of this agent is to manage clusters, defined +/// as HPC batch clusters. It will manage the lifecycle of +/// the cluster, including creating and deleting the cluster +/// +#[tokio::main] +async fn main() -> Result<()> { + // start tracing + let subscriber = tracing_subscriber::FmtSubscriber::new(); + tracing::subscriber::set_global_default(subscriber)?; + + // create the OpenPortal paddington defaults + let defaults = Defaults::parse( + Some("clusters".to_owned()), + Some( + dirs::config_local_dir() + .unwrap_or( + ".".parse() + .expect("Could not parse fallback config directory."), + ) + .join("openportal") + .join("clusters-config.toml"), + ), + Some("ws://localhost:8045".to_owned()), + Some("127.0.0.1".to_owned()), + Some(8045), + None, + None, + Some(AgentType::Platform), + ); + + // now parse the command line arguments to get the service configuration + let config = match process_args(&defaults).await? { + Some(config) => config, + None => { + // Not running the service, so can safely exit + return Ok(()); + } + }; + + // run the agent + run(config).await?; + + Ok(()) +} diff --git a/docs/README.md b/docs/README.md index ad4ceef..8bac9f1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -98,8 +98,8 @@ The key types of Agent are: adding the colleague to the project may require adding them to a slurm cluster. So the `provider` Agent will send a Job to the `platform` Agent to tell it to add the colleague to the slurm cluster. - The `op-cluster` executable implements the `platform` Agent - for clusters, with source code in the [cluster](../cluster) directory. + The `op-clusters` executable implements the `platform` Agent + for clusters, with source code in the [clusters](../clusters) directory. 4. `instance` - these are agents that represent individual instances of a platform. For example, each indvidual slurm cluster or Jupyter notebook @@ -109,8 +109,8 @@ The key types of Agent are: slurm clusters would pass on the request to add the colleague to the individual `instance` Agent that is responsible for managing the specific slurm cluster to which the colleague is being added. - The `op-slurm` executable implements the `instance` Agent for slurm - clusters, with source code in the [slurm](../slurm) directory. + The `op-cluster` executable implements the `instance` Agent for + clusters, with source code in the [cluster](../cluster) directory. 5. `account` - these are Agents that interface with user account management services, e.g. LDAP, FreeIPA etc. There is one `account` Agent per diff --git a/docs/cmdline/cluster/Cargo.toml b/docs/cmdline/cluster/Cargo.toml index 3700f5f..55eb2e7 100644 --- a/docs/cmdline/cluster/Cargo.toml +++ b/docs/cmdline/cluster/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "example-cluster" -version = "0.0.1" +version = "0.0.19" description = "templemeads command line example - cluster agent" edition = "2021" license = "MIT" diff --git a/docs/cmdline/cluster/src/main.rs b/docs/cmdline/cluster/src/main.rs index 67d8de8..80900e2 100644 --- a/docs/cmdline/cluster/src/main.rs +++ b/docs/cmdline/cluster/src/main.rs @@ -24,6 +24,8 @@ async fn main() -> Result<()> { Some("ws://localhost:8091".to_owned()), Some("127.0.0.1".to_owned()), Some(8091), + None, + None, Some(AgentType::Instance), ); diff --git a/docs/cmdline/portal/Cargo.toml b/docs/cmdline/portal/Cargo.toml index 1219c28..792eee4 100644 --- a/docs/cmdline/portal/Cargo.toml +++ b/docs/cmdline/portal/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "example-portal" -version = "0.0.1" +version = "0.0.19" description = "templemeads command line example - portal agent" edition = "2021" license = "MIT" diff --git a/docs/cmdline/portal/src/main.rs b/docs/cmdline/portal/src/main.rs index b132d40..e51931e 100644 --- a/docs/cmdline/portal/src/main.rs +++ b/docs/cmdline/portal/src/main.rs @@ -20,6 +20,8 @@ async fn main() -> Result<()> { Some("ws://localhost:8090".to_owned()), Some("127.0.0.1".to_owned()), Some(8090), + None, + None, Some(AgentType::Portal), ); diff --git a/docs/echo/Cargo.toml b/docs/echo/Cargo.toml index 78caf65..ab9542a 100644 --- a/docs/echo/Cargo.toml +++ b/docs/echo/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "example-echo" -version = "0.0.1" +version = "0.0.19" description = "Paddington Echo Service example" edition = "2021" license = "MIT" diff --git a/docs/echo/src/main.rs b/docs/echo/src/main.rs index 44fba09..1a819c6 100644 --- a/docs/echo/src/main.rs +++ b/docs/echo/src/main.rs @@ -181,8 +181,14 @@ async fn run_client(invitation: &Path) -> Result<(), Error> { // create the echo-client service - note that the url, ip and // port aren't used, as this service won't be listening for any // connecting clients - let mut service: ServiceConfig = - ServiceConfig::new("echo-client", "http://localhost:6502", "127.0.0.1", &6502)?; + let mut service: ServiceConfig = ServiceConfig::new( + "echo-client", + "http://localhost:6502", + "127.0.0.1", + &6502, + &None, + &None, + )?; // now give the invitation to connect to the server to the client service.add_server(invite)?; @@ -257,7 +263,7 @@ async fn run_server( invitation: &Path, ) -> Result<(), Error> { // create the echo-server service - let mut service = ServiceConfig::new("echo-server", url, ip, port)?; + let mut service = ServiceConfig::new("echo-server", url, ip, port, &None, &None)?; let invite = service.add_client("echo-client", range)?; diff --git a/docs/job/Cargo.toml b/docs/job/Cargo.toml index f86d909..9700224 100644 --- a/docs/job/Cargo.toml +++ b/docs/job/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "example-job" -version = "0.0.1" +version = "0.0.19" description = "templemeads job example" edition = "2021" license = "MIT" diff --git a/docs/job/src/main.rs b/docs/job/src/main.rs index 7e26b8e..5164423 100644 --- a/docs/job/src/main.rs +++ b/docs/job/src/main.rs @@ -199,8 +199,14 @@ async fn run_cluster(invitation: &Path) -> Result<(), Error> { // create the paddington service for the cluster agent // - note that the url, ip and port aren't used, as this // agent won't be listening for any connecting clients - let mut service: ServiceConfig = - ServiceConfig::new("cluster", "http://localhost:6502", "127.0.0.1", &6502)?; + let mut service: ServiceConfig = ServiceConfig::new( + "cluster", + "http://localhost:6502", + "127.0.0.1", + &6502, + &None, + &None, + )?; // now give the invitation to connect to the server to the client service.add_server(invite)?; @@ -255,7 +261,7 @@ async fn run_portal( invitation: &Path, ) -> Result<(), Error> { // create a paddington service configuration for the portal agent - let mut service = ServiceConfig::new("portal", url, ip, port)?; + let mut service = ServiceConfig::new("portal", url, ip, port, &None, &None)?; // add the cluster to the portal, returning an invitation let invite = service.add_client("cluster", range)?; diff --git a/filesystem/Cargo.toml b/filesystem/Cargo.toml index 9d80506..17359ae 100644 --- a/filesystem/Cargo.toml +++ b/filesystem/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-filesystem" -version = "0.0.1" +version = "0.0.19" description = "Agent that interfaces OpenPortal with a filesystem" edition = "2021" license = "MIT" diff --git a/filesystem/src/main.rs b/filesystem/src/main.rs index 0d35da0..ae01436 100644 --- a/filesystem/src/main.rs +++ b/filesystem/src/main.rs @@ -39,6 +39,8 @@ async fn main() -> Result<()> { Some("ws://localhost:8047".to_owned()), Some("127.0.0.1".to_owned()), Some(8047), + None, + None, Some(AgentType::Filesystem), ); diff --git a/freeipa/Cargo.toml b/freeipa/Cargo.toml index 5539041..6dd9f3b 100644 --- a/freeipa/Cargo.toml +++ b/freeipa/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-freeipa" -version = "0.0.1" +version = "0.0.19" description = "Agent that interfaces OpenPortal with FreeIPA" edition = "2021" license = "MIT" diff --git a/freeipa/README.md b/freeipa/README.md new file mode 100644 index 0000000..b378b49 --- /dev/null +++ b/freeipa/README.md @@ -0,0 +1,35 @@ + + +# FreeIPA agent + +This requires extra configuration to set the details used to connect +to the FreeIPA server. + +To test, the demo server provided by FreeIPA is very useful. +This is at `[ipa.demo1.freeipa.org](https://ipa.demo1.freeipa.org/), +and you can use the username `admin` and password `Secret123`. + +First, turn on simple encryption for the FreeIPA password + +```bash +op-freeipa encryption --simple +``` + +You set the server details using + +```bash +op-freeipa extra -k freeipa-server -v https://ipa.demo1.freeipa.org +op-freeipa extra -k freeipa-user -v admin +op-freeipa secret -k freeipa-password -v Secret123 +``` + +You can also add the set of system groups that should always be used +when adding users to FreeIPA via this agent. This should be a +comma-separated list of group names. + +```bash +op-freeipa extra -k system-groups -v group1,group2 +``` diff --git a/freeipa/src/freeipa.rs b/freeipa/src/freeipa.rs index 2a4fc4e..e5b85e3 100644 --- a/freeipa/src/freeipa.rs +++ b/freeipa/src/freeipa.rs @@ -753,7 +753,7 @@ fn get_primary_group(user: &UserIdentifier) -> String { /// or removes them as necessary. Groups will match the project group, /// the system groups, and the openportal group. /// -async fn sync_groups(user: &IPAUser) -> Result, Error> { +async fn sync_groups(user: &IPAUser) -> Result { // the user probably doesn't exist, so add them, making sure they // are in the correct groups let mut groups = cache::get_system_groups().await?; @@ -833,12 +833,21 @@ async fn sync_groups(user: &IPAUser) -> Result, Error> { match call_post::("group_add_member", None, Some(kwargs)).await { Ok(_) => tracing::info!("Successfully added user {} to group {}", userid, group_cn), Err(e) => { + // this should not happen - it indicates that the group has disappeared + // since we last updated. Our cache is now likely out of date. tracing::error!( "Could not add user {} to group {}. Error: {}", userid, group_cn, e ); + tracing::info!("Clearing the cache as FreeIPA has changed behind our back."); + cache::clear().await?; + // Return None so that the caller handles this failure case + return Err(Error::InvalidState(format!( + "Could not add user {} to group {}. Error: {}. Likely freeipa was changed behind our back!", + userid, group_cn, e + ))); } } } @@ -846,7 +855,7 @@ async fn sync_groups(user: &IPAUser) -> Result, Error> { // finally - re-fetch the user from FreeIPA to make sure that we have // the correct information match force_get_user(user.identifier()).await? { - Some(user) => Ok(Some(user)), + Some(user) => Ok(user), None => { tracing::warn!( "Failed to sync groups for user {} as this user no longer exists in FreeIPA.", @@ -854,7 +863,11 @@ async fn sync_groups(user: &IPAUser) -> Result, Error> { ); tracing::info!("Clearing the cache as FreeIPA has changed behind our back."); cache::clear().await?; - Ok(None) + // Return None so that the caller handles this failure case + Err(Error::InvalidState(format!( + "Failed to sync groups for user {} as this user no longer exists in FreeIPA. Likely freeipa was changed behind our back!", + user.identifier() + ))) } } } @@ -863,9 +876,22 @@ pub async fn add_user(user: &UserIdentifier) -> Result { // return the user if they already exist if let Some(user) = get_user(user).await? { // make sure that the groups are correct - if let Some(user) = sync_groups(&user).await? { - tracing::info!("Added user [cached] {}", user); - return Ok(user); + match sync_groups(&user).await { + Ok(user) => { + tracing::info!("Added user [cached] {}", user); + return Ok(user); + } + Err(e) => { + tracing::warn!( + "Failed to sync groups for user {} after adding. Error: {}", + user.identifier(), + e + ); + tracing::info!( + "Will try to add user {} again, as the groups are not correct.", + user.identifier() + ); + } } // we get here if the user has been removed from FreeIPA behind @@ -956,19 +982,21 @@ pub async fn add_user(user: &UserIdentifier) -> Result { // now synchronise the groups - this won't do anything if another // thread has already beaten us to creating the user - match sync_groups(&user).await? { - Some(user) => { + match sync_groups(&user).await { + Ok(user) => { tracing::info!("Added user: {}", user); Ok(user) } - None => { + Err(e) => { tracing::warn!( - "Failed to add user {} - they have been removed from FreeIPA?", - user.identifier() + "Failed to add user {} - they have been removed from FreeIPA? {}", + user.identifier(), + e ); Err(Error::Call(format!( - "Failed to add user {} - they have been removed from FreeIPA?", - user.identifier() + "Failed to add user {} - they have been removed from FreeIPA? {}", + user.identifier(), + e ))) } } diff --git a/freeipa/src/main.rs b/freeipa/src/main.rs index 213f765..dc0404b 100644 --- a/freeipa/src/main.rs +++ b/freeipa/src/main.rs @@ -44,6 +44,8 @@ async fn main() -> Result<()> { Some("ws://localhost:8046".to_owned()), Some("127.0.0.1".to_owned()), Some(8046), + None, + None, Some(AgentType::Account), ); diff --git a/helm/bridge/Chart.yaml b/helm/bridge/Chart.yaml index f475d01..5e5ffd6 100644 --- a/helm/bridge/Chart.yaml +++ b/helm/bridge/Chart.yaml @@ -5,6 +5,6 @@ apiVersion: v2 name: op-bridge version: "0.0.0" # Set by release script -appVersion: "" # Set by release script +appVersion: "0.0.6" # Set by release script sources: - https://github.com/isambard-sc/openportal/ diff --git a/helm/bridge/templates/configmap.yaml b/helm/bridge/templates/configmap.yaml deleted file mode 100644 index 1f235f2..0000000 --- a/helm/bridge/templates/configmap.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: © 2024 Christopher Woods -# SPDX-FileCopyrightText: © 2024 Matt Williams -# SPDX-License-Identifier: MIT ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: op-bridge-extra-config - labels: - {{- include "op-bridge.labels" . | indent 4 }} -data: - config-extra.toml: | - secret_key_path = "{{ .Values.secret_key_dir }}/key" - {{ .Values.config | toToml | nindent 4 }} diff --git a/helm/bridge/templates/deployment.yaml b/helm/bridge/templates/deployment.yaml index 4ef10ee..33eb6eb 100644 --- a/helm/bridge/templates/deployment.yaml +++ b/helm/bridge/templates/deployment.yaml @@ -17,11 +17,9 @@ spec: metadata: labels: {{- include "op-bridge.labels" . | indent 8 }} - annotations: - checksum/config: {{ pick (include (print $.Template.BasePath "/configmap.yaml") . | fromYaml) "data" | toString | sha1sum }} # restart if config changed spec: containers: - - name: op-portal + - name: op-bridge image: "{{ print .Values.image.registry "/" }}{{ required "image_name must be set" .Values.image.name }}:{{ default .Chart.AppVersion .Values.image.tag }}" args: ["--config-file=/config/config.toml", "run"] env: @@ -32,10 +30,10 @@ spec: readinessProbe: httpGet: path: /health - port: {{ .Values.port }} + port: {{ .Values.health_port }} volumeMounts: - - mountPath: {{ required "config_dir must be set" .Values.config_dir | quote }} - name: "config-volume" + - mountPath: "/config" + name: "config-file-volume" readOnly: true resources: requests: @@ -56,6 +54,6 @@ spec: add: - "NET_BIND_SERVICE" volumes: - - name: "config-volume" + - name: "config-file-volume" secret: secretName: {{ .Values.secret_name | quote }} diff --git a/helm/bridge/templates/service.yaml b/helm/bridge/templates/service.yaml index 937683e..90e0d7c 100644 --- a/helm/bridge/templates/service.yaml +++ b/helm/bridge/templates/service.yaml @@ -9,7 +9,16 @@ metadata: spec: ports: - protocol: TCP - port: 80 + port: {{ .Values.port }} + name: agent targetPort: {{ .Values.port }} + - protocol: TCP + port: {{ .Values.bridge_port }} + name: bridge + targetPort: {{ .Values.bridge_port }} + - protocol: TCP + port: {{ .Values.health_port }} + name: health + targetPort: {{ .Values.health_port }} selector: app.kubernetes.io/name: op-bridge diff --git a/helm/bridge/values.yaml b/helm/bridge/values.yaml index 9efea24..caabc84 100644 --- a/helm/bridge/values.yaml +++ b/helm/bridge/values.yaml @@ -8,7 +8,7 @@ image: name: op-bridge tag: # defaults to appVersion if not set log_level: info -secret_key_dir: "/secret_key" -config_dir: "/config" secret_name: bridge-config -port: 3000 +port: 80 +bridge_port: 8800 +health_port: 8080 diff --git a/helm/cluster/Chart.yaml b/helm/cluster/Chart.yaml new file mode 100644 index 0000000..31b3b13 --- /dev/null +++ b/helm/cluster/Chart.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: v2 +name: op-cluster +version: "0.0.0" # Set by release script +appVersion: "0.0.6" # Set by release script +sources: + - https://github.com/isambard-sc/openportal/ diff --git a/helm/cluster/templates/_helpers.yaml b/helm/cluster/templates/_helpers.yaml new file mode 100644 index 0000000..4c76b03 --- /dev/null +++ b/helm/cluster/templates/_helpers.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +{{- define "op-cluster.labels" }} +app.kubernetes.io/name: "op-cluster" +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service | quote }} +app.kubernetes.io/instance: {{ .Release.Name | quote }} +helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +{{- end }} diff --git a/helm/cluster/templates/deployment.yaml b/helm/cluster/templates/deployment.yaml new file mode 100644 index 0000000..037e8a3 --- /dev/null +++ b/helm/cluster/templates/deployment.yaml @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: op-cluster + labels: + {{- include "op-cluster.labels" . | indent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: op-cluster + template: + metadata: + labels: + {{- include "op-cluster.labels" . | indent 8 }} + spec: + containers: + - name: op-cluster + image: "{{ print .Values.image.registry "/" }}{{ required "image_name must be set" .Values.image.name }}:{{ default .Chart.AppVersion .Values.image.tag }}" + args: ["--config-file=/config/config.toml", "run"] + env: + - name: RUST_LOG + value: {{ .Values.log_level | quote }} + ports: + - containerPort: {{ .Values.port }} + readinessProbe: + httpGet: + path: /health + port: {{ .Values.health_port }} + volumeMounts: + - mountPath: "/config" + name: "config-file-volume" + readOnly: true + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "128Mi" + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + capabilities: + drop: + - "ALL" + add: + - "NET_BIND_SERVICE" + volumes: + - name: "config-file-volume" + secret: + secretName: {{ .Values.secret_name | quote }} diff --git a/helm/cluster/templates/service.yaml b/helm/cluster/templates/service.yaml new file mode 100644 index 0000000..4b14b89 --- /dev/null +++ b/helm/cluster/templates/service.yaml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: v1 +kind: Service +metadata: + name: op-cluster +spec: + ports: + - protocol: TCP + port: {{ .Values.port }} + name: agent + targetPort: {{ .Values.port }} + - protocol: TCP + port: {{ .Values.health_port }} + name: health + targetPort: {{ .Values.health_port }} + selector: + app.kubernetes.io/name: op-cluster diff --git a/helm/cluster/values.yaml b/helm/cluster/values.yaml new file mode 100644 index 0000000..9a4d2c4 --- /dev/null +++ b/helm/cluster/values.yaml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +config: {} +image: + registry: ghcr.io/isambard-sc + name: op-cluster + tag: # defaults to appVersion if not set +log_level: info +secret_name: cluster-config +port: 80 +health_port: 8080 diff --git a/helm/clusters/Chart.yaml b/helm/clusters/Chart.yaml new file mode 100644 index 0000000..064c3e1 --- /dev/null +++ b/helm/clusters/Chart.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: v2 +name: op-clusters +version: "0.0.0" # Set by release script +appVersion: "0.0.6" # Set by release script +sources: + - https://github.com/isambard-sc/openportal/ diff --git a/helm/clusters/templates/_helpers.yaml b/helm/clusters/templates/_helpers.yaml new file mode 100644 index 0000000..cc4297c --- /dev/null +++ b/helm/clusters/templates/_helpers.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +{{- define "op-clusters.labels" }} +app.kubernetes.io/name: "op-clusters" +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service | quote }} +app.kubernetes.io/instance: {{ .Release.Name | quote }} +helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +{{- end }} diff --git a/helm/clusters/templates/deployment.yaml b/helm/clusters/templates/deployment.yaml new file mode 100644 index 0000000..32257a4 --- /dev/null +++ b/helm/clusters/templates/deployment.yaml @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: op-clusters + labels: + {{- include "op-clusters.labels" . | indent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: op-clusters + template: + metadata: + labels: + {{- include "op-clusters.labels" . | indent 8 }} + spec: + containers: + - name: op-clusters + image: "{{ print .Values.image.registry "/" }}{{ required "image_name must be set" .Values.image.name }}:{{ default .Chart.AppVersion .Values.image.tag }}" + args: ["--config-file=/config/config.toml", "run"] + env: + - name: RUST_LOG + value: {{ .Values.log_level | quote }} + ports: + - containerPort: {{ .Values.port }} + readinessProbe: + httpGet: + path: /health + port: {{ .Values.health_port }} + volumeMounts: + - mountPath: "/config" + name: "config-file-volume" + readOnly: true + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "128Mi" + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + capabilities: + drop: + - "ALL" + add: + - "NET_BIND_SERVICE" + volumes: + - name: "config-file-volume" + secret: + secretName: {{ .Values.secret_name | quote }} diff --git a/helm/clusters/templates/service.yaml b/helm/clusters/templates/service.yaml new file mode 100644 index 0000000..c2fb533 --- /dev/null +++ b/helm/clusters/templates/service.yaml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: v1 +kind: Service +metadata: + name: op-clusters +spec: + ports: + - protocol: TCP + port: {{ .Values.port }} + name: agent + targetPort: {{ .Values.port }} + - protocol: TCP + port: {{ .Values.health_port }} + name: health + targetPort: {{ .Values.health_port }} + selector: + app.kubernetes.io/name: op-clusters diff --git a/helm/clusters/values.yaml b/helm/clusters/values.yaml new file mode 100644 index 0000000..46eb9fc --- /dev/null +++ b/helm/clusters/values.yaml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +config: {} +image: + registry: ghcr.io/isambard-sc + name: op-clusters + tag: # defaults to appVersion if not set +log_level: info +secret_name: clusters-config +port: 80 +health_port: 8080 diff --git a/helm/filesystem/Chart.yaml b/helm/filesystem/Chart.yaml new file mode 100644 index 0000000..70b9ffb --- /dev/null +++ b/helm/filesystem/Chart.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: v2 +name: op-filesystem +version: "0.0.0" # Set by release script +appVersion: "0.0.6" # Set by release script +sources: + - https://github.com/isambard-sc/openportal/ diff --git a/helm/filesystem/templates/_helpers.yaml b/helm/filesystem/templates/_helpers.yaml new file mode 100644 index 0000000..109b128 --- /dev/null +++ b/helm/filesystem/templates/_helpers.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +{{- define "op-filesystem.labels" }} +app.kubernetes.io/name: "op-filesystem" +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service | quote }} +app.kubernetes.io/instance: {{ .Release.Name | quote }} +helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +{{- end }} diff --git a/helm/filesystem/templates/deployment.yaml b/helm/filesystem/templates/deployment.yaml new file mode 100644 index 0000000..51c4856 --- /dev/null +++ b/helm/filesystem/templates/deployment.yaml @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: op-filesystem + labels: + {{- include "op-filesystem.labels" . | indent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: op-filesystem + template: + metadata: + labels: + {{- include "op-filesystem.labels" . | indent 8 }} + spec: + containers: + - name: op-filesystem + image: "{{ print .Values.image.registry "/" }}{{ required "image_name must be set" .Values.image.name }}:{{ default .Chart.AppVersion .Values.image.tag }}" + args: ["--config-file=/config/config.toml", "run"] + env: + - name: RUST_LOG + value: {{ .Values.log_level | quote }} + ports: + - containerPort: {{ .Values.port }} + readinessProbe: + httpGet: + path: /health + port: {{ .Values.health_port }} + volumeMounts: + - mountPath: "/config" + name: "config-file-volume" + readOnly: true + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "128Mi" + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + capabilities: + drop: + - "ALL" + add: + - "NET_BIND_SERVICE" + volumes: + - name: "config-file-volume" + secret: + secretName: {{ .Values.secret_name | quote }} diff --git a/helm/filesystem/templates/service.yaml b/helm/filesystem/templates/service.yaml new file mode 100644 index 0000000..f4d47b2 --- /dev/null +++ b/helm/filesystem/templates/service.yaml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: v1 +kind: Service +metadata: + name: op-filesystem +spec: + ports: + - protocol: TCP + port: {{ .Values.port }} + name: agent + targetPort: {{ .Values.port }} + - protocol: TCP + port: {{ .Values.health_port }} + name: health + targetPort: {{ .Values.health_port }} + selector: + app.kubernetes.io/name: op-filesystem diff --git a/helm/filesystem/values.yaml b/helm/filesystem/values.yaml new file mode 100644 index 0000000..5480d0a --- /dev/null +++ b/helm/filesystem/values.yaml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +config: {} +image: + registry: ghcr.io/isambard-sc + name: op-filesystem + tag: # defaults to appVersion if not set +log_level: info +secret_name: filesystem-config +port: 80 +health_port: 8080 diff --git a/helm/freeipa/Chart.yaml b/helm/freeipa/Chart.yaml new file mode 100644 index 0000000..9035989 --- /dev/null +++ b/helm/freeipa/Chart.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: v2 +name: op-freeipa +version: "0.0.0" # Set by release script +appVersion: "0.0.6" # Set by release script +sources: + - https://github.com/isambard-sc/openportal/ diff --git a/helm/freeipa/templates/_helpers.yaml b/helm/freeipa/templates/_helpers.yaml new file mode 100644 index 0000000..896553e --- /dev/null +++ b/helm/freeipa/templates/_helpers.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +{{- define "op-freeipa.labels" }} +app.kubernetes.io/name: "op-freeipa" +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service | quote }} +app.kubernetes.io/instance: {{ .Release.Name | quote }} +helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +{{- end }} diff --git a/helm/freeipa/templates/deployment.yaml b/helm/freeipa/templates/deployment.yaml new file mode 100644 index 0000000..af2f578 --- /dev/null +++ b/helm/freeipa/templates/deployment.yaml @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: op-freeipa + labels: + {{- include "op-freeipa.labels" . | indent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: op-freeipa + template: + metadata: + labels: + {{- include "op-freeipa.labels" . | indent 8 }} + spec: + containers: + - name: op-freeipa + image: "{{ print .Values.image.registry "/" }}{{ required "image_name must be set" .Values.image.name }}:{{ default .Chart.AppVersion .Values.image.tag }}" + args: ["--config-file=/config/config.toml", "run"] + env: + - name: RUST_LOG + value: {{ .Values.log_level | quote }} + ports: + - containerPort: {{ .Values.port }} + readinessProbe: + httpGet: + path: /health + port: {{ .Values.health_port }} + volumeMounts: + - mountPath: "/config" + name: "config-file-volume" + readOnly: true + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "128Mi" + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + capabilities: + drop: + - "ALL" + add: + - "NET_BIND_SERVICE" + volumes: + - name: "config-file-volume" + secret: + secretName: {{ .Values.secret_name | quote }} diff --git a/helm/freeipa/templates/service.yaml b/helm/freeipa/templates/service.yaml new file mode 100644 index 0000000..9ac1917 --- /dev/null +++ b/helm/freeipa/templates/service.yaml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: v1 +kind: Service +metadata: + name: op-freeipa +spec: + ports: + - protocol: TCP + port: {{ .Values.port }} + name: agent + targetPort: {{ .Values.port }} + - protocol: TCP + port: {{ .Values.health_port }} + name: health + targetPort: {{ .Values.health_port }} + selector: + app.kubernetes.io/name: op-freeipa diff --git a/helm/freeipa/values.yaml b/helm/freeipa/values.yaml new file mode 100644 index 0000000..bb2217d --- /dev/null +++ b/helm/freeipa/values.yaml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +config: {} +image: + registry: ghcr.io/isambard-sc + name: op-freeipa + tag: # defaults to appVersion if not set +log_level: info +secret_name: freeipa-config +port: 80 +health_port: 8080 diff --git a/helm/portal/Chart.yaml b/helm/portal/Chart.yaml index 91176fa..55f54ac 100644 --- a/helm/portal/Chart.yaml +++ b/helm/portal/Chart.yaml @@ -5,6 +5,6 @@ apiVersion: v2 name: op-portal version: "0.0.0" # Set by release script -appVersion: "" # Set by release script +appVersion: "0.0.6" # Set by release script sources: - https://github.com/isambard-sc/openportal/ diff --git a/helm/portal/templates/configmap.yaml b/helm/portal/templates/configmap.yaml deleted file mode 100644 index 7f2d0b9..0000000 --- a/helm/portal/templates/configmap.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: © 2024 Christopher Woods -# SPDX-FileCopyrightText: © 2024 Matt Williams -# SPDX-License-Identifier: MIT ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: op-portal-extra-config - labels: - {{- include "op-portal.labels" . | indent 4 }} -data: - config-extra.toml: | - secret_key_path = "{{ .Values.secret_key_dir }}/key" - {{ .Values.config | toToml | nindent 4 }} diff --git a/helm/portal/templates/deployment.yaml b/helm/portal/templates/deployment.yaml index f89bb39..be3270e 100644 --- a/helm/portal/templates/deployment.yaml +++ b/helm/portal/templates/deployment.yaml @@ -17,8 +17,6 @@ spec: metadata: labels: {{- include "op-portal.labels" . | indent 8 }} - annotations: - checksum/config: {{ pick (include (print $.Template.BasePath "/configmap.yaml") . | fromYaml) "data" | toString | sha1sum }} # restart if config changed spec: containers: - name: op-portal @@ -32,10 +30,10 @@ spec: readinessProbe: httpGet: path: /health - port: {{ .Values.port }} + port: {{ .Values.health_port }} volumeMounts: - - mountPath: {{ required "config_dir must be set" .Values.config_dir | quote }} - name: "config-volume" + - mountPath: "/config" + name: "config-file-volume" readOnly: true resources: requests: @@ -56,6 +54,6 @@ spec: add: - "NET_BIND_SERVICE" volumes: - - name: "config-volume" + - name: "config-file-volume" secret: secretName: {{ .Values.secret_name | quote }} diff --git a/helm/portal/templates/service.yaml b/helm/portal/templates/service.yaml index f4f5a07..fe290dd 100644 --- a/helm/portal/templates/service.yaml +++ b/helm/portal/templates/service.yaml @@ -9,7 +9,12 @@ metadata: spec: ports: - protocol: TCP - port: 80 + port: {{ .Values.port }} + name: agent targetPort: {{ .Values.port }} + - protocol: TCP + port: {{ .Values.health_port }} + name: health + targetPort: {{ .Values.health_port }} selector: app.kubernetes.io/name: op-portal diff --git a/helm/portal/values.yaml b/helm/portal/values.yaml index 3f6f831..42d95be 100644 --- a/helm/portal/values.yaml +++ b/helm/portal/values.yaml @@ -8,7 +8,6 @@ image: name: op-portal tag: # defaults to appVersion if not set log_level: info -secret_key_dir: "/secret_key" -config_dir: "/config" secret_name: portal-config -port: 3000 +port: 80 +health_port: 8080 diff --git a/helm/provider/Chart.yaml b/helm/provider/Chart.yaml new file mode 100644 index 0000000..cfcf420 --- /dev/null +++ b/helm/provider/Chart.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: v2 +name: op-provider +version: "0.0.0" # Set by release script +appVersion: "0.0.6" # Set by release script +sources: + - https://github.com/isambard-sc/openportal/ diff --git a/helm/provider/templates/_helpers.yaml b/helm/provider/templates/_helpers.yaml new file mode 100644 index 0000000..28d0564 --- /dev/null +++ b/helm/provider/templates/_helpers.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +{{- define "op-provider.labels" }} +app.kubernetes.io/name: "op-provider" +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service | quote }} +app.kubernetes.io/instance: {{ .Release.Name | quote }} +helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +{{- end }} diff --git a/helm/provider/templates/deployment.yaml b/helm/provider/templates/deployment.yaml new file mode 100644 index 0000000..bfd7070 --- /dev/null +++ b/helm/provider/templates/deployment.yaml @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: op-provider + labels: + {{- include "op-provider.labels" . | indent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: op-provider + template: + metadata: + labels: + {{- include "op-provider.labels" . | indent 8 }} + spec: + containers: + - name: op-provider + image: "{{ print .Values.image.registry "/" }}{{ required "image_name must be set" .Values.image.name }}:{{ default .Chart.AppVersion .Values.image.tag }}" + args: ["--config-file=/config/config.toml", "run"] + env: + - name: RUST_LOG + value: {{ .Values.log_level | quote }} + ports: + - containerPort: {{ .Values.port }} + readinessProbe: + httpGet: + path: /health + port: {{ .Values.health_port }} + volumeMounts: + - mountPath: "/config" + name: "config-file-volume" + readOnly: true + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "128Mi" + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + capabilities: + drop: + - "ALL" + add: + - "NET_BIND_SERVICE" + volumes: + - name: "config-file-volume" + secret: + secretName: {{ .Values.secret_name | quote }} diff --git a/helm/provider/templates/service.yaml b/helm/provider/templates/service.yaml new file mode 100644 index 0000000..c1b5bfe --- /dev/null +++ b/helm/provider/templates/service.yaml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +apiVersion: v1 +kind: Service +metadata: + name: op-provider +spec: + ports: + - protocol: TCP + port: {{ .Values.port }} + name: agent + targetPort: {{ .Values.port }} + - protocol: TCP + port: {{ .Values.health_port }} + name: health + targetPort: {{ .Values.health_port }} + selector: + app.kubernetes.io/name: op-provider diff --git a/helm/provider/values.yaml b/helm/provider/values.yaml new file mode 100644 index 0000000..d4d64d4 --- /dev/null +++ b/helm/provider/values.yaml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +--- +config: {} +image: + registry: ghcr.io/isambard-sc + name: op-provider + tag: # defaults to appVersion if not set +log_level: info +secret_name: provider-config +port: 80 +health_port: 8080 diff --git a/oci/cluster/Containerfile b/oci/cluster/Containerfile new file mode 100644 index 0000000..94bf78a --- /dev/null +++ b/oci/cluster/Containerfile @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT + +FROM gcr.io/distroless/static-debian12 +COPY op-cluster / +USER 65534:65534 +ENTRYPOINT ["/op-cluster"] diff --git a/oci/cluster/build.sh b/oci/cluster/build.sh new file mode 100755 index 0000000..6240f84 --- /dev/null +++ b/oci/cluster/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +set -euo pipefail + +# Build the project and create an OCI image containing it. + +function artifact_path { + echo "${1}" | jq --raw-output 'select(.reason == "compiler-artifact") | select(.target.name == "'"${2}"'") | .executable' +} + +out=$(cargo build --package op-cluster --target=x86_64-unknown-linux-musl --message-format=json ${@-}) +cp "$(artifact_path "${out}" "op-cluster")" oci/cluster + +cd oci/cluster + +version=$(./op-cluster --version | tail -n1 | cut -d' ' -f 2) +image_id=$( + podman build . --tag=op-cluster:latest --tag=op-cluster:"${version}" \ + --annotation="org.opencontainers.image.source=https://github.com/isambard-sc/openportal" \ + --annotation="org.opencontainers.image.description=OpenPortal" \ + --annotation="org.opencontainers.image.licenses=MIT" \ + | tee /dev/fd/2 \ + | tail -n1 +) +rm op-cluster +echo "Built op-cluster image:" 1>&2 +echo "${image_id}" diff --git a/oci/clusters/Containerfile b/oci/clusters/Containerfile new file mode 100644 index 0000000..d02ebf0 --- /dev/null +++ b/oci/clusters/Containerfile @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT + +FROM gcr.io/distroless/static-debian12 +COPY op-clusters / +USER 65534:65534 +ENTRYPOINT ["/op-clusters"] diff --git a/oci/clusters/build.sh b/oci/clusters/build.sh new file mode 100755 index 0000000..3a16855 --- /dev/null +++ b/oci/clusters/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +set -euo pipefail + +# Build the project and create an OCI image containing it. + +function artifact_path { + echo "${1}" | jq --raw-output 'select(.reason == "compiler-artifact") | select(.target.name == "'"${2}"'") | .executable' +} + +out=$(cargo build --package op-clusters --target=x86_64-unknown-linux-musl --message-format=json ${@-}) +cp "$(artifact_path "${out}" "op-clusters")" oci/clusters + +cd oci/clusters + +version=$(./op-clusters --version | tail -n1 | cut -d' ' -f 2) +image_id=$( + podman build . --tag=op-clusters:latest --tag=op-clusters:"${version}" \ + --annotation="org.opencontainers.image.source=https://github.com/isambard-sc/openportal" \ + --annotation="org.opencontainers.image.description=OpenPortal" \ + --annotation="org.opencontainers.image.licenses=MIT" \ + | tee /dev/fd/2 \ + | tail -n1 +) +rm op-clusters +echo "Built op-clusters image:" 1>&2 +echo "${image_id}" diff --git a/oci/filesystem/Containerfile b/oci/filesystem/Containerfile new file mode 100644 index 0000000..470f826 --- /dev/null +++ b/oci/filesystem/Containerfile @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT + +FROM gcr.io/distroless/static-debian12 +COPY op-filesystem / +USER 65534:65534 +ENTRYPOINT ["/op-filesystem"] diff --git a/oci/filesystem/build.sh b/oci/filesystem/build.sh new file mode 100755 index 0000000..51edc73 --- /dev/null +++ b/oci/filesystem/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +set -euo pipefail + +# Build the project and create an OCI image containing it. + +function artifact_path { + echo "${1}" | jq --raw-output 'select(.reason == "compiler-artifact") | select(.target.name == "'"${2}"'") | .executable' +} + +out=$(cargo build --package op-filesystem --target=x86_64-unknown-linux-musl --message-format=json ${@-}) +cp "$(artifact_path "${out}" "op-filesystem")" oci/filesystem + +cd oci/filesystem + +version=$(./op-filesystem --version | tail -n1 | cut -d' ' -f 2) +image_id=$( + podman build . --tag=op-filesystem:latest --tag=op-filesystem:"${version}" \ + --annotation="org.opencontainers.image.source=https://github.com/isambard-sc/openportal" \ + --annotation="org.opencontainers.image.description=OpenPortal" \ + --annotation="org.opencontainers.image.licenses=MIT" \ + | tee /dev/fd/2 \ + | tail -n1 +) +rm op-filesystem +echo "Built op-filesystem image:" 1>&2 +echo "${image_id}" diff --git a/oci/freeipa/Containerfile b/oci/freeipa/Containerfile new file mode 100644 index 0000000..5260be5 --- /dev/null +++ b/oci/freeipa/Containerfile @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT + +FROM gcr.io/distroless/static-debian12 +COPY op-freeipa / +USER 65534:65534 +ENTRYPOINT ["/op-freeipa"] diff --git a/oci/freeipa/build.sh b/oci/freeipa/build.sh new file mode 100755 index 0000000..d026a35 --- /dev/null +++ b/oci/freeipa/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +set -euo pipefail + +# Build the project and create an OCI image containing it. + +function artifact_path { + echo "${1}" | jq --raw-output 'select(.reason == "compiler-artifact") | select(.target.name == "'"${2}"'") | .executable' +} + +out=$(cargo build --package op-freeipa --target=x86_64-unknown-linux-musl --message-format=json ${@-}) +cp "$(artifact_path "${out}" "op-freeipa")" oci/freeipa + +cd oci/freeipa + +version=$(./op-freeipa --version | tail -n1 | cut -d' ' -f 2) +image_id=$( + podman build . --tag=op-freeipa:latest --tag=op-freeipa:"${version}" \ + --annotation="org.opencontainers.image.source=https://github.com/isambard-sc/openportal" \ + --annotation="org.opencontainers.image.description=OpenPortal" \ + --annotation="org.opencontainers.image.licenses=MIT" \ + | tee /dev/fd/2 \ + | tail -n1 +) +rm op-freeipa +echo "Built op-freeipa image:" 1>&2 +echo "${image_id}" diff --git a/oci/provider/Containerfile b/oci/provider/Containerfile new file mode 100644 index 0000000..2b5e771 --- /dev/null +++ b/oci/provider/Containerfile @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT + +FROM gcr.io/distroless/static-debian12 +COPY op-provider / +USER 65534:65534 +ENTRYPOINT ["/op-provider"] diff --git a/oci/provider/build.sh b/oci/provider/build.sh new file mode 100755 index 0000000..ff37267 --- /dev/null +++ b/oci/provider/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2024 Christopher Woods +# SPDX-FileCopyrightText: © 2024 Matt Williams +# SPDX-License-Identifier: MIT +set -euo pipefail + +# Build the project and create an OCI image containing it. + +function artifact_path { + echo "${1}" | jq --raw-output 'select(.reason == "compiler-artifact") | select(.target.name == "'"${2}"'") | .executable' +} + +out=$(cargo build --package op-provider --target=x86_64-unknown-linux-musl --message-format=json ${@-}) +cp "$(artifact_path "${out}" "op-provider")" oci/provider + +cd oci/provider + +version=$(./op-provider --version | tail -n1 | cut -d' ' -f 2) +image_id=$( + podman build . --tag=op-provider:latest --tag=op-provider:"${version}" \ + --annotation="org.opencontainers.image.source=https://github.com/isambard-sc/openportal" \ + --annotation="org.opencontainers.image.description=OpenPortal" \ + --annotation="org.opencontainers.image.licenses=MIT" \ + | tee /dev/fd/2 \ + | tail -n1 +) +rm op-provider +echo "Built op-provider image:" 1>&2 +echo "${image_id}" diff --git a/paddington/Cargo.toml b/paddington/Cargo.toml index d50232e..a07506c 100644 --- a/paddington/Cargo.toml +++ b/paddington/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "paddington" -version = "0.0.1" +version = "0.0.19" description = "A library for implementing the OpenPortal communication protocol" edition = "2021" license = "MIT" @@ -17,6 +17,7 @@ built = { version = "0.7", default-features = false, features = ["git2"] } [dependencies] anyhow = { version="1.0.86", features = ["backtrace"] } +axum = { version = "0.7", features = ["tracing", "query"] } dirs = "5.0.1" futures = "0.3.30" futures-channel = "0.3.30" @@ -25,15 +26,17 @@ hex = {version="0.4.3", features = ["serde"]} iptools = "0.2.5" once_cell = "1.19.0" orion = "0.17.6" +rustls = { version = "0.23.16", features = ["ring"] } secrecy = { version = "0.8.0", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.120" serde_with = { version="3.9.0", features = ["hex"] } thiserror = "1.0.63" tokio = { version = "1.0", features = ["full", "tracing"] } -tokio-tungstenite = "0.23.1" +tokio-tungstenite = { version = "0.24.0", features = ["rustls-tls-native-roots"] } toml = "0.8.16" tracing = "0.1.40" +tungstenite = "0.24.0" url = {version="2.5.2", features=["serde"]} [lints.rust] diff --git a/paddington/src/client.rs b/paddington/src/client.rs index 0b5ef28..3aa2886 100644 --- a/paddington/src/client.rs +++ b/paddington/src/client.rs @@ -5,6 +5,7 @@ use crate::config::{PeerConfig, ServiceConfig}; use crate::connection::Connection; use crate::error::Error; use crate::exchange; +use crate::healthcheck; pub async fn run_once(config: ServiceConfig, peer: PeerConfig) -> Result<(), Error> { let service_name = config.name(); @@ -44,6 +45,11 @@ pub async fn run(config: ServiceConfig, peer: PeerConfig) -> Result<(), Error> { // set the name of the service in the exchange exchange::set_name(&config.name()).await?; + if let Some(healthcheck_port) = config.healthcheck_port() { + // spawn the health check server + healthcheck::spawn(config.ip(), healthcheck_port).await?; + } + loop { match run_once(config.clone(), peer.clone()).await { Ok(_) => { diff --git a/paddington/src/config.rs b/paddington/src/config.rs index c29fc1d..04aa634 100644 --- a/paddington/src/config.rs +++ b/paddington/src/config.rs @@ -73,6 +73,8 @@ pub struct Defaults { url: String, ip: String, port: u16, + healthcheck_port: Option, + proxy_header: Option, } impl Defaults { @@ -82,6 +84,8 @@ impl Defaults { url: Option, ip: Option, port: Option, + healthcheck_port: Option, + proxy_header: Option, ) -> Self { let config_file = config_file.unwrap_or( dirs::config_local_dir() @@ -99,6 +103,8 @@ impl Defaults { url: url.unwrap_or("http://localhost:8000".to_owned()), ip: ip.unwrap_or("127.0.0.1".to_owned()), port: port.unwrap_or(8042), + healthcheck_port, + proxy_header, } } @@ -121,6 +127,10 @@ impl Defaults { pub fn port(&self) -> u16 { self.port } + + pub fn healthcheck_port(&self) -> Option { + self.healthcheck_port + } } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -155,9 +165,28 @@ fn create_websocket_url(url: &str) -> Result { }; let host = url.host_str().unwrap_or("localhost"); - let port = url.port().unwrap_or(8080); + let port = url.port().unwrap_or(match scheme { + "ws" => 80, + "wss" => 443, + _ => 443, + }); let path = url.path(); + // don't specify the port if it's the default for the protocol + match scheme { + "ws" => { + if port == 80 { + return Ok(format!("{}://{}", scheme, host)); + } + } + "wss" => { + if port == 443 { + return Ok(format!("{}://{}", scheme, host)); + } + } + _ => {} + } + Ok(format!("{}://{}:{}{}", scheme, host, port, path)) } @@ -412,6 +441,8 @@ pub struct ServiceConfig { url: String, ip: IpAddr, port: u16, + heathcheck_port: Option, + proxy_header: Option, servers: Vec, clients: Vec, @@ -419,7 +450,14 @@ pub struct ServiceConfig { } impl ServiceConfig { - pub fn new(name: &str, url: &str, ip: &str, port: &u16) -> Result { + pub fn new( + name: &str, + url: &str, + ip: &str, + port: &u16, + healthcheck_port: &Option, + proxy_header: &Option, + ) -> Result { Ok(ServiceConfig { name: name.to_string(), url: create_websocket_url(url)?, @@ -427,6 +465,8 @@ impl ServiceConfig { .parse() .with_context(|| format!("Could not parse IP address: {}", ip))?, port: *port, + heathcheck_port: *healthcheck_port, + proxy_header: proxy_header.clone(), servers: Vec::new(), clients: Vec::new(), encryption: None, @@ -493,6 +533,14 @@ impl ServiceConfig { self.port } + pub fn healthcheck_port(&self) -> Option { + self.heathcheck_port + } + + pub fn proxy_header(&self) -> Option { + self.proxy_header.clone() + } + pub fn add_client(&mut self, name: &str, ip: &str) -> Result { let ip = IpOrRange::new(ip) .with_context(|| format!("Could not parse into an IP address or IP range: {}", ip))?; @@ -573,6 +621,8 @@ impl ServiceConfig { url: String, ip: IpAddr, port: u16, + healthcheck_port: &Option, + proxy_header: &Option, ) -> Result { // see if this config_dir exists - return an error if it does let config_file = path::absolute(config_file).with_context(|| { @@ -586,7 +636,14 @@ impl ServiceConfig { return Err(Error::NotExists(config_file.to_string_lossy().to_string())); } - let config = ServiceConfig::new(&name, &url, &ip.to_string(), &port)?; + let config = ServiceConfig::new( + &name, + &url, + &ip.to_string(), + &port, + healthcheck_port, + proxy_header, + )?; save::(config.clone(), &config_file)?; // check we can read the config and return it @@ -649,15 +706,29 @@ mod tests { #[test] fn test_invitations() { - let mut primary = ServiceConfig::new("primary", "http://localhost", "127.0.0.1", &5544) - .unwrap_or_else(|e| { - unreachable!("Cannot create service config: {}", e); - }); + let mut primary = ServiceConfig::new( + "primary", + "http://localhost", + "127.0.0.1", + &5544, + &None, + &None, + ) + .unwrap_or_else(|e| { + unreachable!("Cannot create service config: {}", e); + }); - let mut secondary = ServiceConfig::new("secondary", "http://localhost", "127.0.0.1", &5545) - .unwrap_or_else(|e| { - unreachable!("Cannot create service config: {}", e); - }); + let mut secondary = ServiceConfig::new( + "secondary", + "http://localhost", + "127.0.0.1", + &5545, + &None, + &None, + ) + .unwrap_or_else(|e| { + unreachable!("Cannot create service config: {}", e); + }); // introduce the secondary to the primary let invite = primary diff --git a/paddington/src/connection.rs b/paddington/src/connection.rs index 94ba593..b9286e9 100644 --- a/paddington/src/connection.rs +++ b/paddington/src/connection.rs @@ -9,12 +9,15 @@ use futures_channel::mpsc::{unbounded, UnboundedSender}; use futures_util::{future, pin_mut, stream::TryStreamExt}; use secrecy::ExposeSecret; use serde::{de::DeserializeOwned, Serialize}; +use std::sync::Arc; use tokio::net::TcpStream; use tokio::sync::Mutex as TokioMutex; use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::protocol::Message as TokioMessage; - -use std::sync::Arc; +use tungstenite::handshake::server::{ + ErrorResponse as HandshakeErrorResponse, Request as HandshakeRequest, + Response as HandshakeResponse, +}; use crate::command::Command; use crate::config::{ClientConfig, PeerConfig, ServiceConfig}; @@ -318,6 +321,13 @@ impl Connection { exchange::received(Command::connected(peer_name.clone()).into()) .with_context(|| "Error triggering /connected control message")?; + // finally, send a keepalive message to the peer - this will start + // a ping-pong with the peer that should keep it open + // (client sends, as the server should already be set up now) + exchange::send(Message::keepalive(&peer_name)) + .await + .with_context(|| "Error sending keepalive message to peer")?; + pin_mut!(received_from_peer, send_to_peer); future::select(received_from_peer, send_to_peer).await; @@ -358,36 +368,66 @@ impl Connection { // we now know we are the only ones handling the connection, // and are safe to update the keys etc. - let addr: std::net::SocketAddr = stream + let mut client_ip: std::net::IpAddr = stream .peer_addr() - .with_context(|| "Error getting the peer address. Ensure the connection is open.")?; + .with_context(|| "Error getting the peer address. Ensure the connection is open.")? + .ip(); + + let proxy_header = self.config.proxy_header(); + let mut proxy_client = None; + + let process_headers = |request: &HandshakeRequest, + response: HandshakeResponse| + -> Result { + if let Some(proxy_header) = proxy_header { + if let Some(value) = request + .headers() + .get(proxy_header) + .and_then(|value| value.to_str().ok()) + { + proxy_client = Some(value.to_string()); + } + } + + Ok(response) + }; + + let ws_stream = tokio_tungstenite::accept_hdr_async(stream, process_headers) + .await + .with_context(|| { + format!( + "Error accepting WebSocket connection from: {}. Closing connection.", + client_ip + ) + })?; + + if let Some(proxy_client) = proxy_client { + tracing::info!("Proxy client: {:?}", proxy_client); + client_ip = proxy_client + .parse() + .with_context(|| "Error parsing proxy client address")?; + } - tracing::info!("Accepted connection from peer: {}", addr); + // this doesn't need to be mutable any more + let client_ip = client_ip; + + tracing::info!("Accepted connection from peer: {}", client_ip); let clients: Vec = self .config .clients() .iter() - .filter(|client| client.matches(addr.ip())) + .filter(|client| client.matches(client_ip)) .cloned() .collect(); if clients.is_empty() { - tracing::warn!("No matching peer found for address: {}", addr); + tracing::warn!("No matching peer found for address: {}", client_ip); return Err(Error::InvalidPeer( "No matching peer found for address.".to_string(), )); } - let ws_stream = tokio_tungstenite::accept_async(stream) - .await - .with_context(|| { - format!( - "Error accepting WebSocket connection from: {}. Closing connection.", - addr - ) - })?; - // Split the WebSocket stream into incoming and outgoing parts let (mut outgoing, mut incoming) = ws_stream.split(); @@ -428,7 +468,7 @@ impl Connection { tracing::info!( "Client {:?} authenticated for address: {}", client.name().unwrap_or_default(), - addr + client_ip ); true } @@ -439,7 +479,10 @@ impl Connection { .collect(); if clients.is_empty() { - tracing::warn!("No matching peer could authenticate for address: {}", addr); + tracing::warn!( + "No matching peer could authenticate for address: {}", + client_ip + ); return Err(Error::InvalidPeer( "No matching peer could authenticate for address.".to_string(), )); @@ -449,7 +492,7 @@ impl Connection { tracing::warn!( "Multiple matching peers found for address: {} - \ {:?}. Ignoring all but the first...", - addr, + client_ip, clients ); } @@ -541,7 +584,7 @@ impl Connection { pin_mut!(received_from_peer, send_to_peer); future::select(received_from_peer, send_to_peer).await; - tracing::info!("{} disconnected", &addr); + tracing::info!("{} disconnected", &client_ip); // we've exited, meaning that this connection is now closed self.closed_connection().await; diff --git a/paddington/src/eventloop.rs b/paddington/src/eventloop.rs index 943f6fe..e34b792 100644 --- a/paddington/src/eventloop.rs +++ b/paddington/src/eventloop.rs @@ -8,6 +8,16 @@ use crate::error::Error; use crate::{client, server}; pub async fn run(config: ServiceConfig) -> Result<(), Error> { + match rustls::crypto::ring::default_provider().install_default() { + Ok(_) => {} + Err(e) => { + tracing::error!("Could not install default ring provider: {:?}", e); + return Err(Error::NotExists( + "Could not install default ring provider".to_owned(), + )); + } + } + let mut server_handles = vec![]; let mut client_handles = vec![]; @@ -57,7 +67,14 @@ mod tests { async fn test_run() -> Result<()> { // this tests that the service can be configured and will run // (it will exit immediately as there are no clients or servers) - let config = ServiceConfig::new("test_server", "http://localhost", "127.0.0.1", &5544)?; + let config = ServiceConfig::new( + "test_server", + "http://localhost", + "127.0.0.1", + &5544, + &None, + &None, + )?; run(config).await?; Ok(()) diff --git a/paddington/src/healthcheck.rs b/paddington/src/healthcheck.rs new file mode 100644 index 0000000..6438327 --- /dev/null +++ b/paddington/src/healthcheck.rs @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: © 2024 Christopher Woods +// SPDX-License-Identifier: MIT + +use crate::Error; + +use anyhow::Result; +use axum::{ + extract::Json, + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use once_cell::sync::Lazy; +use serde_json::json; +use std::net::IpAddr; +use std::sync::RwLock; +use tokio::net::TcpListener; + +// +// Health check endpoint for the web API +// +#[tracing::instrument(skip_all)] +async fn health() -> Result, AppError> { + Ok(Json(json!({"status": "ok"}))) +} + +/// +/// Function spawned to run the API server in a background thread +/// +async fn run_server(app: Router, listener: TcpListener) -> Result<()> { + match axum::serve(listener, app).await { + Ok(_) => { + tracing::info!("Server ran successfully"); + } + Err(e) => { + tracing::error!("Error starting server: {}", e); + } + } + + Ok(()) +} + +static IS_RUNNING: Lazy> = Lazy::new(|| RwLock::new(false)); + +/// +/// Spawn a small http server that responds to health checks +/// +pub async fn spawn(ip: IpAddr, port: u16) -> Result<(), Error> { + // check if the server is already running + match IS_RUNNING.read() { + Ok(guard) => { + if *guard { + // already running + return Ok(()); + } + } + Err(e) => { + // not running? + tracing::error!("Error getting read lock: {}", e); + return Ok(()); + } + } + + // set the flag to indicate the server is running + match IS_RUNNING.write() { + Ok(mut guard) => { + if *guard { + // someone else set it first + return Ok(()); + } + + *guard = true; + } + Err(e) => { + // not running? + tracing::error!("Error getting write lock: {}", e); + return Ok(()); + } + } + + tracing::info!("Starting health check server on {}:{}/health", ip, port); + + // create the web API + let app = Router::new().route("/health", get(health)); + + // create a TCP listener on the specified port + let listener = tokio::net::TcpListener::bind(&std::net::SocketAddr::new(ip, port)).await?; + + // spawn a new task to run the web server to listen for requests + tokio::spawn(run_server(app, listener)); + + Ok(()) +} + +// Errors + +#[derive(Debug)] +struct AppError(anyhow::Error, Option); + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + ( + self.1.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + Json(json!({"message":format!("Something went wrong: {:?}", self.0)})), + ) + .into_response() + } +} + +impl From for AppError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into(), None) + } +} diff --git a/paddington/src/lib.rs b/paddington/src/lib.rs index e7a0466..c3f6ad8 100644 --- a/paddington/src/lib.rs +++ b/paddington/src/lib.rs @@ -8,6 +8,7 @@ mod crypto; mod error; mod eventloop; mod exchange; +mod healthcheck; mod server; // public API diff --git a/paddington/src/message.rs b/paddington/src/message.rs index a809f68..5071390 100644 --- a/paddington/src/message.rs +++ b/paddington/src/message.rs @@ -24,6 +24,12 @@ impl Display for Message { } } +pub enum MessageType { + Control, + KeepAlive, + Message, +} + impl Message { pub fn new(sender: &str, payload: &str) -> Self { Self { @@ -45,6 +51,32 @@ impl Message { self.sender.is_empty() } + pub fn keepalive(sender: &str) -> Self { + Self { + sender: sender.to_owned(), + recipient: "".to_owned(), + payload: "KEEPALIVE".to_owned(), + } + } + + pub fn is_keepalive(&self) -> bool { + self.payload == "KEEPALIVE" + } + + pub fn is_message(&self) -> bool { + !self.is_control() && !self.is_keepalive() + } + + pub fn typ(&self) -> MessageType { + if self.is_control() { + MessageType::Control + } else if self.is_keepalive() { + MessageType::KeepAlive + } else { + MessageType::Message + } + } + pub fn set_recipient(&mut self, recipient: &str) { self.recipient = recipient.to_owned(); } diff --git a/paddington/src/server.rs b/paddington/src/server.rs index 4f99e88..6cb5925 100644 --- a/paddington/src/server.rs +++ b/paddington/src/server.rs @@ -7,6 +7,7 @@ use crate::config::ServiceConfig; use crate::connection::Connection; use crate::error::Error; use crate::exchange; +use crate::healthcheck; /// /// Internal function used to handle a single connection to the server. @@ -52,8 +53,6 @@ pub async fn run_once(config: ServiceConfig) -> Result<(), Error> { // Let's spawn the handling of each connection in a separate task. loop { - tracing::info!("Awaiting the next connection..."); - match listener.accept().await { Ok((stream, addr)) => { tracing::info!("New connection from: {}", addr); @@ -74,6 +73,11 @@ pub async fn run(config: ServiceConfig) -> Result<(), Error> { // set the name of the service in the exchange exchange::set_name(&config.name()).await?; + // spawn the healthcheck server if enabled + if let Some(healthcheck_port) = config.healthcheck_port() { + healthcheck::spawn(config.ip(), healthcheck_port).await?; + } + loop { let result = run_once(config.clone()).await; diff --git a/portal/Cargo.toml b/portal/Cargo.toml index 71b9a35..78f5be7 100644 --- a/portal/Cargo.toml +++ b/portal/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-portal" -version = "0.0.1" +version = "0.0.19" description = "An example of an OpenPortal portal interface service" edition = "2021" license = "MIT" diff --git a/portal/src/main.rs b/portal/src/main.rs index a1c4549..d5749b0 100644 --- a/portal/src/main.rs +++ b/portal/src/main.rs @@ -40,6 +40,8 @@ async fn main() -> Result<()> { Some("ws://localhost:8040".to_owned()), Some("127.0.0.1".to_owned()), Some(8040), + None, + None, Some(AgentType::Portal), ); diff --git a/provider/Cargo.toml b/provider/Cargo.toml index 4f61812..3627977 100644 --- a/provider/Cargo.toml +++ b/provider/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-provider" -version = "0.0.1" +version = "0.0.19" description = "An example of an OpenPortal provider interface service" edition = "2021" license = "MIT" diff --git a/provider/src/main.rs b/provider/src/main.rs index 5d8922d..02b59ff 100644 --- a/provider/src/main.rs +++ b/provider/src/main.rs @@ -40,6 +40,8 @@ async fn main() -> Result<()> { Some("ws://localhost:8041".to_owned()), Some("127.0.0.1".to_owned()), Some(8041), + None, + None, Some(AgentType::Provider), ); diff --git a/python/Cargo.toml b/python/Cargo.toml index c7c9ddd..e975d00 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "openportal" -version = "0.0.1" +version = "0.0.19" description = "Python wrappers for OpenPortal" edition = "2021" license = "MIT" @@ -32,3 +32,10 @@ tracing = "0.1.40" tracing-subscriber = "0.3.18" url = {version="2.5.2", features=["serde"]} uuid = { version="1.10.0", features=["serde", "v4", "fast-rng", "macro-diagnostics"] } + +[build-dependencies] +pyo3-build-config = "0.22.5" + +[features] +extension-module = ["pyo3/extension-module"] +default = ["extension-module"] diff --git a/python/build.rs b/python/build.rs new file mode 100644 index 0000000..5de5ce2 --- /dev/null +++ b/python/build.rs @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: © 2024 Christopher Woods +// SPDX-License-Identifier: MIT + +fn main() { + pyo3_build_config::add_extension_module_link_args(); +} diff --git a/slurm/src/main.rs b/slurm/src/main.rs deleted file mode 100644 index 3efddbf..0000000 --- a/slurm/src/main.rs +++ /dev/null @@ -1,212 +0,0 @@ -// SPDX-FileCopyrightText: © 2024 Christopher Woods -// SPDX-License-Identifier: MIT - -use anyhow::Result; - -use templemeads::agent; -use templemeads::agent::instance::{process_args, run, Defaults}; -use templemeads::agent::Type as AgentType; -use templemeads::async_runnable; -use templemeads::grammar::Instruction::{AddUser, RemoveUser}; -use templemeads::grammar::{UserIdentifier, UserMapping}; -use templemeads::job::{Envelope, Job}; -use templemeads::Error; - -/// -/// Main function for the slurm cluster instance agent -/// -/// This purpose of this agent is to manage an individual instance -/// of a slurm batch cluster. It will manage the lifecycle of -/// users and projects on the cluster. -/// -#[tokio::main] -async fn main() -> Result<()> { - // start tracing - let subscriber = tracing_subscriber::FmtSubscriber::new(); - tracing::subscriber::set_global_default(subscriber)?; - - // create the OpenPortal paddington defaults - let defaults = Defaults::parse( - Some("slurm".to_owned()), - Some( - dirs::config_local_dir() - .unwrap_or( - ".".parse() - .expect("Could not parse fallback config directory."), - ) - .join("openportal") - .join("slurm-config.toml"), - ), - Some("ws://localhost:8046".to_owned()), - Some("127.0.0.1".to_owned()), - Some(8046), - Some(AgentType::Instance), - ); - - // now parse the command line arguments to get the service configuration - let config = match process_args(&defaults).await? { - Some(config) => config, - None => { - // Not running the service, so can safely exit - return Ok(()); - } - }; - - async_runnable! { - /// - /// Runnable function that will be called when a job is received - /// by the agent - /// - pub async fn slurm_runner(envelope: Envelope) -> Result - { - tracing::info!("Using the slurm runner"); - - let me = envelope.recipient(); - let sender = envelope.sender(); - let mut job = envelope.job(); - - match job.instruction() { - AddUser(user) => { - // add the user to the slurm cluster - tracing::info!("Adding user to slurm cluster: {}", user); - let mapping = create_account(&me, &user).await?; - - job = job.running(Some("Step 1/3: Account created".to_string()))?; - job = job.update(&sender).await?; - - let homedir = create_directories(&me, &mapping).await?; - - job = job.running(Some("Step 2/3: Directories created".to_string()))?; - job = job.update(&sender).await?; - - let _ = update_homedir(&me, &user, &homedir).await?; - - job = job.completed(mapping)?; - } - RemoveUser(user) => { - // remove the user from the slurm cluster - tracing::info!("Removing user from slurm cluster: {}", user); - job = job.completed("User removed")?; - } - _ => { - tracing::error!("Unknown instruction: {:?}", job.instruction()); - return Err(Error::UnknownInstruction( - format!("Unknown instruction: {:?}", job.instruction()).to_string(), - )); - } - } - - Ok(job) - } - } - - // run the agent - run(config, slurm_runner).await?; - - Ok(()) -} - -async fn create_account(me: &str, user: &UserIdentifier) -> Result { - // find the Account agent - match agent::account().await { - Some(account) => { - // send the add_job to the account agent - let job = Job::parse(&format!("{}.{} add_user {}", me, account, user))? - .put(&account) - .await?; - - // Wait for the add_job to complete - let result = job.wait().await?.result::()?; - - match result { - Some(mapping) => { - tracing::info!("User added to account agent: {:?}", mapping); - Ok(mapping) - } - None => { - tracing::error!("Error creating the user's account: {:?}", job); - Err(Error::Call( - format!("Error creating the user's account: {:?}", job).to_string(), - )) - } - } - } - None => { - tracing::error!("No account agent found"); - Err(Error::MissingAgent( - "Cannot run the job because there is no account agent".to_string(), - )) - } - } -} - -async fn create_directories(me: &str, mapping: &UserMapping) -> Result { - // find the Filesystem agent - match agent::filesystem().await { - Some(filesystem) => { - // send the add_job to the filesystem agent - let job = Job::parse(&format!("{}.{} add_local_user {}", me, filesystem, mapping))? - .put(&filesystem) - .await?; - - // Wait for the add_job to complete - let result = job.wait().await?.result::()?; - - match result { - Some(homedir) => { - tracing::info!("Directories created for user: {:?}", mapping); - Ok(homedir) - } - None => { - tracing::error!("Error creating the user's directories: {:?}", job); - Err(Error::Call( - format!("Error creating the user's directories: {:?}", job).to_string(), - )) - } - } - } - None => { - tracing::error!("No filesystem agent found"); - Err(Error::MissingAgent( - "Cannot run the job because there is no filesystem agent".to_string(), - )) - } - } -} - -async fn update_homedir(me: &str, user: &UserIdentifier, homedir: &str) -> Result { - // find the Account agent - match agent::account().await { - Some(account) => { - // send the add_job to the account agent - let job = Job::parse(&format!( - "{}.{} update_homedir {} {}", - me, account, user, homedir - ))? - .put(&account) - .await?; - - // Wait for the add_job to complete - let result = job.wait().await?.result::()?; - - match result { - Some(homedir) => { - tracing::info!("User {} homedir updated: {:?}", user, homedir); - Ok(homedir) - } - None => { - tracing::error!("Error updating the user's homedir: {:?}", job); - Err(Error::Call( - format!("Error updating the user's homedir: {:?}", job).to_string(), - )) - } - } - } - None => { - tracing::error!("No account agent found"); - Err(Error::MissingAgent( - "Cannot run the job because there is no account agent".to_string(), - )) - } - } -} diff --git a/templemeads/Cargo.toml b/templemeads/Cargo.toml index 3edbd7d..f09a63d 100644 --- a/templemeads/Cargo.toml +++ b/templemeads/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "templemeads" -version = "0.0.1" +version = "0.0.19" description = "A library for interfacing OpenPortal with specific portals" edition = "2021" license = "MIT" diff --git a/templemeads/src/agent_bridge.rs b/templemeads/src/agent_bridge.rs index d67ee90..12b15ed 100644 --- a/templemeads/src/agent_bridge.rs +++ b/templemeads/src/agent_bridge.rs @@ -72,12 +72,22 @@ impl Defaults { url: Option, ip: Option, port: Option, + healthcheck_port: Option, + proxy_header: Option, bridge_url: Option, bridge_ip: Option, bridge_port: Option, ) -> Self { Self { - service: ServiceDefaults::parse(name, config_file, url, ip, port), + service: ServiceDefaults::parse( + name, + config_file, + url, + ip, + port, + healthcheck_port, + proxy_header, + ), bridge: BridgeDefaults::parse(bridge_url, bridge_ip, bridge_port), } } @@ -115,10 +125,21 @@ pub async fn process_args(defaults: &Defaults) -> Result, Error> url, ip, port, + bridge_url, bridge_ip, bridge_port, + healthcheck_port, + proxy_header, force, }) => { + let local_healthcheck_port; + + if let Some(healthcheck_port) = healthcheck_port { + local_healthcheck_port = Some(*healthcheck_port); + } else { + local_healthcheck_port = defaults.service.healthcheck_port(); + } + let config = Config { service: { ServiceConfig::new( @@ -129,10 +150,12 @@ pub async fn process_args(defaults: &Defaults) -> Result, Error> .parse::()? .to_string(), &port.unwrap_or_else(|| defaults.service.port()), + &local_healthcheck_port, + proxy_header, )? }, bridge: BridgeConfig::new( - &bridge_ip.clone().unwrap_or(defaults.bridge.url()), + &bridge_url.clone().unwrap_or(defaults.bridge.url()), bridge_ip .clone() .unwrap_or(defaults.bridge.ip()) @@ -360,10 +383,17 @@ enum Commands { )] port: Option, + #[arg( + long, + short = 'r', + help = "URL of the bridge API server including port and route (e.g. http://localhost:3000)" + )] + bridge_url: Option, + #[arg( long, short = 'b', - help = "IP address on which to listen for bridge connections (e.g. '::')" + help = "IP address on which to listen for bridge connections (e.g. '0.0.0.0')" )] bridge_ip: Option, @@ -374,6 +404,20 @@ enum Commands { )] bridge_port: Option, + #[arg( + long, + short = 'k', + help = "Optional port on which to listen for health checks (e.g. 3001)" + )] + healthcheck_port: Option, + + #[arg( + long, + short = 'x', + help = "Optional header to use for proxying requests - look in this for the client IP address" + )] + proxy_header: Option, + #[arg(long, short = 'f', help = "Force reinitialisation")] force: bool, }, diff --git a/templemeads/src/agent_core.rs b/templemeads/src/agent_core.rs index d628861..e477b0f 100644 --- a/templemeads/src/agent_core.rs +++ b/templemeads/src/agent_core.rs @@ -74,16 +74,27 @@ pub struct Defaults { } impl Defaults { + #[allow(clippy::too_many_arguments)] pub fn parse( name: Option, config_file: Option, url: Option, ip: Option, port: Option, + healthcheck_port: Option, + proxy_header: Option, agent: Option, ) -> Self { Self { - service: ServiceDefaults::parse(name, config_file, url, ip, port), + service: ServiceDefaults::parse( + name, + config_file, + url, + ip, + port, + healthcheck_port, + proxy_header, + ), agent: agent.unwrap_or(AgentType::Portal), extras: HashMap::new(), } @@ -130,8 +141,18 @@ pub async fn process_args(defaults: &Defaults) -> Result, Error> url, ip, port, + healthcheck_port, + proxy_header, force, }) => { + let local_healthcheck_port; + + if let Some(healthcheck_port) = healthcheck_port { + local_healthcheck_port = Some(*healthcheck_port); + } else { + local_healthcheck_port = defaults.service.healthcheck_port(); + } + let config = Config { service: { ServiceConfig::new( @@ -142,6 +163,8 @@ pub async fn process_args(defaults: &Defaults) -> Result, Error> .parse::()? .to_string(), &port.unwrap_or_else(|| defaults.service.port()), + &local_healthcheck_port, + proxy_header, )? }, agent: defaults.agent.clone(), @@ -375,6 +398,20 @@ enum Commands { )] port: Option, + #[arg( + long, + short = 'k', + help = "Optional port on which to listen for health checks (e.g. 8080)" + )] + healthcheck_port: Option, + + #[arg( + long, + short = 'x', + help = "Proxy header to use for the client IP address - look here for the client IP address" + )] + proxy_header: Option, + #[arg(long, short = 'f', help = "Force reinitialisation")] force: bool, }, diff --git a/templemeads/src/bridge_server.rs b/templemeads/src/bridge_server.rs index c5feb17..d368d74 100644 --- a/templemeads/src/bridge_server.rs +++ b/templemeads/src/bridge_server.rs @@ -70,9 +70,28 @@ fn create_webserver_url(url: &str) -> Result { }; let host = url.host_str().unwrap_or("localhost"); - let port = url.port().unwrap_or(3000); + let port = url.port().unwrap_or(match scheme { + "http" => 80, + "https" => 443, + _ => 443, + }); let path = url.path(); + // don't add the port if it is the default for the protocol + match scheme { + "http" => { + if port == 80 { + return Ok(format!("{}://{}{}", scheme, host, path).parse::()?); + } + } + "https" => { + if port == 443 { + return Ok(format!("{}://{}{}", scheme, host, path).parse::()?); + } + } + _ => {} + } + Ok(format!("{}://{}:{}{}", scheme, host, port, path).parse::()?) } @@ -81,12 +100,12 @@ impl Config { Self { url: create_webserver_url(url).unwrap_or_else(|e| { tracing::error!( - "Could not parse URL: {} because {}. Using http://localhost:3000 instead.", - e, - url + "Could not parse URL: {} because '{}'. Using http://localhost:{port} instead.", + url, + e ); #[allow(clippy::unwrap_used)] - "http://localhost:3000".parse().unwrap() + format!("http://localhost:{port}").parse().unwrap() }), ip, port, @@ -105,7 +124,7 @@ pub struct Defaults { impl Defaults { pub fn parse(url: Option, ip: Option, port: Option) -> Self { Self { - url: url.unwrap_or("http://localhost:3000".to_owned()), + url: url.unwrap_or("http://localhost:8042".to_owned()), ip: ip.unwrap_or("127.0.0.1".to_owned()), port: port.unwrap_or(8042), } @@ -365,6 +384,7 @@ pub async fn spawn(config: Config) -> Result<(), Error> { Ok(()) } + // Errors #[derive(Debug)] diff --git a/templemeads/src/handler.rs b/templemeads/src/handler.rs index 5ca8bf9..d3f85ff 100644 --- a/templemeads/src/handler.rs +++ b/templemeads/src/handler.rs @@ -13,7 +13,7 @@ use crate::runnable::{default_runner, AsyncRunnable}; use anyhow::Result; use once_cell::sync::Lazy; use paddington::async_message_handler; -use paddington::message::Message; +use paddington::message::{Message, MessageType}; use std::boxed::Box; use tokio::sync::RwLock; @@ -176,9 +176,33 @@ async_message_handler! { pub async fn process_message(message: Message) -> Result<(), paddington::Error> { let service_info: ServiceDetails = SERVICE_DETAILS.read().await.to_owned(); - match message.is_control() { - true => Ok(process_control_message(&service_info.agent_type, message.into()).await?), - false => { + match message.typ() { + MessageType::Control => { + process_control_message(&service_info.agent_type, message.into()).await?; + + Ok(()) + } + MessageType::KeepAlive => { + let sender: String = message.sender().to_owned(); + let recipient: String = message.recipient().to_owned(); + + if (recipient != service_info.service) { + return Err(Error::Delivery(format!("Recipient {} does not match service {}", recipient, service_info.service)).into()); + } + + // wait 20 seconds and send a keep alive message back + tokio::time::sleep(tokio::time::Duration::from_secs(20)).await; + + match paddington::send(Message::keepalive(&sender)).await { + Ok(_) => {} + Err(e) => { + tracing::warn!("Error sending keepalive message to {} : {}", sender, e); + } + } + + Ok(()) + } + MessageType::Message => { let sender: String = message.sender().to_owned(); let recipient: String = message.recipient().to_owned(); let command: Command = message.into(); @@ -187,7 +211,9 @@ async_message_handler! { return Err(Error::Delivery(format!("Recipient {} does not match service {}", recipient, service_info.service)).into()); } - Ok(process_command(&recipient, &sender, &command, &service_info.runner).await?) + process_command(&recipient, &sender, &command, &service_info.runner).await?; + + Ok(()) } } }