├── .credo.exs ├── .dialyzer_ignore.exs ├── .dockerignore ├── .envrc ├── .formatter.exs ├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ ├── build.yaml │ ├── code_quality.yaml │ ├── docs.yaml │ └── helm_chart.yaml ├── .gitignore ├── .tool-versions ├── .vscode └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── config ├── bonny.exs ├── config.exs ├── prod.exs ├── runtime.exs └── test.exs ├── docs ├── getting_started.md ├── index.md ├── postgres │ ├── index.md │ ├── postgres_cluster_instance.md │ ├── postgres_database.md │ └── postgres_instance.md └── temporal │ └── index.md ├── lib ├── kompost.ex ├── kompost │ ├── application.ex │ ├── k8s_conn.ex │ ├── kompo.ex │ ├── kompo │ │ ├── postgres │ │ │ ├── controller │ │ │ │ ├── database_controller.ex │ │ │ │ └── instance_controller.ex │ │ │ ├── database.ex │ │ │ ├── database │ │ │ │ └── params.ex │ │ │ ├── instance.ex │ │ │ ├── operator.ex │ │ │ ├── privileges.ex │ │ │ ├── supervisor.ex │ │ │ ├── user.ex │ │ │ ├── v1alpha1 │ │ │ │ ├── postgres_database.ex │ │ │ │ └── postgres_instance.ex │ │ │ └── webhooks │ │ │ │ ├── admission_control_handler.ex │ │ │ │ └── router.ex │ │ └── temporal │ │ │ ├── api_client.ex │ │ │ ├── conn.ex │ │ │ ├── controller │ │ │ ├── api_server_controller.ex │ │ │ └── namespace_controller.ex │ │ │ ├── operator.ex │ │ │ ├── supervisor.ex │ │ │ └── v1alpha1 │ │ │ ├── temporal_api_server.ex │ │ │ └── temporal_namespace.ex │ ├── pluggable │ │ └── init_conditions.ex │ ├── tools │ │ ├── namespace_access.ex │ │ ├── password.ex │ │ └── resource.ex │ ├── webhooks.ex │ └── webhooks │ │ └── router.ex └── mix │ └── tasks │ ├── bonny │ └── gen │ │ └── manifest │ │ └── kompost_customizer.ex │ ├── kompost.gen.manifest.ex │ └── kompost.gen.periphery.ex ├── mix.exs ├── mix.lock ├── mkdocs.yml ├── priv ├── charts │ ├── kompost │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── LICENSE │ │ ├── README.md │ │ ├── README.md.gotmpl │ │ ├── artifacthub-repo.yml │ │ ├── templates │ │ │ ├── _helpers.tpl │ │ │ ├── clusterrole.yaml │ │ │ ├── clusterrolebinding.yaml │ │ │ ├── crds │ │ │ │ ├── postgresclusterinstance.crd.yaml │ │ │ │ ├── postgresdatabase.crd.yaml │ │ │ │ ├── postgresinstance.crd.yaml │ │ │ │ ├── temporalapiserver.crd.yaml │ │ │ │ └── temporalnamespace.crd.yaml │ │ │ ├── deployment.yaml │ │ │ ├── serviceaccount.yaml │ │ │ ├── validatingwebhookconfiguration.yaml │ │ │ ├── webhook.certificate.yaml │ │ │ └── webhook.service.yaml │ │ └── values.yaml │ └── pubkey.asc ├── manifest │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── deployment.yaml │ ├── postgresclusterinstance.crd.yaml │ ├── postgresdatabase.crd.yaml │ ├── postgresinstance.crd.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ ├── temporalapiserver.crd.yaml │ ├── temporalnamespace.crd.yaml │ └── validatingwebhookconfiguration.yaml └── periphery │ ├── postgres.yaml │ ├── temporal.yaml │ └── temporal │ └── development-sql.yaml └── test ├── integration ├── .env └── kind-cluster.yml ├── kompost ├── kompo │ ├── postgres │ │ ├── controller │ │ │ ├── cluster_instance_controller_integration_test.exs │ │ │ ├── database_controller_e2e_test.exs │ │ │ ├── database_controller_integration_test.exs │ │ │ └── instance_controller_integration_test.exs │ │ ├── database_test.exs │ │ ├── instance_integration_test.exs │ │ └── instance_test.exs │ └── temporal │ │ └── controller │ │ ├── api_server_controller_integration_test.exs │ │ └── namespace_controller_integration_test.exs └── tools │ ├── namespace_access_test.exs │ └── password_test.exs ├── support ├── global_resource_helper.ex ├── integration_helper.ex └── kompo │ ├── postgres │ └── resource_helper.ex │ └── temporal │ └── resource_helper.ex └── test_helper.exs /.dialyzer_ignore.exs: -------------------------------------------------------------------------------- 1 | [ 2 | # Using mix functions generates warnings 3 | {"lib/mix/tasks/kompost.gen.manifest.ex"}, 4 | {"lib/mix/tasks/kompost.gen.periphery.ex"} 5 | ] 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | _build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | .fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | .github/ 23 | _build/ 24 | test/ 25 | priv/ 26 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | direnv_version 2.30.3 2 | # IEx Persistent History, see https://tylerpachal.medium.com/iex-persistent-history-5d7d64e905d3 3 | export ERL_AFLAGS="-kernel shell_history enabled" 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | import_deps: [:pluggable, :plug] 5 | ] 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mruoss] 4 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "dependencyDashboard": true, 4 | "ignoreDeps": [], 5 | "rebaseWhen": "behind-base-branch", 6 | "branchConcurrentLimit": 10, 7 | "lockFileMaintenance": { 8 | "enabled": true, 9 | "automerge": true 10 | }, 11 | "addLabels": ["renovate-update"] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/code_quality.yaml: -------------------------------------------------------------------------------- 1 | name: Code Quality 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | env: 9 | MIX_ENV: test 10 | KUBECONFIG: /home/runner/.kube/config 11 | 12 | jobs: 13 | code-quality: 14 | name: Code Quality 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | k8s_version: [v1.26.0] 19 | steps: 20 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 21 | 22 | - uses: engineerd/setup-kind@v0.5.0 23 | id: kind 24 | with: 25 | version: v0.20.0 26 | image: kindest/node:${{ matrix.k8s_version }} 27 | name: kompost-test 28 | config: ./test/integration/kind-cluster.yml 29 | 30 | - name: Setup elixir 31 | id: beam 32 | uses: erlef/setup-beam@v1 33 | with: 34 | version-file: .tool-versions 35 | version-type: strict 36 | install-rebar: true 37 | install-hex: true 38 | 39 | - name: Retrieve Build Cache 40 | uses: actions/cache@v4 41 | id: build-folder-cache 42 | with: 43 | path: _build/test 44 | key: ${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-build-test-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 45 | 46 | - name: Retrieve Mix Dependencies Cache 47 | uses: actions/cache@v4 48 | id: mix-cache 49 | with: 50 | path: deps 51 | key: ${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 52 | 53 | - name: Install Mix Dependencies 54 | run: mix deps.get 55 | 56 | - name: Setup Manifests 57 | env: 58 | MIX_ENV: test 59 | run: | 60 | mix kompost.gen.manifest 61 | mix kompost.gen.periphery 62 | 63 | - name: Check Formatting 64 | run: mix format --check-formatted 65 | 66 | - name: Compile 67 | run: MIX_ENV=test mix compile --warnings-as-errors 68 | 69 | - name: Run Credo 70 | run: MIX_ENV=test mix credo --strict 71 | 72 | - name: Run Coverage 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | MIX_ENV: test 76 | run: TEST_WAIT_TIMEOUT=10000 mix coveralls.github --include integration --timeout 20000 77 | 78 | - name: Retrieve PLT Cache 79 | uses: actions/cache/restore@v4 80 | id: plt-cache 81 | with: 82 | key: | 83 | ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt-${{ hashFiles('**/mix.lock') }} 84 | restore-keys: | 85 | ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt- 86 | path: | 87 | priv/plts 88 | 89 | # Create PLTs if no cache was found 90 | - name: Create PLTs 91 | if: steps.plt-cache.outputs.cache-hit != 'true' 92 | run: | 93 | mkdir -p priv/plts 94 | MIX_ENV=test mix dialyzer --plt 95 | 96 | - name: Save PLT cache 97 | id: plt_cache_save 98 | uses: actions/cache/save@v4 99 | if: steps.plt-cache.outputs.cache-hit != 'true' 100 | with: 101 | key: | 102 | ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt-${{ hashFiles('**/mix.lock') }} 103 | path: | 104 | priv/plts 105 | 106 | - name: Run dialyzer 107 | run: MIX_ENV=test mix dialyzer --format github 108 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Publish docs via GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | name: Build docs 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout main 15 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 16 | 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: 3.x 20 | 21 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 22 | 23 | - uses: actions/cache@v4 24 | with: 25 | key: mkdocs-material-${{ env.cache_id }} 26 | path: .cache 27 | restore-keys: | 28 | mkdocs-material- 29 | 30 | - run: pip install mkdocs-material 31 | 32 | - run: mkdocs build 33 | 34 | - name: Upload GitHub Pages Artifact 35 | uses: actions/upload-pages-artifact@v3 36 | with: 37 | path: site 38 | 39 | deploy: 40 | runs-on: ubuntu-latest 41 | name: Deploy to GH Pages 42 | needs: build 43 | if: github.event_name == 'push' 44 | permissions: 45 | pages: write 46 | id-token: write 47 | environment: 48 | name: github-pages 49 | url: ${{ steps.deployment.outputs.page_url }} 50 | steps: 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v4 54 | -------------------------------------------------------------------------------- /.github/workflows/helm_chart.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Helm Repository 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | env: 9 | MIX_ENV: prod 10 | KUBECONFIG: /home/runner/.kube/config 11 | HELM_DOCS_VERSION: 1.13.1 12 | 13 | permissions: 14 | contents: read 15 | packages: write 16 | 17 | jobs: 18 | publish_chart: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 22 | - name: install helm-docs 23 | run: | 24 | cd /tmp 25 | wget https://github.com/norwoodj/helm-docs/releases/download/v${{ env.HELM_DOCS_VERSION }}/helm-docs_${{ env.HELM_DOCS_VERSION }}_Linux_x86_64.tar.gz 26 | tar -xvf helm-docs_${{ env.HELM_DOCS_VERSION }}_Linux_x86_64.tar.gz 27 | sudo mv helm-docs /usr/local/sbin 28 | 29 | - name: helm docs 30 | run: | 31 | cd priv/charts/kompost 32 | helm-docs 33 | - name: helm lint 34 | run: | 35 | helm lint priv/charts/kompost 36 | - name: helm login 37 | run: | 38 | echo ${{ secrets.GITHUB_TOKEN }} | helm registry login ghcr.io -u $ --password-stdin 39 | - name: helm package 40 | if: ${{ github.event_name != 'push' }} 41 | run: helm package priv/charts/kompost 42 | - name: helm package 43 | if: ${{ github.event_name == 'push' }} 44 | run: | 45 | export KEY_NAME=$(cat /tmp/private.pgp | gpg --show-keys --with-colons | awk -F: '$1=="uid" {print $10; exit}' 46 | echo "${{ secrets.CHART_SIGN_PRIVATE_KEY }}" | gpg --dearmor --output keyring.gpg 47 | helm package --sign --key $KEY_NAME --keyring keyring.gpg priv/charts/kompost 48 | - name: helm push 49 | if: ${{ github.event_name == 'push' }} 50 | run: | 51 | helm push kompost-*.tgz oci://ghcr.io/${{ github.repository_owner }}/charts 52 | - name: Upload artifacthub-repo.yml 53 | run: | 54 | echo ${{ secrets.GITHUB_TOKEN }} | oras login ghcr.io -u mruoss --password-stdin 55 | oras push \ 56 | ghcr.io/${{ github.repository_owner }}/charts/kompost:artifacthub.io \ 57 | --config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \ 58 | priv/charts/kompost/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | kompost-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | /test/integration/kubeconfig*.yaml 29 | 30 | /manifest*.yaml 31 | 32 | /site 33 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 26.2.5 2 | elixir 1.16.3 3 | kind 0.23.0 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "yaml.schemas": { 3 | "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" 4 | }, 5 | "yaml.customTags": [ 6 | "!ENV scalar", 7 | "!ENV sequence", 8 | "tag:yaml.org,2002:python/name:materialx.emoji.to_svg", 9 | "tag:yaml.org,2002:python/name:materialx.emoji.twemoji", 10 | "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.3.2] - 2024-03-07 9 | 10 | ### Chores 11 | 12 | - Several dependencies were updated 13 | 14 | ## [0.3.1] - 2023-07-31 15 | 16 | ### Added 17 | 18 | - `PostgresDatabase` Resource - Add database naming strategy [#38](https://github.com/mruoss/kompost/pull/38), [#42](https://github.com/mruoss/kompost/pull/42) 19 | 20 | ## [0.3.0] - 2023-07-13 21 | 22 | ### Changed 23 | 24 | - **Breaking:** `PostgresDatabase` - `.spec.instanceRef.namespace` in favor of `PostgresClusterInstance` [#25](https://github.com/mruoss/kompost/pull/25) 25 | 26 | ### Added 27 | 28 | - `Kompost.Webhooks` - Pass secret name as variable 29 | - `Postgres` Kompo - Support for SSL connections [#27](https://github.com/mruoss/kompost/pull/27) 30 | - `PostgresClusterInstance` - Cluster scoped instance accessible by databases in any or a defined set of namespaces. [#25](https://github.com/mruoss/kompost/pull/25) 31 | 32 | ## [0.2.3] - 2023-06-30 33 | 34 | ### Added 35 | 36 | - `PostgresDatabase` - Additional parameters passed to `CREATE DATABASE` in `.spec.params` 37 | - Kompost now serves Admission Webhooks. Currently used for the new (immutable) fields in `.spec.params` 38 | - The Build GH workflow now runs e2e tests after building the image. 39 | 40 | ## [0.2.2] - 2023-06-11 41 | 42 | ### Changed 43 | 44 | - Upgraded Erlang OTP to version 26 45 | - Use docker images form hexpm 46 | 47 | ## [0.2.0] - 2023-05-24 48 | 49 | ### Added 50 | 51 | - Temporal Kompo was added. Used to create resources in a [Temporal](https://temporal.io) Cluster. 52 | 53 | ## [0.1.5] - 2023-02-27 54 | 55 | ### Fixed 56 | 57 | - `PostgresInstance`, `PostgresDatabase` - Misconfigured RBAC rules prevented user secrets from being created. 58 | 59 | ## [0.1.4] - 2023-02-26 60 | 61 | ### Fixed 62 | 63 | - `PostgresDatabase` - duplicate users were added to resource status infinitely 64 | 65 | ### Added 66 | 67 | - `PostgresDatabase` - Added printer columns to resource 68 | - `PostgresInstance`, `PostgresDatabase` - Warning logs 69 | 70 | ### Changed 71 | 72 | - Upgreaded `bonny` to v1.1.1 73 | 74 | ## [0.1.3] - 2023-02-25 75 | 76 | - Remove defaults from CRD manifests (stop fighting ArgoCD) 77 | 78 | ## [0.1.2] - 2023-02-25 79 | 80 | ### Fixed 81 | 82 | - Disable TLS peer verification as most likely we're connecting to an IP address. 83 | 84 | ## [0.1.1] - 2023-02-25 85 | 86 | ### Changed 87 | 88 | - Watching all namespaces now 89 | 90 | ## [0.1.0] - 2023-02-25 91 | 92 | Initial release with Postgres Kompo. 93 | 94 | ## 95 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | ARG ERLANG_IMAGE 3 | ARG ELIXIR_IMAGE 4 | ######################### 5 | ###### Build Image ###### 6 | ######################### 7 | 8 | FROM --platform=$BUILDPLATFORM ${ELIXIR_IMAGE} as builder 9 | 10 | ENV MIX_ENV=prod \ 11 | MIX_HOME=/opt/mix \ 12 | HEX_HOME=/opt/hex 13 | 14 | RUN mix local.hex --force && \ 15 | mix local.rebar --force 16 | 17 | WORKDIR /app 18 | 19 | COPY mix.lock mix.exs ./ 20 | COPY config config 21 | 22 | RUN mix deps.get --only-prod && \ 23 | mix deps.clean --unused && \ 24 | mix deps.compile 25 | 26 | # COPY priv priv 27 | COPY lib lib 28 | 29 | RUN mix release 30 | 31 | ######################### 32 | ##### Release Image ##### 33 | ######################### 34 | 35 | FROM --platform=$BUILDPLATFORM ${ERLANG_IMAGE} 36 | 37 | # elixir expects utf8. 38 | ENV LANG=C.UTF-8 39 | 40 | WORKDIR /opt/kompost 41 | COPY --from=builder /app/_build/prod/rel/kompost ./ 42 | RUN chmod g+rwX /opt/kompost 43 | 44 | LABEL org.opencontainers.image.source="https://github.com/mruoss/kompost" 45 | LABEL org.opencontainers.image.authors="michael@michaelruoss.ch" 46 | 47 | USER 1001 48 | 49 | ENTRYPOINT ["/opt/kompost/bin/kompost"] 50 | CMD ["start"] 51 | 52 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CLUSTER_NAME=kompost-test 2 | ELIXIR_IMAGE=hexpm/elixir:1.15.0-erlang-26.0.1-alpine-3.18.2 3 | ERLANG_IMAGE=hexpm/erlang:26.0.1-alpine-3.18.2 4 | MANIFEST_OUT_DIR=priv/manifest 5 | 6 | .PHONY: docker_compose 7 | docker_compose: 8 | docker-compose -f test/integration/docker-compose.yml up -d --remove-orphans 9 | 10 | .PHONY: setup 11 | #test: docker_compose 12 | setup: ## Run integration tests using k3d `make cluster` 13 | mix compile 14 | mix kompost.gen.manifest 15 | 16 | .PHONY: test 17 | #test: docker_compose 18 | test: ## Run integration tests using k3d `make cluster` 19 | MIX_ENV=test mix compile 20 | MIX_ENV=test mix kompost.gen.manifest 21 | MIX_ENV=test mix kompost.gen.periphery 22 | mix test --include integration --cover --timeout 30000 23 | 24 | .PHONY: e2e 25 | e2e: SHELL := /bin/bash 26 | e2e: 27 | MIX_ENV=test mix compile 28 | MIX_ENV=test mix kompost.gen.manifest 29 | docker buildx build --build-arg ELIXIR_IMAGE=${ELIXIR_IMAGE} --build-arg ERLANG_IMAGE=${ERLANG_IMAGE} -t kompost:e2e --load . 30 | kind load docker-image --name ${CLUSTER_NAME} kompost:e2e 31 | MIX_ENV=test mix kompost.gen.periphery 32 | kubectl config use-context kind-${CLUSTER_NAME} 33 | MIX_ENV=prod mix compile 34 | MIX_ENV=prod mix kompost.gen.manifest --image kompost:e2e --out - | kubectl apply -f - 35 | kubectl wait pods -n kompost -l k8s-app=kompost --for condition=Ready --timeout=300s 36 | POSTGRES_HOST=postgres.postgres.svc.cluster.local TEMPORAL_HOST=temporal.temporal.svc.cluster.local mix test --include integration --include e2e --no-start --cover 37 | kubectl delete ns kompost 38 | 39 | .PHONY: delete 40 | delete: 41 | kind delete cluster --name kompost-test 42 | rm -f test/integration/kubeconfig-test.yaml 43 | kind delete cluster --name kompost-dev 44 | rm -f test/integration/kubeconfig-dev.yaml 45 | 46 | .PHONY: docs 47 | docs: 48 | docker run --name kompost-docs --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material 49 | 50 | .PHONY: helm 51 | helm: 52 | rm -rf ${MANIFEST_OUT_DIR} 53 | mkdir -p ${MANIFEST_OUT_DIR} 54 | MIX_ENV=prod mix compile 55 | MIX_ENV=prod mix kompost.gen.manifest --image kompost:e2e --out ${MANIFEST_OUT_DIR} 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kompost 2 | 3 | Kompost is a Kubernetes operator providing self-service management for 4 | developers to install infrastructure resources. 5 | 6 | It is meant to be installed by operators running their applications on 7 | Kubernetes to give their development teams a way to create certain 8 | infrastructure resources by applying Kubernetes resources to their clusters or 9 | committing to their infra repo when using ArgoCD. 10 | 11 | Kompost was written in Elixir, using [`bonny`](https://hexdocs.pm/bonny), a Kubernetes development 12 | framework written in Elixir. 13 | 14 | ## Links 15 | 16 | - [GitHub Repository](https://github.com/mruoss/kompost) 17 | - [Documentation](https://kompost.chuge.li) 18 | - [Helm Chart on ArtifactHUB](https://artifacthub.io/packages/helm/kompost/kompost) 19 | 20 | ## Usage Example 21 | 22 | If you're in charge of managing infrastructure like setting up and maintaining 23 | postgres instances you can install **Kompost** to give your developer teams a 24 | way to install databases on those instances on their own. 25 | 26 | ## Installing Kompost 27 | 28 | To install Kompost, just download the manifest from the [release 29 | page](https://github.com/mruoss/kompost/releases) and apply it to your cluster. 30 | 31 | Or you can use the [helm chart](https://artifacthub.io/packages/helm/kompost/kompost) to install Kompost. 32 | -------------------------------------------------------------------------------- /config/bonny.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :bonny, 4 | # TODO: remove when bonny upgraded to 1.1.2 5 | operator_name: "kompost", 6 | service_account_name: "kompost", 7 | manifest_override_callback: &Mix.Tasks.Bonny.Gen.Manifest.KompostCustomizer.override/1 8 | 9 | # Labels to apply to the operator's resources. 10 | # labels: %{ 11 | # "k8s-app" => "<%= assigns[:operator_name] %>" 12 | # }, 13 | 14 | # Operator deployment resources. These are the defaults. 15 | # resources: <%= inspect(assigns[:resources]) %>, 16 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | import_config "bonny.exs" 4 | 5 | config :logger, 6 | level: :debug 7 | 8 | if File.exists?("config/#{Mix.env()}.exs") do 9 | import_config("#{Mix.env()}.exs") 10 | end 11 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, 4 | compile_time_purge_matching: [ 5 | [library: :bonny], 6 | [library: :k8s], 7 | [library: :k8s_webhoox] 8 | ], 9 | level: :info 10 | -------------------------------------------------------------------------------- /config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :kompost, 4 | operator_namespace: System.get_env("BONNY_POD_NAMESPACE", "kompost") 5 | 6 | config :kompost, Kompost.Kompo, 7 | postgres: System.get_env("KOMPO_POSTGRES_ENABLED", "true") in ["true", "1"], 8 | temporal: System.get_env("KOMPO_TEMPORAL_ENABLED", "true") in ["true", "1"] 9 | 10 | log_level = System.get_env("LOG_LEVEL") 11 | 12 | if log_level do 13 | config :logger, 14 | level: log_level |> String.to_atom() 15 | end 16 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, 4 | # use LOG_LEVEL env variable to get logs 5 | level: :none 6 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | 5 | Each release comes with a set of [container 6 | images](https://github.com/mruoss/kompost/pkgs/container/kompost) (debian, slim 7 | and alpine). Download the manifest from the [release 8 | page](https://github.com/mruoss/kompost/releases/tag/v0.2.3) and apply it to 9 | your cluster: 10 | 11 | ```bash 12 | curl -L https://github.com/mruoss/kompost/releases/download/v0.3.0/manifest.yaml | kubectl apply -f - 13 | ``` 14 | 15 | This installs the CRDs, creates a namespace `kompost` and installs Kompost inside it. 16 | Once installed, make sure the operator runs: 17 | 18 | ``` 19 | kubectl describe -n kompost deploy/kompost 20 | ``` 21 | 22 | ## Helm Chart 23 | 24 | There is a helm chart you can use to install Kompost. Checkout the templates and 25 | default values on [Artifact 26 | Hub](https://artifacthub.io/packages/helm/kompost/kompost) 27 | 28 | ``` 29 | helm template -n kompost kompost oci://ghcr.io/mruoss/kompost --version 0.1.0 30 | ``` 31 | 32 | ## Work with Kompos 33 | 34 | Once the operator is installed, you can work with the Kompos. Check out their 35 | documentation: 36 | 37 | - [Postgres](/postgres) 38 | - [Temporal](/temporal) 39 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Kompost 2 | 3 | Kompost is a Kubernetes operator providing self-service management for 4 | developers to install infrastructure resources. 5 | 6 | It is meant to be installed by infrastructure admins running their applications 7 | on Kubernetes. Once installed it prvides development teams a declarateive way of 8 | creating infrastructure resources by applying Kubernetes resources to their 9 | clusters or committing to their infra repo when using GitOps. 10 | 11 | ## Links 12 | 13 | - [:simple-github: GitHub Repository](https://github.com/mruoss/kompost) 14 | - [:simple-readthedocs: Documentation](https://kompost.chuge.li) 15 | - [:simple-artifacthub: Helm Chart on ArtifactHUB](https://artifacthub.io/packages/helm/kompost/kompost) 16 | 17 | ## Kompos 18 | 19 | Kompost comes with a set of components, aka. Kompos. Kompos are independent 20 | from each other. Each one serves its own set of CRDs and provides a service 21 | on its own. 22 | 23 | The most mature Kompo at this moment is [Postgres](postgres). Besides, there 24 | is the [Temporal](temporal) Kompo currently being developed. 25 | -------------------------------------------------------------------------------- /docs/postgres/index.md: -------------------------------------------------------------------------------- 1 | # Postgres Kompo 2 | 3 | The Postgres Kompo manages databases inside your Postgres servers. 4 | 5 | In order to use it, you need a running Postgres server and the information about 6 | how to connect to it using an account with privileges to manage databases and users. You then declare databases to 7 | be created on that server using this operator. 8 | 9 | ## How it Works 10 | 11 | The Postgres Kompo comes with [`PostgresInstance`](postgres_instance.md) and 12 | [`PostgresClusterInstance`](postgres_cluster_instance.md) CRDs which serve as 13 | connectors to your Postgres server. 14 | 15 | The [`PostgresDatabase`](postgres_database.md) CRD defines a database to be created inside the instance 16 | referenced by `.spec.instanceRef` or `.spec.clusterInstanceRef` respectively. 17 | 18 | The operator uses the information from the instance resource to connect to 19 | the server in order to create the requested database. 20 | 21 | ## Reconciliation 22 | 23 | In case the state in the target (the Postgres server) diverges, Kompost tries to 24 | reconcile it. E.g. if a database gets deleted on the cluster, Kompost recreates 25 | it. However, Kompost does not backup and restore schemas and/or data. 26 | -------------------------------------------------------------------------------- /docs/postgres/postgres_cluster_instance.md: -------------------------------------------------------------------------------- 1 | # The `PostgresClusterInstance` Resource 2 | 3 | `PostgresClusterInstance` is the cluster-scoped version of the 4 | [`PostgresInstance`](postgres_instance.md) resource and is used almost exactly 5 | the same way. However, since it is cluster-scoped, the `passwordSecretRef` must 6 | reference a secret residing in the **operator namespace**. The operator 7 | namespace is the namespace kompost runs in (defaults to `"kompost"`) 8 | 9 | ```yaml 10 | apiVersion: v1 11 | kind: Secret 12 | metadata: 13 | name: server-credentials 14 | namespace: kompost 15 | stringData: 16 | password: secure-password 17 | ``` 18 | 19 | The `PostgresClusterInstance` then references the secret in `spec.passwordSecretRef`: 20 | 21 | ```yaml 22 | apiVersion: kompost.chuge.li/v1alpha1 23 | kind: PostgresClusterInstance 24 | metadata: 25 | name: staging-server 26 | namespace: default 27 | spec: 28 | hostname: postgres.svc 29 | port: 5432 30 | username: postgres 31 | passwordSecretRef: 32 | name: server-credentials 33 | key: password 34 | ssl: 35 | enabled: false 36 | ``` 37 | 38 | ## Limiting Allowed Namespaces 39 | 40 | By default, the `PostgresClusterInstance` can be referenced by 41 | `PostgresDatabase` resources from any namespace. Access can be limited to a set 42 | of namespaces throug the `kompost.chuge.li/allowed-namespaces` annotation. This 43 | annotation can be set to a list of namespaces as regular expressions. 44 | 45 | !!! note Start and End Anchors are added automatically 46 | 47 | Note that Kompost wraps all regular expressions in `$` and `^` anchors if 48 | they aren't already. 49 | 50 | ### Examples 51 | 52 | The following resource can be referenced by `PostgresDatabase` resources in 53 | exactly two namespaces: `default` and `staging`. 54 | 55 | ```yaml 56 | apiVersion: kompost.chuge.li/v1alpha1 57 | kind: PostgresClusterInstance 58 | metadata: 59 | name: staging-server 60 | namespace: default 61 | annotations: 62 | kompost.chuge.li/allowed-namespaces: "default, staging" 63 | spec: 64 | hostname: postgres.svc 65 | port: 5432 66 | username: postgres 67 | passwordSecretRef: 68 | name: server-credentials 69 | key: password 70 | ``` 71 | 72 | The following resource can be referenced by `PostgresDatabase` resources in 73 | namespace `staging`, any namespace starting with `test-` and any namespace 74 | ending in `-alpha`. 75 | 76 | ```yaml 77 | apiVersion: kompost.chuge.li/v1alpha1 78 | kind: PostgresClusterInstance 79 | metadata: 80 | name: staging-server 81 | namespace: default 82 | annotations: 83 | kompost.chuge.li/allowed-namespaces: "staging, test-.*, .*-alpha" 84 | spec: 85 | hostname: postgres.svc 86 | port: 5432 87 | username: postgres 88 | passwordSecretRef: 89 | name: server-credentials 90 | key: password 91 | ``` 92 | 93 | ## Referencing Cluster Intances 94 | 95 | When declaring the `PostgresDatabase` resource, use the field `.spec.clusterInstanceRef` 96 | to reference a cluster instance: 97 | 98 | ```yaml 99 | apiVersion: kompost.chuge.li/v1alpha1 100 | kind: PostgresDatabase 101 | metadata: 102 | name: staging-server 103 | namespace: default 104 | spec: 105 | clusterInstanceRef: 106 | name: app-database 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/postgres/postgres_database.md: -------------------------------------------------------------------------------- 1 | # The `PostgresDatabase` Resource 2 | 3 | The `PostgresDatabase` resource defines a database on your Postgres server. 4 | By creating this resource on the cluster, you're telling Kompost to ensure 5 | such a database exists on the server. 6 | 7 | ## Basic Usage 8 | 9 | Once your [`PostgresInstance`](postgres_instance.md) or 10 | [`PostgresClusterInstance`](postgres_cluster_instance.md) is set up and 11 | connected, you can declare your database resources. 12 | 13 | ```yaml 14 | apiVersion: kompost.chuge.li/v1alpha1 15 | kind: PostgresDatabase 16 | metadata: 17 | name: some-database 18 | namespace: default 19 | spec: 20 | instanceRef: 21 | name: staging-server 22 | ``` 23 | 24 | ## Connection Details 25 | 26 | Once applied to the cluster, Kompost creates a database and **two users**. One 27 | is called `inspector` having read-only access to the database, a second is one 28 | called `app` with read-write access to it. For each user, a secret is created 29 | which can be used in your deployments to pass the connection details to your 30 | application. Check the resource status to see the usernames created and the 31 | names of their secrets. 32 | 33 | The name of the users and the name of the secrets holding the information can be 34 | taken from the `PostgresDatabase` resource's status: 35 | 36 | ```bash 37 | kubectl get pgdb -n default some-database -o jsonpath="{.status.users}" |jq 38 | [ 39 | { 40 | "secret": "psql-some-database-inspector", 41 | "username": "default_some_database_inspector" 42 | }, 43 | { 44 | "secret": "psql-some-database-app", 45 | "username": "default_some_database_app" 46 | } 47 | ] 48 | ``` 49 | 50 | The generated secrets hold all the information your apps require to connect to the database. They look something like this: 51 | 52 | ```yaml 53 | apiVersion: v1 54 | kind: Secret 55 | metadata: 56 | name: psql-some-database-app 57 | namespace: default 58 | type: Opaque 59 | data: 60 | DB_HOST: cG9zdGdyZXMuc3Zj 61 | DB_NAME: ZGVmYXVsdF9zb21lX2RhdGFiYXNl 62 | DB_PASS: cGFzc3dvcmQ= 63 | DB_PORT: MzE0MzY= 64 | DB_SSL: ZmFsc2U= 65 | DB_SSL_VERIFY: ZmFsc2U= 66 | DB_USER: ZGVmYXVsdF9zb21lX2RhdGFiYXNlX2FwcA== 67 | ``` 68 | 69 | ## Database Naming Strategy 70 | 71 | The field `databaseNamingStrategy` controls how the database name is derived 72 | from the resource name. Possible values are `resource_name` and 73 | `prefix_namespace`. The default strategy is `prefix_namespace`. 74 | 75 | ### Strategies 76 | 77 | - `resource_name`: Use the resource name as database name 78 | - `prefix_namespace`: Prefix the resource name with the namespace to get a 79 | cluster-wide unique name. 80 | 81 | ### Example 82 | 83 | Taking the [basic usage example](#basic-usage) from above, the resulting 84 | database on the server is named `default_some_database`. 85 | 86 | ```yaml 87 | apiVersion: kompost.chuge.li/v1alpha1 88 | kind: PostgresDatabase 89 | metadata: 90 | name: some-database 91 | namespace: default 92 | spec: 93 | instanceRef: 94 | name: staging-server 95 | databaseNamingStrategy: resource_name 96 | ``` 97 | 98 | The resulting database on the server is named `some_database`. 99 | 100 | !!! warning Strategy resource_name can lead to conflicts 101 | 102 | Using the `resource_name` strategy can lead to conflicts. If you define 103 | `PostgresDatabase` resources with the same name in different namespaces, 104 | both resources would control the same database on the server. Therefore the 105 | default strategy is `prefix_namespace` 106 | 107 | ## Deletion Policy - Abandoning Underlying Resources 108 | 109 | When using Kompost on a live environment, you might want to protect the 110 | underlying resources (i.e. the databases, users, etc.) from accidental deletion 111 | if the Kubernetes resource gets deleted. That's the purpose of the 112 | `kompost.chuge.li/deletion-policy` annotation. When set to `abandon`, it prevents 113 | Kompost form adding the finalizers to your resource. 114 | 115 | ```yaml 116 | apiVersion: kompost.chuge.li/v1alpha1 117 | kind: PostgresDatabase 118 | metadata: 119 | name: some-database 120 | namespace: default 121 | annotations: 122 | kompost.chuge.li/deletion-policy: abandon # <-- underlying resources are abandoned (not deleted) when this resource gets deleted 123 | spec: 124 | instanceRef: 125 | name: staging-server 126 | ``` 127 | 128 | ## Database Creation Parameters 129 | 130 | Postgres takes [optional 131 | parameters](https://www.postgresql.org/docs/current/sql-createdatabase.html) 132 | when creating a database. Some of these parameters are supported by Kompost and 133 | can be passed in `spec.params`. 134 | 135 | !!! note Creation params cannot be changed 136 | 137 | These parameters are only used when the database is created. Kompost therefore denies requests to change them on an existing resource. 138 | 139 | The currently supported parameters are: 140 | 141 | - `template` 142 | - `encoding` 143 | - `locale` 144 | - `lc_collate` 145 | - `lc_ctype` 146 | - `connection_limit` 147 | - `is_template` 148 | 149 | ```yaml 150 | apiVersion: kompost.chuge.li/v1alpha1 151 | kind: PostgresDatabase 152 | metadata: 153 | name: some-database 154 | namespace: default 155 | spec: 156 | instanceRef: 157 | name: staging-server 158 | params: 159 | locale: en_US.UTF8 160 | connection_limit: 100 161 | ``` 162 | -------------------------------------------------------------------------------- /docs/temporal/index.md: -------------------------------------------------------------------------------- 1 | # Temporal Kompo 2 | 3 | The Temporal Kompo manages resources inside your [Temporal](https://temporal.io/) servers. 4 | 5 | The Temporal Kompos is still being developed. There's no documentation yet. 6 | -------------------------------------------------------------------------------- /lib/kompost.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost do 2 | @moduledoc false 3 | end 4 | -------------------------------------------------------------------------------- /lib/kompost/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Application do 2 | @moduledoc false 3 | 4 | @spec bandit(atom()) :: {module(), keyword()} 5 | defp bandit(:prod) do 6 | {Bandit, 7 | plug: Kompost.Webhooks.Router, 8 | port: 4000, 9 | certfile: "/mnt/cert/tls.crt", 10 | keyfile: "/mnt/cert/tls.key", 11 | scheme: :https} 12 | end 13 | 14 | defp bandit(_) do 15 | {Bandit, plug: Kompost.Webhooks.Router, port: 4000, scheme: :http} 16 | end 17 | 18 | use Application 19 | @impl true 20 | def start(_type, env: env) do 21 | children = [bandit(env) | kompos(env)] 22 | opts = [strategy: :one_for_one, name: Kompost.Supervisor] 23 | Supervisor.start_link(children, opts) 24 | end 25 | 26 | @spec kompos(atom()) :: list({module(), term()}) 27 | defp kompos(env) do 28 | env 29 | |> Kompost.Kompo.get_enabled_kompos() 30 | |> Enum.map(fn 31 | :postgres -> 32 | {Kompost.Kompo.Postgres.Supervisor, operator_args: [conn: conn(env)]} 33 | 34 | :temporal -> 35 | {Kompost.Kompo.Temporal.Supervisor, operator_args: [conn: conn(env)]} 36 | end) 37 | end 38 | 39 | @spec conn(atom()) :: K8s.Conn.t() 40 | defp conn(env), do: Kompost.K8sConn.get!(env) 41 | end 42 | -------------------------------------------------------------------------------- /lib/kompost/k8s_conn.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.K8sConn do 2 | @moduledoc """ 3 | Initializes the %K8s.Conn{} struct depending on the mix environment. To be used in config.exs (bonny.exs): 4 | 5 | ``` 6 | # Function to call to get a K8s.Conn object. 7 | # The function should return a %K8s.Conn{} struct or a {:ok, %K8s.Conn{}} tuple 8 | get_conn: {MichiOperator.K8sConn, :get, [config_env()]}, 9 | ``` 10 | """ 11 | 12 | @spec get!(env :: atom()) :: K8s.Conn.t() 13 | def get!(:dev) do 14 | {:ok, conn} = 15 | "KUBECONFIG" 16 | |> System.get_env("./test/integration/kubeconfig-dev.yaml") 17 | |> K8s.Conn.from_file() 18 | 19 | conn 20 | end 21 | 22 | def get!(:test) do 23 | {:ok, conn} = 24 | "KUBECONFIG" 25 | |> System.get_env("./test/integration/kubeconfig-test.yaml") 26 | |> K8s.Conn.from_file() 27 | 28 | conn 29 | end 30 | 31 | def get!(_) do 32 | kubeconfig = System.get_env("KUBECONFIG") 33 | 34 | {:ok, conn} = 35 | if not is_nil(kubeconfig) and File.exists?(kubeconfig) do 36 | K8s.Conn.from_file(kubeconfig) 37 | else 38 | K8s.Conn.from_service_account() 39 | end 40 | 41 | conn 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/kompost/kompo.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo do 2 | @moduledoc """ 3 | Helpers around Kompos 4 | """ 5 | 6 | @dev_kompos [:postgres, :temporal] 7 | 8 | @spec get_enabled_kompos(atom()) :: list({module(), term()}) 9 | def get_enabled_kompos(env) when env in [:dev, :test] do 10 | @dev_kompos 11 | end 12 | 13 | def get_enabled_kompos(_env) do 14 | config = Application.get_env(:kompost, __MODULE__, %{}) 15 | Enum.filter(@dev_kompos, &config[&1]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/controller/instance_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Controller.InstanceController do 2 | use Bonny.ControllerV2 3 | 4 | require Logger 5 | 6 | alias Kompost.Kompo.Postgres.Instance 7 | 8 | alias Kompost.Tools.NamespaceAccess 9 | 10 | step Bonny.Pluggable.SkipObservedGenerations 11 | 12 | step Bonny.Pluggable.Finalizer, 13 | id: "kompost.chuge.li/databases", 14 | impl: &__MODULE__.check_for_depending_databases/1, 15 | add_to_resource: true, 16 | log_level: :debug 17 | 18 | step Kompost.Pluggable.InitConditions, conditions: ["Credentials", "Connected", "Privileged"] 19 | step :handle_event 20 | 21 | @impl true 22 | def rbac_rules() do 23 | [to_rbac_rule({"", "secrets", ["get", "list"]})] 24 | end 25 | 26 | @spec handle_event(Bonny.Axn.t(), Keyword.t()) :: Bonny.Axn.t() 27 | def handle_event(%Bonny.Axn{action: action} = axn, _opts) 28 | when action in [:add, :modify, :reconcile] do 29 | id = instance_id(axn.resource) 30 | allowed_ns = allowed_namespaces(axn.resource) 31 | 32 | with {:cred, {:ok, connection_args}} <- {:cred, get_connection_args(axn.resource, axn.conn)}, 33 | axn <- set_condition(axn, "Credentials", true), 34 | {:conn, axn, {:ok, conn}} <- 35 | {:conn, axn, Instance.connect(id, connection_args, allowed_ns)}, 36 | axn <- set_condition(axn, "Connected", true, "Connection to database was established"), 37 | {:privileges, axn, :ok} <- {:privileges, axn, Instance.check_privileges(conn)}, 38 | axn <- 39 | set_condition(axn, "Privileged", true, "The conneted user has the required privileges") do 40 | success_event(axn) 41 | else 42 | {:cred, {:error, error}} -> 43 | Logger.warning("Error when trying to fetch password.", error: error) 44 | 45 | axn 46 | |> failure_event(message: Exception.message(error)) 47 | |> set_condition("Credentials", false, Exception.message(error)) 48 | 49 | {:conn, axn, {:error, error}} -> 50 | message = if is_exception(error), do: Exception.message(error), else: inspect(error) 51 | Logger.warning("#{axn.action} failed. #{message}") 52 | 53 | axn 54 | |> failure_event(message: message) 55 | |> set_condition("Connected", false, message) 56 | 57 | {:privileges, axn, {:error, message}} -> 58 | Logger.warning("#{axn.action} failed. #{message}") 59 | 60 | axn 61 | |> failure_event(message: message) 62 | |> set_condition("Privileged", false, message) 63 | end 64 | end 65 | 66 | def handle_event(%Bonny.Axn{action: :delete} = axn, _opts) do 67 | :ok = 68 | axn.resource 69 | |> instance_id() 70 | |> Instance.disconnect() 71 | 72 | success_event(axn) 73 | end 74 | 75 | @spec get_connection_args(map(), K8s.Conn.t()) :: 76 | {:ok, [Postgrex.start_option()]} | {:error, K8s.Client.Runner.Base.error_t()} 77 | defp get_connection_args(%{"spec" => %{"plainPassword" => _} = spec}, _conn) do 78 | cacerts = 79 | case spec["ssl"]["ca"] do 80 | nil -> 81 | :public_key.cacerts_get() 82 | 83 | cacert -> 84 | cacert 85 | |> :public_key.pem_decode() 86 | |> Enum.map(fn {_, der, _} -> der end) 87 | end 88 | 89 | {:ok, 90 | [ 91 | hostname: spec["hostname"], 92 | port: spec["port"], 93 | username: spec["username"], 94 | password: spec["plainPassword"], 95 | database: "postgres", 96 | ssl: spec["ssl"]["enabled"] || false, 97 | ssl_opts: [ 98 | verify: (spec["ssl"]["verify"] || "verify_none") |> String.to_atom(), 99 | cacerts: cacerts, 100 | server_name_indication: String.to_charlist(spec["hostname"]) 101 | ] 102 | ]} 103 | end 104 | 105 | defp get_connection_args(instance, conn) do 106 | %{ 107 | "spec" => %{"passwordSecretRef" => %{"name" => secret_name, "key" => key}} 108 | } = instance 109 | 110 | with {:ok, secret} <- 111 | K8s.Client.get("v1", "Secret", 112 | name: secret_name, 113 | namespace: 114 | instance["metadata"]["namespace"] || 115 | Application.fetch_env!(:kompost, :operator_namespace) 116 | ) 117 | |> K8s.Client.put_conn(conn) 118 | |> K8s.Client.run() do 119 | password = Base.decode64!(secret["data"][key]) 120 | instance = put_in(instance, ~w(spec plainPassword), password) 121 | get_connection_args(instance, conn) 122 | end 123 | end 124 | 125 | @doc """ 126 | A finalizer preventing the deletion of an instance if there are database 127 | resources in the cluster which still depend on it. 128 | """ 129 | @spec check_for_depending_databases(Bonny.Axn.t()) :: 130 | {:ok, Bonny.Axn.t()} | {:error, Bonny.Axn.t()} 131 | def check_for_depending_databases(%Bonny.Axn{resource: resource, conn: conn} = axn) do 132 | {:ok, result} = 133 | K8s.Client.list("kompost.chuge.li/v1alpha1", "PostgresDatabase", namespace: :all) 134 | |> K8s.Client.put_conn(conn) 135 | |> K8s.Client.run() 136 | 137 | if Enum.any?( 138 | result["items"], 139 | &(is_nil(&1["metadata"]["deletionTimestamp"]) && 140 | &1["spec"]["instanceRef"]["name"] == resource["metadata"]["name"] and 141 | &1["spec"]["instanceRef"]["namespace"] == resource["metadata"]["namespace"]) 142 | ) do 143 | {:error, axn} 144 | else 145 | {:ok, axn} 146 | end 147 | end 148 | 149 | @spec instance_id(resource :: map()) :: Instance.id() 150 | defp instance_id(%{"kind" => "PostgresInstance", "metadata" => metadata}), 151 | do: {metadata["namespace"], metadata["name"]} 152 | 153 | defp instance_id(%{"kind" => "PostgresClusterInstance", "metadata" => metadata}), 154 | do: {:cluster, metadata["name"]} 155 | 156 | @spec allowed_namespaces(map()) :: NamespaceAccess.allowed_namespaces() 157 | defp(allowed_namespaces(%{"kind" => "PostgresInstance"} = resource)) do 158 | [~r/^#{resource["metadata"]["namespace"]}$/] 159 | end 160 | 161 | defp allowed_namespaces(%{"kind" => "PostgresClusterInstance"} = resource) do 162 | NamespaceAccess.allowed_namespaces!(resource) 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Database do 2 | @moduledoc """ 3 | Connector to operate on Postgres databases via Postgrex. 4 | """ 5 | 6 | alias Kompost.Kompo.Postgres.Database.Params 7 | 8 | @doc """ 9 | Creates a Postgres safe db name from a resource. 10 | 11 | ### Example 12 | 13 | iex> resource = %{"metadata" => %{"namespace" => "default", "name" => "foo-bar"}} 14 | ...> Kompost.Kompo.Postgres.Database.name(resource) 15 | "default_foo_bar" 16 | 17 | iex> resource = %{"metadata" => %{"namespace" => "default", "name" => "foo-bar"}} 18 | ...> Kompost.Kompo.Postgres.Database.name(resource, strategy: "resource_name") 19 | "foo_bar" 20 | """ 21 | @spec name(map(), Keyword.t()) :: binary() 22 | def name(resource, opts \\ []) 23 | 24 | def name(resource, opts) do 25 | case Keyword.get(opts, :strategy, "prefix_namespace") do 26 | "resource_name" -> 27 | Slugger.slugify_downcase( 28 | "#{resource["metadata"]["name"]}", 29 | ?_ 30 | ) 31 | 32 | "prefix_namespace" -> 33 | Slugger.slugify_downcase( 34 | "#{resource["metadata"]["namespace"]}_#{resource["metadata"]["name"]}", 35 | ?_ 36 | ) 37 | end 38 | end 39 | 40 | @spec apply(db_name :: binary(), db_params :: Params.t(), Postgrex.conn()) :: 41 | :ok | {:error, message :: binary()} 42 | def apply(db_name, db_params, conn) do 43 | if exists?(db_name, conn), do: :ok, else: create(db_name, db_params, conn) 44 | end 45 | 46 | @spec create(db_name :: binary(), db_params :: Params.t(), Postgrex.conn()) :: 47 | :ok | {:error, message :: binary()} 48 | defp create(db_name, db_params, conn) do 49 | case Postgrex.query(conn, ~s(CREATE DATABASE "#{db_name}" #{Params.render(db_params)}), []) do 50 | {:ok, %Postgrex.Result{}} -> 51 | :ok 52 | 53 | {:error, exception} when is_exception(exception) -> 54 | {:error, Exception.message(exception)} 55 | end 56 | end 57 | 58 | @spec exists?(db_name :: binary(), Postgrex.conn()) :: boolean() 59 | defp exists?(db_name, conn) do 60 | result = Postgrex.query!(conn, ~s(SELECT 1 FROM pg_database WHERE datname='#{db_name}'), []) 61 | result.num_rows == 1 62 | end 63 | 64 | @spec drop(db_name :: binary(), Postgrex.conn()) :: :ok | :error 65 | def drop(db_name, conn) do 66 | with {:ok, %Postgrex.Result{}} <- 67 | Postgrex.query(conn, ~s(REVOKE CONNECT ON DATABASE "#{db_name}" FROM public), []), 68 | {:ok, %Postgrex.Result{}} <- 69 | Postgrex.query( 70 | conn, 71 | ~s|SELECT pid, pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '#{db_name}' AND pid <> pg_backend_pid()|, 72 | [] 73 | ), 74 | {:ok, %Postgrex.Result{}} <- Postgrex.query(conn, ~s(DROP DATABASE "#{db_name}"), []) do 75 | :ok 76 | else 77 | {:error, exception} when is_exception(exception) -> 78 | {:error, Exception.message(exception)} 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/database/params.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Database.Params do 2 | @moduledoc """ 3 | Database creation parameters. 4 | See PostgreSQL documentation: https://www.postgresql.org/docs/current/sql-createdatabase.html 5 | """ 6 | 7 | defstruct [ 8 | :template, 9 | :encoding, 10 | :locale, 11 | :lc_collate, 12 | :lc_ctype, 13 | :connection_limit, 14 | :is_template 15 | ] 16 | 17 | @type t :: %__MODULE__{ 18 | template: binary(), 19 | encoding: binary(), 20 | locale: binary() | integer(), 21 | lc_collate: binary(), 22 | lc_ctype: binary(), 23 | connection_limit: non_neg_integer(), 24 | is_template: boolean() 25 | } 26 | 27 | @spec new!(map()) :: t() 28 | def new!(params) do 29 | params 30 | |> Map.new(fn {key, value} -> {String.to_existing_atom(key), value} end) 31 | |> then(&struct(__MODULE__, &1)) 32 | end 33 | 34 | @spec render(t()) :: iodata() 35 | def render(params) do 36 | params 37 | |> Map.from_struct() 38 | |> Enum.reject(&is_nil(elem(&1, 1))) 39 | |> Enum.map(fn {key, value} -> render_param(key, value) end) 40 | end 41 | 42 | @spec render_param(atom(), term()) :: iodata() 43 | defp render_param(:template, value), do: [" TEMPLATE '", value, "'"] 44 | defp render_param(:encoding, value), do: [" ENCODING '", value, "'"] 45 | defp render_param(:locale, value) when is_binary(value), do: [" LOCALE '", value, "'"] 46 | 47 | defp render_param(:locale, value) when is_integer(value), 48 | do: [" LOCALE ", Integer.to_string(value)] 49 | 50 | defp render_param(:lc_collate, value), do: [" LC_COLLATE '", value, "'"] 51 | defp render_param(:lc_ctype, value), do: [" LC_CTYPE '", value, "'"] 52 | 53 | defp render_param(:connection_limit, value), 54 | do: [" CONNECTION LIMIT ", Integer.to_string(value)] 55 | 56 | defp render_param(:is_template, true), do: [" IS_TEMPLATE TRUE"] 57 | defp render_param(:is_template, false), do: [" IS_TEMPLATE FALSE"] 58 | end 59 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/instance.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Instance do 2 | @moduledoc """ 3 | Provides an interface to Postgrex. `connect/2` starts Postgrex under 4 | a dynamic supervisor (`Kompost.Kompo.Postgres.ConnectionSupervisor`) and 5 | register the process at the `Kompost.Kompo.Postgres.ConnectionRegistry` 6 | """ 7 | 8 | alias Kompost.Kompo.Postgres.ConnectionRegistry 9 | alias Kompost.Kompo.Postgres.ConnectionSupervisor 10 | alias Kompost.Kompo.Postgres.Privileges 11 | 12 | alias Kompost.Tools.NamespaceAccess 13 | 14 | @typedoc """ 15 | Defines the ID for an instance. The ID used as key when registering the 16 | Postgrex process at the `Kompost.Kompo.Postgres.ConnectionRegistry` 17 | """ 18 | @type id :: {namespace :: binary() | :cluster, name :: binary()} 19 | 20 | @doc """ 21 | Checks in the `Kompost.Kompo.Postgres.ConnectionRegistry` for an existing 22 | connection defined by the given `id`. If no such connection exists, a new 23 | `Postgrex` process is started, connecting to a Postgres instance defined by 24 | the `conn_args`. The process is then registered at the 25 | `Kompost.Kompo.Postgres.ConnectionRegistry`. 26 | """ 27 | @spec connect( 28 | id(), 29 | conn_args :: Keyword.t(), 30 | allowed_namespaces :: NamespaceAccess.allowed_namespaces() 31 | ) :: 32 | {:ok, Postgrex.conn()} 33 | | {:error, Postgrex.Error.t() | Exception.t()} 34 | | DynamicSupervisor.on_start_child() 35 | def connect(id, conn_args, allowed_namespaces) do 36 | with {:lookup, []} <- {:lookup, lookup(id)}, 37 | {:ok, _} <- 38 | conn_args 39 | |> Postgrex.Utils.default_opts() 40 | |> Postgrex.Protocol.connect() do 41 | args = 42 | Keyword.put( 43 | conn_args, 44 | :name, 45 | {:via, Registry, 46 | {ConnectionRegistry, id, 47 | Keyword.put(conn_args, :allowed_namespaces, allowed_namespaces)}} 48 | ) 49 | 50 | DynamicSupervisor.start_child(ConnectionSupervisor, {Postgrex, args}) 51 | else 52 | {:lookup, [{conn, _, _}]} -> {:ok, conn} 53 | connection_error -> connection_error 54 | end 55 | end 56 | 57 | @doc """ 58 | Checks in the `Kompost.Kompo.Postgres.ConnectionRegistry` for an existing 59 | connection defined by the given `id`. 60 | """ 61 | @spec lookup(id()) :: [ 62 | {pid, conn_args :: keyword(), 63 | allowed_namespaces :: NamespaceAccess.allowed_namespaces()} 64 | ] 65 | def lookup(id) do 66 | Registry.lookup(ConnectionRegistry, id) 67 | |> Enum.map(fn {pid, args} -> 68 | {allowed_namespaces, conn_args} = Keyword.pop!(args, :allowed_namespaces) 69 | {pid, conn_args, allowed_namespaces} 70 | end) 71 | end 72 | 73 | @doc """ 74 | Checks if the user connected to the given `conn` has all the privileges 75 | required to work with this `Kompo`. 76 | """ 77 | @spec check_privileges(conn :: Postgrex.conn()) :: :ok | {:error, binary()} 78 | def check_privileges(conn) do 79 | with :ok <- Privileges.check_create_role_privilege(conn) do 80 | Privileges.check_create_database_privilege(conn) 81 | end 82 | end 83 | 84 | @doc """ 85 | Disconnects the connection with the given `id` by stopping the referenced 86 | `Postgrex` process. 87 | """ 88 | @spec disconnect(id()) :: :ok 89 | def disconnect(id) do 90 | case lookup(id) do 91 | [{conn, _, _}] -> 92 | # This will also unregister the process at the ConnectionRegistry: 93 | DynamicSupervisor.terminate_child(ConnectionSupervisor, conn) 94 | :ok 95 | 96 | [] -> 97 | :ok 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/operator.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Operator do 2 | @moduledoc """ 3 | This operator handles resources regarding postgres instances and databases. 4 | """ 5 | 6 | use Bonny.Operator, default_watch_namespace: :all 7 | 8 | alias Kompost.Kompo.Postgres.Controller 9 | alias Kompost.Kompo.Postgres.V1Alpha1 10 | 11 | step(Bonny.Pluggable.Logger, level: :info) 12 | step(:delegate_to_controller) 13 | step(Bonny.Pluggable.ApplyStatus) 14 | step(Bonny.Pluggable.ApplyDescendants) 15 | 16 | @impl Bonny.Operator 17 | def controllers(watching_namespace, _opts) do 18 | [ 19 | %{ 20 | query: 21 | K8s.Client.watch("kompost.chuge.li/v1alpha1", "PostgresInstance", 22 | namespace: watching_namespace 23 | ), 24 | controller: Controller.InstanceController 25 | }, 26 | %{ 27 | query: K8s.Client.watch("kompost.chuge.li/v1alpha1", "PostgresClusterInstance"), 28 | controller: Controller.InstanceController 29 | }, 30 | %{ 31 | query: 32 | K8s.Client.watch("kompost.chuge.li/v1alpha1", "PostgresDatabase", 33 | namespace: watching_namespace 34 | ), 35 | controller: Controller.DatabaseController 36 | } 37 | ] 38 | end 39 | 40 | @impl Bonny.Operator 41 | def crds() do 42 | [ 43 | %Bonny.API.CRD{ 44 | group: "kompost.chuge.li", 45 | scope: :Namespaced, 46 | names: Bonny.API.CRD.kind_to_names("PostgresInstance", ["pginst"]), 47 | versions: [V1Alpha1.PostgresInstance] 48 | }, 49 | %Bonny.API.CRD{ 50 | group: "kompost.chuge.li", 51 | scope: :Cluster, 52 | names: Bonny.API.CRD.kind_to_names("PostgresClusterInstance", ["pgcinst"]), 53 | versions: [V1Alpha1.PostgresInstance] 54 | }, 55 | %Bonny.API.CRD{ 56 | group: "kompost.chuge.li", 57 | scope: :Namespaced, 58 | names: Bonny.API.CRD.kind_to_names("PostgresDatabase", ["pgdb"]), 59 | versions: [V1Alpha1.PostgresDatabase] 60 | } 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Supervisor do 2 | @moduledoc """ 3 | Supervisor for the Postgres Kompo. 4 | """ 5 | 6 | use Supervisor 7 | 8 | alias Kompost.Kompo.Postgres.{ 9 | ConnectionRegistry, 10 | ConnectionSupervisor, 11 | Operator 12 | } 13 | 14 | @spec start_link(Keyword.t()) :: Supervisor.on_start() 15 | def start_link(init_args) do 16 | Supervisor.start_link(__MODULE__, init_args, name: __MODULE__) 17 | end 18 | 19 | @impl true 20 | def init(operator_args: operator_args) do 21 | children = [ 22 | {Registry, keys: :unique, name: ConnectionRegistry}, 23 | {DynamicSupervisor, name: ConnectionSupervisor}, 24 | {Operator, Keyword.put(operator_args, :name, Operator)} 25 | ] 26 | 27 | Supervisor.init(children, strategy: :one_for_one) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.User do 2 | @moduledoc """ 3 | Connector to operate on Postgres users/roles via Postgrex. 4 | """ 5 | 6 | @doc """ 7 | Creates a user if it does not exist. The user created on the instance will be 8 | named `db_name`_`username` in order for the username to be unique. 9 | """ 10 | @spec apply(username :: binary(), Postgrex.conn(), db_name :: binary(), password :: binary()) :: 11 | {:ok, map()} | {:error, Exception.t()} 12 | def apply(username, conn, db_name, password) do 13 | db_username = Slugger.slugify_downcase("#{db_name}_#{username}", ?_) 14 | op = if exists?(db_username, conn), do: "ALTER", else: "CREATE" 15 | 16 | case Postgrex.query( 17 | conn, 18 | ~s(#{op} ROLE "#{db_username}" WITH PASSWORD '#{password}' NOCREATEDB NOCREATEROLE NOINHERIT LOGIN), 19 | [] 20 | ) do 21 | {:ok, %Postgrex.Result{}} -> 22 | {:ok, %{DB_USER: db_username, DB_PASS: password, DB_NAME: db_name}} 23 | 24 | {:error, exception} when is_exception(exception) -> 25 | Postgrex.query(conn, ~s(DROP ROLE "#{db_username}" IF EXISTS), []) 26 | {:error, ~s(User "#{db_username}" could not be applied: #{Exception.message(exception)})} 27 | end 28 | end 29 | 30 | @spec exists?(db_username :: binary(), conn :: Postgrex.conn()) :: boolean() 31 | defp exists?(db_username, conn) do 32 | result = Postgrex.query!(conn, "SELECT 1 FROM pg_roles WHERE rolname=$1", [db_username]) 33 | result.num_rows == 1 34 | end 35 | 36 | @doc """ 37 | Transfers all resources owned by the given `user` to the `new_owner` and drops 38 | the `user` thereafter. The `new_owner` has to exist on the instance prior to 39 | calling this function. 40 | """ 41 | @spec drop(user :: binary(), new_owner :: binary(), Postgrex.conn()) :: 42 | :ok | {:error, message :: binary()} 43 | def drop(user, new_owner, conn) do 44 | Postgrex.transaction(conn, fn trx_conn -> 45 | with {:ok, %Postgrex.Result{}} <- 46 | Postgrex.query(trx_conn, ~s(REASSIGN OWNED BY "#{user}" TO "#{new_owner}"), []), 47 | {:ok, %Postgrex.Result{}} <- Postgrex.query(trx_conn, ~s(DROP ROLE "#{user}"), []) do 48 | :ok 49 | else 50 | {:error, exception} when is_exception(exception) -> 51 | {:error, Exception.message(exception)} 52 | end 53 | end) 54 | |> process_trx_result() 55 | end 56 | 57 | @spec process_trx_result({:ok, :ok} | term()) :: :ok | term() 58 | defp process_trx_result({:ok, :ok}), do: :ok 59 | defp process_trx_result(error), do: error 60 | end 61 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/v1alpha1/postgres_database.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.V1Alpha1.PostgresDatabase do 2 | @moduledoc """ 3 | Postgres Database CRD v1alpha1 version. 4 | """ 5 | 6 | import YamlElixir.Sigil 7 | 8 | use Bonny.API.Version, 9 | hub: true 10 | 11 | @impl true 12 | def manifest() do 13 | struct!( 14 | defaults(), 15 | name: "v1alpha1", 16 | schema: ~y""" 17 | :openAPIV3Schema: 18 | :type: object 19 | :required: ["spec"] 20 | :properties: 21 | :spec: 22 | type: object 23 | anyOf: 24 | - required: ["instanceRef"] 25 | - required: ["clusterInstanceRef"] 26 | properties: 27 | instanceRef: 28 | type: object 29 | properties: 30 | name: 31 | type: string 32 | clusterInstanceRef: 33 | type: object 34 | properties: 35 | name: 36 | type: string 37 | databaseNamingStrategy: 38 | type: string 39 | enum: ["resource_name", "prefix_namespace"] 40 | description: "(Optional) Strategy to derive the name of the `database` on the server. resource_name uses the resource name as DB name. `prefix_namespace` prefixes the resource name with the namespace. Defaults to `prefix_namespace`" 41 | params: 42 | description: "Parameters passed to CREATE TEMPLATE." 43 | type: object 44 | properties: 45 | template: 46 | description: "(Optional) The name of the template from which to create the new database." 47 | type: string 48 | encoding: 49 | description: "(Optional) Character set encoding to use in the new database. Specify a string constant (e.g., 'SQL_ASCII'), or an integer encoding number." 50 | x-kubernetes-int-or-string: true 51 | anyOf: 52 | - type: integer 53 | - type: string 54 | locale: 55 | description: "(Optional) This is a shortcut for setting lc_collate and lc_type at once." 56 | type: string 57 | lc_collate: 58 | description: "(Optional) Collation order (LC_COLLATE) to use in the new database." 59 | type: string 60 | lc_ctype: 61 | description: "(Optional) Character classification (LC_CTYPE) to use in the new database." 62 | type: string 63 | connection_limit: 64 | description: "(Optional) How many concurrent connections can be made to this database. -1 (the default) means no limit." 65 | type: integer 66 | is_template: 67 | description: "(Optional) If true, then this database can be cloned by any user with CREATEDB privileges; if false (the default), then only superusers or the owner of the database can clone it." 68 | type: boolean 69 | :status: 70 | :type: :object 71 | :properties: 72 | sql_db_name: 73 | type: string 74 | users: 75 | type: array 76 | items: 77 | type: object 78 | properties: 79 | username: 80 | type: string 81 | secret: 82 | type: string 83 | """a, 84 | additionalPrinterColumns: [ 85 | %{ 86 | name: "Postgres DB name", 87 | type: "string", 88 | description: "Name of the database on the Postgres instance", 89 | jsonPath: ".status.sql_db_name" 90 | } 91 | ] 92 | ) 93 | |> add_observed_generation_status() 94 | |> add_conditions() 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/v1alpha1/postgres_instance.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.V1Alpha1.PostgresInstance do 2 | @moduledoc """ 3 | Postgres Instance CRD v1alpha1 version. 4 | """ 5 | 6 | import YamlElixir.Sigil 7 | 8 | use Bonny.API.Version, 9 | hub: true 10 | 11 | @impl true 12 | def manifest() do 13 | struct!( 14 | defaults(), 15 | name: "v1alpha1", 16 | schema: ~y""" 17 | :openAPIV3Schema: 18 | :type: object 19 | :required: ["spec"] 20 | :properties: 21 | :spec: 22 | type: object 23 | anyOf: 24 | - required: ["hostname", "port", "username", "passwordSecretRef"] 25 | - required: ["hostname", "port", "username", "plainPassword"] 26 | properties: 27 | hostname: 28 | type: string 29 | port: 30 | type: integer 31 | username: 32 | type: string 33 | passwordSecretRef: 34 | type: object 35 | required: ["name", "key"] 36 | properties: 37 | name: 38 | type: string 39 | key: 40 | type: string 41 | plainPassword: 42 | type: string 43 | description: "It's not safe to save passwords in plaintext. Consider using passwordSecretRef instead." 44 | ssl: 45 | type: object 46 | properties: 47 | enabled: 48 | type: boolean 49 | description: "Set to true if ssl should be used." 50 | verify: 51 | type: string 52 | description: "'verify_none' or 'verify_peer'. Defaults to 'verify_none'" 53 | ca: 54 | type: string 55 | description: "CA certificates used to validate the server cert against." 56 | """a 57 | ) 58 | |> add_observed_generation_status() 59 | |> add_conditions() 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/webhooks/admission_control_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Webhooks.AdmissionControlHandler do 2 | @moduledoc """ 3 | Admission Webhook Handler for Postgres Kompo 4 | """ 5 | alias Kompost.Tools.NamespaceAccess 6 | 7 | use K8sWebhoox.AdmissionControl.Handler 8 | 9 | import K8sWebhoox.AdmissionControl.AdmissionReview 10 | 11 | validate "kompost.chuge.li/v1alpha1/postgresdatabases", conn do 12 | conn 13 | |> check_immutable(["spec", "params", "template"]) 14 | |> check_immutable(["spec", "params", "encoding"]) 15 | |> check_immutable(["spec", "params", "locale"]) 16 | |> check_immutable(["spec", "params", "lc_collate"]) 17 | |> check_immutable(["spec", "params", "lc_ctype"]) 18 | |> check_immutable(["spec", "params", "connection_limit"]) 19 | |> check_immutable(["spec", "params", "is_template"]) 20 | |> check_immutable(["spec", "databaseNamingStrategy"]) 21 | end 22 | 23 | validate "kompost.chuge.li/v1alpha1/postgresclusterinstances", conn do 24 | try do 25 | NamespaceAccess.allowed_namespaces!(conn.request["object"]) 26 | 27 | conn 28 | |> check_allowed_values( 29 | ~w(spec ssl verify), 30 | ~w(verify_none verify_peer), 31 | ".spec.ssl.verify" 32 | ) 33 | |> check_certificate() 34 | catch 35 | %Regex.CompileError{} = error -> 36 | deny( 37 | conn, 38 | ~s(Invalid regular expression in the annotation "kompost.chuge.li/allowed-namespaces": #{Exception.message(error)}) 39 | ) 40 | end 41 | end 42 | 43 | validate "kompost.chuge.li/v1alpha1/postgresinstances", conn do 44 | try do 45 | NamespaceAccess.allowed_namespaces!(conn.request["object"]) 46 | 47 | conn 48 | |> check_allowed_values(~w(spec ssl verify), ~w(verify_none verify_peer), ".spec.verify") 49 | |> check_certificate() 50 | catch 51 | %Regex.CompileError{} = error -> 52 | deny( 53 | conn, 54 | ~s(Invalid regular expression in the annotation "kompost.chuge.li/allowed-namespaces": #{Exception.message(error)}) 55 | ) 56 | end 57 | end 58 | 59 | @spec check_certificate(K8sWebhoox.Conn.t()) :: K8sWebhoox.Conn.t() 60 | defp check_certificate(conn) do 61 | case conn.request["object"]["spec"]["ssl"]["ca"] do 62 | nil -> 63 | conn 64 | 65 | cacert -> 66 | cacert 67 | |> :public_key.pem_decode() 68 | |> Enum.map(fn {_, der, _} -> der end) 69 | |> then(fn 70 | [] -> deny(conn, "The CA certificate you passed in .spec.ssl.ca cannot be parsed.") 71 | _ -> conn 72 | end) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/kompost/kompo/postgres/webhooks/router.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Webhooks.Router do 2 | use Plug.Router 3 | 4 | plug(:match) 5 | plug(:dispatch) 6 | 7 | post("/admission-review/validating", 8 | to: K8sWebhoox.Plug, 9 | init_opts: [ 10 | webhook_handler: 11 | {Kompost.Kompo.Postgres.Webhooks.AdmissionControlHandler, webhook_type: :validating} 12 | ] 13 | ) 14 | end 15 | -------------------------------------------------------------------------------- /lib/kompost/kompo/temporal/api_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Temporal.APIClient do 2 | @moduledoc """ 3 | Connector to Tepmoral's `WorkflowService`. Uses the gRPC API 4 | to apply resources on the Temporal cluster. 5 | """ 6 | 7 | alias Temporal.Api.Operatorservice.V1.DeleteNamespaceResponse 8 | 9 | alias Temporal.Api.Workflowservice.V1.{ 10 | ListNamespacesRequest, 11 | RegisterNamespaceRequest, 12 | RegisterNamespaceResponse, 13 | UpdateNamespaceRequest, 14 | UpdateNamespaceResponse, 15 | WorkflowService 16 | } 17 | 18 | alias Temporal.Api.Namespace.V1.{ 19 | NamespaceConfig, 20 | NamespaceInfo 21 | } 22 | 23 | alias Temporal.Api.Operatorservice.V1.{ 24 | DeleteNamespaceRequest, 25 | DeleteNamespaceResponse, 26 | OperatorService 27 | } 28 | 29 | alias Google.Protobuf.Duration 30 | 31 | @list_ns_req %ListNamespacesRequest{} 32 | 33 | @doc """ 34 | Applies a namespace. 35 | """ 36 | 37 | @spec apply_namespace(GRPC.Channel.t(), name :: String.t(), spec :: map()) :: 38 | {:ok, response :: UpdateNamespaceResponse.t() | RegisterNamespaceResponse.t()} 39 | | {:error, GRPC.RPCError.t()} 40 | def apply_namespace(channel, name, spec) do 41 | with {:list, {:ok, list_ns_resp}} <- 42 | {:list, WorkflowService.Stub.list_namespaces(channel, @list_ns_req)}, 43 | {:ns, namespace} when not is_nil(namespace) <- 44 | {:ns, Enum.find(list_ns_resp.namespaces, &(&1.namespace_info.name == name))} do 45 | update_namespace(channel, name, spec) 46 | else 47 | {:list, {:error, error}} -> {:error, error} 48 | {:ns, nil} -> register_namespace(channel, name, spec) 49 | end 50 | end 51 | 52 | @spec register_namespace(GRPC.Channel.t(), String.t(), map()) :: 53 | {:ok, response :: RegisterNamespaceResponse.t()} | {:error, GRPC.RPCError.t()} 54 | def register_namespace(channel, name, spec) do 55 | request = %RegisterNamespaceRequest{ 56 | namespace: name, 57 | description: spec["description"], 58 | owner_email: spec["ownerEmail"], 59 | workflow_execution_retention_period: %Duration{ 60 | seconds: spec["workflowExecutionRetentionPeriod"] 61 | } 62 | } 63 | 64 | WorkflowService.Stub.register_namespace(channel, request) 65 | end 66 | 67 | @spec update_namespace(GRPC.Channel.t(), String.t(), map()) :: 68 | {:ok, response :: UpdateNamespaceResponse.t()} | {:error, GRPC.RPCError.t()} 69 | def update_namespace(channel, name, spec) do 70 | request = %UpdateNamespaceRequest{ 71 | namespace: name, 72 | update_info: %NamespaceInfo{ 73 | description: spec["description"], 74 | owner_email: spec["ownerEmail"] 75 | }, 76 | config: %NamespaceConfig{ 77 | workflow_execution_retention_ttl: %Duration{ 78 | seconds: spec["workflowExecutionRetentionPeriod"] 79 | } 80 | } 81 | } 82 | 83 | WorkflowService.Stub.update_namespace(channel, request) 84 | end 85 | 86 | @spec delete_namespace(GRPC.Channel.t(), String.t()) :: 87 | {:ok, response :: DeleteNamespaceResponse.t()} | {:error, GRPC.RPCError.t()} 88 | def delete_namespace(channel, name) do 89 | request = %DeleteNamespaceRequest{namespace: name} 90 | OperatorService.Stub.delete_namespace(channel, request) 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/kompost/kompo/temporal/conn.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Temporal.Conn do 2 | @moduledoc """ 3 | Provides an interface to gRPC. `connect/2` establishes a connection with the 4 | gRPC server and stores the channel information in the 5 | `Kompost.Kompo.Temporal.ConnectionRegistry` 6 | """ 7 | 8 | require Logger 9 | 10 | alias Temporal.Api.Workflowservice.V1.{ 11 | ListNamespacesRequest, 12 | WorkflowService 13 | } 14 | 15 | alias Kompost.Kompo.Temporal.ConnectionRegistry 16 | 17 | @typedoc """ 18 | Defines the ID for an instance. The ID is used as key when registering the 19 | Channel info at the `Kompost.Kompo.Temporal.ConnectionRegistry` 20 | """ 21 | @type server_t :: {namespace :: binary(), name :: binary()} 22 | 23 | @doc """ 24 | Checks in the `Kompost.Kompo.Temporal.ConnectionRegistry` for an existing 25 | connection defined by the given `id`. If no such connection exists, a new 26 | connection is established. The returned channel info is then registered at the 27 | `Kompost.Kompo.Temporal.ConnectionRegistry`. 28 | """ 29 | @spec connect(server :: server_t(), addr :: String.t()) :: 30 | {:ok, GRPC.Channel.t()} | {:error, any()} 31 | def connect(server, addr) do 32 | with {:lookup, nil} <- {:lookup, lookup(server)}, 33 | {:ok, channel} <- GRPC.Stub.connect(addr, adapter: GRPC.Client.Adapters.Mint), 34 | :ok <- verify_connection(server, channel) do 35 | Agent.update(ConnectionRegistry, &Map.put(&1, server, channel)) 36 | {:ok, channel} 37 | else 38 | {:lookup, channel} -> {:ok, channel} 39 | :error -> {:error, "Connection failed."} 40 | connection_error -> connection_error 41 | end 42 | end 43 | 44 | @spec verify_connection(server :: server_t(), channel :: GRPC.Channel.t()) :: :ok | :error 45 | defp verify_connection(server, channel) do 46 | case WorkflowService.Stub.list_namespaces(channel, %ListNamespacesRequest{}) do 47 | {:ok, _} -> 48 | :ok 49 | 50 | {:error, %GRPC.RPCError{} = error} -> 51 | Logger.warning( 52 | "Temporal connection #{inspect(server)} was found but connection failed: #{Exception.message(error)}" 53 | ) 54 | 55 | Agent.update(ConnectionRegistry, &Map.delete(&1, server)) 56 | :error 57 | 58 | {:error, error} -> 59 | Logger.warning( 60 | "Temporal connection #{inspect(server)} was found but connection failed: #{inspect(error)}" 61 | ) 62 | 63 | Agent.update(ConnectionRegistry, &Map.delete(&1, server)) 64 | :error 65 | 66 | :error -> 67 | Logger.warning("Temporal connection #{inspect(server)} was not found.") 68 | :error 69 | end 70 | end 71 | 72 | @doc """ 73 | Checks in the `Kompost.Kompo.Temporal.ConnectionRegistry` for an existing 74 | connection defined by the given `id`. If one is found, checks if the 75 | connection is alive by requesting a list of namespaces. 76 | """ 77 | @spec lookup(server_t()) :: GRPC.Channel.t() | nil 78 | def lookup(server) do 79 | with {:find, {:ok, channel}} <- 80 | {:find, Agent.get(ConnectionRegistry, &Map.fetch(&1, server))}, 81 | {:verify, :ok} <- {:verify, verify_connection(server, channel)} do 82 | channel 83 | else 84 | {:find, :error} -> 85 | Logger.warning("Temporal connection #{inspect(server)} was not found.") 86 | nil 87 | 88 | _ -> 89 | nil 90 | end 91 | end 92 | 93 | @doc """ 94 | Creates an instance id tuple from a resource. 95 | 96 | ### Example 97 | 98 | iex> resource = %{"metadata" => %{"namespace" => "default", "name" => "foo-bar"}} 99 | ...> Kompost.Kompo.Postgres.Instance.get_id(resource) 100 | {"default", "foo-bar"} 101 | """ 102 | @spec get_id(resource :: map()) :: server_t() 103 | def get_id(%{"metadata" => metadata}), do: {metadata["namespace"], metadata["name"]} 104 | def get_id(reference), do: {reference["namespace"], reference["name"]} 105 | 106 | @doc """ 107 | Removes the channel from the `ConnectionRegistry`. 108 | """ 109 | @spec disconnect(server_t()) :: :ok 110 | def disconnect(server) do 111 | case Agent.get_and_update(ConnectionRegistry, &Map.pop(&1, server)) do 112 | nil -> 113 | :ok 114 | 115 | channel -> 116 | GRPC.Stub.disconnect(channel) 117 | :ok 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/kompost/kompo/temporal/controller/api_server_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Temporal.Controller.APIServerController do 2 | use Bonny.ControllerV2 3 | 4 | require Logger 5 | 6 | alias Kompost.Kompo.Temporal.Conn 7 | 8 | step Bonny.Pluggable.SkipObservedGenerations 9 | 10 | step Kompost.Pluggable.InitConditions, conditions: ["Connected"] 11 | step :handle_event 12 | 13 | @spec handle_event(Bonny.Axn.t(), Keyword.t()) :: Bonny.Axn.t() 14 | def handle_event(%Bonny.Axn{action: action} = axn, _opts) 15 | when action in [:add, :modify, :reconcile] do 16 | id = Conn.get_id(axn.resource) 17 | addr = "#{axn.resource["spec"]["host"]}:#{axn.resource["spec"]["port"]}" 18 | 19 | case Conn.connect(id, addr) do 20 | {:ok, _} -> 21 | axn 22 | |> success_event(message: "gRPC connection to Temporal established successfully.") 23 | |> set_condition( 24 | "Connected", 25 | true, 26 | "gRPC connection to Temporal established successfully." 27 | ) 28 | 29 | {:error, reason} -> 30 | message = "Could not connect to Temporal cluster: #{reason}" 31 | Logger.warning("#{axn.action} failed. #{message}") 32 | 33 | axn 34 | |> failure_event(message: message) 35 | |> set_condition("Connected", false, message) 36 | end 37 | end 38 | 39 | def handle_event(%Bonny.Axn{action: :delete} = axn, _opts) do 40 | axn.resource 41 | |> Conn.get_id() 42 | |> Conn.disconnect() 43 | 44 | success_event(axn) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/kompost/kompo/temporal/controller/namespace_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Temporal.Controller.NamespaceController do 2 | use Bonny.ControllerV2 3 | 4 | require Logger 5 | 6 | alias Kompost.Kompo.Temporal.APIClient 7 | alias Kompost.Kompo.Temporal.Conn 8 | 9 | step Bonny.Pluggable.SkipObservedGenerations 10 | 11 | step Kompost.Pluggable.InitConditions, conditions: ["Connected", "Created"] 12 | 13 | step Bonny.Pluggable.Finalizer, 14 | id: "kompost.chuge.li/delete-resources", 15 | impl: &__MODULE__.delete_resources/1, 16 | add_to_resource: &__MODULE__.add_finalizer?/1, 17 | log_level: :debug 18 | 19 | step :handle_event 20 | 21 | @spec handle_event(Bonny.Axn.t(), Keyword.t()) :: Bonny.Axn.t() 22 | def handle_event(%Bonny.Axn{action: action} = axn, _opts) 23 | when action in [:add, :modify, :reconcile] do 24 | {api_server_ref, namespace_spec} = Map.pop!(axn.resource["spec"], "apiServerRef") 25 | id = Conn.get_id(api_server_ref) 26 | namespace = axn.resource["metadata"]["namespace"] <> "-" <> axn.resource["metadata"]["name"] 27 | 28 | with {:channel, %GRPC.Channel{} = channel} <- {:channel, Conn.lookup(id)}, 29 | axn <- 30 | set_condition( 31 | axn, 32 | "Connected", 33 | true, 34 | "Connected to the referenced Temporal API Server." 35 | ), 36 | {:namespace, axn, {:ok, _namespace}} <- 37 | {:namespace, axn, APIClient.apply_namespace(channel, namespace, namespace_spec)} do 38 | axn 39 | |> success_event(message: "The namespace was created successfully.") 40 | |> set_condition( 41 | "Created", 42 | true, 43 | "The namespace was created successfully." 44 | ) 45 | else 46 | {:channel, nil} -> 47 | message = "Could not connect to Temporal cluster: No active connection was found" 48 | Logger.warning("#{axn.action} failed. #{message}") 49 | 50 | axn 51 | |> failure_event(message: message) 52 | |> set_condition("Connected", false, message) 53 | 54 | {:namespace, axn, {:error, exception}} when is_exception(exception) -> 55 | message = Exception.message(exception) 56 | Logger.warning("#{axn.action} failed. #{message}") 57 | 58 | axn 59 | |> failure_event(message: message) 60 | |> set_condition("Created", false, message) 61 | end 62 | end 63 | 64 | def handle_event(%Bonny.Axn{action: :delete} = axn, _opts) do 65 | success_event(axn) 66 | end 67 | 68 | @doc """ 69 | Finalizer preventing the deletion of the database resource until underlying 70 | resources on the postgres instance are cleaned up. 71 | """ 72 | @spec delete_resources(Bonny.Axn.t()) :: {:ok, Bonny.Axn.t()} | {:error, Bonny.Axn.t()} 73 | def delete_resources(axn) do 74 | resource = axn.resource 75 | namespace = resource["metadata"]["namespace"] <> "-" <> resource["metadata"]["name"] 76 | id = Conn.get_id(resource["spec"]["apiServerRef"]) 77 | 78 | with {:channel, %GRPC.Channel{} = channel} <- {:channel, Conn.lookup(id)}, 79 | {:namespace, axn, {:ok, _namespace}} <- 80 | {:namespace, axn, APIClient.delete_namespace(channel, namespace)} do 81 | {:ok, axn} 82 | else 83 | {:channel, nil} -> 84 | message = 85 | "The referenced Temporal API Server was not found. We consider the resource gone." 86 | 87 | Logger.warning("Failed to finalize. #{message}") 88 | 89 | {:ok, axn} 90 | 91 | {:namespace, axn, {:error, exception}} when is_exception(exception) -> 92 | message = Exception.message(exception) 93 | Logger.warning("#{axn.action} failed. #{message}") 94 | 95 | axn 96 | |> failure_event(message: message) 97 | |> set_condition("Deleted", false, message) 98 | 99 | {:error, axn} 100 | end 101 | end 102 | 103 | @spec add_finalizer?(Bonny.Axn.t()) :: boolean() 104 | def add_finalizer?(%Bonny.Axn{resource: resource}) do 105 | conditions = 106 | resource 107 | |> get_in([Access.key("status", %{}), Access.key("conditions", [])]) 108 | |> Map.new(&{&1["type"], &1}) 109 | 110 | resource["metadata"]["annotations"]["kompost.chuge.li/deletion-policy"] != "abandon" and 111 | conditions["Connected"]["status"] == "True" 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/kompost/kompo/temporal/operator.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Temporal.Operator do 2 | @moduledoc """ 3 | This operator handles resources in temproal servers 4 | """ 5 | 6 | use Bonny.Operator, default_watch_namespace: :all 7 | 8 | alias Kompost.Kompo.Temporal.Controller 9 | alias Kompost.Kompo.Temporal.V1Alpha1 10 | 11 | step(Bonny.Pluggable.Logger, level: :info) 12 | step(:delegate_to_controller) 13 | step(Bonny.Pluggable.ApplyStatus) 14 | step(Bonny.Pluggable.ApplyDescendants) 15 | 16 | @impl Bonny.Operator 17 | def controllers(watching_namespace, _opts) do 18 | [ 19 | %{ 20 | query: 21 | K8s.Client.watch("kompost.chuge.li/v1alpha1", "TemporalApiServer", 22 | namespace: watching_namespace 23 | ), 24 | controller: Controller.APIServerController 25 | }, 26 | %{ 27 | query: 28 | K8s.Client.watch("kompost.chuge.li/v1alpha1", "TemporalNamespace", 29 | namespace: watching_namespace 30 | ), 31 | controller: Controller.NamespaceController 32 | } 33 | ] 34 | end 35 | 36 | @impl Bonny.Operator 37 | def crds() do 38 | [ 39 | %Bonny.API.CRD{ 40 | group: "kompost.chuge.li", 41 | scope: :Namespaced, 42 | names: Bonny.API.CRD.kind_to_names("TemporalApiServer", ["tmprlas"]), 43 | versions: [V1Alpha1.TemporalApiServer] 44 | }, 45 | %Bonny.API.CRD{ 46 | group: "kompost.chuge.li", 47 | scope: :Namespaced, 48 | names: Bonny.API.CRD.kind_to_names("TemporalNamespace", ["tmprlns"]), 49 | versions: [V1Alpha1.TemporalNamespace] 50 | } 51 | ] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/kompost/kompo/temporal/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Temporal.Supervisor do 2 | @moduledoc """ 3 | Supervisor for the Temporal Kompo. 4 | """ 5 | 6 | use Supervisor 7 | 8 | alias Kompost.Kompo.Temporal.{ 9 | ConnectionRegistry, 10 | Operator 11 | } 12 | 13 | @spec start_link(Keyword.t()) :: Supervisor.on_start() 14 | def start_link(init_args) do 15 | Supervisor.start_link(__MODULE__, init_args, name: __MODULE__) 16 | end 17 | 18 | @impl true 19 | def init(operator_args: operator_args) do 20 | children = [ 21 | %{ 22 | id: ConnectionRegistry, 23 | start: {Agent, :start_link, [fn -> %{} end, [name: ConnectionRegistry]]} 24 | }, 25 | {Operator, Keyword.put(operator_args, :name, Operator)} 26 | ] 27 | 28 | Supervisor.init(children, strategy: :one_for_one) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/kompost/kompo/temporal/v1alpha1/temporal_api_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Temporal.V1Alpha1.TemporalApiServer do 2 | @moduledoc """ 3 | Connection to a Temporal Cluster 4 | """ 5 | 6 | import YamlElixir.Sigil 7 | 8 | use Bonny.API.Version, 9 | hub: true 10 | 11 | @impl true 12 | def manifest() do 13 | struct!( 14 | defaults(), 15 | name: "v1alpha1", 16 | schema: ~y""" 17 | :openAPIV3Schema: 18 | :type: object 19 | :required: ["spec"] 20 | :properties: 21 | :spec: 22 | type: object 23 | anyOf: 24 | - required: ["host", "port"] 25 | - required: ["host", "port"] 26 | properties: 27 | host: 28 | type: string 29 | port: 30 | type: integer 31 | """a 32 | ) 33 | |> add_observed_generation_status() 34 | |> add_conditions() 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/kompost/kompo/temporal/v1alpha1/temporal_namespace.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Temporal.V1Alpha1.TemporalNamespace do 2 | @moduledoc """ 3 | Connection to a Temporal Cluster 4 | """ 5 | 6 | import YamlElixir.Sigil 7 | 8 | use Bonny.API.Version, 9 | hub: true 10 | 11 | @impl true 12 | def manifest() do 13 | struct!( 14 | defaults(), 15 | name: "v1alpha1", 16 | schema: ~y""" 17 | :openAPIV3Schema: 18 | :type: object 19 | :required: ["spec"] 20 | :properties: 21 | :spec: 22 | type: object 23 | required: ["apiServerRef", "description", "workflowExecutionRetentionPeriod"] 24 | properties: 25 | apiServerRef: 26 | description: "Referemce to a resource of kind TemporalApiServer" 27 | type: object 28 | properties: 29 | namespace: 30 | type: string 31 | name: 32 | type: string 33 | description: 34 | type: string 35 | description: "Namespace description" 36 | ownerEmail: 37 | type: string 38 | workflowExecutionRetentionPeriod: 39 | description: "Workflow execution retention period in seconds" 40 | type: integer 41 | minimum: 3600 42 | """a 43 | ) 44 | |> add_observed_generation_status() 45 | |> add_conditions() 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/kompost/pluggable/init_conditions.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Pluggable.InitConditions do 2 | @moduledoc """ 3 | Initializes all conditions passed to `init/1` with a status of "False". 4 | """ 5 | 6 | @behaviour Pluggable 7 | 8 | @impl true 9 | def init(opts), do: Keyword.fetch!(opts, :conditions) 10 | 11 | @impl true 12 | def call(%Bonny.Axn{action: :add} = axn, conditions) do 13 | Enum.reduce(conditions, axn, &Bonny.Axn.set_condition(&2, &1, false)) 14 | end 15 | 16 | def call(axn, _), do: axn 17 | end 18 | -------------------------------------------------------------------------------- /lib/kompost/tools/namespace_access.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Tools.NamespaceAccess do 2 | @moduledoc """ 3 | Module handling access to resources across namespaces. 4 | """ 5 | 6 | @allowed_namespaces_annotation "kompost.chuge.li/allowed-namespaces" 7 | 8 | @type allowed_namespaces :: list(Regex.t()) 9 | 10 | @doc ~S""" 11 | Compiles the given string of comma separated Regex patterns. 12 | 13 | ### Examples 14 | 15 | iex> nil 16 | ...> |> then(&(%{"metadata" => %{"annotations" => %{"kompost.chuge.li/allowed-namespaces" => &1}}})) 17 | ...> |> Kompost.Tools.NamespaceAccess.allowed_namespaces!() 18 | [~r//] 19 | 20 | iex> "default, prefix-[a-z]{3}, ^.*-suffix$" 21 | ...> |> then(&(%{"metadata" => %{"annotations" => %{"kompost.chuge.li/allowed-namespaces" => &1}}})) 22 | ...> |> Kompost.Tools.NamespaceAccess.allowed_namespaces!() 23 | [~r/^default$/, ~r/^prefix-[a-z]{3}$/, ~r/^.*-suffix$/] 24 | """ 25 | @spec allowed_namespaces!(map()) :: allowed_namespaces() 26 | def allowed_namespaces!(resource) do 27 | resource 28 | |> allowed_namespaces_annotation() 29 | |> compile_allowed_namespaces!() 30 | end 31 | 32 | @doc ~S""" 33 | Returns `true` if the given namespace can be accessed according to the list of 34 | `allowed_namespaces`. 35 | 36 | ### Examples 37 | 38 | iex> Kompost.Tools.NamespaceAccess.can_access?( 39 | ...> "default", 40 | ...> [~r/^default$/, ~r/^prefix-[a-z]{3}$/, ~r/^.*-suffix$/] 41 | ...> ) 42 | true 43 | 44 | iex> Kompost.Tools.NamespaceAccess.can_access?( 45 | ...> "prefix-one", 46 | ...> [~r/^default$/, ~r/^prefix-[a-z]{3}$/, ~r/^.*-suffix$/] 47 | ...> ) 48 | true 49 | 50 | iex> Kompost.Tools.NamespaceAccess.can_access?( 51 | ...> "prefix-four", 52 | ...> [~r/^default$/, ~r/^prefix-[a-z]{3}$/, ~r/^.*-suffix$/] 53 | ...> ) 54 | false 55 | """ 56 | @spec can_access?(binary(), allowed_namespaces()) :: boolean() 57 | def can_access?(namespace, allowed_namespaces) do 58 | Enum.any?(allowed_namespaces, &Regex.match?(&1, namespace)) 59 | end 60 | 61 | @spec compile_allowed_namespaces!(binary() | nil) :: allowed_namespaces() 62 | defp compile_allowed_namespaces!(nil), do: [~r//] 63 | 64 | defp compile_allowed_namespaces!(allowed_ns) do 65 | allowed_ns 66 | |> String.split([" ", ","], trim: true) 67 | |> Enum.map(&String.trim_leading(&1, "^")) 68 | |> Enum.map(&String.trim_trailing(&1, "$")) 69 | |> Enum.map(&~r/^#{&1}$/) 70 | end 71 | 72 | @spec allowed_namespaces_annotation(map()) :: binary() | nil 73 | def allowed_namespaces_annotation(resource) do 74 | resource["metadata"]["annotations"][@allowed_namespaces_annotation] 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/kompost/tools/password.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Tools.Password do 2 | @moduledoc """ 3 | Helpers to generate and verify passwords using `:crypto`. 4 | """ 5 | 6 | @default_length 32 7 | 8 | @doc """ 9 | Generates a random string to be used as password. 10 | """ 11 | @spec random_string(length :: non_neg_integer()) :: binary() 12 | def random_string(length \\ @default_length) do 13 | :crypto.strong_rand_bytes(length) |> Base.url_encode64() |> binary_part(0, length) 14 | end 15 | 16 | @doc """ 17 | Checks the given password against the given hash for the given algorithm and return true if they match, 18 | false otherwise. 19 | 20 | iex> Kompost.Tools.Password.verify_password("rabbit_password_hashing_sha256", "123123", "PuSRFCZ90wY8W/N4yIrv88MwxI8bftMEawWdciqRUf0WLzoV") 21 | true 22 | 23 | iex> Kompost.Tools.Password.verify_password("rabbit_password_hashing_sha256", "incorrect", "PuSRFCZ90wY8W/N4yIrv88MwxI8bftMEawWdciqRUf0WLzoV") 24 | false 25 | """ 26 | @spec verify_password(binary(), binary(), binary()) :: bool() 27 | def verify_password("rabbit_password_hashing_sha256", password, password_hash64) do 28 | password_hash = password_hash64 |> Base.decode64!() 29 | salt = String.slice(password_hash, 0, 4) 30 | 31 | salted_pw = salt <> password 32 | hashed_password = :crypto.hash(:sha256, salted_pw) 33 | salt <> hashed_password == password_hash 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/kompost/tools/resource.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Tools.Resource do 2 | @moduledoc """ 3 | Helper functions to work with Kubernetes Resources 4 | """ 5 | @spec k8s_apply!(map, K8s.Conn.t()) :: map() 6 | def k8s_apply!(resource, conn) do 7 | {:ok, applied_resource} = k8s_apply(resource, conn) 8 | 9 | applied_resource 10 | end 11 | 12 | @spec k8s_apply(map, K8s.Conn.t()) :: {:ok, map()} | {:error, any} 13 | def k8s_apply(resource, conn) do 14 | resource 15 | |> K8s.Client.apply() 16 | |> K8s.Client.put_conn(conn) 17 | |> K8s.Client.run() 18 | end 19 | 20 | @spec delete!(map, K8s.Conn.t()) :: {:ok, map()} 21 | def delete!(resource, conn) do 22 | {:ok, _} = 23 | resource 24 | |> K8s.Client.delete() 25 | |> K8s.Client.put_conn(conn) 26 | |> K8s.Client.run() 27 | end 28 | 29 | @spec wait_until_observed!(map, K8s.Conn.t(), non_neg_integer()) :: map 30 | def wait_until_observed!(resource, conn, timeout) do 31 | wait_until!( 32 | resource, 33 | conn, 34 | ["status", "observedGeneration"], 35 | resource["metadata"]["generation"], 36 | timeout 37 | ) 38 | end 39 | 40 | @spec wait_for_condition!(map, K8s.Conn.t(), binary(), binary(), non_neg_integer()) :: map 41 | def wait_for_condition!(resource, conn, condition, status \\ "True", timeout) do 42 | wait_until!( 43 | resource, 44 | conn, 45 | &find_condition_status(&1, condition), 46 | status, 47 | timeout 48 | ) 49 | end 50 | 51 | @spec wait_until!( 52 | resource :: map(), 53 | K8s.Conn.t(), 54 | find :: list | function(), 55 | term, 56 | non_neg_integer() 57 | ) :: map 58 | def wait_until!(%K8s.Operation{} = op, conn, find, eval, timeout) do 59 | {:ok, resource} = 60 | op 61 | |> K8s.Client.put_conn(conn) 62 | |> K8s.Client.wait_until( 63 | find: find, 64 | eval: eval, 65 | timeout: timeout 66 | ) 67 | 68 | resource 69 | end 70 | 71 | def wait_until!(resource, conn, find, eval, timeout) do 72 | get_op = 73 | resource 74 | |> K8s.Client.get() 75 | |> K8s.Client.put_conn(conn) 76 | 77 | wait_until!(get_op, conn, find, eval, timeout) 78 | end 79 | 80 | @spec find_condition_status(map(), binary()) :: binary() 81 | defp find_condition_status(resource, condition) do 82 | get_in(resource, [ 83 | "status", 84 | "conditions", 85 | Access.filter(&(&1["type"] == condition)), 86 | "status" 87 | ]) 88 | |> List.wrap() 89 | |> List.first() 90 | end 91 | 92 | @spec config_map!( 93 | name :: binary(), 94 | namespace :: binary(), 95 | files :: binary() | list(binary()) 96 | ) :: map() 97 | def config_map!(name, namespace, files) do 98 | data = 99 | files 100 | |> List.wrap() 101 | |> Map.new(fn file_path -> 102 | filename = Path.basename(file_path) 103 | content = File.read!(file_path) 104 | {filename, content} 105 | end) 106 | 107 | %{ 108 | "apiVersion" => "v1", 109 | "kind" => "ConfigMap", 110 | "metadata" => %{"namespace" => namespace, "name" => name}, 111 | "data" => data 112 | } 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/kompost/webhooks.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Webhooks do 2 | @moduledoc false 3 | 4 | alias Kompost.K8sConn 5 | 6 | require Logger 7 | 8 | @spec bootstrap_tls(atom(), binary()) :: :ok 9 | def bootstrap_tls(env, secret_name) do 10 | Application.ensure_all_started(:k8s) 11 | conn = K8sConn.get!(env) 12 | 13 | with {:certs, {:ok, ca_bundle}} <- 14 | {:certs, 15 | K8sWebhoox.ensure_certificates(conn, "kompost", "kompost", "kompost", secret_name)}, 16 | {:webhook_config, :ok} <- 17 | {:webhook_config, 18 | K8sWebhoox.update_admission_webhook_configs(conn, "kompost", ca_bundle)} do 19 | Logger.info("TLS Bootstrap completed.") 20 | else 21 | error -> 22 | Logger.error("TLS Bootstrap failed: #{inspect(error)}") 23 | exit({:shutdown, 1}) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/kompost/webhooks/router.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Webhooks.Router do 2 | use Plug.Router 3 | 4 | plug :match 5 | plug :dispatch 6 | 7 | forward "/postgres", to: Kompost.Kompo.Postgres.Webhooks.Router 8 | end 9 | -------------------------------------------------------------------------------- /lib/mix/tasks/bonny/gen/manifest/kompost_customizer.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Bonny.Gen.Manifest.KompostCustomizer do 2 | @moduledoc """ 3 | Implements a callback to override manifests generated by `mix bonny.gen.manifest` 4 | """ 5 | 6 | @doc """ 7 | This function is called for every resource generated by `mix bonny.gen.manifest`. 8 | Use pattern matching to override specific resources. 9 | 10 | Be careful in your pattern matching. Sometimes the map keys are strings, 11 | sometimes they are atoms. 12 | 13 | ### Examples 14 | 15 | def override(%{"kind" => "ServiceAccount"} = resource) do 16 | put_in(resource, ~w(metadata labels foo)a, "bar") 17 | end 18 | """ 19 | 20 | import YamlElixir.Sigil 21 | 22 | @spec override(Bonny.Resource.t()) :: Bonny.Resource.t() 23 | 24 | def override(%{"kind" => "Deployment"} = resource) do 25 | image = 26 | get_in( 27 | resource, 28 | [ 29 | "spec", 30 | "template", 31 | "spec", 32 | "containers", 33 | Access.filter(&(&1["name"] == "kompost")), 34 | "image" 35 | ] 36 | ) 37 | |> List.first() 38 | 39 | resource 40 | |> update_in( 41 | ["spec", "template", "spec", Access.key("volumes", [])], 42 | &[%{"name" => "certs", "secret" => %{"secretName" => "tls-certs", "optional" => true}} | &1] 43 | ) 44 | |> put_in( 45 | [ 46 | "spec", 47 | "template", 48 | "spec", 49 | "containers", 50 | Access.all(), 51 | "securityContext", 52 | "runAsUser" 53 | ], 54 | 1001 55 | ) 56 | |> update_in( 57 | [ 58 | "spec", 59 | "template", 60 | "spec", 61 | "containers", 62 | Access.filter(&(&1["name"] == "kompost")), 63 | Access.key("volumeMounts", []) 64 | ], 65 | &[%{"name" => "certs", "mountPath" => "/mnt/cert"} | &1] 66 | ) 67 | |> update_in( 68 | [ 69 | "spec", 70 | "template", 71 | "spec", 72 | Access.key("initContainers", []) 73 | ], 74 | fn init_containers -> 75 | certs = %{ 76 | "name" => "init-certificates", 77 | "image" => image, 78 | "args" => ["eval", ~s|Kompost.Webhooks.bootstrap_tls(:prod, "tls-certs")|] 79 | } 80 | 81 | [certs | init_containers] 82 | end 83 | ) 84 | |> put_in( 85 | [ 86 | "spec", 87 | "template", 88 | "spec", 89 | "containers", 90 | Access.filter(&(&1["name"] == "kompost")), 91 | "ports" 92 | ], 93 | [%{"containerPort" => 4000, "name" => "webhooks"}] 94 | ) 95 | end 96 | 97 | def override(%{"kind" => "ClusterRole"} = resource) do 98 | Map.update!(resource, "rules", fn rules -> 99 | [ 100 | ~y""" 101 | apiGroups: ["admissionregistration.k8s.io"] 102 | resources: 103 | - validatingwebhookconfigurations 104 | - mutatingwebhookconfigurations 105 | verbs: ["get", "list", "update", "patch"] 106 | """, 107 | ~y""" 108 | apiGroups: ["apiextensions.k8s.io"] 109 | resources: ["customresourcedefinitions"] 110 | verbs: ["get", "list", "update", "patch"] 111 | """ 112 | | rules 113 | ] 114 | end) 115 | end 116 | 117 | def override(%{"kind" => "CustomResourceDefinition"} = resource) do 118 | resource 119 | |> Map.update!("metadata", fn 120 | %{"labels" => labels} = metadata when labels == %{} -> Map.delete(metadata, "labels") 121 | metadata -> metadata 122 | end) 123 | |> update_in(["spec", "versions", Access.all()], fn 124 | version -> 125 | version 126 | |> Enum.reject(fn 127 | {"additionalPrinterColumns", []} -> true 128 | {"deprecated", false} -> true 129 | _ -> false 130 | end) 131 | |> Map.new() 132 | end) 133 | end 134 | 135 | # fallback 136 | def override(resource), do: resource 137 | end 138 | -------------------------------------------------------------------------------- /lib/mix/tasks/kompost.gen.manifest.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Kompost.Gen.Manifest do 2 | @moduledoc """ 3 | Generates the Kubernetes YAML manifest for Kompost. For :dev and :test, this 4 | task also creats a local cluster using Kind if it does not already exit, 5 | creates the namespace and applies the resulting manifest to it. 6 | """ 7 | 8 | use Mix.Task 9 | 10 | alias Bonny.Mix.Operator 11 | alias Kompost.K8sConn 12 | alias Mix.Tasks.Bonny.Gen.Manifest.KompostCustomizer 13 | 14 | import YamlElixir.Sigil 15 | 16 | @default_opts [namespace: "kompost"] 17 | @switches [namespace: :string, image: :string, out: :string] 18 | @aliases [n: :namespace, i: :image, o: :out] 19 | 20 | @shortdoc "Generate Kubernetes YAML manifest for this operator" 21 | 22 | @spec run(list()) :: :ok 23 | def run(args) do 24 | Mix.Task.run("compile") 25 | 26 | {opts, _, _} = 27 | Mix.Bonny.parse_args(args, @default_opts, switches: @switches, aliases: @aliases) 28 | 29 | do_run(Mix.env(), opts) 30 | end 31 | 32 | @spec do_run(atom(), Keyword.t()) :: :ok 33 | def do_run(env, opts) when env in [:dev, :test] do 34 | cluster_name = "kompost-#{env}" 35 | 36 | Application.ensure_all_started(:k8s) 37 | ensure_cluster(cluster_name, "./test/integration/kubeconfig-#{env}.yaml") 38 | conn = K8sConn.get!(env) 39 | ensure_namespace(conn, Keyword.fetch!(opts, :namespace)) 40 | 41 | operations = 42 | generate_manifest(env, opts) 43 | |> Ymlr.documents!() 44 | |> YamlElixir.read_all_from_string!() 45 | |> Enum.map(&K8s.Client.apply/1) 46 | 47 | errors = 48 | conn 49 | |> K8s.Client.async(operations) 50 | |> Enum.reject(&match?({:ok, _}, &1)) 51 | 52 | if errors == [] do 53 | Mix.Shell.IO.info("Manifest applied to cluster #{cluster_name}") 54 | else 55 | Mix.Shell.IO.error("Error occurred when applying manifests to cluster:") 56 | dbg(errors) 57 | end 58 | 59 | :ok 60 | end 61 | 62 | def do_run(:prod, opts) do 63 | out = Keyword.fetch!(opts, :out) 64 | 65 | generate_manifest(:prod, opts) 66 | |> Ymlr.documents!() 67 | |> YamlElixir.read_all_from_string!() 68 | |> Enum.map(&KompostCustomizer.override/1) 69 | |> render(out) 70 | end 71 | 72 | @spec render(list(map()), binary()) :: :ok 73 | defp render(documents, out) do 74 | if File.dir?(out) do 75 | documents 76 | |> Ymlr.documents!() 77 | |> YamlElixir.read_all_from_string!() 78 | |> Enum.map(fn 79 | %{"kind" => "CustomResourceDefinition"} = resource -> 80 | {"#{resource["spec"]["names"]["singular"]}.crd.yaml", resource} 81 | 82 | resource -> 83 | {"#{String.downcase(resource["kind"])}.yaml", resource} 84 | end) 85 | |> Enum.each(fn {filename, resource} -> 86 | Mix.Bonny.render(Ymlr.document!(resource), Path.join(out, filename)) 87 | end) 88 | else 89 | documents 90 | |> Ymlr.documents!() 91 | |> Mix.Bonny.render(out) 92 | end 93 | 94 | :ok 95 | end 96 | 97 | @spec ensure_cluster(cluster_name :: binary(), kubeconfig_path :: binary()) :: :ok 98 | defp ensure_cluster(cluster_name, kubeconfig_path) do 99 | {clusters, 0} = System.cmd("kind", ~w(get clusters)) 100 | 101 | if cluster_name not in String.split(clusters, "\n", trim: true) do 102 | Mix.Shell.IO.info("Creating kind cluster #{cluster_name}") 103 | 104 | 0 = 105 | Mix.Shell.IO.cmd( 106 | "kind create cluster --name #{cluster_name} --config ./test/integration/kind-cluster.yml" 107 | ) 108 | end 109 | 110 | if not File.exists?(kubeconfig_path) do 111 | Mix.Shell.IO.info("Generating kubeconfig file: #{kubeconfig_path}") 112 | 113 | 0 = 114 | Mix.Shell.IO.cmd( 115 | ~s(kind export kubeconfig --kubeconfig "#{kubeconfig_path}" --name "#{cluster_name}") 116 | ) 117 | end 118 | 119 | :ok 120 | end 121 | 122 | @spec ensure_namespace(K8s.Conn.t(), binary()) :: :ok 123 | defp ensure_namespace(conn, namespace) do 124 | {:ok, _} = 125 | K8s.Client.apply(~y""" 126 | apiVersion: v1 127 | kind: Namespace 128 | metadata: 129 | name: #{namespace} 130 | """) 131 | |> K8s.Client.put_conn(conn) 132 | |> K8s.Client.run() 133 | 134 | :ok 135 | end 136 | 137 | @spec generate_manifest(atom(), Keyword.t()) :: list(map()) 138 | defp generate_manifest(:prod, opts) do 139 | image = Keyword.fetch!(opts, :image) 140 | namespace = Keyword.fetch!(opts, :namespace) 141 | 142 | [ 143 | Operator.deployment(image, namespace), 144 | svc_manifest(namespace), 145 | webhook_config_manifest(namespace) 146 | | generate_manifest(:dev, opts) 147 | ] 148 | end 149 | 150 | defp generate_manifest(_, opts) do 151 | namespace = Keyword.fetch!(opts, :namespace) 152 | operators = Operator.find_operators() 153 | 154 | Operator.crds(operators) ++ 155 | [ 156 | Operator.cluster_role(operators), 157 | Operator.service_account(namespace), 158 | Operator.cluster_role_binding(namespace) 159 | ] 160 | end 161 | 162 | @spec svc_manifest(namespace :: binary()) :: map() 163 | def svc_manifest(namespace) do 164 | ~y""" 165 | apiVersion: v1 166 | kind: Service 167 | metadata: 168 | name: kompost 169 | namespace: #{namespace} 170 | labels: 171 | k8s-app: kompost 172 | spec: 173 | ports: 174 | - name: webhooks 175 | port: 443 176 | targetPort: webhooks 177 | protocol: TCP 178 | selector: 179 | k8s-app: kompost 180 | """ 181 | end 182 | 183 | @spec webhook_config_manifest(namespace :: binary()) :: map() 184 | defp webhook_config_manifest(namespace) do 185 | ~y""" 186 | apiVersion: admissionregistration.k8s.io/v1 187 | kind: ValidatingWebhookConfiguration 188 | metadata: 189 | name: "kompost" 190 | webhooks: 191 | - name: "postgres.kompost.chuge.li" 192 | admissionReviewVersions: ["v1"] 193 | matchPolicy: Equivalent 194 | rules: 195 | - operations: ['CREATE', 'UPDATE'] 196 | apiGroups: ['kompost.chuge.li'] 197 | apiVersions: ['v1alpha1'] 198 | resources: 199 | - postgresdatabases 200 | - postgresclusterinstances 201 | - postgresinstances 202 | failurePolicy: 'Ignore' # Fail-open (optional) 203 | sideEffects: None 204 | clientConfig: 205 | service: 206 | namespace: #{namespace} 207 | name: kompost 208 | path: /postgres/admission-review/validating 209 | port: 443 210 | """ 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /lib/mix/tasks/kompost.gen.periphery.ex: -------------------------------------------------------------------------------- 1 | if Mix.env() in [:dev, :test] do 2 | defmodule Mix.Tasks.Kompost.Gen.Periphery do 3 | @moduledoc """ 4 | Generates the manifest for peripheral systems for testing 5 | (Postgres, etc). 6 | """ 7 | alias Kompost.Tools.Resource 8 | 9 | use Mix.Task 10 | 11 | @spec run([]) :: :ok 12 | def run([]) do 13 | Application.ensure_all_started(:k8s) 14 | DotenvParser.load_file("test/integration/.env") 15 | 16 | conn = Kompost.K8sConn.get!(Mix.env()) 17 | gen_kompo(:postgres, conn) 18 | gen_kompo(:temporal, conn) 19 | 20 | Mix.Shell.IO.info("Waiting for temporal deployment to become ready") 21 | 22 | K8s.Client.get("apps/v1", "Deployment", name: "temporal", namespace: "temporal") 23 | |> Resource.wait_for_condition!(conn, "Available", 120_000) 24 | 25 | Mix.Shell.IO.info("Waiting for postgres deployment to become ready") 26 | 27 | K8s.Client.get("apps/v1", "Deployment", name: "postgres", namespace: "postgres") 28 | |> Resource.wait_for_condition!(conn, "Available", 120_000) 29 | 30 | Mix.Shell.IO.info("We're good to go, captain!") 31 | end 32 | 33 | @spec gen_kompo(atom(), K8s.Conn.t()) :: :ok 34 | defp gen_kompo(:postgres, conn) do 35 | "priv/periphery/postgres.yaml" 36 | |> YamlElixir.read_all_from_file!() 37 | |> Enum.map(&override/1) 38 | |> Enum.map(&K8s.Client.apply/1) 39 | |> then(&K8s.Client.async(conn, &1)) 40 | |> Enum.each(fn 41 | {:ok, _} -> 42 | :ok 43 | 44 | {:error, error} when is_exception(error) -> 45 | Mix.Shell.IO.error("Error applying postgres manifest: #{Exception.message(error)}") 46 | end) 47 | 48 | :ok 49 | end 50 | 51 | defp gen_kompo(:temporal, conn) do 52 | configmap = 53 | Resource.config_map!( 54 | "dynamicconfig", 55 | "temporal", 56 | "./priv/periphery/temporal/development-sql.yaml" 57 | ) 58 | 59 | "priv/periphery/temporal.yaml" 60 | |> YamlElixir.read_all_from_file!() 61 | |> then(&[configmap | &1]) 62 | |> Enum.map(&override/1) 63 | |> Enum.map(&K8s.Client.apply/1) 64 | |> then(&K8s.Client.async(conn, &1)) 65 | |> Enum.each(fn 66 | {:ok, _} -> 67 | :ok 68 | 69 | {:error, error} when is_exception(error) -> 70 | Mix.Shell.IO.error("Error applying temporal manifest: #{Exception.message(error)}") 71 | end) 72 | 73 | :ok 74 | end 75 | 76 | @spec override(map()) :: map() 77 | defp override(%{"kind" => "Deployment", "metadata" => %{"name" => "postgres"}} = resource) do 78 | resource 79 | |> put_in( 80 | [ 81 | "spec", 82 | "template", 83 | "spec", 84 | "containers", 85 | Access.filter(&(&1["name"] == "postgres")), 86 | "env" 87 | ], 88 | [ 89 | %{"name" => "POSTGRES_USER", "value" => System.get_env("POSTGRES_USER")}, 90 | %{"name" => "POSTGRES_PASSWORD", "value" => System.get_env("POSTGRES_PASSWORD")}, 91 | %{"name" => "POSTGRES_DB", "value" => System.get_env("POSTGRES_DB")} 92 | ] 93 | ) 94 | end 95 | 96 | defp override(%{"kind" => "Deployment", "metadata" => %{"name" => "temporal"}} = resource) do 97 | resource 98 | |> update_in( 99 | [ 100 | "spec", 101 | "template", 102 | "spec", 103 | "containers", 104 | Access.filter(&(&1["name"] == "temporal")), 105 | "env" 106 | ], 107 | &[ 108 | %{"name" => "DB_PORT", "value" => System.get_env("POSTGRES_EXPOSED_PORT")}, 109 | %{"name" => "POSTGRES_USER", "value" => System.get_env("POSTGRES_USER")}, 110 | %{"name" => "POSTGRES_PWD", "value" => System.get_env("POSTGRES_PASSWORD")} 111 | | &1 112 | ] 113 | ) 114 | end 115 | 116 | defp override(%{"kind" => "Service", "metadata" => %{"name" => "postgres"}} = resource) do 117 | port = System.get_env("POSTGRES_EXPOSED_PORT") |> String.to_integer() 118 | 119 | resource 120 | |> update_in( 121 | ["spec", "ports", Access.filter(&(&1["name"] == "postgres"))], 122 | &Map.merge(&1, %{"nodePort" => port, "port" => port}) 123 | ) 124 | end 125 | 126 | defp override(%{"kind" => "Service", "metadata" => %{"name" => "temporal"}} = resource) do 127 | port = System.get_env("TEMPORAL_EXPOSED_PORT") |> String.to_integer() 128 | 129 | resource 130 | |> update_in( 131 | ["spec", "ports", Access.filter(&(&1["name"] == "temporal"))], 132 | &Map.merge(&1, %{"nodePort" => port, "port" => port}) 133 | ) 134 | end 135 | 136 | defp override(resource), do: resource 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :kompost, 7 | version: "0.3.2", 8 | elixir: "~> 1.14", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | elixirc_paths: elixirc_paths(Mix.env()), 12 | preferred_cli_env: cli_env(), 13 | test_coverage: [tool: ExCoveralls], 14 | dialyzer: [ignore_warnings: ".dialyzer_ignore.exs"], 15 | releases: releases() 16 | ] 17 | end 18 | 19 | defp cli_env do 20 | [ 21 | coveralls: :test, 22 | "coveralls.detail": :test, 23 | "coveralls.post": :test, 24 | "coveralls.html": :test, 25 | "coveralls.travis": :test, 26 | "coveralls.github": :test, 27 | "coveralls.xml": :test, 28 | "coveralls.json": :test 29 | ] 30 | end 31 | 32 | # Run "mix help compile.app" to learn about applications. 33 | def application do 34 | [ 35 | mod: {Kompost.Application, [env: Mix.env()]}, 36 | extra_applications: [:logger] 37 | ] 38 | end 39 | 40 | defp elixirc_paths(:test), do: ["test/support" | elixirc_paths(:prod)] 41 | defp elixirc_paths(_), do: ["lib"] 42 | 43 | # Run "mix help deps" to learn about dependencies. 44 | defp deps do 45 | [ 46 | {:bandit, "~> 1.5.0"}, 47 | # {:bonny, path: "/Users/mruoss/src/community/bonny"}, 48 | # {:bonny, github: "coryodaniel/bonny", branch: "master"}, 49 | {:bonny, "~> 1.0"}, 50 | {:jason, "~> 1.0"}, 51 | {:k8s_webhoox, "~> 0.2.0"}, 52 | {:plug, "~> 1.0"}, 53 | {:postgrex, "~> 0.18.0"}, 54 | {:slugger, "~> 0.3.0"}, 55 | 56 | # Temporal.io 57 | {:temporalio, "~> 1.0"}, 58 | {:google_protos, "~> 0.4.0"}, 59 | {:grpc, "~> 0.8.0", override: true}, 60 | 61 | # Dev dependencies 62 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 63 | {:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false}, 64 | {:dotenv_parser, "~> 2.0", only: [:dev, :test], runtime: false}, 65 | {:excoveralls, "~> 0.18.0", only: :test} 66 | ] 67 | end 68 | 69 | defp releases do 70 | [ 71 | kompost: [ 72 | include_erts: false, 73 | include_executables_for: [:unix] 74 | ] 75 | ] 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project information 2 | site_name: Kompost 3 | site_url: https://kompost.github.io 4 | site_author: Michael Ruoss 5 | 6 | # Repository 7 | repo_name: mruoss/kompost 8 | repo_url: https://github.com/mruoss/kompost 9 | edit_uri: edit/main/docs/ 10 | 11 | # Copyright 12 | copyright: Copyright © 2023 Michael Ruoss 13 | 14 | nav: 15 | - Kompost: 16 | - Introduction: index.md 17 | - Getting Started: getting_started.md 18 | - Postgres: 19 | - Introduction: postgres/index.md 20 | - PostgresInstance: postgres/postgres_instance.md 21 | - PostgresClusterInstance: postgres/postgres_cluster_instance.md 22 | - PostgresDatabase: postgres/postgres_database.md 23 | - Temporal: 24 | - Introduction: temporal/index.md 25 | theme: 26 | name: material 27 | palette: 28 | # Palette toggle for light mode 29 | - media: "(prefers-color-scheme: light)" 30 | scheme: default 31 | primary: indigo 32 | accent: indigo 33 | toggle: 34 | icon: material/brightness-7 35 | name: Switch to dark mode 36 | 37 | # Palette toggle for dark mode 38 | - media: "(prefers-color-scheme: dark)" 39 | scheme: slate 40 | primary: indigo 41 | accent: indigo 42 | toggle: 43 | icon: material/brightness-4 44 | name: Switch to light mode 45 | features: 46 | - announce.dismiss 47 | - content.action.edit 48 | # - content.action.view 49 | - content.code.annotate 50 | - content.code.copy 51 | # - content.tabs.link 52 | - content.tooltips 53 | # - header.autohide 54 | - navigation.expand 55 | - navigation.footer 56 | - navigation.indexes 57 | - navigation.sections 58 | - navigation.tabs 59 | # - navigation.tabs.sticky 60 | - navigation.top 61 | - navigation.tracking 62 | - search.highlight 63 | - search.share 64 | - search.suggest 65 | - toc.follow 66 | 67 | # Plugins 68 | plugins: 69 | - search: 70 | separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' 71 | 72 | # Customization 73 | extra: 74 | annotate: 75 | json: [.s2] 76 | analytics: 77 | provider: google 78 | property: G-MW2L0WNJRF 79 | social: 80 | - icon: fontawesome/brands/github 81 | link: https://github.com/mruoss 82 | - icon: fontawesome/regular/heart 83 | link: https://github.com/sponsors/mruoss 84 | - icon: fontawesome/brands/mastodon 85 | link: https://tooting.ch/@mruoss 86 | - icon: fontawesome/brands/twitter 87 | link: https://twitter.com/miruoss 88 | markdown_extensions: 89 | - admonition 90 | - attr_list 91 | - def_list 92 | - footnotes 93 | - toc: 94 | permalink: true 95 | - pymdownx.arithmatex: 96 | generic: true 97 | - pymdownx.betterem: 98 | smart_enable: all 99 | - pymdownx.caret 100 | - pymdownx.details 101 | - pymdownx.emoji: 102 | emoji_generator: !!python/name:materialx.emoji.to_svg 103 | emoji_index: !!python/name:materialx.emoji.twemoji 104 | - pymdownx.highlight: 105 | anchor_linenums: true 106 | line_spans: __span 107 | pygments_lang_class: true 108 | - pymdownx.inlinehilite 109 | - pymdownx.snippets 110 | - pymdownx.superfences 111 | - pymdownx.keys 112 | - pymdownx.mark 113 | # - pymdownx.smartsymbols 114 | - pymdownx.tabbed: 115 | alternate_style: true 116 | - pymdownx.tasklist: 117 | custom_checkbox: true 118 | - pymdownx.tilde 119 | -------------------------------------------------------------------------------- /priv/charts/kompost/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /priv/charts/kompost/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: kompost 3 | description: A Helm chart for Kompost 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 1.0.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "0.3.2" 25 | 26 | annotations: 27 | artifacthub.io/category: database 28 | artifacthub.io/containsSecurityUpdates: "false" 29 | artifacthub.io/changes: | 30 | - kind: fixed 31 | description: Chart URL in README 32 | artifacthub.io/license: Apache-2.0 33 | artifacthub.io/maintainers: | 34 | - name: Michael Ruoss 35 | email: michael@michaelruoss.ch 36 | artifacthub.io/operator: "true" 37 | artifacthub.io/prerelease: "false" 38 | artifacthub.io/links: | 39 | - name: GitHub 40 | url: https://github.com/mruoss/kompost 41 | - name: Documentation 42 | url: https://kompost.chuge.li 43 | artifacthub.io/signKey: | 44 | fingerprint: 4B95C7BE876C3E22ED79A56F15B49C3F51E14030 45 | url: https://github.com/mruoss/kompost/raw/main/priv/charts/pubkey.asc 46 | artifacthub.io/images: | 47 | - name: kompost 48 | image: ghcr.io/mruoss/kompost:latest 49 | platforms: 50 | - linux/amd64 51 | - linux/arm/v7 52 | - linux/arm64 -------------------------------------------------------------------------------- /priv/charts/kompost/README.md: -------------------------------------------------------------------------------- 1 | # Kompost 2 | 3 | To regenerate this document, from the root of this chart directory run: 4 | ```shell 5 | docker run --rm --volume "$(pwd):/helm-docs" -u $(id -u) jnorwood/helm-docs:latest 6 | ``` 7 | 8 | ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) 9 | 10 | A Helm chart for Kompost 11 | 12 | ## TL;DR 13 | ```bash 14 | helm install --version 1.0.0 -n kompost kompost oci://ghcr.io/mruoss/charts/kompost 15 | ``` 16 | 17 | ## Installing the Chart 18 | To install the chart with the release name `kompost`: 19 | ```bash 20 | helm install --version 1.0.0 -n kompost kompost oci://ghcr.io/mruoss/charts/kompost 21 | ``` 22 | 23 | ## Uninstalling the Chart 24 | To uninstall the `kompost` deployment: 25 | ```bash 26 | helm uninstall kompost 27 | ``` 28 | The command removes all the Kubernetes components associated with the chart and deletes the release. 29 | 30 | ## Values 31 | 32 | | Key | Type | Default | Description | 33 | |-----|------|---------|-------------| 34 | | affinity | object | `{}` | | 35 | | containerSecurityContext.capabilities.drop[0] | string | `"ALL"` | | 36 | | containerSecurityContext.readOnlyRootFilesystem | bool | `true` | | 37 | | containerSecurityContext.runAsNonRoot | bool | `true` | | 38 | | containerSecurityContext.runAsUser | int | `1001` | | 39 | | fullnameOverride | string | `""` | String to fully override `"kompost.fullname"` | 40 | | image.pullPolicy | string | `"IfNotPresent"` | imagePullPolicy applied to the containers | 41 | | image.repository | string | `"ghcr.io/mruoss/kompost"` | Kompost container image repository | 42 | | image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | 43 | | imagePullSecrets | list | `[]` | Secrets with credentials to pull images from a private registry | 44 | | kompos.postgres.enabled | bool | `true` | Install CRDS for and run postgres kompo | 45 | | kompos.temporal.enabled | bool | `true` | Install CRDS for and run temporal kompo | 46 | | nameOverride | string | `"kompost"` | Provide a name in place of `kompost` | 47 | | nodeSelector | object | `{}` | | 48 | | podAnnotations | object | `{}` | | 49 | | podSecurityContext.runAsNonRoot | bool | `true` | | 50 | | podSecurityContext.runAsUser | int | `1001` | | 51 | | replicaCount | int | `1` | The number of pods to run | 52 | | resources | object | `{}` | | 53 | | revisionHistoryLimit | int | `10` | The number of revisions to keep in history | 54 | | serviceAccount.annotations | object | `{}` | Annotations to add to the service account | 55 | | serviceAccount.create | bool | `true` | Specifies whether a service account should be created | 56 | | serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template | 57 | | tolerations | list | `[]` | | 58 | | webhook.certManager.addInjectorAnnotations | bool | `true` | Automatically add the cert-manager.io/inject-ca-from annotation to the webhooks and CRDs. As long as you have the cert-manager CA Injector enabled, this will automatically setup your webhook's CA to the one used by cert-manager. See https://cert-manager.io/docs/concepts/ca-injector | 59 | | webhook.certManager.cert.annotations | object | `{}` | Add extra annotations to the Certificate resource. | 60 | | webhook.certManager.cert.create | bool | `true` | Create a certificate resource within this chart. See https://cert-manager.io/docs/usage/certificate/ | 61 | | webhook.certManager.cert.duration | string | `""` | Set the requested duration (i.e. lifetime) of the Certificate. See https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec | 62 | | webhook.certManager.cert.issuerRef | object | `{"group":"cert-manager.io","kind":"Issuer","name":"my-issuer"}` | For the Certificate created by this chart, setup the issuer. See https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.IssuerSpec | 63 | | webhook.certManager.cert.renewBefore | string | `""` | How long before the currently issued certificate’s expiry cert-manager should renew the certificate. See https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec Note that renewBefore should be greater than .webhook.lookaheadInterval since the webhook will check this far in advance that the certificate is valid. | 64 | | webhook.certManager.enabled | bool | `false` | Enabling cert-manager support will disable the built in secret creation and switch to using cert-manager (installed separately) to automatically issue and renew the webhook certificate. This chart does not install cert-manager for you, See https://cert-manager.io/docs/ | -------------------------------------------------------------------------------- /priv/charts/kompost/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | {{- $chartRepo := "oci://ghcr.io/mruoss/charts" -}} 2 | {{- $org := "kompost" -}} 3 | # Kompost 4 | 5 | To regenerate this document, from the root of this chart directory run: 6 | ```shell 7 | docker run --rm --volume "$(pwd):/helm-docs" -u $(id -u) jnorwood/helm-docs:latest 8 | ``` 9 | 10 | {{ template "chart.typeBadge" . }}{{ template "chart.versionBadge" . }} 11 | 12 | {{ template "chart.description" . }} 13 | 14 | ## TL;DR 15 | ```bash 16 | helm install --version {{ template "chart.version" . }} -n kompost kompost {{ $chartRepo }}/{{ template "chart.name" . }} 17 | ``` 18 | 19 | ## Installing the Chart 20 | To install the chart with the release name `{{ template "chart.name" . }}`: 21 | ```bash 22 | helm install --version {{ template "chart.version" . }} -n {{ template "chart.name" . }} {{ template "chart.name" . }} {{ $chartRepo }}/{{ template "chart.name" . }} 23 | ``` 24 | 25 | ## Uninstalling the Chart 26 | To uninstall the `{{ template "chart.name" . }}` deployment: 27 | ```bash 28 | helm uninstall {{ template "chart.name" . }} 29 | ``` 30 | The command removes all the Kubernetes components associated with the chart and deletes the release. 31 | 32 | {{ template "chart.valuesSection" . }} -------------------------------------------------------------------------------- /priv/charts/kompost/artifacthub-repo.yml: -------------------------------------------------------------------------------- 1 | # Artifact Hub repository metadata file 2 | # 3 | # Some settings like the verified publisher flag or the ignored packages won't 4 | # be applied until the next time the repository is processed. Please keep in 5 | # mind that the repository won't be processed if it has not changed since the 6 | # last time it was processed. Depending on the repository kind, this is checked 7 | # in a different way. For Helm http based repositories, we consider it has 8 | # changed if the `index.yaml` file changes. For git based repositories, it does 9 | # when the hash of the last commit in the branch you set up changes. This does 10 | # NOT apply to ownership claim operations, which are processed immediately. 11 | # 12 | repositoryID: 4be1eb73-afb3-49c0-a1d6-32e7bc330d51 13 | owners: # (optional, used to claim repository ownership) 14 | - name: mruoss 15 | email: michael@michaelruoss.ch 16 | -------------------------------------------------------------------------------- /priv/charts/kompost/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "kompost.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "kompost.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "kompost.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "kompost.labels" -}} 37 | helm.sh/chart: {{ include "kompost.chart" . }} 38 | {{ include "kompost.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "kompost.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "kompost.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "kompost.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "kompost.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /priv/charts/kompost/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "kompost.fullname" . }} 6 | labels: 7 | {{- include "kompost.labels" . | nindent 4 }} 8 | rules: 9 | - apiGroups: 10 | - admissionregistration.k8s.io 11 | resources: 12 | - validatingwebhookconfigurations 13 | - mutatingwebhookconfigurations 14 | verbs: 15 | - get 16 | - list 17 | - update 18 | - patch 19 | - apiGroups: 20 | - apiextensions.k8s.io 21 | resources: 22 | - customresourcedefinitions 23 | verbs: 24 | - get 25 | - list 26 | - update 27 | - patch 28 | - apiGroups: 29 | - "" 30 | resources: 31 | - secrets 32 | verbs: 33 | - '*' 34 | - apiGroups: 35 | - apiextensions.k8s.io 36 | resources: 37 | - customresourcedefinitions 38 | verbs: 39 | - '*' 40 | - apiGroups: 41 | - coordination.k8s.io 42 | resources: 43 | - leases 44 | verbs: 45 | - '*' 46 | - apiGroups: 47 | - events.k8s.io 48 | resources: 49 | - events 50 | verbs: 51 | - '*' 52 | - apiGroups: 53 | - kompost.chuge.li 54 | resources: 55 | - postgresclusterinstances 56 | verbs: 57 | - '*' 58 | - apiGroups: 59 | - kompost.chuge.li 60 | resources: 61 | - postgresclusterinstances/status 62 | verbs: 63 | - '*' 64 | - apiGroups: 65 | - kompost.chuge.li 66 | resources: 67 | - postgresdatabases 68 | verbs: 69 | - '*' 70 | - apiGroups: 71 | - kompost.chuge.li 72 | resources: 73 | - postgresdatabases/status 74 | verbs: 75 | - '*' 76 | - apiGroups: 77 | - kompost.chuge.li 78 | resources: 79 | - postgresinstances 80 | verbs: 81 | - '*' 82 | - apiGroups: 83 | - kompost.chuge.li 84 | resources: 85 | - postgresinstances/status 86 | verbs: 87 | - '*' 88 | - apiGroups: 89 | - kompost.chuge.li 90 | resources: 91 | - temporalapiservers 92 | verbs: 93 | - '*' 94 | - apiGroups: 95 | - kompost.chuge.li 96 | resources: 97 | - temporalapiservers/status 98 | verbs: 99 | - '*' 100 | - apiGroups: 101 | - kompost.chuge.li 102 | resources: 103 | - temporalnamespaces 104 | verbs: 105 | - '*' 106 | - apiGroups: 107 | - kompost.chuge.li 108 | resources: 109 | - temporalnamespaces/status 110 | verbs: 111 | - '*' 112 | -------------------------------------------------------------------------------- /priv/charts/kompost/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ include "kompost.fullname" . }} 6 | labels: 7 | {{- include "kompost.labels" . | nindent 4 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: {{ include "kompost.fullname" . }} 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "kompost.serviceAccountName" . }} 15 | namespace: {{ .Release.Namespace }} 16 | -------------------------------------------------------------------------------- /priv/charts/kompost/templates/crds/postgresclusterinstance.crd.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.kompos.postgres.enabled }} 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: postgresclusterinstances.kompost.chuge.li 7 | spec: 8 | group: kompost.chuge.li 9 | names: 10 | kind: PostgresClusterInstance 11 | plural: postgresclusterinstances 12 | shortNames: 13 | - pgcinst 14 | singular: postgresclusterinstance 15 | scope: Cluster 16 | versions: 17 | - deprecationWarning: 18 | name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | properties: 22 | spec: 23 | anyOf: 24 | - required: 25 | - hostname 26 | - port 27 | - username 28 | - passwordSecretRef 29 | - required: 30 | - hostname 31 | - port 32 | - username 33 | - plainPassword 34 | properties: 35 | hostname: 36 | type: string 37 | passwordSecretRef: 38 | properties: 39 | key: 40 | type: string 41 | name: 42 | type: string 43 | required: 44 | - name 45 | - key 46 | type: object 47 | plainPassword: 48 | description: It's not safe to save passwords in plaintext. Consider using passwordSecretRef instead. 49 | type: string 50 | port: 51 | type: integer 52 | ssl: 53 | properties: 54 | ca: 55 | description: CA certificates used to validate the server cert against. 56 | type: string 57 | enabled: 58 | description: Set to true if ssl should be used. 59 | type: boolean 60 | verify: 61 | description: "'verify_none' or 'verify_peer'. Defaults to 'verify_none'" 62 | type: string 63 | type: object 64 | username: 65 | type: string 66 | type: object 67 | status: 68 | properties: 69 | conditions: 70 | items: 71 | properties: 72 | lastHeartbeatTime: 73 | format: date-time 74 | type: string 75 | lastTransitionTime: 76 | format: date-time 77 | type: string 78 | message: 79 | type: string 80 | status: 81 | enum: 82 | - 'True' 83 | - 'False' 84 | type: string 85 | type: 86 | type: string 87 | type: object 88 | type: array 89 | observedGeneration: 90 | type: integer 91 | type: object 92 | required: 93 | - spec 94 | type: object 95 | served: true 96 | storage: true 97 | subresources: 98 | status: {} 99 | {{- end }} -------------------------------------------------------------------------------- /priv/charts/kompost/templates/crds/postgresdatabase.crd.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.kompos.postgres.enabled }} 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: postgresdatabases.kompost.chuge.li 7 | spec: 8 | group: kompost.chuge.li 9 | names: 10 | kind: PostgresDatabase 11 | plural: postgresdatabases 12 | shortNames: 13 | - pgdb 14 | singular: postgresdatabase 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - description: Name of the database on the Postgres instance 19 | jsonPath: .status.sql_db_name 20 | name: Postgres DB name 21 | type: string 22 | deprecationWarning: 23 | name: v1alpha1 24 | schema: 25 | openAPIV3Schema: 26 | properties: 27 | spec: 28 | anyOf: 29 | - required: 30 | - instanceRef 31 | - required: 32 | - clusterInstanceRef 33 | properties: 34 | clusterInstanceRef: 35 | properties: 36 | name: 37 | type: string 38 | type: object 39 | instanceRef: 40 | properties: 41 | name: 42 | type: string 43 | type: object 44 | params: 45 | description: Parameters passed to CREATE TEMPLATE. 46 | properties: 47 | connection_limit: 48 | description: (Optional) How many concurrent connections can be made to this database. -1 (the default) means no limit. 49 | type: integer 50 | encoding: 51 | anyOf: 52 | - type: integer 53 | - type: string 54 | description: (Optional) Character set encoding to use in the new database. Specify a string constant (e.g., 'SQL_ASCII'), or an integer encoding number. 55 | x-kubernetes-int-or-string: true 56 | is_template: 57 | description: (Optional) If true, then this database can be cloned by any user with CREATEDB privileges; if false (the default), then only superusers or the owner of the database can clone it. 58 | type: boolean 59 | lc_collate: 60 | description: (Optional) Collation order (LC_COLLATE) to use in the new database. 61 | type: string 62 | lc_ctype: 63 | description: (Optional) Character classification (LC_CTYPE) to use in the new database. 64 | type: string 65 | locale: 66 | description: (Optional) This is a shortcut for setting lc_collate and lc_type at once. 67 | type: string 68 | template: 69 | description: (Optional) The name of the template from which to create the new database. 70 | type: string 71 | type: object 72 | type: object 73 | status: 74 | properties: 75 | conditions: 76 | items: 77 | properties: 78 | lastHeartbeatTime: 79 | format: date-time 80 | type: string 81 | lastTransitionTime: 82 | format: date-time 83 | type: string 84 | message: 85 | type: string 86 | status: 87 | enum: 88 | - 'True' 89 | - 'False' 90 | type: string 91 | type: 92 | type: string 93 | type: object 94 | type: array 95 | observedGeneration: 96 | type: integer 97 | sql_db_name: 98 | type: string 99 | users: 100 | items: 101 | properties: 102 | secret: 103 | type: string 104 | username: 105 | type: string 106 | type: object 107 | type: array 108 | type: object 109 | required: 110 | - spec 111 | type: object 112 | served: true 113 | storage: true 114 | subresources: 115 | status: {} 116 | {{- end }} -------------------------------------------------------------------------------- /priv/charts/kompost/templates/crds/postgresinstance.crd.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.kompos.postgres.enabled }} 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: postgresinstances.kompost.chuge.li 7 | spec: 8 | group: kompost.chuge.li 9 | names: 10 | kind: PostgresInstance 11 | plural: postgresinstances 12 | shortNames: 13 | - pginst 14 | singular: postgresinstance 15 | scope: Namespaced 16 | versions: 17 | - deprecationWarning: 18 | name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | properties: 22 | spec: 23 | anyOf: 24 | - required: 25 | - hostname 26 | - port 27 | - username 28 | - passwordSecretRef 29 | - required: 30 | - hostname 31 | - port 32 | - username 33 | - plainPassword 34 | properties: 35 | hostname: 36 | type: string 37 | passwordSecretRef: 38 | properties: 39 | key: 40 | type: string 41 | name: 42 | type: string 43 | required: 44 | - name 45 | - key 46 | type: object 47 | plainPassword: 48 | description: It's not safe to save passwords in plaintext. Consider using passwordSecretRef instead. 49 | type: string 50 | port: 51 | type: integer 52 | ssl: 53 | properties: 54 | ca: 55 | description: CA certificates used to validate the server cert against. 56 | type: string 57 | enabled: 58 | description: Set to true if ssl should be used. 59 | type: boolean 60 | verify: 61 | description: "'verify_none' or 'verify_peer'. Defaults to 'verify_none'" 62 | type: string 63 | type: object 64 | username: 65 | type: string 66 | type: object 67 | status: 68 | properties: 69 | conditions: 70 | items: 71 | properties: 72 | lastHeartbeatTime: 73 | format: date-time 74 | type: string 75 | lastTransitionTime: 76 | format: date-time 77 | type: string 78 | message: 79 | type: string 80 | status: 81 | enum: 82 | - 'True' 83 | - 'False' 84 | type: string 85 | type: 86 | type: string 87 | type: object 88 | type: array 89 | observedGeneration: 90 | type: integer 91 | type: object 92 | required: 93 | - spec 94 | type: object 95 | served: true 96 | storage: true 97 | subresources: 98 | status: {} 99 | {{- end }} -------------------------------------------------------------------------------- /priv/charts/kompost/templates/crds/temporalapiserver.crd.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.kompos.temporal.enabled }} 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: temporalapiservers.kompost.chuge.li 7 | spec: 8 | group: kompost.chuge.li 9 | names: 10 | kind: TemporalApiServer 11 | plural: temporalapiservers 12 | shortNames: 13 | - tmprlas 14 | singular: temporalapiserver 15 | scope: Namespaced 16 | versions: 17 | - deprecationWarning: 18 | name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | properties: 22 | spec: 23 | anyOf: 24 | - required: 25 | - host 26 | - port 27 | - required: 28 | - host 29 | - port 30 | properties: 31 | host: 32 | type: string 33 | port: 34 | type: integer 35 | type: object 36 | status: 37 | properties: 38 | conditions: 39 | items: 40 | properties: 41 | lastHeartbeatTime: 42 | format: date-time 43 | type: string 44 | lastTransitionTime: 45 | format: date-time 46 | type: string 47 | message: 48 | type: string 49 | status: 50 | enum: 51 | - 'True' 52 | - 'False' 53 | type: string 54 | type: 55 | type: string 56 | type: object 57 | type: array 58 | observedGeneration: 59 | type: integer 60 | type: object 61 | required: 62 | - spec 63 | type: object 64 | served: true 65 | storage: true 66 | subresources: 67 | status: {} 68 | {{- end }} -------------------------------------------------------------------------------- /priv/charts/kompost/templates/crds/temporalnamespace.crd.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.kompos.temporal.enabled }} 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: temporalnamespaces.kompost.chuge.li 7 | spec: 8 | group: kompost.chuge.li 9 | names: 10 | kind: TemporalNamespace 11 | plural: temporalnamespaces 12 | shortNames: 13 | - tmprlns 14 | singular: temporalnamespace 15 | scope: Namespaced 16 | versions: 17 | - deprecationWarning: 18 | name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | properties: 22 | spec: 23 | properties: 24 | apiServerRef: 25 | description: Referemce to a resource of kind TemporalApiServer 26 | properties: 27 | name: 28 | type: string 29 | namespace: 30 | type: string 31 | type: object 32 | description: 33 | description: Namespace description 34 | type: string 35 | ownerEmail: 36 | type: string 37 | workflowExecutionRetentionPeriod: 38 | description: Workflow execution retention period in seconds 39 | minimum: 3600 40 | type: integer 41 | required: 42 | - apiServerRef 43 | - description 44 | - workflowExecutionRetentionPeriod 45 | type: object 46 | status: 47 | properties: 48 | conditions: 49 | items: 50 | properties: 51 | lastHeartbeatTime: 52 | format: date-time 53 | type: string 54 | lastTransitionTime: 55 | format: date-time 56 | type: string 57 | message: 58 | type: string 59 | status: 60 | enum: 61 | - 'True' 62 | - 'False' 63 | type: string 64 | type: 65 | type: string 66 | type: object 67 | type: array 68 | observedGeneration: 69 | type: integer 70 | type: object 71 | required: 72 | - spec 73 | type: object 74 | served: true 75 | storage: true 76 | subresources: 77 | status: {} 78 | {{- end }} -------------------------------------------------------------------------------- /priv/charts/kompost/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "kompost.fullname" . }} 6 | namespace: {{ .Release.Namespace | quote }} 7 | labels: 8 | {{- include "kompost.labels" . | nindent 4 }} 9 | spec: 10 | replicas: {{ .Values.replicaCount }} 11 | revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} 12 | selector: 13 | matchLabels: 14 | {{- include "kompost.selectorLabels" . | nindent 6 }} 15 | template: 16 | metadata: 17 | {{- with .Values.podAnnotations }} 18 | annotations: 19 | {{- toYaml . | nindent 8 }} 20 | {{- end }} 21 | labels: 22 | {{- include "kompost.selectorLabels" . | nindent 8 }} 23 | spec: 24 | {{- with .Values.imagePullSecrets }} 25 | imagePullSecrets: 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | serviceAccountName: {{ include "kompost.serviceAccountName" . }} 29 | {{- with .Values.podSecurityContext }} 30 | securityContext: 31 | {{- toYaml . | nindent 8 }} 32 | {{- end }} {{ if not .Values.webhook.certManager.enabled }} 33 | initContainers: 34 | - args: 35 | - eval 36 | - Kompost.Webhooks.bootstrap_tls(:prod, "{{ include "kompost.fullname" . }}-webhook-tls") 37 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 38 | imagePullPolicy: {{ .Values.image.pullPolicy }} 39 | name: init-certificates 40 | securityContext: 41 | {{- toYaml .Values.containerSecurityContext | nindent 12 }} 42 | resources: 43 | {{- toYaml .Values.resources | nindent 12 }} 44 | {{- end }} 45 | containers: 46 | - name: {{ .Chart.Name }} 47 | {{- with .Values.containerSecurityContext }} 48 | securityContext: 49 | {{- toYaml . | nindent 12 }} 50 | {{- end }} 51 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 52 | imagePullPolicy: {{ .Values.image.pullPolicy }} 53 | ports: 54 | - containerPort: 4000 55 | name: webhooks 56 | protocol: TCP 57 | env: 58 | - name : MIX_ENV 59 | value: prod 60 | - name: BONNY_OPERATOR_NAME 61 | value: {{ include "kompost.fullname" . }} 62 | - name: BONNY_POD_NAME 63 | valueFrom: 64 | fieldRef: 65 | fieldPath: metadata.name 66 | - name: BONNY_POD_NAMESPACE 67 | valueFrom: 68 | fieldRef: 69 | fieldPath: metadata.namespace 70 | - name: BONNY_POD_IP 71 | valueFrom: 72 | fieldRef: 73 | fieldPath: status.podIP 74 | - name: BONNY_POD_SERVICE_ACCOUNT 75 | valueFrom: 76 | fieldRef: 77 | fieldPath: spec.serviceAccountName 78 | - name: KOMPO_POSTGRES_ENABLED 79 | value: "{{ ternary "true" "false" .Values.kompos.postgres.enabled }}" 80 | - name: KOMPO_TEMPORAL_ENABLED 81 | value: "{{ ternary "true" "false" .Values.kompos.temporal.enabled }}" 82 | # Not implemented yet: 83 | # livenessProbe: 84 | # httpGet: 85 | # path: / 86 | # port: http 87 | # readinessProbe: 88 | # httpGet: 89 | # path: / 90 | # port: http 91 | resources: 92 | {{- toYaml .Values.resources | nindent 12 }} 93 | volumeMounts: 94 | - name: certs 95 | mountPath: /mnt/cert 96 | readOnly: true 97 | {{- with .Values.nodeSelector }} 98 | nodeSelector: 99 | {{- toYaml . | nindent 8 }} 100 | {{- end }} 101 | {{- with .Values.affinity }} 102 | affinity: 103 | {{- toYaml . | nindent 8 }} 104 | {{- end }} 105 | {{- with .Values.tolerations }} 106 | tolerations: 107 | {{- toYaml . | nindent 8 }} 108 | {{- end }} 109 | volumes: 110 | - name: certs 111 | secret: 112 | optional: true 113 | secretName: {{ include "kompost.fullname" . }}-webhook-tls 114 | -------------------------------------------------------------------------------- /priv/charts/kompost/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | --- 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | name: {{ include "kompost.serviceAccountName" . }} 7 | namespace: {{ .Release.Namespace | quote }} 8 | labels: 9 | {{- include "kompost.labels" . | nindent 4 }} 10 | {{- with .Values.serviceAccount.annotations }} 11 | annotations: 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /priv/charts/kompost/templates/validatingwebhookconfiguration.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: ValidatingWebhookConfiguration 4 | metadata: 5 | name: {{ include "kompost.fullname" . }} 6 | labels: 7 | {{- include "kompost.labels" . | nindent 4 }} 8 | {{- if and .Values.webhook.certManager.enabled .Values.webhook.certManager.addInjectorAnnotations }} 9 | annotations: 10 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "kompost.fullname" . }}-webhook 11 | {{- end }} 12 | webhooks: 13 | - admissionReviewVersions: 14 | - v1 15 | clientConfig: 16 | service: 17 | name: {{ include "kompost.fullname" . }}-webhook 18 | namespace: {{ .Release.Namespace | quote }} 19 | path: /postgres/admission-review/validating 20 | port: 443 21 | failurePolicy: Ignore 22 | matchPolicy: Equivalent 23 | name: postgres.kompost.chuge.li 24 | rules: 25 | - apiGroups: 26 | - kompost.chuge.li 27 | apiVersions: 28 | - v1alpha1 29 | operations: 30 | - CREATE 31 | - UPDATE 32 | resources: 33 | - postgresdatabases 34 | - postgresclusterinstances 35 | - postgresinstances 36 | sideEffects: None 37 | -------------------------------------------------------------------------------- /priv/charts/kompost/templates/webhook.certificate.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.certManager.enabled }} 2 | --- 3 | apiVersion: cert-manager.io/v1 4 | kind: Certificate 5 | metadata: 6 | name: {{ include "kompost.fullname" . }}-webhook 7 | namespace: {{ .Release.Namespace | quote }} 8 | labels: 9 | {{- include "kompost.labels" . | nindent 4 }} 10 | {{- with .Values.webhook.certManager.cert.annotations }} 11 | annotations: 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | commonName: {{ include "kompost.fullname" . }}-webhook 16 | dnsNames: 17 | - {{ include "kompost.fullname" . }}-webhook 18 | - {{ include "kompost.fullname" . }}-webhook.{{ .Release.Namespace }} 19 | - {{ include "kompost.fullname" . }}-webhook.{{ .Release.Namespace }}.svc 20 | issuerRef: 21 | {{- toYaml .Values.webhook.certManager.cert.issuerRef | nindent 4 }} 22 | {{- with .Values.webhook.certManager.cert.duration }} 23 | duration: {{ . | quote }} 24 | {{- end }} 25 | {{- with .Values.webhook.certManager.cert.renewBefore }} 26 | renewBefore: {{ . | quote }} 27 | {{- end }} 28 | secretName: {{ include "kompost.fullname" . }}-webhook-tls 29 | {{- end }} -------------------------------------------------------------------------------- /priv/charts/kompost/templates/webhook.service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "kompost.fullname" . }}-webhook 6 | namespace: {{ .Release.Namespace | quote }} 7 | labels: 8 | {{- include "kompost.labels" . | nindent 4 }} 9 | spec: 10 | type: ClusterIP 11 | ports: 12 | - port: 443 13 | targetPort: webhook 14 | protocol: TCP 15 | name: http 16 | selector: 17 | {{- include "kompost.selectorLabels" . | nindent 4 }} 18 | -------------------------------------------------------------------------------- /priv/charts/kompost/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for kompost. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | # -- The number of pods to run 6 | replicaCount: 1 7 | 8 | # -- The number of revisions to keep in history 9 | revisionHistoryLimit: 10 10 | 11 | image: 12 | # -- Kompost container image repository 13 | repository: ghcr.io/mruoss/kompost 14 | # -- imagePullPolicy applied to the containers 15 | pullPolicy: IfNotPresent 16 | # -- Overrides the image tag whose default is the chart appVersion. 17 | tag: "" 18 | 19 | kompos: 20 | postgres: 21 | # -- Install CRDS for and run postgres kompo 22 | enabled: true 23 | temporal: 24 | # -- Install CRDS for and run temporal kompo 25 | enabled: true 26 | 27 | # -- Secrets with credentials to pull images from a private registry 28 | imagePullSecrets: [] 29 | # -- Provide a name in place of `kompost` 30 | nameOverride: kompost 31 | # -- String to fully override `"kompost.fullname"` 32 | fullnameOverride: "" 33 | 34 | serviceAccount: 35 | # -- Specifies whether a service account should be created 36 | create: true 37 | # -- Annotations to add to the service account 38 | annotations: {} 39 | # -- The name of the service account to use. 40 | # -- If not set and create is true, a name is generated using the fullname template 41 | name: "" 42 | 43 | webhook: 44 | certManager: 45 | # -- Enabling cert-manager support will disable the built in secret creation and 46 | # switch to using cert-manager (installed separately) to automatically issue 47 | # and renew the webhook certificate. This chart does not install 48 | # cert-manager for you, See https://cert-manager.io/docs/ 49 | enabled: false 50 | 51 | # -- Automatically add the cert-manager.io/inject-ca-from annotation to the 52 | # webhooks and CRDs. As long as you have the cert-manager CA Injector 53 | # enabled, this will automatically setup your webhook's CA to the one used 54 | # by cert-manager. See https://cert-manager.io/docs/concepts/ca-injector 55 | addInjectorAnnotations: true 56 | 57 | cert: 58 | # -- Create a certificate resource within this chart. See 59 | # https://cert-manager.io/docs/usage/certificate/ 60 | create: true 61 | 62 | # -- For the Certificate created by this chart, setup the issuer. See 63 | # https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.IssuerSpec 64 | issuerRef: 65 | group: cert-manager.io 66 | kind: "Issuer" 67 | name: "my-issuer" 68 | 69 | # -- Set the requested duration (i.e. lifetime) of the Certificate. See 70 | # https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec 71 | duration: "" 72 | 73 | # -- How long before the currently issued certificate’s expiry 74 | # cert-manager should renew the certificate. See 75 | # https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec 76 | # Note that renewBefore should be greater than .webhook.lookaheadInterval 77 | # since the webhook will check this far in advance that the certificate is 78 | # valid. 79 | renewBefore: "" 80 | 81 | # -- Add extra annotations to the Certificate resource. 82 | annotations: {} 83 | 84 | podAnnotations: {} 85 | 86 | podSecurityContext: 87 | runAsNonRoot: true 88 | runAsUser: 1001 89 | 90 | containerSecurityContext: 91 | capabilities: 92 | drop: 93 | - ALL 94 | readOnlyRootFilesystem: true 95 | runAsNonRoot: true 96 | runAsUser: 1001 97 | 98 | # No endpoints other than admission webhooks at the moment 99 | # ingress: 100 | # enabled: false 101 | # className: "" 102 | # annotations: {} 103 | # # kubernetes.io/ingress.class: nginx 104 | # # kubernetes.io/tls-acme: "true" 105 | # hosts: 106 | # - host: chart-example.local 107 | # paths: 108 | # - path: / 109 | # pathType: ImplementationSpecific 110 | # tls: [] 111 | # # - secretName: chart-example-tls 112 | # # hosts: 113 | # # - chart-example.local 114 | 115 | resources: {} 116 | # limits: 117 | # cpu: 100m 118 | # memory: 128Mi 119 | # requests: 120 | # cpu: 100m 121 | # memory: 128Mi 122 | 123 | nodeSelector: {} 124 | 125 | tolerations: [] 126 | 127 | affinity: {} 128 | -------------------------------------------------------------------------------- /priv/charts/pubkey.asc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mruoss/kompost/53aa5ba30af0c8a98c5f8ce30c1856c3abe73faa/priv/charts/pubkey.asc -------------------------------------------------------------------------------- /priv/manifest/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | k8s-app: kompost 7 | name: kompost 8 | rules: 9 | - apiGroups: 10 | - admissionregistration.k8s.io 11 | resources: 12 | - validatingwebhookconfigurations 13 | - mutatingwebhookconfigurations 14 | verbs: 15 | - get 16 | - list 17 | - update 18 | - patch 19 | - apiGroups: 20 | - apiextensions.k8s.io 21 | resources: 22 | - customresourcedefinitions 23 | verbs: 24 | - get 25 | - list 26 | - update 27 | - patch 28 | - apiGroups: 29 | - "" 30 | resources: 31 | - secrets 32 | verbs: 33 | - '*' 34 | - apiGroups: 35 | - apiextensions.k8s.io 36 | resources: 37 | - customresourcedefinitions 38 | verbs: 39 | - '*' 40 | - apiGroups: 41 | - coordination.k8s.io 42 | resources: 43 | - leases 44 | verbs: 45 | - '*' 46 | - apiGroups: 47 | - events.k8s.io 48 | resources: 49 | - events 50 | verbs: 51 | - '*' 52 | - apiGroups: 53 | - kompost.chuge.li 54 | resources: 55 | - postgresclusterinstances 56 | verbs: 57 | - '*' 58 | - apiGroups: 59 | - kompost.chuge.li 60 | resources: 61 | - postgresclusterinstances/status 62 | verbs: 63 | - '*' 64 | - apiGroups: 65 | - kompost.chuge.li 66 | resources: 67 | - postgresdatabases 68 | verbs: 69 | - '*' 70 | - apiGroups: 71 | - kompost.chuge.li 72 | resources: 73 | - postgresdatabases/status 74 | verbs: 75 | - '*' 76 | - apiGroups: 77 | - kompost.chuge.li 78 | resources: 79 | - postgresinstances 80 | verbs: 81 | - '*' 82 | - apiGroups: 83 | - kompost.chuge.li 84 | resources: 85 | - postgresinstances/status 86 | verbs: 87 | - '*' 88 | - apiGroups: 89 | - kompost.chuge.li 90 | resources: 91 | - temporalapiservers 92 | verbs: 93 | - '*' 94 | - apiGroups: 95 | - kompost.chuge.li 96 | resources: 97 | - temporalapiservers/status 98 | verbs: 99 | - '*' 100 | - apiGroups: 101 | - kompost.chuge.li 102 | resources: 103 | - temporalnamespaces 104 | verbs: 105 | - '*' 106 | - apiGroups: 107 | - kompost.chuge.li 108 | resources: 109 | - temporalnamespaces/status 110 | verbs: 111 | - '*' 112 | -------------------------------------------------------------------------------- /priv/manifest/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | labels: 6 | k8s-app: kompost 7 | name: kompost 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: kompost 12 | subjects: 13 | - kind: ServiceAccount 14 | name: kompost 15 | namespace: kompost 16 | -------------------------------------------------------------------------------- /priv/manifest/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | labels: 6 | k8s-app: kompost 7 | name: kompost 8 | namespace: kompost 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | k8s-app: kompost 14 | template: 15 | metadata: 16 | labels: 17 | k8s-app: kompost 18 | spec: 19 | containers: 20 | - env: 21 | - name: MIX_ENV 22 | value: prod 23 | - name: BONNY_OPERATOR_NAME 24 | value: kompost 25 | - name: BONNY_POD_NAME 26 | valueFrom: 27 | fieldRef: 28 | fieldPath: metadata.name 29 | - name: BONNY_POD_NAMESPACE 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.namespace 33 | - name: BONNY_POD_IP 34 | valueFrom: 35 | fieldRef: 36 | fieldPath: status.podIP 37 | - name: BONNY_POD_SERVICE_ACCOUNT 38 | valueFrom: 39 | fieldRef: 40 | fieldPath: spec.serviceAccountName 41 | image: kompost:e2e 42 | name: kompost 43 | ports: 44 | - containerPort: 4000 45 | name: webhooks 46 | resources: 47 | limits: 48 | cpu: 200m 49 | memory: 200Mi 50 | requests: 51 | cpu: 200m 52 | memory: 200Mi 53 | securityContext: 54 | allowPrivilegeEscalation: false 55 | readOnlyRootFilesystem: true 56 | runAsNonRoot: true 57 | runAsUser: 1001 58 | volumeMounts: 59 | - mountPath: /mnt/cert 60 | name: certs 61 | initContainers: 62 | - args: 63 | - eval 64 | - Kompost.Webhooks.bootstrap_tls(:prod, "tls-certs") 65 | image: kompost:e2e 66 | name: init-certificates 67 | serviceAccountName: kompost 68 | volumes: 69 | - name: certs 70 | secret: 71 | optional: true 72 | secretName: tls-certs 73 | -------------------------------------------------------------------------------- /priv/manifest/postgresclusterinstance.crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: postgresclusterinstances.kompost.chuge.li 6 | spec: 7 | group: kompost.chuge.li 8 | names: 9 | kind: PostgresClusterInstance 10 | plural: postgresclusterinstances 11 | shortNames: 12 | - pgcinst 13 | singular: postgresclusterinstance 14 | scope: Cluster 15 | versions: 16 | - deprecationWarning: 17 | name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | properties: 21 | spec: 22 | anyOf: 23 | - required: 24 | - hostname 25 | - port 26 | - username 27 | - passwordSecretRef 28 | - required: 29 | - hostname 30 | - port 31 | - username 32 | - plainPassword 33 | properties: 34 | hostname: 35 | type: string 36 | passwordSecretRef: 37 | properties: 38 | key: 39 | type: string 40 | name: 41 | type: string 42 | required: 43 | - name 44 | - key 45 | type: object 46 | plainPassword: 47 | description: It's not safe to save passwords in plaintext. Consider using passwordSecretRef instead. 48 | type: string 49 | port: 50 | type: integer 51 | ssl: 52 | properties: 53 | ca: 54 | description: CA certificates used to validate the server cert against. 55 | type: string 56 | enabled: 57 | description: Set to true if ssl should be used. 58 | type: boolean 59 | verify: 60 | description: "'verify_none' or 'verify_peer'. Defaults to 'verify_none'" 61 | type: string 62 | type: object 63 | username: 64 | type: string 65 | type: object 66 | status: 67 | properties: 68 | conditions: 69 | items: 70 | properties: 71 | lastHeartbeatTime: 72 | format: date-time 73 | type: string 74 | lastTransitionTime: 75 | format: date-time 76 | type: string 77 | message: 78 | type: string 79 | status: 80 | enum: 81 | - 'True' 82 | - 'False' 83 | type: string 84 | type: 85 | type: string 86 | type: object 87 | type: array 88 | observedGeneration: 89 | type: integer 90 | type: object 91 | required: 92 | - spec 93 | type: object 94 | served: true 95 | storage: true 96 | subresources: 97 | status: {} 98 | -------------------------------------------------------------------------------- /priv/manifest/postgresdatabase.crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: postgresdatabases.kompost.chuge.li 6 | spec: 7 | group: kompost.chuge.li 8 | names: 9 | kind: PostgresDatabase 10 | plural: postgresdatabases 11 | shortNames: 12 | - pgdb 13 | singular: postgresdatabase 14 | scope: Namespaced 15 | versions: 16 | - additionalPrinterColumns: 17 | - description: Name of the database on the Postgres instance 18 | jsonPath: .status.sql_db_name 19 | name: Postgres DB name 20 | type: string 21 | deprecationWarning: 22 | name: v1alpha1 23 | schema: 24 | openAPIV3Schema: 25 | properties: 26 | spec: 27 | anyOf: 28 | - required: 29 | - instanceRef 30 | - required: 31 | - clusterInstanceRef 32 | properties: 33 | clusterInstanceRef: 34 | properties: 35 | name: 36 | type: string 37 | type: object 38 | instanceRef: 39 | properties: 40 | name: 41 | type: string 42 | type: object 43 | databaseNamingStrategy: 44 | type: string 45 | enum: ["resource_name", "prefix_namespace"] 46 | description: "(Optional) Strategy to derive the name of the `database` on the server. resource_name uses the resource name as DB name. `prefix_namespace` prefixes the resource name with the namespace. Defaults to `prefix_namespace`" 47 | params: 48 | description: Parameters passed to CREATE TEMPLATE. 49 | properties: 50 | connection_limit: 51 | description: (Optional) How many concurrent connections can be made to this database. -1 (the default) means no limit. 52 | type: integer 53 | encoding: 54 | anyOf: 55 | - type: integer 56 | - type: string 57 | description: (Optional) Character set encoding to use in the new database. Specify a string constant (e.g., 'SQL_ASCII'), or an integer encoding number. 58 | x-kubernetes-int-or-string: true 59 | is_template: 60 | description: (Optional) If true, then this database can be cloned by any user with CREATEDB privileges; if false (the default), then only superusers or the owner of the database can clone it. 61 | type: boolean 62 | lc_collate: 63 | description: (Optional) Collation order (LC_COLLATE) to use in the new database. 64 | type: string 65 | lc_ctype: 66 | description: (Optional) Character classification (LC_CTYPE) to use in the new database. 67 | type: string 68 | locale: 69 | description: (Optional) This is a shortcut for setting lc_collate and lc_type at once. 70 | type: string 71 | template: 72 | description: (Optional) The name of the template from which to create the new database. 73 | type: string 74 | type: object 75 | type: object 76 | status: 77 | properties: 78 | conditions: 79 | items: 80 | properties: 81 | lastHeartbeatTime: 82 | format: date-time 83 | type: string 84 | lastTransitionTime: 85 | format: date-time 86 | type: string 87 | message: 88 | type: string 89 | status: 90 | enum: 91 | - "True" 92 | - "False" 93 | type: string 94 | type: 95 | type: string 96 | type: object 97 | type: array 98 | observedGeneration: 99 | type: integer 100 | sql_db_name: 101 | type: string 102 | users: 103 | items: 104 | properties: 105 | secret: 106 | type: string 107 | username: 108 | type: string 109 | type: object 110 | type: array 111 | type: object 112 | required: 113 | - spec 114 | type: object 115 | served: true 116 | storage: true 117 | subresources: 118 | status: {} 119 | -------------------------------------------------------------------------------- /priv/manifest/postgresinstance.crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: postgresinstances.kompost.chuge.li 6 | spec: 7 | group: kompost.chuge.li 8 | names: 9 | kind: PostgresInstance 10 | plural: postgresinstances 11 | shortNames: 12 | - pginst 13 | singular: postgresinstance 14 | scope: Namespaced 15 | versions: 16 | - deprecationWarning: 17 | name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | properties: 21 | spec: 22 | anyOf: 23 | - required: 24 | - hostname 25 | - port 26 | - username 27 | - passwordSecretRef 28 | - required: 29 | - hostname 30 | - port 31 | - username 32 | - plainPassword 33 | properties: 34 | hostname: 35 | type: string 36 | passwordSecretRef: 37 | properties: 38 | key: 39 | type: string 40 | name: 41 | type: string 42 | required: 43 | - name 44 | - key 45 | type: object 46 | plainPassword: 47 | description: It's not safe to save passwords in plaintext. Consider using passwordSecretRef instead. 48 | type: string 49 | port: 50 | type: integer 51 | ssl: 52 | properties: 53 | ca: 54 | description: CA certificates used to validate the server cert against. 55 | type: string 56 | enabled: 57 | description: Set to true if ssl should be used. 58 | type: boolean 59 | verify: 60 | description: "'verify_none' or 'verify_peer'. Defaults to 'verify_none'" 61 | type: string 62 | type: object 63 | username: 64 | type: string 65 | type: object 66 | status: 67 | properties: 68 | conditions: 69 | items: 70 | properties: 71 | lastHeartbeatTime: 72 | format: date-time 73 | type: string 74 | lastTransitionTime: 75 | format: date-time 76 | type: string 77 | message: 78 | type: string 79 | status: 80 | enum: 81 | - 'True' 82 | - 'False' 83 | type: string 84 | type: 85 | type: string 86 | type: object 87 | type: array 88 | observedGeneration: 89 | type: integer 90 | type: object 91 | required: 92 | - spec 93 | type: object 94 | served: true 95 | storage: true 96 | subresources: 97 | status: {} 98 | -------------------------------------------------------------------------------- /priv/manifest/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | labels: 6 | k8s-app: kompost 7 | name: kompost 8 | namespace: kompost 9 | spec: 10 | ports: 11 | - name: webhooks 12 | port: 443 13 | protocol: TCP 14 | targetPort: webhooks 15 | selector: 16 | k8s-app: kompost 17 | -------------------------------------------------------------------------------- /priv/manifest/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | k8s-app: kompost 7 | name: kompost 8 | namespace: kompost 9 | -------------------------------------------------------------------------------- /priv/manifest/temporalapiserver.crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: temporalapiservers.kompost.chuge.li 6 | spec: 7 | group: kompost.chuge.li 8 | names: 9 | kind: TemporalApiServer 10 | plural: temporalapiservers 11 | shortNames: 12 | - tmprlas 13 | singular: temporalapiserver 14 | scope: Namespaced 15 | versions: 16 | - deprecationWarning: 17 | name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | properties: 21 | spec: 22 | anyOf: 23 | - required: 24 | - host 25 | - port 26 | - required: 27 | - host 28 | - port 29 | properties: 30 | host: 31 | type: string 32 | port: 33 | type: integer 34 | type: object 35 | status: 36 | properties: 37 | conditions: 38 | items: 39 | properties: 40 | lastHeartbeatTime: 41 | format: date-time 42 | type: string 43 | lastTransitionTime: 44 | format: date-time 45 | type: string 46 | message: 47 | type: string 48 | status: 49 | enum: 50 | - 'True' 51 | - 'False' 52 | type: string 53 | type: 54 | type: string 55 | type: object 56 | type: array 57 | observedGeneration: 58 | type: integer 59 | type: object 60 | required: 61 | - spec 62 | type: object 63 | served: true 64 | storage: true 65 | subresources: 66 | status: {} 67 | -------------------------------------------------------------------------------- /priv/manifest/temporalnamespace.crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: temporalnamespaces.kompost.chuge.li 6 | spec: 7 | group: kompost.chuge.li 8 | names: 9 | kind: TemporalNamespace 10 | plural: temporalnamespaces 11 | shortNames: 12 | - tmprlns 13 | singular: temporalnamespace 14 | scope: Namespaced 15 | versions: 16 | - deprecationWarning: 17 | name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | properties: 21 | spec: 22 | properties: 23 | apiServerRef: 24 | description: Referemce to a resource of kind TemporalApiServer 25 | properties: 26 | name: 27 | type: string 28 | namespace: 29 | type: string 30 | type: object 31 | description: 32 | description: Namespace description 33 | type: string 34 | ownerEmail: 35 | type: string 36 | workflowExecutionRetentionPeriod: 37 | description: Workflow execution retention period in seconds 38 | minimum: 3600 39 | type: integer 40 | required: 41 | - apiServerRef 42 | - description 43 | - workflowExecutionRetentionPeriod 44 | type: object 45 | status: 46 | properties: 47 | conditions: 48 | items: 49 | properties: 50 | lastHeartbeatTime: 51 | format: date-time 52 | type: string 53 | lastTransitionTime: 54 | format: date-time 55 | type: string 56 | message: 57 | type: string 58 | status: 59 | enum: 60 | - 'True' 61 | - 'False' 62 | type: string 63 | type: 64 | type: string 65 | type: object 66 | type: array 67 | observedGeneration: 68 | type: integer 69 | type: object 70 | required: 71 | - spec 72 | type: object 73 | served: true 74 | storage: true 75 | subresources: 76 | status: {} 77 | -------------------------------------------------------------------------------- /priv/manifest/validatingwebhookconfiguration.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: ValidatingWebhookConfiguration 4 | metadata: 5 | name: kompost 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | clientConfig: 10 | service: 11 | name: kompost 12 | namespace: kompost 13 | path: /postgres/admission-review/validating 14 | port: 443 15 | failurePolicy: Ignore 16 | matchPolicy: Equivalent 17 | name: postgres.kompost.chuge.li 18 | rules: 19 | - apiGroups: 20 | - kompost.chuge.li 21 | apiVersions: 22 | - v1alpha1 23 | operations: 24 | - CREATE 25 | - UPDATE 26 | resources: 27 | - postgresdatabases 28 | - postgresclusterinstances 29 | - postgresinstances 30 | sideEffects: None 31 | -------------------------------------------------------------------------------- /priv/periphery/postgres.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: postgres 6 | --- 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | metadata: 10 | name: postgres 11 | namespace: postgres 12 | labels: 13 | app: postgres 14 | spec: 15 | selector: 16 | matchLabels: 17 | app: postgres 18 | replicas: 1 19 | strategy: 20 | rollingUpdate: 21 | maxSurge: 25% 22 | maxUnavailable: 25% 23 | type: RollingUpdate 24 | template: 25 | metadata: 26 | labels: 27 | app: postgres 28 | spec: 29 | containers: 30 | - name: postgres 31 | image: postgres:alpine 32 | imagePullPolicy: IfNotPresent 33 | # args: ['-c', 'max_connections=100'] 34 | ports: 35 | - name: main 36 | containerPort: 5432 37 | protocol: TCP 38 | readinessProbe: 39 | exec: 40 | command: 41 | - /bin/sh 42 | - -c 43 | - pg_isready 44 | failureThreshold: 6 45 | initialDelaySeconds: 5 46 | periodSeconds: 10 47 | successThreshold: 1 48 | timeoutSeconds: 5 49 | livenessProbe: 50 | exec: 51 | command: 52 | - /bin/sh 53 | - -c 54 | - pg_isready 55 | failureThreshold: 6 56 | initialDelaySeconds: 30 57 | periodSeconds: 10 58 | successThreshold: 1 59 | timeoutSeconds: 5 60 | resources: 61 | limits: 62 | cpu: 250m 63 | memory: 256Mi 64 | requests: 65 | cpu: 250m 66 | memory: 32Mi 67 | # env: filled by mix kompost.gen.periphery 68 | --- 69 | apiVersion: v1 70 | kind: Service 71 | metadata: 72 | name: postgres 73 | namespace: postgres 74 | spec: 75 | selector: 76 | app: postgres 77 | type: NodePort 78 | ports: 79 | - name: postgres 80 | protocol: TCP 81 | targetPort: main 82 | # nodePort and port: filled by mix kompost.gen.periphery -------------------------------------------------------------------------------- /priv/periphery/temporal.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: temporal 6 | --- 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | metadata: 10 | name: temporal 11 | namespace: temporal 12 | labels: 13 | app: temporal 14 | spec: 15 | selector: 16 | matchLabels: 17 | app: temporal 18 | replicas: 1 19 | strategy: 20 | rollingUpdate: 21 | maxSurge: 25% 22 | maxUnavailable: 25% 23 | type: RollingUpdate 24 | template: 25 | metadata: 26 | labels: 27 | app: temporal 28 | spec: 29 | containers: 30 | - name: temporal 31 | image: temporalio/auto-setup 32 | imagePullPolicy: IfNotPresent 33 | # args: ['-c', 'max_connections=100'] 34 | ports: 35 | - name: main 36 | containerPort: 7233 37 | protocol: TCP 38 | resources: 39 | limits: 40 | cpu: 250m 41 | memory: 256Mi 42 | requests: 43 | cpu: 50m 44 | memory: 32Mi 45 | env: 46 | - name: DB 47 | value: postgresql 48 | - name: POSTGRES_SEEDS 49 | value: postgres.postgres.svc.cluster.local 50 | - name: TEMPORAL_ADDRESS 51 | value: 127.0.0.1:7223 52 | - name: DYNAMIC_CONFIG_FILE_PATH 53 | value: config/dynamicconfig/development-sql.yaml 54 | # DB_PORT filled by mix kompost.gen.periphery 55 | # POSTGRES_USER filled by mix kompost.gen.periphery 56 | # POSTGRES_PWD filled by mix kompost.gen.periphery 57 | volumeMounts: 58 | - name: dynamicconfig 59 | mountPath: /etc/temporal/config/dynamicconfig 60 | volumes: 61 | - name: dynamicconfig 62 | configMap: 63 | name: dynamicconfig 64 | 65 | # configmap: creted by mix kompost.gen.periphery 66 | --- 67 | apiVersion: v1 68 | kind: Service 69 | metadata: 70 | name: temporal 71 | namespace: temporal 72 | spec: 73 | selector: 74 | app: temporal 75 | type: NodePort 76 | ports: 77 | - name: temporal 78 | protocol: TCP 79 | targetPort: main 80 | # nodePort and port: filled by mix kompost.gen.periphery -------------------------------------------------------------------------------- /priv/periphery/temporal/development-sql.yaml: -------------------------------------------------------------------------------- 1 | limit.maxIDLength: 2 | - value: 255 3 | constraints: {} 4 | system.forceSearchAttributesCacheRefreshOnRead: 5 | - value: true # Dev setup only. Please don't turn this on in production. 6 | constraints: {} 7 | -------------------------------------------------------------------------------- /test/integration/.env: -------------------------------------------------------------------------------- 1 | POSTGRES_PASSWORD=password 2 | POSTGRES_USER=postgres 3 | POSTGRES_DB=foo 4 | POSTGRES_EXPOSED_PORT=31436 5 | TEMPORAL_EXPOSED_PORT=31437 6 | TEMPORAL_VERSION=1.20.3 7 | TEMPORAL_UI_VERSION=2.16.1 8 | -------------------------------------------------------------------------------- /test/integration/kind-cluster.yml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | extraPortMappings: 6 | - containerPort: 31436 7 | hostPort: 31436 8 | listenAddress: "127.0.0.1" 9 | - containerPort: 31437 10 | hostPort: 31437 11 | listenAddress: "127.0.0.1" 12 | kubeadmConfigPatches: 13 | - | 14 | kind: ClusterConfiguration 15 | apiServer: 16 | extraArgs: 17 | enable-admission-plugins: MutatingAdmissionWebhook,ValidatingAdmissionWebhook 18 | -------------------------------------------------------------------------------- /test/kompost/kompo/postgres/controller/cluster_instance_controller_integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Controller.InstanceControllerIntegrationTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Kompost.Test.GlobalResourceHelper 5 | alias Kompost.Test.Kompo.Postgres.ResourceHelper 6 | 7 | @namespace "pgcinst-controller-integration" 8 | 9 | setup_all do 10 | timeout = 11 | "TEST_WAIT_TIMEOUT" 12 | |> System.get_env("5000") 13 | |> String.to_integer() 14 | 15 | conn = Kompost.Test.IntegrationHelper.conn!() 16 | GlobalResourceHelper.create_namespace(@namespace, conn) 17 | 18 | on_exit(fn -> 19 | {:ok, _} = 20 | K8s.Client.delete_all("kompost.chuge.li/v1alpha1", "PostgresDatabase", 21 | namespace: @namespace 22 | ) 23 | |> K8s.Client.put_conn(conn) 24 | |> K8s.Client.run() 25 | 26 | Process.sleep(500) 27 | 28 | {:ok, _} = 29 | K8s.Client.delete_all("kompost.chuge.li/v1alpha1", "PostgresClusterInstance") 30 | |> K8s.Client.put_conn(conn) 31 | |> K8s.Client.run() 32 | 33 | Process.sleep(500) 34 | end) 35 | 36 | [conn: conn, timeout: timeout] 37 | end 38 | 39 | setup do 40 | [resource_name: "test-#{:rand.uniform(10000)}"] 41 | end 42 | 43 | describe "Secret Reference" do 44 | @tag :integration 45 | @tag :postgres 46 | test "Reads Secret from operator namespace", %{ 47 | conn: conn, 48 | timeout: timeout, 49 | resource_name: resource_name 50 | } do 51 | GlobalResourceHelper.k8s_apply!( 52 | ResourceHelper.password_secret(resource_name, "kompost"), 53 | conn 54 | ) 55 | 56 | created_instance = 57 | resource_name 58 | |> ResourceHelper.cluster_instance_with_secret_ref() 59 | |> GlobalResourceHelper.k8s_apply!(conn) 60 | 61 | GlobalResourceHelper.wait_for_condition!(created_instance, conn, "Privileged", timeout) 62 | end 63 | end 64 | 65 | describe "Allowed Namespace Annotations" do 66 | @tag :integration 67 | @tag :postgres 68 | test "Can be accessed by any namespace if no annotation set", %{ 69 | conn: conn, 70 | timeout: timeout, 71 | resource_name: resource_name 72 | } do 73 | created_instance = 74 | resource_name 75 | |> ResourceHelper.cluster_instance_with_plain_pw() 76 | |> GlobalResourceHelper.k8s_apply!(conn) 77 | 78 | GlobalResourceHelper.wait_for_condition!(created_instance, conn, "Privileged", timeout) 79 | 80 | created_db = 81 | resource_name 82 | |> ResourceHelper.database(@namespace, {:cluster, created_instance}) 83 | |> GlobalResourceHelper.k8s_apply!(conn) 84 | 85 | GlobalResourceHelper.wait_for_condition!(created_db, conn, "InspectorUser", timeout) 86 | GlobalResourceHelper.wait_for_condition!(created_db, conn, "AppUser", timeout) 87 | end 88 | 89 | @tag :integration 90 | @tag :postgres 91 | test "Can be accessed if namespace is allowed literally", %{ 92 | conn: conn, 93 | timeout: timeout, 94 | resource_name: resource_name 95 | } do 96 | created_instance = 97 | resource_name 98 | |> ResourceHelper.cluster_instance_with_plain_pw( 99 | System.fetch_env!("POSTGRES_PASSWORD"), 100 | annotations: %{"kompost.chuge.li/allowed-namespaces" => "#{@namespace}, other"} 101 | ) 102 | |> GlobalResourceHelper.k8s_apply!(conn) 103 | 104 | GlobalResourceHelper.wait_for_condition!(created_instance, conn, "Privileged", timeout) 105 | 106 | created_db = 107 | resource_name 108 | |> ResourceHelper.database(@namespace, {:cluster, created_instance}) 109 | |> GlobalResourceHelper.k8s_apply!(conn) 110 | 111 | GlobalResourceHelper.wait_for_condition!(created_db, conn, "InspectorUser", timeout) 112 | GlobalResourceHelper.wait_for_condition!(created_db, conn, "AppUser", timeout) 113 | end 114 | 115 | @tag :integration 116 | @tag :postgres 117 | test "Can be accessed if namespace is allowed via regex", %{ 118 | conn: conn, 119 | timeout: timeout, 120 | resource_name: resource_name 121 | } do 122 | created_instance = 123 | resource_name 124 | |> ResourceHelper.cluster_instance_with_plain_pw( 125 | System.fetch_env!("POSTGRES_PASSWORD"), 126 | annotations: %{"kompost.chuge.li/allowed-namespaces" => "pgcinst-[a-z\-]+, other"} 127 | ) 128 | |> GlobalResourceHelper.k8s_apply!(conn) 129 | 130 | GlobalResourceHelper.wait_for_condition!(created_instance, conn, "Privileged", timeout) 131 | 132 | created_db = 133 | resource_name 134 | |> ResourceHelper.database(@namespace, {:cluster, created_instance}) 135 | |> GlobalResourceHelper.k8s_apply!(conn) 136 | 137 | GlobalResourceHelper.wait_for_condition!(created_db, conn, "InspectorUser", timeout) 138 | GlobalResourceHelper.wait_for_condition!(created_db, conn, "AppUser", timeout) 139 | end 140 | 141 | @tag :integration 142 | @tag :postgres 143 | test "Cannot be accessed if namespace is not allowed", %{ 144 | conn: conn, 145 | timeout: timeout, 146 | resource_name: resource_name 147 | } do 148 | created_instance = 149 | resource_name 150 | |> ResourceHelper.cluster_instance_with_plain_pw( 151 | System.fetch_env!("POSTGRES_PASSWORD"), 152 | annotations: %{ 153 | "kompost.chuge.li/allowed-namespaces" => 154 | "pgcinst-controller, pgcinst-controller-integration-2" 155 | } 156 | ) 157 | |> GlobalResourceHelper.k8s_apply!(conn) 158 | 159 | GlobalResourceHelper.wait_for_condition!(created_instance, conn, "Privileged", timeout) 160 | 161 | created_db = 162 | resource_name 163 | |> ResourceHelper.database(@namespace, {:cluster, created_instance}) 164 | |> GlobalResourceHelper.k8s_apply!(conn) 165 | 166 | created_db = GlobalResourceHelper.wait_until_observed!(created_db, conn, timeout) 167 | 168 | conditions = Map.new(created_db["status"]["conditions"], &{&1["type"], &1}) 169 | assert "False" == conditions["ClusterInstanceAccess"]["status"] 170 | 171 | assert conditions["ClusterInstanceAccess"]["message"] =~ 172 | "The referenced PostgresClusterInstance cannot be accesed." 173 | end 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /test/kompost/kompo/postgres/controller/database_controller_e2e_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Controller.DatabaseControllerE2eTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Kompost.Test.GlobalResourceHelper 5 | alias Kompost.Test.Kompo.Postgres.ResourceHelper 6 | 7 | @namespace "postgres-database-controller-e2e" 8 | 9 | setup_all do 10 | timeout = 11 | "TEST_WAIT_TIMEOUT" 12 | |> System.get_env("5000") 13 | |> String.to_integer() 14 | 15 | conn = Kompost.Test.IntegrationHelper.conn!() 16 | GlobalResourceHelper.create_namespace(@namespace, conn) 17 | 18 | on_exit(fn -> 19 | {:ok, _} = 20 | K8s.Client.delete_all("kompost.chuge.li/v1alpha1", "PostgresDatabase", 21 | namespace: @namespace 22 | ) 23 | |> K8s.Client.put_conn(conn) 24 | |> K8s.Client.run() 25 | 26 | Process.sleep(500) 27 | 28 | {:ok, _} = 29 | K8s.Client.delete_all("kompost.chuge.li/v1alpha1", "PostgresInstance", 30 | namespace: @namespace 31 | ) 32 | |> K8s.Client.put_conn(conn) 33 | |> K8s.Client.run() 34 | 35 | Process.sleep(500) 36 | end) 37 | 38 | instance = 39 | "database-integration-test-#{:rand.uniform(10000)}" 40 | |> ResourceHelper.instance_with_plain_pw(@namespace) 41 | |> GlobalResourceHelper.k8s_apply!(conn) 42 | |> GlobalResourceHelper.wait_until_observed!(conn, timeout) 43 | 44 | [conn: conn, instance: instance, timeout: timeout] 45 | end 46 | 47 | setup do 48 | resource_name = "test-#{:rand.uniform(10000)}" 49 | 50 | [resource_name: resource_name] 51 | end 52 | 53 | describe "Admission Webhooks" do 54 | @tag :e2e 55 | @tag :postgres 56 | test "Parameters are applied upon DB creation", %{ 57 | conn: conn, 58 | resource_name: resource_name, 59 | instance: instance, 60 | timeout: timeout 61 | } do 62 | created_resource = 63 | resource_name 64 | |> ResourceHelper.database( 65 | @namespace, 66 | {:namespaced, instance}, 67 | %{ 68 | template: "template0", 69 | encoding: "SQL_ASCII", 70 | locale: "C", 71 | lc_collate: "C", 72 | lc_ctype: "C", 73 | connection_limit: 50, 74 | is_template: true 75 | } 76 | ) 77 | |> GlobalResourceHelper.k8s_apply!(conn) 78 | 79 | created_resource = 80 | GlobalResourceHelper.wait_until_observed!(created_resource, conn, timeout) 81 | 82 | result = 83 | created_resource 84 | |> Bonny.Resource.drop_managed_fields() 85 | |> Bonny.Resource.drop_rv() 86 | |> put_in(~w(spec params encoding), "UTF8") 87 | |> GlobalResourceHelper.k8s_apply(conn) 88 | 89 | assert {:error, %K8s.Client.APIError{message: message}} = result 90 | assert message =~ "The field .spec.params.encoding is immutable." 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/kompost/kompo/postgres/controller/instance_controller_integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.Controller.ClusterInstanceControllerIntegrationTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Kompost.Test.GlobalResourceHelper 5 | alias Kompost.Test.Kompo.Postgres.ResourceHelper 6 | 7 | @namespace "pginst-controller-integration" 8 | 9 | setup_all do 10 | timeout = 11 | "TEST_WAIT_TIMEOUT" 12 | |> System.get_env("5000") 13 | |> String.to_integer() 14 | 15 | conn = Kompost.Test.IntegrationHelper.conn!() 16 | GlobalResourceHelper.create_namespace(@namespace, conn) 17 | 18 | on_exit(fn -> 19 | {:ok, _} = 20 | K8s.Client.delete_all("kompost.chuge.li/v1alpha1", "PostgresDatabase", 21 | namespace: @namespace 22 | ) 23 | |> K8s.Client.put_conn(conn) 24 | |> K8s.Client.run() 25 | 26 | Process.sleep(500) 27 | 28 | {:ok, _} = 29 | K8s.Client.delete_all("kompost.chuge.li/v1alpha1", "PostgresInstance", 30 | namespace: @namespace 31 | ) 32 | |> K8s.Client.put_conn(conn) 33 | |> K8s.Client.run() 34 | 35 | Process.sleep(500) 36 | end) 37 | 38 | [conn: conn, timeout: timeout] 39 | end 40 | 41 | setup do 42 | [resource_name: "test-#{:rand.uniform(10000)}"] 43 | end 44 | 45 | describe "credentials" do 46 | @tag :integration 47 | @tag :postgres 48 | test "Credentials condition status is False if the password secret does not exist", %{ 49 | conn: conn, 50 | timeout: timeout, 51 | resource_name: resource_name 52 | } do 53 | created_resource = 54 | resource_name 55 | |> ResourceHelper.instance_with_secret_ref(@namespace) 56 | |> GlobalResourceHelper.k8s_apply!(conn) 57 | 58 | created_resource = 59 | GlobalResourceHelper.wait_until_observed!(created_resource, conn, timeout) 60 | 61 | conditions = Map.new(created_resource["status"]["conditions"], &{&1["type"], &1}) 62 | assert "False" == conditions["Credentials"]["status"] 63 | end 64 | 65 | @tag :integration 66 | @tag :postgres 67 | test "Credentials condition status is True if password secret exists", %{ 68 | conn: conn, 69 | timeout: timeout, 70 | resource_name: resource_name 71 | } do 72 | GlobalResourceHelper.k8s_apply!( 73 | ResourceHelper.password_secret(resource_name, @namespace), 74 | conn 75 | ) 76 | 77 | created_resource = 78 | resource_name 79 | |> ResourceHelper.instance_with_secret_ref(@namespace) 80 | |> GlobalResourceHelper.k8s_apply!(conn) 81 | 82 | created_resource = 83 | GlobalResourceHelper.wait_until_observed!(created_resource, conn, timeout) 84 | 85 | conditions = Map.new(created_resource["status"]["conditions"], &{&1["type"], &1}) 86 | assert "True" == conditions["Credentials"]["status"] 87 | end 88 | 89 | @tag :integration 90 | @tag :postgres 91 | test "Credentials condition status is True if the plain password in resource", %{ 92 | conn: conn, 93 | timeout: timeout, 94 | resource_name: resource_name 95 | } do 96 | created_resource = 97 | resource_name 98 | |> ResourceHelper.instance_with_plain_pw(@namespace) 99 | |> GlobalResourceHelper.k8s_apply!(conn) 100 | 101 | created_resource = 102 | GlobalResourceHelper.wait_until_observed!(created_resource, conn, timeout) 103 | 104 | conditions = Map.new(created_resource["status"]["conditions"], &{&1["type"], &1}) 105 | assert "True" == conditions["Credentials"]["status"] 106 | end 107 | end 108 | 109 | describe "connection arguments" do 110 | @tag :integration 111 | @tag :postgres 112 | test "Connected condition status is true if arguments are correct", %{ 113 | conn: conn, 114 | timeout: timeout, 115 | resource_name: resource_name 116 | } do 117 | GlobalResourceHelper.k8s_apply!( 118 | ResourceHelper.password_secret(resource_name, @namespace), 119 | conn 120 | ) 121 | 122 | created_resource = 123 | resource_name 124 | |> ResourceHelper.instance_with_secret_ref(@namespace) 125 | |> GlobalResourceHelper.k8s_apply!(conn) 126 | 127 | GlobalResourceHelper.wait_for_condition!(created_resource, conn, "Connected", timeout) 128 | end 129 | 130 | @tag :integration 131 | @tag :postgres 132 | test "Connected condition status is false if arguments are incorrect", %{ 133 | conn: conn, 134 | timeout: timeout, 135 | resource_name: resource_name 136 | } do 137 | created_resource = 138 | resource_name 139 | |> ResourceHelper.instance_with_plain_pw(@namespace, "wrong_password") 140 | |> GlobalResourceHelper.k8s_apply!(conn) 141 | 142 | created_resource = 143 | GlobalResourceHelper.wait_until_observed!(created_resource, conn, timeout) 144 | 145 | conditions = Map.new(created_resource["status"]["conditions"], &{&1["type"], &1}) 146 | assert "False" == conditions["Connected"]["status"] 147 | end 148 | end 149 | 150 | describe "privileges" do 151 | @tag :integration 152 | @tag :postgres 153 | test "Privileged condition status is true if user has privileges", %{ 154 | conn: conn, 155 | timeout: timeout, 156 | resource_name: resource_name 157 | } do 158 | GlobalResourceHelper.k8s_apply!( 159 | ResourceHelper.password_secret(resource_name, @namespace), 160 | conn 161 | ) 162 | 163 | created_resource = 164 | resource_name 165 | |> ResourceHelper.instance_with_secret_ref(@namespace) 166 | |> GlobalResourceHelper.k8s_apply!(conn) 167 | 168 | GlobalResourceHelper.wait_for_condition!(created_resource, conn, "Privileged", timeout) 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /test/kompost/kompo/postgres/database_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.DatabaseTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Kompost.Kompo.Postgres.Database, as: MUT 5 | doctest MUT 6 | end 7 | -------------------------------------------------------------------------------- /test/kompost/kompo/postgres/instance_integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.InstanceIntegrationTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Kompost.Kompo.Postgres.Instance, as: MUT 5 | 6 | setup_all do 7 | {:ok, conn} = 8 | Postgrex.start_link( 9 | host: System.get_env("POSTGRES_HOST", "127.0.0.1"), 10 | port: System.fetch_env!("POSTGRES_EXPOSED_PORT"), 11 | username: System.fetch_env!("POSTGRES_USER"), 12 | password: System.fetch_env!("POSTGRES_PASSWORD"), 13 | host: "127.0.0.1", 14 | database: "postgres" 15 | ) 16 | 17 | Postgrex.query( 18 | conn, 19 | ~s/CREATE ROLE nocreaterole WITH PASSWORD 'password' CREATEDB NOCREATEROLE NOINHERIT LOGIN/, 20 | [] 21 | ) 22 | 23 | Postgrex.query( 24 | conn, 25 | ~s/CREATE ROLE nocreatedb WITH PASSWORD 'password' NOCREATEDB CREATEROLE NOINHERIT LOGIN/, 26 | [] 27 | ) 28 | 29 | on_exit(fn -> 30 | Postgrex.query!(conn, ~s/DROP ROLE IF EXISTS nocreatedb/, []) 31 | Postgrex.query!(conn, ~s/DROP ROLE IF EXISTS nocreaterole/, []) 32 | end) 33 | 34 | :ok 35 | end 36 | 37 | @tag :integration 38 | @tag :postgres 39 | test "returns :ok when all good" do 40 | result = 41 | start_supervised!({ 42 | Postgrex, 43 | host: System.get_env("POSTGRES_HOST", "127.0.0.1"), 44 | port: System.fetch_env!("POSTGRES_EXPOSED_PORT"), 45 | username: System.fetch_env!("POSTGRES_USER"), 46 | password: System.fetch_env!("POSTGRES_PASSWORD"), 47 | host: "127.0.0.1", 48 | database: "postgres" 49 | }) 50 | |> MUT.check_privileges() 51 | 52 | assert :ok == result 53 | end 54 | 55 | @tag :integration 56 | @tag :postgres 57 | test "returns error when user has no privilege to create databases" do 58 | result = 59 | start_supervised!( 60 | {Postgrex, 61 | host: System.get_env("POSTGRES_HOST", "127.0.0.1"), 62 | port: System.fetch_env!("POSTGRES_EXPOSED_PORT"), 63 | username: "nocreatedb", 64 | password: "password", 65 | host: "127.0.0.1", 66 | database: "postgres"} 67 | ) 68 | |> MUT.check_privileges() 69 | 70 | assert {:error, "The user does not have the privilege to create databases"} == 71 | result 72 | end 73 | 74 | @tag :integration 75 | @tag :postgres 76 | test "returns error when user has no privilege to create roles" do 77 | result = 78 | start_supervised!( 79 | {Postgrex, 80 | host: System.get_env("POSTGRES_HOST", "127.0.0.1"), 81 | port: System.fetch_env!("POSTGRES_EXPOSED_PORT"), 82 | username: "nocreaterole", 83 | password: "password", 84 | host: "127.0.0.1", 85 | database: "postgres"} 86 | ) 87 | |> MUT.check_privileges() 88 | 89 | assert {:error, reason} = result 90 | assert reason =~ "The user does not have the privilege to create users" 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/kompost/kompo/postgres/instance_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Postgres.InstanceTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Kompost.Kompo.Postgres.Instance, as: MUT 5 | doctest MUT 6 | end 7 | -------------------------------------------------------------------------------- /test/kompost/kompo/temporal/controller/api_server_controller_integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Temporal.Controller.ApiServerControllerIntegrationTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Kompost.Test.GlobalResourceHelper 5 | alias Kompost.Test.Kompo.Temporal.ResourceHelper 6 | 7 | @namespace "temporal-api-server-controller-integration" 8 | 9 | setup_all do 10 | timeout = 11 | "TEST_WAIT_TIMEOUT" 12 | |> System.get_env("5000") 13 | |> String.to_integer() 14 | 15 | conn = Kompost.Test.IntegrationHelper.conn!() 16 | GlobalResourceHelper.create_namespace(@namespace, conn) 17 | 18 | on_exit(fn -> 19 | {:ok, _} = 20 | K8s.Client.delete_all("kompost.chuge.li/v1alpha1", "TemporalApiServer", 21 | namespace: @namespace 22 | ) 23 | |> K8s.Client.put_conn(conn) 24 | |> K8s.Client.run() 25 | 26 | Process.sleep(1000) 27 | end) 28 | 29 | api_server = 30 | "namespace-integration-test-#{:rand.uniform(10000)}" 31 | |> ResourceHelper.api_server(@namespace) 32 | |> GlobalResourceHelper.k8s_apply!(conn) 33 | |> GlobalResourceHelper.wait_until_observed!(conn, timeout) 34 | 35 | [conn: conn, timeout: timeout, api_server: api_server] 36 | end 37 | 38 | setup do 39 | [resource_name: "test-#{:rand.uniform(10000)}"] 40 | end 41 | 42 | describe "Connected" do 43 | @tag :integration 44 | @tag :temporal 45 | test "Connected condition status is False if connection to temporal could not be established", 46 | %{ 47 | conn: conn, 48 | timeout: timeout, 49 | resource_name: resource_name 50 | } do 51 | created_resource = 52 | resource_name 53 | |> ResourceHelper.api_server(@namespace) 54 | |> put_in(~w(spec host), "nonexistent") 55 | |> GlobalResourceHelper.k8s_apply!(conn) 56 | 57 | created_resource = 58 | GlobalResourceHelper.wait_until_observed!(created_resource, conn, timeout) 59 | 60 | conditions = Map.new(created_resource["status"]["conditions"], &{&1["type"], &1}) 61 | assert "False" == conditions["Connected"]["status"] 62 | end 63 | 64 | @tag :integration 65 | @tag :temporal 66 | test "Connected condition status is True if connection to temporal was established", 67 | %{ 68 | conn: conn, 69 | timeout: timeout, 70 | resource_name: resource_name 71 | } do 72 | created_resource = 73 | resource_name 74 | |> ResourceHelper.api_server(@namespace) 75 | |> GlobalResourceHelper.k8s_apply!(conn) 76 | 77 | GlobalResourceHelper.wait_for_condition!(created_resource, conn, "Connected", timeout) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/kompost/kompo/temporal/controller/namespace_controller_integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Kompo.Temporal.Controller.NamespaceControllerIntegrationTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Kompost.Test.GlobalResourceHelper 5 | alias Kompost.Test.Kompo.Temporal.ResourceHelper 6 | 7 | @namespace "temporal-namespace-controller-integration" 8 | 9 | setup_all do 10 | timeout = 11 | "TEST_WAIT_TIMEOUT" 12 | |> System.get_env("5000") 13 | |> String.to_integer() 14 | 15 | conn = Kompost.Test.IntegrationHelper.conn!() 16 | GlobalResourceHelper.create_namespace(@namespace, conn) 17 | 18 | on_exit(fn -> 19 | {:ok, _} = 20 | K8s.Client.delete_all("kompost.chuge.li/v1alpha1", "TemporalNamespace", 21 | namespace: @namespace 22 | ) 23 | |> K8s.Client.put_conn(conn) 24 | |> K8s.Client.run() 25 | 26 | Process.sleep(500) 27 | 28 | {:ok, _} = 29 | K8s.Client.delete_all("kompost.chuge.li/v1alpha1", "TemporalApiServer", 30 | namespace: @namespace 31 | ) 32 | |> K8s.Client.put_conn(conn) 33 | |> K8s.Client.run() 34 | 35 | Process.sleep(500) 36 | end) 37 | 38 | api_server = 39 | "namespace-integration-test-#{:rand.uniform(10000)}" 40 | |> ResourceHelper.api_server(@namespace) 41 | |> GlobalResourceHelper.k8s_apply!(conn) 42 | |> GlobalResourceHelper.wait_until_observed!(conn, timeout) 43 | 44 | GlobalResourceHelper.wait_for_condition!(api_server, conn, "Connected", timeout) 45 | 46 | [conn: conn, timeout: timeout, api_server: api_server] 47 | end 48 | 49 | setup do 50 | [resource_name: "test-#{:rand.uniform(10000)}"] 51 | end 52 | 53 | describe "Connected" do 54 | @tag :integration 55 | @tag :temporal 56 | test "Connected condition status is False if connection to temporal could not be established", 57 | %{ 58 | conn: conn, 59 | timeout: timeout, 60 | resource_name: resource_name 61 | } do 62 | created_resource = 63 | resource_name 64 | |> ResourceHelper.namespace( 65 | @namespace, 66 | %{"metadata" => %{"namespace" => @namespace, "name" => "inexistent"}}, 67 | %{ 68 | "description" => "Test Namespace", 69 | "workflowExecutionRetentionPeriod" => 7000 70 | } 71 | ) 72 | |> GlobalResourceHelper.k8s_apply!(conn) 73 | 74 | created_resource = 75 | GlobalResourceHelper.wait_until_observed!(created_resource, conn, timeout) 76 | 77 | conditions = Map.new(created_resource["status"]["conditions"], &{&1["type"], &1}) 78 | assert "False" == conditions["Connected"]["status"] 79 | end 80 | 81 | @tag :integration 82 | @tag :temporal 83 | test "Connected condition status is True if connection to temporal was established", 84 | %{ 85 | conn: conn, 86 | timeout: timeout, 87 | resource_name: resource_name, 88 | api_server: api_server 89 | } do 90 | created_resource = 91 | resource_name 92 | |> ResourceHelper.namespace( 93 | @namespace, 94 | api_server, 95 | %{ 96 | "description" => "Test Namespace", 97 | "workflowExecutionRetentionPeriod" => 7000 98 | } 99 | ) 100 | |> GlobalResourceHelper.k8s_apply!(conn) 101 | 102 | GlobalResourceHelper.wait_for_condition!(created_resource, conn, "Connected", timeout) 103 | end 104 | end 105 | 106 | describe "Created" do 107 | # Currently don't know how to provoke this 108 | # @tag :integration 109 | # @tag :temporal 110 | # test "Created condition status is False if Namespace could not be created", 111 | # %{ 112 | # conn: conn, 113 | # timeout: timeout, 114 | # resource_name: resource_name, 115 | # api_server: api_server 116 | # } do 117 | # created_resource = 118 | # resource_name 119 | # |> ResourceHelper.namespace( 120 | # @namespace, 121 | # api_server, 122 | # %{ 123 | # "description" => "Test Namespace", 124 | # "workflowExecutionRetentionPeriod" => -7000 125 | # } 126 | # ) 127 | # |> GlobalResourceHelper.k8s_apply!(conn) 128 | 129 | # created_resource = GlobalResourceHelper.wait_until_observed!(created_resource, conn, timeout) 130 | 131 | # conditions = Map.new(created_resource["status"]["conditions"], &{&1["type"], &1}) 132 | # assert "True" == conditions["Connected"]["status"] 133 | # assert "False" == conditions["Created"]["status"] 134 | # end 135 | 136 | @tag :integration 137 | @tag :temporal 138 | test "Created condition status is True if Namespace was created", 139 | %{ 140 | conn: conn, 141 | timeout: timeout, 142 | resource_name: resource_name, 143 | api_server: api_server 144 | } do 145 | created_resource = 146 | resource_name 147 | |> ResourceHelper.namespace( 148 | @namespace, 149 | api_server, 150 | %{ 151 | "description" => "Test Namespace", 152 | "workflowExecutionRetentionPeriod" => 24 * 60 * 60 153 | } 154 | ) 155 | |> GlobalResourceHelper.k8s_apply!(conn) 156 | 157 | GlobalResourceHelper.wait_for_condition!(created_resource, conn, "Connected", timeout) 158 | GlobalResourceHelper.wait_for_condition!(created_resource, conn, "Created", timeout) 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /test/kompost/tools/namespace_access_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Tools.NamespaceAccessTest do 2 | use ExUnit.Case 3 | 4 | doctest Kompost.Tools.NamespaceAccess 5 | end 6 | -------------------------------------------------------------------------------- /test/kompost/tools/password_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Tools.PasswordTest do 2 | use ExUnit.Case 3 | doctest Kompost.Tools.Password 4 | end 5 | -------------------------------------------------------------------------------- /test/support/global_resource_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Test.GlobalResourceHelper do 2 | @moduledoc """ 3 | Global Resource Helper 4 | """ 5 | 6 | import YamlElixir.Sigil 7 | 8 | @spec create_namespace(name :: binary(), conn :: K8s.Conn.t()) :: map() 9 | def create_namespace(name, conn) do 10 | ~y""" 11 | apiVersion: v1 12 | kind: Namespace 13 | metadata: 14 | name: #{name} 15 | """ 16 | |> k8s_apply!(conn) 17 | end 18 | 19 | @spec apply_opts(resource :: map(), opts :: Keyword.t()) :: map() 20 | def apply_opts(resource, opts) do 21 | labels = Keyword.get(opts, :labels, %{}) 22 | annotations = Keyword.get(opts, :annotations, %{}) 23 | 24 | resource 25 | |> put_in(~w(metadata labels), labels) 26 | |> put_in(~w(metadata annotations), annotations) 27 | end 28 | 29 | defdelegate k8s_apply!(resource, conn), to: Kompost.Tools.Resource 30 | defdelegate k8s_apply(resource, conn), to: Kompost.Tools.Resource 31 | defdelegate delete!(resource, conn), to: Kompost.Tools.Resource 32 | defdelegate wait_until_observed!(resource, conn, timeout), to: Kompost.Tools.Resource 33 | defdelegate wait_until!(resource, conn, find, eval, timeout), to: Kompost.Tools.Resource 34 | 35 | defdelegate wait_for_condition!(resource, conn, condition, status \\ "True", timeout), 36 | to: Kompost.Tools.Resource 37 | end 38 | -------------------------------------------------------------------------------- /test/support/integration_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Test.IntegrationHelper do 2 | @moduledoc false 3 | 4 | @spec conn!() :: K8s.Conn.t() 5 | def conn!(), do: Kompost.K8sConn.get!(:test) 6 | end 7 | -------------------------------------------------------------------------------- /test/support/kompo/postgres/resource_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Test.Kompo.Postgres.ResourceHelper do 2 | @moduledoc false 3 | 4 | import Kompost.Test.GlobalResourceHelper 5 | import YamlElixir.Sigil 6 | 7 | @spec cluster_instance(name :: binary(), opts :: Keyword.t()) :: map() 8 | defp cluster_instance(name, opts) do 9 | ~y""" 10 | apiVersion: kompost.chuge.li/v1alpha1 11 | kind: PostgresClusterInstance 12 | metadata: 13 | name: #{name} 14 | spec: 15 | hostname: #{System.get_env("POSTGRES_HOST", "127.0.0.1")} 16 | port: #{System.fetch_env!("POSTGRES_EXPOSED_PORT")} 17 | username: #{System.fetch_env!("POSTGRES_USER")} 18 | """ 19 | |> apply_opts(opts) 20 | end 21 | 22 | @spec cluster_instance_with_secret_ref(name :: binary(), opts :: Keyword.t()) :: map() 23 | def cluster_instance_with_secret_ref(name, opts \\ []) do 24 | name 25 | |> cluster_instance(opts) 26 | |> put_in(~w(spec passwordSecretRef), %{ 27 | "name" => name, 28 | "key" => "password" 29 | }) 30 | end 31 | 32 | @spec cluster_instance_with_plain_pw( 33 | name :: binary(), 34 | password :: binary(), 35 | opts :: Keyword.t() 36 | ) :: 37 | map() 38 | def cluster_instance_with_plain_pw( 39 | name, 40 | password \\ System.fetch_env!("POSTGRES_PASSWORD"), 41 | opts \\ [] 42 | ) do 43 | name 44 | |> cluster_instance(opts) 45 | |> put_in(~w(spec plainPassword), password) 46 | end 47 | 48 | @spec instance(name :: binary(), namespace :: binary(), opts :: Keyword.t()) :: map() 49 | def instance(name, namespace, opts \\ []) do 50 | ~y""" 51 | apiVersion: kompost.chuge.li/v1alpha1 52 | kind: PostgresInstance 53 | metadata: 54 | name: #{name} 55 | namespace: #{namespace} 56 | spec: 57 | hostname: #{System.get_env("POSTGRES_HOST", "127.0.0.1")} 58 | port: #{System.fetch_env!("POSTGRES_EXPOSED_PORT")} 59 | username: #{System.fetch_env!("POSTGRES_USER")} 60 | """ 61 | |> apply_opts(opts) 62 | end 63 | 64 | @spec instance_with_secret_ref(name :: binary(), namespace :: binary(), opts :: Keyword.t()) :: 65 | map() 66 | def instance_with_secret_ref(name, namespace, opts \\ []) do 67 | name 68 | |> instance(namespace, opts) 69 | |> put_in(~w(spec passwordSecretRef), %{ 70 | "name" => name, 71 | "key" => "password" 72 | }) 73 | end 74 | 75 | @spec instance_with_plain_pw( 76 | name :: binary(), 77 | namespace :: binary(), 78 | password :: binary(), 79 | opts :: Keyword.t() 80 | ) :: 81 | map() 82 | def instance_with_plain_pw( 83 | name, 84 | namespace, 85 | password \\ System.fetch_env!("POSTGRES_PASSWORD"), 86 | opts \\ [] 87 | ) do 88 | name 89 | |> instance(namespace, opts) 90 | |> put_in(~w(spec plainPassword), password) 91 | end 92 | 93 | @spec database( 94 | name :: binary(), 95 | namespace :: binary(), 96 | instance :: {:cluster | :namespaced, map()}, 97 | params :: map(), 98 | opts :: Keyword.t() 99 | ) :: 100 | map() 101 | def database(name, namespace, instance, params \\ %{}, opts \\ []) do 102 | database = 103 | ~y""" 104 | apiVersion: kompost.chuge.li/v1alpha1 105 | kind: PostgresDatabase 106 | metadata: 107 | name: #{name} 108 | namespace: #{namespace} 109 | """ 110 | |> Map.put("spec", %{"params" => params}) 111 | |> apply_opts(opts) 112 | 113 | case instance do 114 | {:cluster, cluster_instance} -> 115 | put_in(database, ~w(spec clusterInstanceRef), %{ 116 | "name" => cluster_instance["metadata"]["name"] 117 | }) 118 | 119 | {:namespaced, ns_instance} -> 120 | put_in(database, ~w(spec instanceRef), %{ 121 | "name" => ns_instance["metadata"]["name"] 122 | }) 123 | end 124 | end 125 | 126 | @spec password_secret(name :: binary(), namespace :: binary()) :: map() 127 | def password_secret(name, namespace) do 128 | ~y""" 129 | apiVersion: v1 130 | kind: Secret 131 | metadata: 132 | name: #{name} 133 | namespace: #{namespace} 134 | stringData: 135 | password: #{System.fetch_env!("POSTGRES_PASSWORD")} 136 | """ 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/support/kompo/temporal/resource_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Kompost.Test.Kompo.Temporal.ResourceHelper do 2 | @moduledoc false 3 | 4 | import Kompost.Test.GlobalResourceHelper 5 | import YamlElixir.Sigil 6 | 7 | @spec api_server(name :: binary(), ns :: binary(), opts :: Keyword.t()) :: map() 8 | def api_server(name, ns, opts \\ []) do 9 | ~y""" 10 | apiVersion: kompost.chuge.li/v1alpha1 11 | kind: TemporalApiServer 12 | metadata: 13 | name: #{name} 14 | namespace: #{ns} 15 | spec: 16 | host: #{System.get_env("TEMPORAL_HOST", "127.0.0.1")} 17 | port: #{System.fetch_env!("TEMPORAL_EXPOSED_PORT")} 18 | """ 19 | |> apply_opts(opts) 20 | end 21 | 22 | @spec namespace( 23 | name :: binary(), 24 | ns :: binary(), 25 | spec :: map(), 26 | api_server :: map(), 27 | opts :: Keyword.t() 28 | ) :: 29 | map() 30 | def namespace(name, ns, api_server, spec, opts \\ []) do 31 | ~y""" 32 | apiVersion: kompost.chuge.li/v1alpha1 33 | kind: TemporalNamespace 34 | metadata: 35 | name: #{name} 36 | namespace: #{ns} 37 | """ 38 | |> Map.put("spec", spec) 39 | |> put_in(~w(spec apiServerRef), %{ 40 | "name" => api_server["metadata"]["name"], 41 | "namespace" => api_server["metadata"]["namespace"] 42 | }) 43 | |> apply_opts(opts) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | DotenvParser.load_file("test/integration/.env") 2 | Application.ensure_all_started([:k8s, :postgrex, :db_connection]) 3 | ExUnit.start(exclude: [:integration, :e2e, :skip]) 4 | --------------------------------------------------------------------------------