├── charm_version ├── workload_version ├── .jujuignore ├── tests └── integration │ ├── __init__.py │ ├── test_charm.py │ ├── test_sharding.py │ └── helpers.py ├── .github ├── actionlint.yaml ├── workflows │ ├── cla.yaml │ ├── release.yaml │ ├── tics_run_sh_ghaction_test.yml │ └── ci.yaml ├── .jira_sync_config.yaml └── ISSUE_TEMPLATE │ └── bug_report.md ├── terraform ├── charm │ ├── replica_set │ │ ├── versions.tf │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── variables.tf │ │ └── README.md │ └── sharded │ │ ├── versions.tf │ │ ├── offers.tf │ │ ├── integrations.tf │ │ ├── outputs.tf │ │ ├── variables.tf │ │ ├── main.tf │ │ └── README.md └── product │ ├── sharded │ ├── versions.tf │ ├── offers.tf │ ├── outputs.tf │ ├── integrations.tf │ ├── main.tf │ ├── variables.tf │ └── README.md │ └── replica_set │ ├── versions.tf │ ├── offers.tf │ ├── outputs.tf │ ├── integrations.tf │ ├── main.tf │ ├── variables.tf │ └── README.md ├── src └── charm.py ├── .gitignore ├── renovate.json ├── config.yaml ├── tox.ini ├── icon.svg ├── metadata.yaml ├── actions.yaml ├── CONTRIBUTING.md ├── Makefile ├── pyproject.toml ├── charmcraft.yaml ├── README.md └── LICENSE /charm_version: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /workload_version: -------------------------------------------------------------------------------- 1 | 6.0.24 2 | -------------------------------------------------------------------------------- /.jujuignore: -------------------------------------------------------------------------------- 1 | /.tox 2 | /venv 3 | *.py[cod] 4 | *.charm 5 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | -------------------------------------------------------------------------------- /.github/actionlint.yaml: -------------------------------------------------------------------------------- 1 | self-hosted-runner: 2 | labels: 3 | - self-hosted 4 | - linux 5 | - amd64 6 | - tiobe 7 | - jammy 8 | -------------------------------------------------------------------------------- /.github/workflows/cla.yaml: -------------------------------------------------------------------------------- 1 | name: cla-check 2 | on: [pull_request] 3 | 4 | jobs: 5 | cla-check: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Check if CLA signed 9 | uses: canonical/has-signed-canonical-cla@v2 10 | -------------------------------------------------------------------------------- /terraform/charm/replica_set/versions.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | terraform { 5 | required_version = ">= 1.6" 6 | required_providers { 7 | juju = { 8 | source = "juju/juju" 9 | version = "~> 1.0" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /terraform/charm/sharded/versions.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | terraform { 5 | required_version = ">= 1.6" 6 | required_providers { 7 | juju = { 8 | source = "juju/juju" 9 | version = "~> 1.0" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /terraform/product/sharded/versions.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | terraform { 5 | required_version = ">= 1.6" 6 | required_providers { 7 | juju = { 8 | source = "juju/juju" 9 | version = "~> 1.0" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /terraform/product/replica_set/versions.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | terraform { 5 | required_version = ">= 1.6" 6 | required_providers { 7 | juju = { 8 | source = "juju/juju" 9 | version = "~> 1.0" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /terraform/charm/sharded/offers.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | 5 | #-------------------------------------------------------- 6 | # 2. OFFERS (if cross model) 7 | #-------------------------------------------------------- 8 | 9 | resource "juju_offer" "mongodb_config_server_offer" { 10 | for_each = length(local.shards_not_in_config_server_model) > 1 ? { "offered" = true } : {} 11 | 12 | application_name = var.config_server.app_name 13 | endpoints = ["config-server"] 14 | model_uuid = var.config_server.model_uuid 15 | } 16 | -------------------------------------------------------------------------------- /terraform/charm/replica_set/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | resource "juju_application" "mongodb_k8s" { 5 | charm { 6 | name = "mongodb-k8s" 7 | channel = var.channel 8 | revision = var.revision 9 | base = var.base 10 | } 11 | config = var.config 12 | name = var.app_name 13 | units = (var.machines == null || length(var.machines) == 0) ? var.units : null 14 | machines = (var.machines == null || length(var.machines) == 0) ? null : var.machines 15 | constraints = var.constraints 16 | storage_directives = var.storage 17 | endpoint_bindings = var.endpoint_bindings 18 | trust = true 19 | 20 | model_uuid = var.model_uuid 21 | } 22 | -------------------------------------------------------------------------------- /.github/.jira_sync_config.yaml: -------------------------------------------------------------------------------- 1 | # Sync GitHub issues to Jira issues 2 | 3 | # Configuration syntax: 4 | # https://github.com/canonical/gh-jira-sync-bot/blob/main/README.md#client-side-configuration 5 | settings: 6 | # Repository specific settings 7 | components: # Jira components that will be added to Jira issue 8 | - mongodb-k8s 9 | 10 | # Settings shared across Data Platform repositories 11 | label_mapping: 12 | # If the GitHub issue does not have a label in this mapping, the Jira issue will be created as a Bug 13 | enhancement: Story 14 | jira_project_key: DPE # https://warthogs.atlassian.net/browse/DPE 15 | status_mapping: 16 | opened: untriaged 17 | closed: done # GitHub issue closed as completed 18 | not_planned: rejected # GitHub issue closed as not planned 19 | add_gh_comment: true 20 | sync_description: false 21 | sync_comments: false 22 | -------------------------------------------------------------------------------- /src/charm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Charm code for MongoDB service on Kubernetes.""" 3 | 4 | # Copyright 2024 Canonical Ltd. 5 | # See LICENSE file for licensing details. 6 | from ops.main import main 7 | from single_kernel_mongo.abstract_charm import AbstractMongoCharm 8 | from single_kernel_mongo.config.literals import Substrates 9 | from single_kernel_mongo.config.relations import PeerRelationNames 10 | from single_kernel_mongo.core.structured_config import MongoDBCharmConfig 11 | from single_kernel_mongo.managers.mongodb_operator import MongoDBOperator 12 | 13 | 14 | class MongoDBK8sCharm(AbstractMongoCharm[MongoDBCharmConfig, MongoDBOperator]): 15 | """Charm the service.""" 16 | 17 | config_type = MongoDBCharmConfig 18 | operator_type = MongoDBOperator 19 | substrate = Substrates.K8S 20 | peer_rel_name = PeerRelationNames.PEERS 21 | name = "mongodb-k8s" 22 | 23 | 24 | if __name__ == "__main__": 25 | main(MongoDBK8sCharm) 26 | -------------------------------------------------------------------------------- /terraform/charm/replica_set/outputs.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | output "app_names" { 5 | description = "Names of of all deployed applications." 6 | value = { 7 | mongodb_k8s = juju_application.mongodb_k8s.name 8 | } 9 | } 10 | 11 | 12 | # Provided integration endpoints 13 | output "provides" { 14 | description = "Map of all \"provides\" endpoints" 15 | value = { 16 | database = "database" 17 | config_server = "config-server" 18 | cluster = "cluster" 19 | grafana_dashboard = "grafana-dashboard" 20 | metrics_endpoint = "metrics-endpoint" 21 | } 22 | } 23 | 24 | # Required integration endpoints 25 | output "requires" { 26 | description = "Map of all \"requires\" endpoints" 27 | value = { 28 | sharding = "sharding" 29 | certificates = "certificates" 30 | s3_credentials = "s3-credentials" 31 | ldap = "ldap" 32 | ldap_certificate_transfer = "ldap-certificate-transfer" 33 | logging = "logging" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: File a bug report 4 | labels: bug 5 | 6 | --- 7 | 8 | 9 | 10 | ## Steps to reproduce 11 | 12 | 1. 13 | 14 | ## Expected behavior 15 | 16 | 17 | ## Actual behavior 18 | 19 | 20 | 21 | ## Versions 22 | 23 | 24 | Operating system: 25 | 26 | 27 | Juju CLI: 28 | 29 | 30 | Juju agent: 31 | 32 | 33 | Charm revision: 34 | 35 | 36 | microk8s: 37 | 38 | ## Log output 39 | 40 | 41 | Juju debug log: 42 | 43 | 44 | 45 | 46 | ## Additional context 47 | 48 | -------------------------------------------------------------------------------- /terraform/product/sharded/offers.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | #-------------------------------------------------------- 5 | # 2. Offers 6 | #-------------------------------------------------------- 7 | 8 | resource "juju_offer" "config_server_mongos_offer" { 9 | for_each = var.config_server.model_uuid != var.mongos_k8s.model_uuid ? { "offered" = true } : {} 10 | 11 | application_name = var.config_server.app_name 12 | endpoints = ["cluster"] 13 | depends_on = [module.mongodb_k8s] 14 | model_uuid = var.config_server.model_uuid 15 | } 16 | 17 | resource "juju_offer" "tls_provider_offer" { 18 | for_each = local.enable_tls && length(local.tls_cross_model_mongo_apps) > 0 ? { "offered" = true } : {} 19 | 20 | application_name = var.self_signed_certificates.app_name 21 | endpoints = ["certificates"] 22 | depends_on = [juju_application.self-signed-certificates["deployed"]] 23 | model_uuid = var.self_signed_certificates.model_uuid 24 | } 25 | 26 | resource "juju_offer" "s3_integrator_offer" { 27 | for_each = var.s3_integrator.model_uuid != var.config_server.model_uuid ? { "offered" = true } : {} 28 | 29 | application_name = var.s3_integrator.app_name 30 | endpoints = ["s3-credentials"] 31 | depends_on = [juju_application.s3_integrator] 32 | model_uuid = var.s3_integrator.model_uuid 33 | } 34 | -------------------------------------------------------------------------------- /terraform/charm/sharded/integrations.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | 5 | #-------------------------------------------------------- 6 | # 3. INTEGRATIONS 7 | #-------------------------------------------------------- 8 | 9 | resource "juju_integration" "mongodb_config_server_same_model_integrations" { 10 | for_each = tomap({ for shard in local.shards_in_config_server_model : shard.app_name => shard }) 11 | model_uuid = each.value.model_uuid 12 | 13 | application { 14 | name = var.config_server.app_name 15 | endpoint = "config-server" 16 | } 17 | application { 18 | name = each.value.app_name 19 | endpoint = "sharding" 20 | } 21 | 22 | depends_on = [ 23 | module.mongodb_config_server, 24 | module.mongodb_shards, 25 | ] 26 | } 27 | 28 | resource "juju_integration" "mongodb_config_server_cross_model_integrations" { 29 | for_each = tomap({ for shard in local.shards_not_in_config_server_model : shard.app_name => shard }) 30 | model_uuid = each.value.model_uuid 31 | 32 | application { 33 | offer_url = juju_offer.mongodb_config_server_offer["offered"].url 34 | } 35 | application { 36 | name = each.value.app_name 37 | endpoint = "sharding" 38 | } 39 | 40 | depends_on = [ 41 | module.mongodb_config_server, 42 | module.mongodb_shards, 43 | juju_offer.mongodb_config_server_offer, 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /terraform/product/replica_set/offers.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | #-------------------------------------------------------- 5 | # 2. Offers 6 | #-------------------------------------------------------- 7 | 8 | resource "juju_offer" "mongodb_client_offer" { 9 | for_each = var.data_integrator.model_uuid != var.mongodb_k8s.model_uuid ? { "offered" = true } : {} 10 | 11 | application_name = var.data_integrator.app_name 12 | endpoints = ["database"] 13 | depends_on = [module.mongodb_k8s] 14 | model_uuid = var.data_integrator.model_uuid 15 | } 16 | 17 | resource "juju_offer" "tls_provider_offer" { 18 | for_each = local.enable_tls && var.self_signed_certificates.model_uuid != var.mongodb_k8s.model_uuid ? { "offered" = true } : {} 19 | 20 | application_name = var.self_signed_certificates.app_name 21 | endpoints = ["certificates"] 22 | depends_on = [juju_application.self-signed-certificates["deployed"]] 23 | model_uuid = var.self_signed_certificates.model_uuid 24 | } 25 | 26 | resource "juju_offer" "s3_integrator_offer" { 27 | for_each = var.s3_integrator.model_uuid != var.mongodb_k8s.model_uuid ? { "offered" = true } : {} 28 | 29 | application_name = var.s3_integrator.app_name 30 | endpoints = ["s3-credentials"] 31 | depends_on = [juju_application.s3_integrator] 32 | model_uuid = var.s3_integrator.model_uuid 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release to 6/edge 2 | 3 | on: 4 | push: 5 | branches: 6 | - 6/edge 7 | 8 | jobs: 9 | lib-check: 10 | name: Check libraries 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 5 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - run: | 19 | # Workaround for https://github.com/canonical/charmcraft/issues/1389#issuecomment-1880921728 20 | touch requirements.txt 21 | - name: Check libs 22 | uses: canonical/charming-actions/check-libraries@2.6.2 23 | with: 24 | # NOTE: CHARMHUB_TOKEN is only allowed in latest/edge, latest/candidate 25 | credentials: "${{ secrets.CHARMHUB_TOKEN }}" 26 | github-token: "${{ secrets.GITHUB_TOKEN }}" 27 | 28 | ci-tests: 29 | needs: 30 | - lib-check 31 | uses: ./.github/workflows/ci.yaml 32 | secrets: inherit 33 | permissions: 34 | contents: write # Needed for Allure Report beta 35 | 36 | release: 37 | name: Release charm 38 | needs: 39 | - ci-tests 40 | uses: canonical/data-platform-workflows/.github/workflows/release_charm_edge.yaml@v35.0.2 41 | with: 42 | track: 6 43 | artifact-prefix: ${{ needs.ci-tests.outputs.artifact-prefix }} 44 | secrets: 45 | charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} 46 | permissions: 47 | contents: write # Needed to create git tags 48 | -------------------------------------------------------------------------------- /terraform/charm/sharded/outputs.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | # Names of deployed applications 5 | output "app_names" { 6 | description = "Names of of all deployed applications." 7 | value = { 8 | mongodb_config_server = module.mongodb_config_server.app_names["mongodb_k8s"] 9 | shards = [ 10 | for shard_module in module.mongodb_shards : shard_module.app_names["mongodb_k8s"] 11 | ] 12 | } 13 | } 14 | 15 | # Provided integration endpoints 16 | output "provides" { 17 | description = "Map of all \"provides\" endpoints" 18 | value = { 19 | database = "database" 20 | config_server = "config-server" 21 | cluster = "cluster" 22 | grafana_dashboard = "grafana-dashboard" 23 | metrics_endpoint = "metrics-endpoint" 24 | } 25 | } 26 | 27 | # Required integration endpoints 28 | output "requires" { 29 | description = "Map of all \"requires\" endpoints" 30 | value = { 31 | sharding = "sharding" 32 | certificates = "certificates" 33 | s3_credentials = "s3-credentials" 34 | ldap = "ldap" 35 | ldap_certificate_transfer = "ldap-certificate-transfer" 36 | logging = "logging" 37 | } 38 | } 39 | 40 | # Offers 41 | output "offers" { 42 | description = "List of offers URLs." 43 | value = { 44 | mongodb_config_server = try(juju_offer.mongodb_config_server_offer["offered"].url, null) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/tics_run_sh_ghaction_test.yml: -------------------------------------------------------------------------------- 1 | name: TICS run self-hosted test (github-action) 2 | 3 | on: 4 | schedule: 5 | - cron: "0 2 * * 6" # Every Saturday 2:00 AM UTC 6 | workflow_dispatch: # Allows manual triggering 7 | 8 | jobs: 9 | build: 10 | runs-on: [self-hosted, linux, amd64, tiobe, jammy] 11 | 12 | steps: 13 | - name: Checkout the project 14 | uses: actions/checkout@v4 15 | 16 | - name: Install system dependencies 17 | run: sudo apt-get update && sudo apt-get install -y python3.10-venv 18 | 19 | - name: Install pipx 20 | run: python3 -m pip install --user pipx && python3 -m pipx ensurepath 21 | 22 | - name: Add pipx to PATH 23 | run: echo "${HOME}/.local/bin" >> "${GITHUB_PATH}" 24 | 25 | - name: Install tox and poetry using pipx 26 | run: | 27 | pipx install tox 28 | pipx install poetry 29 | 30 | - name: Run tox tests to create coverage.xml 31 | run: tox run -e unit 32 | 33 | - name: move results to necessary folder for TICS 34 | run: | 35 | mkdir .cover 36 | mv coverage.xml .cover/coverage.xml 37 | 38 | - name: Run TICS analysis with github-action 39 | uses: tiobe/tics-github-action@v3 40 | with: 41 | mode: qserver 42 | project: mongodb-k8s-operator 43 | branchdir: . 44 | viewerUrl: https://canonical.tiobe.com/tiobeweb/TICS/api/cfg?name=default 45 | ticsAuthToken: ${{ secrets.TICSAUTHTOKEN }} 46 | installTics: true 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | build/ 3 | *.charm 4 | 5 | coverage* 6 | .coverage 7 | __pycache__/ 8 | *.py[cod] 9 | 10 | .vscode 11 | bin/ 12 | lib64 13 | pyvenv.cfg 14 | share/ 15 | .idea/ 16 | .tox/ 17 | 18 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 19 | # example: *tfplan* 20 | 21 | # Ignore CLI configuration files 22 | ######################################################## 23 | # 24 | # Terraform .gitignore 25 | # 26 | ######################################################## 27 | 28 | 29 | # Local .terraform directories 30 | **/.terraform/* 31 | *.terraform.lock.hcl 32 | 33 | # .tfstate files 34 | *.tfstate 35 | *.tfstate.* 36 | 37 | # Crash log files 38 | crash.log 39 | crash.*.log 40 | 41 | # Generated files 42 | *.key 43 | credentials* 44 | 45 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 46 | # password, private keys, and other secrets. These should not be part of version 47 | # control as they are data points which are potentially sensitive and subject 48 | # to change depending on the environment. 49 | *.tfvars 50 | *.tfvars.json 51 | 52 | # Ignore override files as they are usually used to override resources locally and so 53 | # are not checked in 54 | override.tf 55 | override.tf.json 56 | *_override.tf 57 | *_override.tf.json 58 | 59 | # Include override files you do wish to add to version control using negated pattern 60 | # !example_override.tf 61 | 62 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 63 | # example: *tfplan* 64 | 65 | # Ignore CLI configuration files 66 | .terraformrc 67 | terraform.rc 68 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "reviewers": ["delgod", "MiaAltieri"], 7 | "enabledManagers": ["poetry", "pip_requirements", "github-actions", "regex"], 8 | "schedule": ["after 1am and before 2am every weekday"], 9 | "timezone": "Etc/UTC", 10 | "prHourlyLimit": 0, 11 | "packageRules": [ 12 | { 13 | "matchManagers": ["poetry", "pip_requirements", "regex"], 14 | "matchDatasources": ["pypi"], 15 | "groupName": "Python dependencies" 16 | }, { 17 | "matchManagers": ["github-actions"], 18 | "groupName": "GitHub actions" 19 | }, { 20 | "matchPackageNames": ["juju/juju"], 21 | "allowedVersions": "<3.0.0", 22 | "extractVersion": "^juju-(?.*)$", 23 | "groupName": "Juju agent" 24 | }, { 25 | "matchPackageNames": ["juju"], 26 | "allowedVersions": "<3.0.0" 27 | } 28 | ], 29 | "regexManagers": [ 30 | { 31 | "fileMatch": ["^(workflow-templates|\\.github/workflows)/[^/]+\\.ya?ml$"], 32 | "matchStrings": ["agents-version: \\[\"(?.*?)\"\\] +# renovate: latest"], 33 | "depNameTemplate": "juju/juju", 34 | "datasourceTemplate": "github-releases", 35 | "versioningTemplate": "loose", 36 | "extractVersionTemplate": "Juju release" 37 | }, { 38 | "fileMatch": ["(^|/)([\\w-]*)charmcraft\\.ya?ml$"], 39 | "matchStrings": ["- (?.*?)==(?.*?) +# renovate"], 40 | "datasourceTemplate": "pypi", 41 | "versioningTemplate": "loose" 42 | } 43 | ], 44 | "ignorePaths": [], 45 | "ignoreDeps": [] 46 | } 47 | -------------------------------------------------------------------------------- /terraform/product/replica_set/outputs.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | output "app_names" { 5 | description = "Names of of all deployed applications." 6 | value = { 7 | mongodb_k8s = module.mongodb_k8s.app_names["mongodb_k8s"] 8 | data_integrator = juju_application.s3_integrator.name 9 | s3_integrator = juju_application.s3_integrator.name 10 | self_signed_certificates = var.self_signed_certificates != null ? juju_application.self-signed-certificates["deployed"].name : null 11 | } 12 | } 13 | 14 | 15 | # Provided integration endpoints 16 | output "provides" { 17 | description = "Map of all \"provides\" endpoints" 18 | value = { 19 | database = "database" 20 | config_server = "config-server" 21 | cluster = "cluster" 22 | grafana_dashboard = "grafana-dashboard" 23 | metrics_endpoint = "metrics-endpoint" 24 | } 25 | } 26 | 27 | # Required integration endpoints 28 | output "requires" { 29 | description = "Map of all \"requires\" endpoints" 30 | value = { 31 | sharding = "sharding" 32 | certificates = "certificates" 33 | s3_credentials = "s3-credentials" 34 | ldap = "ldap" 35 | ldap_certificate_transfer = "ldap-certificate-transfer" 36 | logging = "logging" 37 | } 38 | } 39 | 40 | # Offers 41 | output "offers" { 42 | description = "List of offers URLs." 43 | value = { 44 | mongodb_client = try(juju_offer.mongodb_client_offer["offered"].url, null) 45 | tls_provider = try(juju_offer.tls_provider_offer["offered"].url, null) 46 | s3_credentials = try(juju_offer.s3_integrator_offer["offered"].url, null) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /terraform/charm/replica_set/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | variable "app_name" { 5 | description = "Application name" 6 | type = string 7 | default = "mongodb-k8s" 8 | } 9 | 10 | variable "channel" { 11 | description = "Charm channel" 12 | type = string 13 | default = "6/stable" 14 | } 15 | 16 | variable "base" { 17 | description = "Charm base (old name: series)" 18 | type = string 19 | default = "ubuntu@22.04" 20 | } 21 | 22 | variable "config" { 23 | description = "Map of charm configuration options" 24 | type = map(string) 25 | default = {} 26 | } 27 | 28 | variable "model_uuid" { 29 | description = "Model UUID" 30 | type = string 31 | } 32 | 33 | variable "revision" { 34 | description = "Charm revision" 35 | type = number 36 | default = null 37 | } 38 | 39 | variable "units" { 40 | description = "Charm units" 41 | type = number 42 | default = 3 43 | } 44 | 45 | variable "constraints" { 46 | description = "String listing constraints for this application" 47 | type = string 48 | default = "arch=amd64" 49 | } 50 | 51 | variable "machines" { 52 | description = "List of machines for placement" 53 | type = set(string) 54 | default = null 55 | } 56 | 57 | variable "storage" { 58 | description = "Map of storage used by the application" 59 | type = map(string) 60 | default = {} 61 | 62 | validation { 63 | condition = length(var.storage) == 0 || lookup(var.storage, "count", 0) <= 2 64 | error_message = "Only two storages are supported" 65 | } 66 | } 67 | 68 | variable "endpoint_bindings" { 69 | description = "Map of endpoint bindings" 70 | type = set(map(string)) 71 | default = [] 72 | } 73 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | options: 5 | auto-delete: 6 | type: boolean 7 | description: | 8 | When a relation is removed, auto-delete ensures that any relevant databases 9 | associated with the relation are also removed 10 | default: false 11 | role: 12 | description: | 13 | role config option exists to deploy the charmed-mongodb application as a shard, 14 | config-server, or as a replica set. 15 | type: string 16 | default: replication 17 | ldap-user-to-dn-mapping: 18 | default: '' 19 | type: string 20 | description: | 21 | A quote-enclosed JSON-string representing an ordered array of documents. Each document contains a regular expression match and either a substitution or ldapQuery template used for transforming the incoming username. Since the list is ordered, this order can be used to provide fine-grained tuning for building the right DN or the right ldapQuery for the user. 22 | example: '[{ 23 | match : "([^@]+)@([^@\\.]+)\\.example\\.com", 24 | substitution: "CN={0},CN=Users,DC={1},DC=example,DC=com" 25 | }]' 26 | ldap-query-template: 27 | default: '' 28 | type: string 29 | description: | 30 | A RFC4516 formatted LDAP query URL template, which is used for authorization. 31 | It must contain either `{USER}` representing the authenticated user, or `{PROVIDED_USER}` representing the supplied username (before authentication or LDAP transformation). 32 | `{PROVIDED_USER}` should be used only if no value in the `ldap-user-to-do-mapping` config option is provided. 33 | If this configuration is not provided, a default string will be computed based on the base_dn returned by the GLAuth k8s charm. 34 | example: “dc=example,dc=com??sub?(&(objectClass=groupOfNames)(member={USER}))" 35 | -------------------------------------------------------------------------------- /terraform/product/sharded/outputs.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | output "app_names" { 5 | description = "Names of of all deployed applications." 6 | value = merge( 7 | module.mongodb_k8s.app_names, 8 | { 9 | "data_integrator" : juju_application.data_integrator.name 10 | "s3_integrator" : juju_application.s3_integrator.name 11 | "self_signed_certificates" : var.self_signed_certificates != null ? juju_application.self-signed-certificates["deployed"].name : null 12 | "mongos_k8s" : juju_application.mongos_k8s.name 13 | } 14 | ) 15 | } 16 | 17 | 18 | # Provided integration endpoints 19 | output "provides" { 20 | description = "Map of all \"provides\" endpoints" 21 | value = { 22 | database = "database" 23 | config_server = "config-server" 24 | cluster = "cluster" 25 | grafana_dashboard = "grafana-dashboard" 26 | metrics_endpoint = "metrics-endpoint" 27 | } 28 | } 29 | 30 | # Required integration endpoints 31 | output "requires" { 32 | description = "Map of all \"requires\" endpoints" 33 | value = { 34 | sharding = "sharding" 35 | certificates = "certificates" 36 | s3_credentials = "s3-credentials" 37 | ldap = "ldap" 38 | ldap_certificate_transfer = "ldap-certificate-transfer" 39 | logging = "logging" 40 | } 41 | } 42 | 43 | # Offers 44 | output "offers" { 45 | description = "List of offers URLs." 46 | value = merge( 47 | module.mongodb_k8s.offers, 48 | { 49 | "config_server_mongos" : try(juju_offer.config_server_mongos_offer["offered"].url, null), 50 | "tls_provider" : try(juju_offer.tls_provider_offer["offered"].url, null), 51 | "s3_credentials" : try(juju_offer.s3_integrator_offer["offered"].url, null) 52 | } 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /terraform/charm/sharded/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | variable "config_server" { 5 | description = "Config server app definition" 6 | type = object({ 7 | app_name = string 8 | model_uuid = string 9 | config = optional(map(string), { "role" : "config-server" }) 10 | channel = optional(string, "6/stable") 11 | base = optional(string, "ubuntu@22.04") 12 | revision = optional(string, null) 13 | units = optional(number, 3) 14 | constraints = optional(string, "arch=amd64") 15 | machines = optional(set(string), null) 16 | storage = optional(map(string), {}) 17 | endpoint_bindings = optional(set(map(string)), []) 18 | }) 19 | 20 | validation { 21 | condition = var.config_server.config["role"] == "config-server" 22 | error_message = "Config option: 'role' must be set to 'config-server'." 23 | } 24 | } 25 | 26 | variable "shards" { 27 | description = "Shard apps" 28 | type = list(object({ 29 | app_name = string 30 | model_uuid = string 31 | config = optional(map(string), { "role" : "shard" }) 32 | channel = optional(string, "6/stable") 33 | base = optional(string, "ubuntu@22.04") 34 | revision = optional(string, null) 35 | units = optional(number, 3) 36 | constraints = optional(string, "arch=amd64") 37 | machines = optional(set(string), null) 38 | storage = optional(map(string), {}) 39 | endpoint_bindings = optional(set(map(string)), []) 40 | })) 41 | default = [] 42 | 43 | validation { 44 | condition = alltrue([for shard in var.shards : (shard.config["role"] == "shard")]) 45 | error_message = "Config option: 'role' must be set to 'shard' in all shard objects." 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | [tox] 5 | no_package = True 6 | skip_missing_interpreters = True 7 | env_list = format, lint 8 | 9 | [vars] 10 | src_path = {tox_root}/src 11 | tests_path = {tox_root}/tests 12 | all_path = {[vars]src_path} {[vars]tests_path} 13 | 14 | [testenv] 15 | set_env = 16 | PYTHONPATH = :{[vars]src_path} 17 | PY_COLORS = 1 18 | allowlist_externals = 19 | poetry 20 | 21 | 22 | [testenv:format] 23 | description = Apply coding style standards to code 24 | commands_pre = 25 | poetry install --only format 26 | commands = 27 | poetry lock 28 | poetry run isort {[vars]all_path} 29 | poetry run black {[vars]all_path} 30 | 31 | [testenv:lint] 32 | description = Check code against coding style standards 33 | allowlist_externals = 34 | {[testenv]allowlist_externals} 35 | find 36 | commands_pre = 37 | poetry install --only lint 38 | commands = 39 | poetry check --lock 40 | poetry run codespell {[vars]all_path} --skip {[vars]src_path}/grafana_dashboards/*.json --skip {[vars]tests_path}/**/data_interfaces.py 41 | poetry run pflake8 --exclude '.git,__pycache__,.tox,build,dist,*.egg_info,venv,tests/integration/relation_tests/application-charm/lib/charms/data_platform_libs/,tests/integration/ha_tests/application_charm/lib/charms/data_platform_libs/' {[vars]all_path} 42 | poetry run isort --check-only --diff {[vars]all_path} 43 | poetry run black --check --diff {[vars]all_path} 44 | find {[vars]all_path} -type f \( -name "*.sh" -o -name "*.bash" \) -exec poetry run shellcheck --color=always \{\} + 45 | 46 | [testenv:integration] 47 | description = Run integration tests 48 | pass_env = 49 | CI 50 | GITHUB_OUTPUT 51 | SECRETS_FROM_GITHUB 52 | commands_pre = 53 | poetry install --only integration 54 | commands = 55 | poetry run pytest -v --tb native --log-cli-level=INFO -s --ignore={[vars]tests_path}/unit/ {posargs} 56 | -------------------------------------------------------------------------------- /terraform/charm/sharded/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | locals { 5 | shards = [ 6 | for app in concat(var.shards != null ? var.shards : []) : app if app != null 7 | ] 8 | 9 | shards_in_config_server_model = [ 10 | for shard in local.shards : 11 | shard if shard != null && shard.model_uuid == var.config_server.model_uuid 12 | ] 13 | 14 | shards_not_in_config_server_model = [ 15 | for shard in local.shards : 16 | shard if shard != null && shard.model_uuid != var.config_server.model_uuid 17 | ] 18 | } 19 | 20 | #-------------------------------------------------------- 21 | # 1. DEPLOYMENTS 22 | #-------------------------------------------------------- 23 | 24 | # config server mongodb app 25 | module "mongodb_config_server" { 26 | source = "../replica_set" 27 | 28 | channel = var.config_server.channel 29 | revision = var.config_server.revision 30 | base = var.config_server.base 31 | 32 | app_name = var.config_server.app_name 33 | units = var.config_server.units 34 | machines = var.config_server.machines 35 | config = merge(var.config_server.config, { "role" : "config-server" }) 36 | model_uuid = var.config_server.model_uuid 37 | constraints = var.config_server.constraints 38 | storage = var.config_server.storage 39 | endpoint_bindings = var.config_server.endpoint_bindings 40 | } 41 | 42 | # shard apps 43 | module "mongodb_shards" { 44 | for_each = { for idx, app in local.shards : idx => app if app != null } 45 | source = "../replica_set" 46 | 47 | channel = each.value.channel 48 | revision = each.value.revision 49 | base = each.value.base 50 | 51 | app_name = each.value.app_name 52 | units = each.value.units 53 | machines = each.value.machines 54 | config = merge(each.value.config, { "role" : "shard" }) 55 | model_uuid = each.value.model_uuid 56 | constraints = each.value.constraints 57 | storage = each.value.storage 58 | } 59 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 14 | 17 | 19 | 22 | 24 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/integration/test_charm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2024 Canonical Ltd. 3 | # See LICENSE file for licensing details. 4 | 5 | import logging 6 | 7 | import pytest 8 | from pymongo import MongoClient 9 | from pytest_operator.plugin import OpsTest 10 | 11 | from .helpers import ( 12 | APP_NAME, 13 | DEPLOYMENT_TIMEOUT, 14 | METADATA, 15 | UNIT_IDS, 16 | check_or_scale_app, 17 | get_address_of_unit, 18 | get_app_name, 19 | ) 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | @pytest.mark.group(1) 25 | @pytest.mark.abort_on_fail 26 | async def test_build_and_deploy(ops_test: OpsTest): 27 | """Build the charm-under-test and deploy it together with related charms. 28 | 29 | Assert on the unit status before any relations/configurations take place. 30 | """ 31 | app_name = await get_app_name(ops_test) 32 | if app_name: 33 | return await check_or_scale_app(ops_test, app_name, len(UNIT_IDS)) 34 | 35 | app_name = APP_NAME 36 | # build and deploy charm from local source folder 37 | charm = await ops_test.build_charm(".") 38 | resources = {"mongodb-image": METADATA["resources"]["mongodb-image"]["upstream-source"]} 39 | await ops_test.model.deploy( 40 | charm, 41 | resources=resources, 42 | application_name=app_name, 43 | num_units=len(UNIT_IDS), 44 | series="jammy", 45 | trust=True, 46 | ) 47 | 48 | # issuing dummy update_status just to trigger an event 49 | await ops_test.model.set_config({"update-status-hook-interval": "10s"}) 50 | 51 | # TODO: remove raise_on_error when we move to juju 3.5 (DPE-4996) 52 | await ops_test.model.wait_for_idle( 53 | apps=[app_name], 54 | status="active", 55 | raise_on_blocked=True, 56 | timeout=DEPLOYMENT_TIMEOUT, 57 | raise_on_error=False, 58 | ) 59 | assert ops_test.model.applications[app_name].units[0].workload_status == "active" 60 | 61 | # effectively disable the update status from firing 62 | await ops_test.model.set_config({"update-status-hook-interval": "60m"}) 63 | 64 | 65 | @pytest.mark.group(1) 66 | @pytest.mark.abort_on_fail 67 | @pytest.mark.parametrize("unit_id", UNIT_IDS) 68 | async def test_application_is_up(ops_test: OpsTest, unit_id: int): 69 | address = await get_address_of_unit(ops_test, unit_id=unit_id) 70 | response = MongoClient(address, directConnection=True).admin.command("ping") 71 | assert response["ok"] == 1 72 | -------------------------------------------------------------------------------- /terraform/charm/replica_set/README.md: -------------------------------------------------------------------------------- 1 | ## Requirements 2 | 3 | | Name | Version | 4 | |------|---------| 5 | | [terraform](#requirement\_terraform) | >= 1.6 | 6 | | [juju](#requirement\_juju) | ~> 1.0 | 7 | 8 | ## Providers 9 | 10 | | Name | Version | 11 | |------|---------| 12 | | [juju](#provider\_juju) | 0.22.0 | 13 | 14 | ## Modules 15 | 16 | No modules. 17 | 18 | ## Resources 19 | 20 | | Name | Type | 21 | |------|------| 22 | | [juju_application.mongodb_k8s](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 23 | 24 | ## Inputs 25 | 26 | | Name | Description | Type | Default | Required | 27 | |------|-------------|------|---------|:--------:| 28 | | [app\_name](#input\_app\_name) | Application name | `string` | `"mongodb-k8s"` | no | 29 | | [base](#input\_base) | Charm base (old name: series) | `string` | `"ubuntu@22.04"` | no | 30 | | [channel](#input\_channel) | Charm channel | `string` | `"6/stable"` | no | 31 | | [config](#input\_config) | Map of charm configuration options | `map(string)` | `{}` | no | 32 | | [constraints](#input\_constraints) | String listing constraints for this application | `string` | `"arch=amd64"` | no | 33 | | [endpoint\_bindings](#input\_endpoint\_bindings) | Map of endpoint bindings | `set(map(string))` | `[]` | no | 34 | | [machines](#input\_machines) | List of machines for placement | `set(string)` | `null` | no | 35 | | [model\_uuid](#input\_model\_uuid) | Model UUID | `string` | n/a | yes | 36 | | [revision](#input\_revision) | Charm revision | `number` | `null` | no | 37 | | [storage](#input\_storage) | Map of storage used by the application | `map(string)` | `{}` | no | 38 | | [units](#input\_units) | Charm units | `number` | `3` | no | 39 | 40 | ## Outputs 41 | 42 | | Name | Description | 43 | |------|-------------| 44 | | [app\_names](#output\_app\_names) | Names of of all deployed applications. | 45 | | [provides](#output\_provides) | Map of all "provides" endpoints | 46 | | [requires](#output\_requires) | Map of all "requires" endpoints | 47 | -------------------------------------------------------------------------------- /metadata.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | name: mongodb-k8s 5 | display-name: | 6 | Charmed Operator for MongoDB 7 | description: | 8 | MongoDB is a general purpose distributed document database. This 9 | charm deploys and operates MongoDB on kubernetes Clusters. It 10 | supports replicated MongoDB databases. 11 | summary: A MongoDB operator charm for Kubernetes 12 | docs: https://discourse.charmhub.io/t/charmed-mongodb-6-k8s-docs/10265 13 | source: https://github.com/canonical/mongodb-k8s-operator 14 | issues: https://github.com/canonical/mongodb-k8s-operator/issues 15 | website: 16 | - https://ubuntu.com/data/mongodb 17 | - https://charmhub.io/mongodb-k8s 18 | - https://github.com/canonical/mongodb-k8s-operator 19 | - https://chat.charmhub.io/charmhub/channels/data-platform 20 | 21 | peers: 22 | database-peers: 23 | interface: mongodb-peers 24 | upgrade-version-a: 25 | interface: upgrade 26 | ldap-peers: 27 | # Stores the data for the ldap relation. 28 | interface: ldap-peers 29 | status-peers: 30 | interface: status-peers 31 | 32 | provides: 33 | database: 34 | interface: mongodb_client 35 | optional: true 36 | obsolete: 37 | interface: mongodb 38 | optional: true 39 | metrics-endpoint: 40 | interface: prometheus_scrape 41 | optional: true 42 | grafana-dashboard: 43 | interface: grafana_dashboard 44 | optional: true 45 | config-server: 46 | interface: shards 47 | optional: true 48 | cluster: 49 | interface: config-server 50 | optional: true 51 | 52 | requires: 53 | certificates: 54 | interface: tls-certificates 55 | limit: 1 56 | optional: true 57 | logging: 58 | interface: loki_push_api 59 | limit: 1 60 | optional: true 61 | s3-credentials: 62 | interface: s3 63 | optional: true 64 | sharding: 65 | interface: shards 66 | # shards can only relate to one config-server 67 | limit: 1 68 | optional: true 69 | ldap: 70 | interface: ldap 71 | limit: 1 72 | optional: true 73 | ldap-certificate-transfer: 74 | interface: certificate_transfer 75 | limit: 1 76 | optional: true 77 | 78 | containers: 79 | mongod: 80 | resource: mongodb-image 81 | mounts: 82 | - storage: mongodb 83 | location: /var/lib/mongodb 84 | resources: 85 | mongodb-image: 86 | type: oci-image 87 | description: OCI image for mongodb 88 | # TODO: Update sha whenever upstream rock changes 89 | upstream-source: ghcr.io/canonical/charmed-mongodb@sha256:b41b746029790f4e65ecc4c60994bd028e1777ceb7d78d38c909014a6abebb1c 90 | storage: 91 | mongodb: 92 | type: filesystem 93 | location: /var/lib/mongodb 94 | mongodb-logs: 95 | type: filesystem 96 | location: /var/log/mongodb 97 | -------------------------------------------------------------------------------- /actions.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | # 4 | get-primary: 5 | description: Report primary replica 6 | 7 | get-password: 8 | description: Change the system user's password used by charm. 9 | It is for internal charm users and SHOULD NOT be used by applications. 10 | params: 11 | username: 12 | type: string 13 | description: The username, the default value 'operator'. 14 | Possible values - operator, monitor. 15 | set-password: 16 | description: Change the system user's password used by charm. 17 | It is for internal charm users and SHOULD NOT be used by applications. 18 | params: 19 | username: 20 | type: string 21 | description: The username, the default value 'operator'. 22 | Possible values - operator, monitor. 23 | password: 24 | type: string 25 | description: The password will be auto-generated if this option is not specified. 26 | 27 | create-backup: 28 | description: Create a database backup. 29 | S3 credentials are retrieved from a relation with the S3 integrator charm. 30 | 31 | list-backups: 32 | description: List available backup_ids in the S3 bucket and path provided by the S3 integrator charm. 33 | 34 | restore: 35 | description: Restore a database backup. 36 | S3 credentials are retrieved from a relation with the S3 integrator charm. 37 | params: 38 | backup-id: 39 | type: string 40 | description: A backup-id to identify the backup to restore. Format of <%Y-%m-%dT%H:%M:%SZ> 41 | 42 | set-tls-private-key: 43 | description: Set the privates key, which will be used for certificate signing requests (CSR). Run for each unit separately. 44 | params: 45 | external-key: 46 | type: string 47 | description: The content of private key for external communications with clients. Content will be auto-generated if this option is not specified. 48 | internal-key: 49 | type: string 50 | description: The content of private key for internal communications with clients. Content will be auto-generated if this option is not specified. 51 | 52 | pre-refresh-check: 53 | description: Check if charm is ready to refresh. 54 | 55 | resume-refresh: 56 | description: Refresh remaining units (after you manually verified that refreshed units are healthy). 57 | params: 58 | force: 59 | type: boolean 60 | default: false 61 | description: | 62 | Potential of *data loss* and *downtime* 63 | 64 | Force refresh of next unit. 65 | 66 | Use to 67 | - force incompatible refresh and/or 68 | - continue refresh if 1+ refreshed units have non-active status 69 | 70 | force-refresh-start: 71 | description: | 72 | Force refresh of this unit. 73 | Potential of data loss and downtime. 74 | 75 | status-detail: 76 | description: Gets statuses of the charm 77 | params: 78 | recompute: 79 | type: boolean 80 | default: false 81 | description: a boolean indicating whether a unit should recompute all statuses. 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Overview 4 | 5 | This documents explains the processes and practices recommended for contributing enhancements to 6 | this operator. 7 | 8 | **Note:** The charm's python business logic is written in a shared library that can be found [here](https://github.com/canonical/mongo-single-kernel-library). This is where python contributions should be made. 9 | 10 | - Generally, before developing enhancements to this charm, you should consider opening an issue [on Single Kernel repository](https://github.com/canonical/mongo-single-kernel-library/issues) explaining your use case. 11 | - If you would like to chat with us about your use-cases or proposed implementation, you can reach us on our [Matrix channel](https://matrix.to/#/#charmhub-data-platform:ubuntu.com) or in [Discourse](https://discourse.charmhub.io/). 12 | - Familiarising yourself with the [Charmed Operator Framework](https://juju.is/docs/sdk) library will help you a lot when working on new features or bug fixes. 13 | - All enhancements require review before being merged. Additionally, new code must pass the tests. Code review typically examines 14 | - code quality 15 | - test coverage 16 | - user experience for Juju administrators of this charm. 17 | - Please help us out in ensuring easy to review branches by rebasing your pull request branch onto the `main` branch. This also avoids merge commits and creates a linear Git commit history. 18 | - Once the code has been merged on the [repository](https://github.com/canonical/mongo-single-kernel-library/) of the Mongo Single Kernel lib, wait for a new version of the [python package](https://pypi.org/project/mongo-charms-single-kernel/) to be published, and create a PR on this repository that bumps the version of the package, and on the 3 other repositories ([MongoDB VM](https://github.com/canonical/mongodb-operator), [Mongos VM](https://github.com/canonical/mongos-operator) and [Mongos k8s](https://github.com/canonical/mongos-k8s-operator)). 19 | - If you added some new interfaces, please don't forget to add them here. 20 | 21 | ### Testing 22 | 23 | ```shell 24 | tox run -e fmt # update your code according to linting rules 25 | tox run -e lint # code style 26 | tox run -e integration # integration tests 27 | tox run -e integration -- 'tests/integration/test_charm.py' --group='1' # charm integration tests 28 | tox # runs 'fmt' and 'lint'environments 29 | ``` 30 | 31 | ## Build charm 32 | 33 | Build the charm in this git repository using: 34 | 35 | ```shell 36 | charmcraft pack 37 | ``` 38 | 39 | ### Deploy 40 | 41 | ```bash 42 | # Create a model 43 | juju add-model dev 44 | 45 | # Enable DEBUG logging 46 | juju model-config logging-config="=INFO;unit=DEBUG" 47 | 48 | # Deploy the charm 49 | juju deploy ./mongodb-k8s_ubuntu-22.04-amd64.charm --resource mongodb-image=ghcr.io/canonical/charmed-mongodb@sha256:7ddb80a3b5ddffa95704a8980fc11037ba1a23273a9805214bc42be9f507107f --num-units=1 50 | ``` 51 | 52 | ## Canonical Contributor Agreement 53 | 54 | Canonical welcomes contributions to the Charm for MongoDB on Kubernetes. Please check out our [contributor agreement](https://ubuntu.com/legal/contributors) if you're interested in contributing to the solution. 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | unit := 0 3 | app := $(shell yq '.name' metadata.yaml) 4 | model := $(shell juju models --format=yaml|yq '.current-model') 5 | workload := $(shell yq '.containers | keys' metadata.yaml -o t) 6 | scale := 3 7 | res_name := $(shell yq '.containers.*.resource' metadata.yaml) 8 | res_val := $(shell yq ".resources.$(res_name).upstream-source" metadata.yaml) 9 | 10 | 11 | build: 12 | # pack your charm 13 | charmcraft pack 14 | 15 | deploy-local: 16 | # deploy locally with a given container as defined in metadata.yaml 17 | # default to 3 units, but can be changed with `make deploy-local scale=n` 18 | juju deploy ./*.charm --resource $(res_name)=$(res_val) -n $(scale) 19 | 20 | wait-remove: 21 | # watch status until app is completely removed 22 | while true; do juju status|grep -q $(app) || break; sleep 0.5; done 23 | 24 | remove: 25 | # remove app from current model 26 | juju remove-application $(app) 27 | juju status --storage --format=json 2>/dev/null \ 28 | | jq -r '.storage.volumes[] | select( .status.current == "detached" ) | .storage' \ 29 | | xargs juju remove-storage 30 | 31 | # redeployment chain 32 | redeploy: remove build wait-remove deploy-local 33 | 34 | watch-test-log: 35 | # debug-log for integration test 36 | juju debug-log -m $$(juju models |grep test| awk '{ print $$1 }') 37 | 38 | watch-test-status: 39 | # juju status for integration test 40 | watch -n 1 -c juju status -m $$(juju models |grep test| awk '{ print $$1 }') 41 | 42 | clean: 43 | # mrproper stuff 44 | charmcraft clean 45 | rm -rf .tox .coverage 46 | find . -name __pycache__ -type d -exec rm -rf {} +; 47 | 48 | pod-logs: 49 | # workload container logs 50 | microk8s.kubectl logs pod/$(app)-$(unit) --namespace=$(model) --container $(workload) -f 51 | 52 | debug-hook: 53 | # debug hook for given unit 54 | juju debug-hook $(app)/$(unit) 55 | 56 | ssh-workload-container: 57 | # ssh into workload container for given unit (default=0) 58 | microk8s.kubectl exec -n $(model) -it $(app)-$(unit) --container $(workload) -- /bin/bash 59 | 60 | ssh-charm-container: 61 | # ssh into charm container for given unit (default=0) 62 | microk8s.kubectl exec -n $(model) -it $(app)-$(unit) -- /bin/bash 63 | 64 | prune-pvcs: 65 | # remove dangling pvcs 66 | for i in $$(microk8s.kubectl get pvc -n $(model)|grep database| awk "{ print $$1 }"); do microk8s.kubectl delete pvc/$${i} -n $(model); done 67 | 68 | get-credentials: 69 | # run mysql specific action on leader 70 | juju run-action $(app)/leader get-cluster-admin-credentials --wait 71 | 72 | get-status: 73 | # run mysql specific action on leader 74 | juju run-action $(app)/leader get-cluster-status --wait 75 | 76 | rerun-integration-tests: 77 | # Rerun integration test with non-destructive flags 78 | PYTEST_SKIP_DEPLOY="TRUE" tox -e integration -- --model $(model) --no-deploy 79 | 80 | refresh-charm: build 81 | # refresh charm 82 | juju refresh $(app) --path ./*.charm 83 | 84 | status-update-fast: 85 | # make status updates run faster 86 | juju model-config update-status-hook-interval=30s 87 | 88 | status-update-slow: 89 | # make status updates run slower 90 | juju model-config update-status-hook-interval=5m 91 | 92 | debug-log-level: 93 | # config debug log level for model 94 | juju model-config logging-config="=WARNING;unit=DEBUG" 95 | -------------------------------------------------------------------------------- /terraform/charm/sharded/README.md: -------------------------------------------------------------------------------- 1 | ## Requirements 2 | 3 | | Name | Version | 4 | |------|---------| 5 | | [terraform](#requirement\_terraform) | >= 1.6 | 6 | | [juju](#requirement\_juju) | ~> 1.0 | 7 | 8 | ## Providers 9 | 10 | | Name | Version | 11 | |------|---------| 12 | | [juju](#provider\_juju) | 0.22.0 | 13 | 14 | ## Modules 15 | 16 | | Name | Source | Version | 17 | |------|--------|---------| 18 | | [mongodb\_config\_server](#module\_mongodb\_config\_server) | ../replica_set | n/a | 19 | | [mongodb\_shards](#module\_mongodb\_shards) | ../replica_set | n/a | 20 | 21 | ## Resources 22 | 23 | | Name | Type | 24 | |------|------| 25 | | [juju_integration.mongodb_config_server_cross_model_integrations](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 26 | | [juju_integration.mongodb_config_server_same_model_integrations](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 27 | | [juju_offer.mongodb_config_server_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 28 | 29 | ## Inputs 30 | 31 | | Name | Description | Type | Default | Required | 32 | |------|-------------|------|---------|:--------:| 33 | | [config\_server](#input\_config\_server) | Config server app definition |
object({
app_name = string
model_uuid = string
config = optional(map(string), { "role" : "config-server" })
channel = optional(string, "6/stable")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 3)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
})
| n/a | yes | 34 | | [shards](#input\_shards) | Shard apps |
list(object({
app_name = string
model_uuid = string
config = optional(map(string), { "role" : "shard" })
channel = optional(string, "6/stable")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 3)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
}))
| `[]` | no | 35 | 36 | ## Outputs 37 | 38 | | Name | Description | 39 | |------|-------------| 40 | | [app\_names](#output\_app\_names) | Names of of all deployed applications. | 41 | | [offers](#output\_offers) | List of offers URLs. | 42 | | [provides](#output\_provides) | Map of all "provides" endpoints | 43 | | [requires](#output\_requires) | Map of all "requires" endpoints | 44 | -------------------------------------------------------------------------------- /terraform/product/replica_set/integrations.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | #-------------------------------------------------------- 5 | # 3. Integrations 6 | #-------------------------------------------------------- 7 | 8 | ## Same model integrations 9 | 10 | resource "juju_integration" "mongodb_tls_same_model_integration" { 11 | for_each = local.enable_tls && var.self_signed_certificates.model_uuid == var.mongodb_k8s.model_uuid ? { "integrated" = true } : {} 12 | 13 | application { 14 | name = var.mongodb_k8s.app_name 15 | endpoint = "certificates" 16 | } 17 | application { 18 | name = var.self_signed_certificates.app_name 19 | } 20 | depends_on = [ 21 | module.mongodb_k8s, 22 | juju_application.self-signed-certificates["deployed"], 23 | ] 24 | model_uuid = var.mongodb_k8s.model_uuid 25 | } 26 | 27 | resource "juju_integration" "mongodb_s3_same_model_integration" { 28 | for_each = var.s3_integrator.model_uuid == var.mongodb_k8s.model_uuid ? { "integrated" = true } : {} 29 | 30 | application { 31 | name = var.mongodb_k8s.app_name 32 | } 33 | application { 34 | name = var.s3_integrator.app_name 35 | } 36 | depends_on = [ 37 | module.mongodb_k8s, 38 | juju_application.s3_integrator, 39 | ] 40 | model_uuid = var.mongodb_k8s.model_uuid 41 | } 42 | 43 | resource "juju_integration" "mongodb_data_same_model_integration" { 44 | for_each = var.data_integrator.model_uuid == var.mongodb_k8s.model_uuid ? { "integrated" = true } : {} 45 | 46 | application { 47 | name = var.mongodb_k8s.app_name 48 | } 49 | application { 50 | name = var.data_integrator.app_name 51 | } 52 | depends_on = [ 53 | module.mongodb_k8s, 54 | juju_application.data_integrator, 55 | ] 56 | model_uuid = var.mongodb_k8s.model_uuid 57 | } 58 | 59 | ## Cross model integrations 60 | resource "juju_integration" "mongodb_data_cross_model_integration" { 61 | for_each = var.data_integrator.model_uuid != var.mongodb_k8s.model_uuid ? { "integrated" = true } : {} 62 | 63 | application { 64 | offer_url = juju_offer.mongodb_client_offer["offered"].url 65 | } 66 | application { 67 | name = var.data_integrator.app_name 68 | endpoint = "mongodb" 69 | } 70 | depends_on = [ 71 | juju_application.data_integrator, 72 | juju_offer.mongodb_client_offer, 73 | ] 74 | model_uuid = var.data_integrator.model_uuid 75 | } 76 | 77 | resource "juju_integration" "mongodb_tls_cross_model_integration" { 78 | for_each = local.enable_tls && var.self_signed_certificates.model_uuid != var.mongodb_k8s.model_uuid ? { "integrated" = true } : {} 79 | 80 | application { 81 | offer_url = juju_offer.tls_provider_offer["offered"].url 82 | } 83 | application { 84 | name = var.mongodb_k8s.app_name 85 | endpoint = "certificates" 86 | } 87 | depends_on = [ 88 | module.mongodb_k8s, 89 | juju_offer.tls_provider_offer, 90 | ] 91 | model_uuid = var.mongodb_k8s.model_uuid 92 | } 93 | 94 | resource "juju_integration" "mongodb_s3_cross_model_integration" { 95 | for_each = var.s3_integrator.model_uuid != var.mongodb_k8s.model_uuid ? { "integrated" = true } : {} 96 | 97 | application { 98 | offer_url = juju_offer.s3_integrator_offer["offered"].url 99 | } 100 | application { 101 | name = var.mongodb_k8s.app_name 102 | endpoint = "s3-credentials" 103 | } 104 | depends_on = [ 105 | module.mongodb_k8s, 106 | juju_offer.s3_integrator_offer, 107 | ] 108 | model_uuid = var.mongodb_k8s.model_uuid 109 | } 110 | -------------------------------------------------------------------------------- /terraform/product/replica_set/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | locals { 5 | enable_tls = var.self_signed_certificates != null 6 | } 7 | 8 | #-------------------------------------------------------- 9 | # 1. DEPLOYMENTS 10 | #-------------------------------------------------------- 11 | 12 | # replicaset mongodb app 13 | module "mongodb_k8s" { 14 | source = "../../charm/replica_set" 15 | 16 | channel = var.mongodb_k8s.channel 17 | revision = var.mongodb_k8s.revision 18 | base = var.mongodb_k8s.base 19 | 20 | app_name = var.mongodb_k8s.app_name 21 | units = var.mongodb_k8s.units 22 | machines = var.mongodb_k8s.machines 23 | config = merge(var.mongodb_k8s.config, { "role" : "replication" }) 24 | model_uuid = var.mongodb_k8s.model_uuid 25 | constraints = var.mongodb_k8s.constraints 26 | storage = var.mongodb_k8s.storage 27 | endpoint_bindings = var.mongodb_k8s.endpoint_bindings 28 | } 29 | 30 | # self-signed-certificates app 31 | resource "juju_application" "self-signed-certificates" { 32 | for_each = local.enable_tls ? { "deployed" = true } : {} 33 | 34 | charm { 35 | name = "self-signed-certificates" 36 | channel = var.self_signed_certificates.channel 37 | revision = var.self_signed_certificates.revision 38 | base = var.self_signed_certificates.base 39 | } 40 | 41 | name = var.self_signed_certificates.app_name 42 | units = (var.self_signed_certificates.machines == null || length(var.self_signed_certificates.machines) == 0) ? var.self_signed_certificates.units : null 43 | machines = (var.self_signed_certificates.machines == null || length(var.self_signed_certificates.machines) == 0) ? null : var.self_signed_certificates.machines 44 | config = var.self_signed_certificates.config 45 | constraints = var.self_signed_certificates.constraints 46 | endpoint_bindings = var.self_signed_certificates.endpoint_bindings 47 | model_uuid = var.self_signed_certificates.model_uuid 48 | } 49 | 50 | 51 | # Integrator apps 52 | resource "juju_application" "data_integrator" { 53 | charm { 54 | name = "data-integrator" 55 | channel = var.data_integrator.channel 56 | revision = var.data_integrator.revision 57 | base = var.data_integrator.base 58 | } 59 | 60 | name = var.data_integrator.app_name 61 | units = (var.data_integrator.machines == null || length(var.data_integrator.machines) == 0) ? var.data_integrator.units : null 62 | machines = (var.data_integrator.machines == null || length(var.data_integrator.machines) == 0) ? null : var.data_integrator.machines 63 | config = var.data_integrator.config 64 | constraints = var.data_integrator.constraints 65 | endpoint_bindings = var.data_integrator.endpoint_bindings 66 | model_uuid = var.data_integrator.model_uuid 67 | } 68 | 69 | resource "juju_application" "s3_integrator" { 70 | charm { 71 | name = "s3-integrator" 72 | channel = var.s3_integrator.channel 73 | revision = var.s3_integrator.revision 74 | base = var.s3_integrator.base 75 | } 76 | 77 | name = var.s3_integrator.app_name 78 | units = (var.s3_integrator.machines == null || length(var.s3_integrator.machines) == 0) ? var.s3_integrator.units : null 79 | machines = (var.s3_integrator.machines == null || length(var.s3_integrator.machines) == 0) ? null : var.s3_integrator.machines 80 | config = var.s3_integrator.config 81 | constraints = var.s3_integrator.constraints 82 | endpoint_bindings = var.s3_integrator.endpoint_bindings 83 | model_uuid = var.s3_integrator.model_uuid 84 | } 85 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | [tool.poetry] 5 | package-mode = false 6 | requires-poetry = ">=2.0.0" 7 | 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.10.12" 11 | mongo-charms-single-kernel = "1.6.11" 12 | ops = ">=2.21" 13 | pymongo = "^4.7.3" 14 | pyyaml = "^6.0.1" 15 | jinja2 = "^3.1.3" 16 | poetry-core = "^2.0" 17 | data-platform-helpers = "^0.1.3" 18 | overrides = "^7.7.0" 19 | lightkube = "^0.15.3" 20 | setuptools = "^72.0.0" 21 | rpds-py = "0.18.0" 22 | 23 | [tool.poetry.group.charm-libs.dependencies] 24 | ops = ">=2.21" 25 | cryptography = "^42.0.5" # tls_certificates lib v3 26 | jsonschema = "^4.22.0" # tls_certificates lib v3 27 | cosl = "*" # loki_push_api 28 | 29 | [tool.poetry.requires-plugins] 30 | poetry-plugin-export = ">=1.8" 31 | 32 | [tool.poetry.group.format] 33 | optional = true 34 | 35 | [tool.poetry.group.format.dependencies] 36 | black = "^24.4.2" 37 | isort = "^5.13.2" 38 | 39 | [tool.poetry.group.lint] 40 | optional = true 41 | 42 | [tool.poetry.group.lint.dependencies] 43 | flake8 = "^7.0.0" 44 | flake8-docstrings = "^1.7.0" 45 | flake8-copyright = "^0.2.4" 46 | flake8-builtins = "^2.5.0" 47 | pyproject-flake8 = "^7.0.0" 48 | pep8-naming = "^0.13.3" 49 | codespell = "^2.2.6" 50 | shellcheck-py = "^0.10.0.1" 51 | black = "^24.4.2" 52 | isort = "^5.13.2" 53 | 54 | [tool.poetry.group.integration.dependencies] 55 | allure-pytest = "^2.13.5" 56 | ops = ">=2.21" 57 | mongo-charms-single-kernel = "1.6.11" 58 | pymongo = "^4.7.3" 59 | parameterized = "^0.9.0" 60 | lightkube = "^0.15.3" 61 | more_itertools = "*" 62 | kubernetes = "^30.1.0" 63 | juju = "^3.5.0" 64 | pytest = "^8.1.1" 65 | pytest-asyncio = "^0.21.1" 66 | pytest-mock = "^3.14.0" 67 | pytest-operator = "^0.36.0" 68 | pytest-operator-cache = {git = "https://github.com/canonical/data-platform-workflows", tag = "v35.0.2", subdirectory = "python/pytest_plugins/pytest_operator_cache"} 69 | pytest-operator-groups = {git = "https://github.com/canonical/data-platform-workflows", tag = "v35.0.2", subdirectory = "python/pytest_plugins/pytest_operator_groups"} 70 | pytest-github-secrets = {git = "https://github.com/canonical/data-platform-workflows", tag = "v35.0.2", subdirectory = "python/pytest_plugins/github_secrets"} 71 | allure-pytest-collection-report = {git = "https://github.com/canonical/data-platform-workflows", tag = "v35.0.2", subdirectory = "python/pytest_plugins/allure_pytest_collection_report"} 72 | 73 | [build-system] 74 | build-backend = "poetry.core.masonry.api" 75 | 76 | # Testing tools configuration 77 | [tool.coverage.run] 78 | branch = true 79 | 80 | [tool.coverage.report] 81 | show_missing = true 82 | 83 | [tool.pytest.ini_options] 84 | minversion = "6.0" 85 | log_cli_level = "INFO" 86 | markers = ["unstable"] 87 | asyncio_mode = "auto" 88 | 89 | # Formatting tools configuration 90 | [tool.black] 91 | line-length = 99 92 | target-version = ["py310"] 93 | 94 | [tool.isort] 95 | profile = "black" 96 | 97 | # Linting tools configuration 98 | [tool.flake8] 99 | max-line-length = 99 100 | max-doc-length = 99 101 | max-complexity = 10 102 | exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv", "tests/integration/relation_tests/application-charm/lib/", "tests/integration/ha_tests/application_charm/lib/"] 103 | select = ["E", "W", "F", "C", "N", "R", "D", "H"] 104 | # Ignore W503, E501 because using black creates errors with this 105 | # Ignore D107 Missing docstring in __init__ 106 | ignore = ["W503", "E501", "D107", "N818"] 107 | # D100, D101, D102, D103: Ignore missing docstrings in tests 108 | per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"] 109 | docstring-convention = "google" 110 | # Check for properly formatted copyright header in each file 111 | copyright-check = "True" 112 | copyright-author = "Canonical Ltd." 113 | copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" 114 | -------------------------------------------------------------------------------- /terraform/product/sharded/integrations.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | #-------------------------------------------------------- 5 | # 3. Integrations 6 | #-------------------------------------------------------- 7 | 8 | ## Same model integrations 9 | 10 | resource "juju_integration" "mongos_data_integrator_same_model_integration" { 11 | application { 12 | name = var.data_integrator.app_name 13 | } 14 | application { 15 | name = var.mongos_k8s.app_name 16 | } 17 | depends_on = [ 18 | juju_application.mongos_k8s, 19 | juju_application.data_integrator, 20 | ] 21 | model_uuid = var.data_integrator.model_uuid 22 | } 23 | 24 | resource "juju_integration" "config_server_mongos_same_model_integration" { 25 | application { 26 | name = var.config_server.app_name 27 | } 28 | application { 29 | name = var.mongos_k8s.app_name 30 | } 31 | depends_on = [ 32 | module.mongodb_k8s, 33 | juju_integration.mongos_data_integrator_same_model_integration, 34 | ] 35 | model_uuid = var.mongos_k8s.model_uuid 36 | } 37 | 38 | resource "juju_integration" "tls_mongo_same_model_integration" { 39 | count = length(local.tls_same_model_mongo_apps) 40 | 41 | model_uuid = local.tls_same_model_mongo_apps[count.index].model_uuid 42 | application { 43 | name = local.tls_same_model_mongo_apps[count.index].app_name 44 | endpoint = "certificates" 45 | } 46 | application { 47 | name = var.self_signed_certificates.app_name 48 | } 49 | depends_on = [ 50 | module.mongodb_k8s, 51 | juju_application.self-signed-certificates["deployed"], 52 | ] 53 | } 54 | 55 | resource "juju_integration" "s3_config_server_same_model_integration" { 56 | for_each = var.s3_integrator.model_uuid == var.config_server.model_uuid ? { "integrated" = true } : {} 57 | 58 | application { 59 | name = var.config_server.app_name 60 | } 61 | application { 62 | name = var.s3_integrator.app_name 63 | } 64 | depends_on = [ 65 | module.mongodb_k8s, 66 | juju_application.s3_integrator, 67 | ] 68 | model_uuid = var.config_server.model_uuid 69 | } 70 | 71 | #-------------------------------------------------------- 72 | ## Cross model integrations 73 | 74 | resource "juju_integration" "config_server_mongos_cross_model_integration" { 75 | for_each = var.mongos_k8s.model_uuid != var.config_server.model_uuid ? { "integrated" = true } : {} 76 | 77 | application { 78 | offer_url = juju_offer.config_server_mongos_offer["offered"].url 79 | } 80 | application { 81 | name = var.mongos_k8s.app_name 82 | endpoint = "cluster" 83 | } 84 | depends_on = [ 85 | juju_application.mongos_k8s, 86 | juju_offer.config_server_mongos_offer, 87 | ] 88 | model_uuid = var.mongos_k8s.model_uuid 89 | } 90 | 91 | resource "juju_integration" "tls_mongo_cross_model_integration" { 92 | count = length(local.tls_cross_model_mongo_apps) 93 | 94 | model_uuid = local.tls_cross_model_mongo_apps[count.index].model_uuid 95 | 96 | application { 97 | offer_url = juju_offer.tls_provider_offer["offered"].url 98 | } 99 | application { 100 | name = local.tls_cross_model_mongo_apps[count.index].app_name 101 | endpoint = "certificates" 102 | } 103 | depends_on = [ 104 | module.mongodb_k8s, 105 | juju_offer.tls_provider_offer, 106 | ] 107 | } 108 | 109 | resource "juju_integration" "s3_config_server_cross_model_integration" { 110 | for_each = var.s3_integrator.model_uuid != var.config_server.model_uuid ? { "integrated" = true } : {} 111 | 112 | application { 113 | offer_url = juju_offer.s3_integrator_offer["offered"].url 114 | } 115 | application { 116 | name = var.config_server.app_name 117 | endpoint = "s3-credentials" 118 | } 119 | depends_on = [ 120 | module.mongodb_k8s, 121 | juju_offer.s3_integrator_offer, 122 | ] 123 | model_uuid = var.config_server.model_uuid 124 | } 125 | -------------------------------------------------------------------------------- /terraform/product/sharded/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | locals { 5 | enable_tls = var.self_signed_certificates != null 6 | mongodb_apps = concat([var.config_server], var.shards != null ? var.shards : []) 7 | mongo_apps = concat(local.mongodb_apps, [merge({}, var.mongos_k8s)]) 8 | 9 | tls_same_model_mongo_apps = [ 10 | for app in local.mongo_apps : 11 | app if local.enable_tls && app.model_uuid == var.self_signed_certificates.model_uuid 12 | ] 13 | tls_cross_model_mongo_apps = [ 14 | for app in local.mongo_apps : 15 | app if local.enable_tls && app.model_uuid != var.self_signed_certificates.model_uuid 16 | ] 17 | } 18 | 19 | #-------------------------------------------------------- 20 | # 1. DEPLOYMENTS 21 | #-------------------------------------------------------- 22 | 23 | # replicaset mongodb-k8s app 24 | module "mongodb_k8s" { 25 | source = "../../charm/sharded" 26 | 27 | config_server = var.config_server 28 | shards = var.shards 29 | } 30 | 31 | # self-signed-certificates app 32 | resource "juju_application" "self-signed-certificates" { 33 | for_each = local.enable_tls ? { "deployed" = true } : {} 34 | 35 | charm { 36 | name = "self-signed-certificates" 37 | channel = var.self_signed_certificates.channel 38 | revision = var.self_signed_certificates.revision 39 | base = var.self_signed_certificates.base 40 | } 41 | 42 | name = var.self_signed_certificates.app_name 43 | units = (var.self_signed_certificates.machines == null || length(var.self_signed_certificates.machines) == 0) ? var.self_signed_certificates.units : null 44 | machines = (var.self_signed_certificates.machines == null || length(var.self_signed_certificates.machines) == 0) ? null : var.self_signed_certificates.machines 45 | config = var.self_signed_certificates.config 46 | constraints = var.self_signed_certificates.constraints 47 | endpoint_bindings = var.self_signed_certificates.endpoint_bindings 48 | model_uuid = var.self_signed_certificates.model_uuid 49 | } 50 | 51 | # mongos 52 | resource "juju_application" "mongos_k8s" { 53 | charm { 54 | name = "mongos-k8s" 55 | channel = var.mongos_k8s.channel 56 | revision = var.mongos_k8s.revision 57 | base = var.mongos_k8s.base 58 | } 59 | 60 | name = var.mongos_k8s.app_name 61 | config = var.mongos_k8s.config 62 | model_uuid = var.data_integrator.model_uuid 63 | } 64 | 65 | # Integrator apps 66 | resource "juju_application" "data_integrator" { 67 | charm { 68 | name = "data-integrator" 69 | channel = var.data_integrator.channel 70 | revision = var.data_integrator.revision 71 | base = var.data_integrator.base 72 | } 73 | 74 | name = var.data_integrator.app_name 75 | units = (var.data_integrator.machines == null || length(var.data_integrator.machines) == 0) ? var.data_integrator.units : null 76 | machines = (var.data_integrator.machines == null || length(var.data_integrator.machines) == 0) ? null : var.data_integrator.machines 77 | config = var.data_integrator.config 78 | constraints = var.data_integrator.constraints 79 | endpoint_bindings = var.data_integrator.endpoint_bindings 80 | model_uuid = var.data_integrator.model_uuid 81 | } 82 | 83 | resource "juju_application" "s3_integrator" { 84 | charm { 85 | name = "s3-integrator" 86 | channel = var.s3_integrator.channel 87 | revision = var.s3_integrator.revision 88 | base = var.s3_integrator.base 89 | } 90 | 91 | name = var.s3_integrator.app_name 92 | units = (var.s3_integrator.machines == null || length(var.s3_integrator.machines) == 0) ? var.s3_integrator.units : null 93 | machines = (var.s3_integrator.machines == null || length(var.s3_integrator.machines) == 0) ? null : var.s3_integrator.machines 94 | config = var.s3_integrator.config 95 | constraints = var.s3_integrator.constraints 96 | endpoint_bindings = var.s3_integrator.endpoint_bindings 97 | model_uuid = var.s3_integrator.model_uuid 98 | } 99 | -------------------------------------------------------------------------------- /terraform/product/replica_set/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | variable "mongodb_k8s" { 5 | description = "MongoDB app definition" 6 | type = object({ 7 | app_name = optional(string, "mongodb-k8s") 8 | model_uuid = string 9 | config = optional(map(string), { "role" : "replication" }) 10 | channel = optional(string, "6/stable") 11 | base = optional(string, "ubuntu@22.04") 12 | revision = optional(string, null) 13 | units = optional(number, 3) 14 | constraints = optional(string, "arch=amd64") 15 | machines = optional(set(string), null) 16 | storage = optional(map(string), {}) 17 | endpoint_bindings = optional(set(map(string)), []) 18 | }) 19 | } 20 | 21 | variable "self_signed_certificates" { 22 | description = "Configuration for the self-signed-certificates app" 23 | type = object({ 24 | app_name = optional(string, "self-signed-certificates") 25 | model_uuid = string 26 | config = optional(map(string), { "ca-common-name" : "CA" }) 27 | channel = optional(string, "latest/edge") 28 | base = optional(string, "ubuntu@22.04") 29 | revision = optional(string, null) 30 | units = optional(number, 1) 31 | constraints = optional(string, "arch=amd64") 32 | machines = optional(set(string), null) 33 | storage = optional(map(string), {}) 34 | endpoint_bindings = optional(set(map(string)), []) 35 | }) 36 | 37 | validation { 38 | condition = var.self_signed_certificates == null || var.self_signed_certificates.machines == null || length(var.self_signed_certificates.machines) <= 1 39 | error_message = "Machine count should be at most 1" 40 | } 41 | } 42 | 43 | # Integrators 44 | variable "s3_integrator" { 45 | description = "Configuration for the backup integrator" 46 | type = object({ 47 | app_name = optional(string, "s3-integrator") 48 | model_uuid = string 49 | config = map(string) 50 | channel = optional(string, "latest/edge") 51 | base = optional(string, "ubuntu@22.04") 52 | revision = optional(string, null) 53 | units = optional(number, 1) 54 | constraints = optional(string, "arch=amd64") 55 | machines = optional(set(string), null) 56 | storage = optional(map(string), {}) 57 | endpoint_bindings = optional(set(map(string)), []) 58 | }) 59 | 60 | validation { 61 | condition = var.s3_integrator.machines == null || length(var.s3_integrator.machines) <= 1 62 | error_message = "Machines count should be at most 1" 63 | } 64 | validation { 65 | condition = var.s3_integrator.units == 1 66 | error_message = "Units count should be 1" 67 | } 68 | } 69 | 70 | variable "data_integrator" { 71 | description = "Configuration for the data-integrator" 72 | type = object({ 73 | app_name = optional(string, "data-integrator") 74 | model_uuid = string 75 | config = optional(map(string), { "database-name" : "test", "extra-user-roles" : "admin" }) 76 | channel = optional(string, "latest/edge") 77 | base = optional(string, "ubuntu@22.04") 78 | revision = optional(string, null) 79 | units = optional(number, 1) 80 | constraints = optional(string, "arch=amd64") 81 | machines = optional(set(string), null) 82 | storage = optional(map(string), {}) 83 | endpoint_bindings = optional(set(map(string)), []) 84 | }) 85 | 86 | validation { 87 | condition = var.data_integrator.machines == null || length(var.data_integrator.machines) <= 1 88 | error_message = "Machine count should be at most 1" 89 | } 90 | validation { 91 | condition = var.data_integrator.units == 1 92 | error_message = "Units count should be 1" 93 | } 94 | validation { 95 | condition = ( 96 | lookup(var.data_integrator.config, "database-name", "") != "" 97 | && contains(["default", "admin"], lookup(var.data_integrator.config, "extra-user-roles", "admin")) 98 | ) 99 | error_message = "data-integrator config must contain a non-empty 'database-name' and 'extra-user-roles' must be either 'default' or 'admin'." 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/integration/test_sharding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2024 Canonical Ltd. 3 | # See LICENSE file for licensing details. 4 | import pytest 5 | from pytest_operator.plugin import OpsTest 6 | 7 | from .helpers import ( 8 | DEPLOYMENT_TIMEOUT, 9 | METADATA, 10 | get_direct_mongo_client, 11 | has_correct_shards, 12 | wait_for_mongodb_units_blocked, 13 | ) 14 | 15 | SHARD_ONE_APP_NAME = "shard-one" 16 | SHARD_TWO_APP_NAME = "shard-two" 17 | SHARD_THREE_APP_NAME = "shard-three" 18 | SHARD_APPS = [SHARD_ONE_APP_NAME, SHARD_TWO_APP_NAME, SHARD_THREE_APP_NAME] 19 | CONFIG_SERVER_APP_NAME = "config-server-one" 20 | CLUSTER_APPS = [ 21 | CONFIG_SERVER_APP_NAME, 22 | SHARD_ONE_APP_NAME, 23 | SHARD_TWO_APP_NAME, 24 | SHARD_THREE_APP_NAME, 25 | ] 26 | SHARD_REL_NAME = "sharding" 27 | CONFIG_SERVER_REL_NAME = "config-server" 28 | CONFIG_SERVER_NEEDS_SHARD_STATUS = "Missing relation to shard(s)." 29 | SHARD_NEEDS_CONFIG_SERVER_STATUS = "Missing relation to config-server." 30 | 31 | # for now we have a large timeout due to the slow drainage of the `config.system.sessions` 32 | # collection. More info here: 33 | # https://stackoverflow.com/questions/77364840/mongodb-slow-chunk-migration-for-collection-config-system-sessions-with-remov 34 | TIMEOUT = 30 * 60 35 | 36 | 37 | @pytest.mark.group(1) 38 | @pytest.mark.abort_on_fail 39 | async def test_build_and_deploy(ops_test: OpsTest) -> None: 40 | """Build and deploy a sharded cluster.""" 41 | my_charm = await ops_test.build_charm(".") 42 | resources = {"mongodb-image": METADATA["resources"]["mongodb-image"]["upstream-source"]} 43 | 44 | await ops_test.model.deploy( 45 | my_charm, 46 | resources=resources, 47 | num_units=2, 48 | config={"role": "config-server"}, 49 | application_name=CONFIG_SERVER_APP_NAME, 50 | trust=True, 51 | ) 52 | await ops_test.model.deploy( 53 | my_charm, 54 | resources=resources, 55 | num_units=2, 56 | config={"role": "shard"}, 57 | application_name=SHARD_ONE_APP_NAME, 58 | trust=True, 59 | ) 60 | await ops_test.model.deploy( 61 | my_charm, 62 | resources=resources, 63 | num_units=2, 64 | config={"role": "shard"}, 65 | application_name=SHARD_TWO_APP_NAME, 66 | trust=True, 67 | ) 68 | await ops_test.model.deploy( 69 | my_charm, 70 | resources=resources, 71 | num_units=2, 72 | config={"role": "shard"}, 73 | application_name=SHARD_THREE_APP_NAME, 74 | trust=True, 75 | ) 76 | 77 | # TODO: remove raise_on_error when we move to juju 3.5 (DPE-4996) 78 | await ops_test.model.wait_for_idle( 79 | apps=[ 80 | CONFIG_SERVER_APP_NAME, 81 | SHARD_ONE_APP_NAME, 82 | SHARD_TWO_APP_NAME, 83 | SHARD_THREE_APP_NAME, 84 | ], 85 | idle_period=20, 86 | timeout=DEPLOYMENT_TIMEOUT, 87 | raise_on_blocked=False, 88 | raise_on_error=False, 89 | ) 90 | 91 | # verify that Charmed MongoDB is blocked and reports incorrect credentials 92 | await wait_for_mongodb_units_blocked( 93 | ops_test, 94 | CONFIG_SERVER_APP_NAME, 95 | status=CONFIG_SERVER_NEEDS_SHARD_STATUS, 96 | timeout=300, 97 | ) 98 | await wait_for_mongodb_units_blocked( 99 | ops_test, 100 | SHARD_ONE_APP_NAME, 101 | status=SHARD_NEEDS_CONFIG_SERVER_STATUS, 102 | timeout=300, 103 | ) 104 | await wait_for_mongodb_units_blocked( 105 | ops_test, 106 | SHARD_TWO_APP_NAME, 107 | status=SHARD_NEEDS_CONFIG_SERVER_STATUS, 108 | timeout=300, 109 | ) 110 | await wait_for_mongodb_units_blocked( 111 | ops_test, 112 | SHARD_THREE_APP_NAME, 113 | status=SHARD_NEEDS_CONFIG_SERVER_STATUS, 114 | timeout=300, 115 | ) 116 | 117 | 118 | @pytest.mark.group(1) 119 | @pytest.mark.abort_on_fail 120 | async def test_cluster_active(ops_test: OpsTest) -> None: 121 | """Tests the integration of cluster components works without error.""" 122 | await ops_test.model.integrate( 123 | f"{SHARD_ONE_APP_NAME}:{SHARD_REL_NAME}", 124 | f"{CONFIG_SERVER_APP_NAME}:{CONFIG_SERVER_REL_NAME}", 125 | ) 126 | await ops_test.model.integrate( 127 | f"{SHARD_TWO_APP_NAME}:{SHARD_REL_NAME}", 128 | f"{CONFIG_SERVER_APP_NAME}:{CONFIG_SERVER_REL_NAME}", 129 | ) 130 | await ops_test.model.integrate( 131 | f"{SHARD_THREE_APP_NAME}:{SHARD_REL_NAME}", 132 | f"{CONFIG_SERVER_APP_NAME}:{CONFIG_SERVER_REL_NAME}", 133 | ) 134 | 135 | await ops_test.model.wait_for_idle( 136 | apps=[ 137 | CONFIG_SERVER_APP_NAME, 138 | SHARD_ONE_APP_NAME, 139 | SHARD_TWO_APP_NAME, 140 | SHARD_THREE_APP_NAME, 141 | ], 142 | idle_period=15, 143 | status="active", 144 | ) 145 | mongos_client = await get_direct_mongo_client( 146 | ops_test, app_name=CONFIG_SERVER_APP_NAME, mongos=True 147 | ) 148 | 149 | # verify sharded cluster config 150 | assert has_correct_shards( 151 | mongos_client, 152 | expected_shards=[SHARD_ONE_APP_NAME, SHARD_TWO_APP_NAME, SHARD_THREE_APP_NAME], 153 | ), "Config server did not process config properly" 154 | -------------------------------------------------------------------------------- /charmcraft.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | type: charm 5 | platforms: 6 | ubuntu@22.04:amd64: 7 | # Files implicitly created by charmcraft without a part: 8 | # - dispatch (https://github.com/canonical/charmcraft/pull/1898) 9 | # - manifest.yaml 10 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L259) 11 | # Files implicitly copied/"staged" by charmcraft without a part: 12 | # - actions.yaml, config.yaml, metadata.yaml 13 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L290-L293 14 | # https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L156-L157) 15 | parts: 16 | # "poetry-deps" part name is a magic constant 17 | # https://github.com/canonical/craft-parts/pull/901 18 | poetry-deps: 19 | plugin: nil 20 | build-packages: 21 | - curl 22 | override-build: | 23 | # Use environment variable instead of `--break-system-packages` to avoid failing on older 24 | # versions of pip that do not recognize `--break-system-packages` 25 | # `--user` needed (in addition to `--break-system-packages`) for Ubuntu >=24.04 26 | PIP_BREAK_SYSTEM_PACKAGES=true python3 -m pip install --user --upgrade pip==24.3.1 # renovate: charmcraft-pip-latest 27 | 28 | # Use uv to install poetry so that a newer version of Python can be installed if needed by poetry 29 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.5.15/uv-installer.sh | sh # renovate: charmcraft-uv-latest 30 | # poetry 2.0.0 requires Python >=3.9 31 | if ! "$HOME/.local/bin/uv" python find '>=3.9' 32 | then 33 | # Use first Python version that is >=3.9 and available in an Ubuntu LTS 34 | # (to reduce the number of Python versions we use) 35 | "$HOME/.local/bin/uv" python install 3.10.12 # renovate: charmcraft-python-ubuntu-22.04 36 | fi 37 | "$HOME/.local/bin/uv" tool install --no-python-downloads --python '>=3.9' poetry==2.0.0 --with poetry-plugin-export==1.8.0 # renovate: charmcraft-poetry-latest 38 | 39 | ln -sf "$HOME/.local/bin/poetry" /usr/local/bin/poetry 40 | # "charm-poetry" part name is arbitrary; use for consistency 41 | # Avoid using "charm" part name since that has special meaning to charmcraft 42 | charm-poetry: 43 | # By default, the `poetry` plugin creates/stages these directories: 44 | # - lib, src 45 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/parts/plugins/_poetry.py#L76-L78) 46 | # - venv 47 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/parts/plugins/_poetry.py#L95 48 | # https://github.com/canonical/craft-parts/blob/afb0d652eb330b6aaad4f40fbd6e5357d358de47/craft_parts/plugins/base.py#L270) 49 | plugin: poetry 50 | source: . 51 | after: 52 | - poetry-deps 53 | poetry-export-extra-args: ['--only', 'main,charm-libs'] 54 | build-packages: 55 | - libffi-dev # Needed to build Python dependencies with Rust from source 56 | - libssl-dev # Needed to build Python dependencies with Rust from source 57 | - pkg-config # Needed to build Python dependencies with Rust from source 58 | override-build: | 59 | # Workaround for https://github.com/canonical/charmcraft/issues/2068 60 | # rustup used to install rustc and cargo, which are needed to build Python dependencies with Rust from source 61 | if [[ "$CRAFT_PLATFORM" == ubuntu@20.04:* || "$CRAFT_PLATFORM" == ubuntu@22.04:* ]] 62 | then 63 | snap install rustup --classic 64 | else 65 | apt-get install rustup -y 66 | fi 67 | 68 | # If Ubuntu version < 24.04, rustup was installed from snap instead of from the Ubuntu 69 | # archive—which means the rustup version could be updated at any time. Print rustup version 70 | # to build log to make changes to the snap's rustup version easier to track 71 | rustup --version 72 | 73 | # rpds-py (Python package) >=0.19.0 requires rustc >=1.76, which is not available in the 74 | # Ubuntu 22.04 archive. Install rustc and cargo using rustup instead of the Ubuntu archive 75 | rustup set profile minimal 76 | rustup default 1.83.0 # renovate: charmcraft-rust-latest 77 | 78 | craftctl default 79 | # Include requirements.txt in *.charm artifact for easier debugging 80 | cp requirements.txt "$CRAFT_PART_INSTALL/requirements.txt" 81 | # "files" part name is arbitrary; use for consistency 82 | files: 83 | plugin: dump 84 | source: . 85 | build-packages: 86 | - git 87 | override-build: | 88 | # Workaround to add unique identifier (git hash) to charm version while specification 89 | # DA053 - Charm versioning 90 | # (https://docs.google.com/document/d/1Jv1jhWLl8ejK3iJn7Q3VbCIM9GIhp8926bgXpdtx-Sg/edit?pli=1) 91 | # is pending review. 92 | python3 -c 'import pathlib; import shutil; import subprocess; git_hash=subprocess.run(["git", "describe", "--always", "--dirty"], capture_output=True, check=True, encoding="utf-8").stdout; file = pathlib.Path("charm_version"); shutil.copy(file, pathlib.Path("charm_version.backup")); version = file.read_text().strip(); file.write_text(f"{version}+{git_hash}")' 93 | 94 | craftctl default 95 | stage: 96 | - LICENSE 97 | - charm_version 98 | - workload_version 99 | -------------------------------------------------------------------------------- /terraform/product/sharded/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | variable "config_server" { 5 | description = "Config server app definition" 6 | type = object({ 7 | app_name = optional(string, "config-server") 8 | model_uuid = string 9 | config = optional(map(string), { "role" : "config-server" }) 10 | channel = optional(string, "6/stable") 11 | base = optional(string, "ubuntu@22.04") 12 | revision = optional(string, null) 13 | units = optional(number, 3) 14 | constraints = optional(string, "arch=amd64") 15 | machines = optional(set(string), null) 16 | storage = optional(map(string), {}) 17 | endpoint_bindings = optional(set(map(string)), []) 18 | }) 19 | 20 | validation { 21 | condition = var.config_server.config["role"] == "config-server" 22 | error_message = "Config option: 'role' must be set to 'config-server'." 23 | } 24 | } 25 | 26 | variable "shards" { 27 | description = "Shard apps" 28 | type = list(object({ 29 | app_name = string 30 | model_uuid = string 31 | config = optional(map(string), { "role" : "shard" }) 32 | channel = optional(string, "6/stable") 33 | base = optional(string, "ubuntu@22.04") 34 | revision = optional(string, null) 35 | units = optional(number, 3) 36 | constraints = optional(string, "arch=amd64") 37 | machines = optional(set(string), null) 38 | storage = optional(map(string), {}) 39 | endpoint_bindings = optional(set(map(string)), []) 40 | })) 41 | default = [] 42 | 43 | validation { 44 | condition = alltrue([for shard in var.shards : (shard.config["role"] == "shard")]) 45 | error_message = "Config option: 'role' must be set to 'shard' in all shard objects." 46 | } 47 | } 48 | 49 | variable "mongos_k8s" { 50 | description = "Configuration for mongos" 51 | type = object({ 52 | app_name = optional(string, "mongos-k8s") 53 | model_uuid = string 54 | config = optional(map(string), {}) 55 | channel = optional(string, "6/stable") 56 | base = optional(string, "ubuntu@22.04") 57 | revision = optional(string, null) 58 | }) 59 | } 60 | 61 | 62 | variable "self_signed_certificates" { 63 | description = "Configuration for the self-signed-certificates app" 64 | type = object({ 65 | app_name = optional(string, "self-signed-certificates") 66 | model_uuid = string 67 | config = optional(map(string), { "ca-common-name" : "CA" }) 68 | channel = optional(string, "latest/edge") 69 | base = optional(string, "ubuntu@22.04") 70 | revision = optional(string, null) 71 | units = optional(number, 1) 72 | constraints = optional(string, "arch=amd64") 73 | machines = optional(set(string), null) 74 | storage = optional(map(string), {}) 75 | endpoint_bindings = optional(set(map(string)), []) 76 | }) 77 | 78 | validation { 79 | condition = var.self_signed_certificates == null || var.self_signed_certificates.machines == null || length(var.self_signed_certificates.machines) <= 1 80 | error_message = "Machine count should be at most 1" 81 | } 82 | } 83 | 84 | # Integrators 85 | variable "s3_integrator" { 86 | description = "Configuration for the backup integrator" 87 | type = object({ 88 | app_name = optional(string, "s3-integrator") 89 | model_uuid = string 90 | config = map(string) 91 | channel = optional(string, "latest/edge") 92 | base = optional(string, "ubuntu@22.04") 93 | revision = optional(string, null) 94 | units = optional(number, 1) 95 | constraints = optional(string, "arch=amd64") 96 | machines = optional(set(string), null) 97 | storage = optional(map(string), {}) 98 | endpoint_bindings = optional(set(map(string)), []) 99 | }) 100 | 101 | validation { 102 | condition = var.s3_integrator.machines == null || length(var.s3_integrator.machines) <= 1 103 | error_message = "Machines count should be at most 1" 104 | } 105 | validation { 106 | condition = var.s3_integrator.units == 1 107 | error_message = "Units count should be 1" 108 | } 109 | } 110 | 111 | variable "data_integrator" { 112 | description = "Configuration for the data-integrator" 113 | type = object({ 114 | app_name = optional(string, "data-integrator") 115 | model_uuid = string 116 | config = optional(map(string), { "database-name" : "test", "extra-user-roles" : "admin" }) 117 | channel = optional(string, "latest/edge") 118 | base = optional(string, "ubuntu@22.04") 119 | revision = optional(string, null) 120 | units = optional(number, 1) 121 | constraints = optional(string, "arch=amd64") 122 | machines = optional(set(string), null) 123 | storage = optional(map(string), {}) 124 | endpoint_bindings = optional(set(map(string)), []) 125 | }) 126 | 127 | validation { 128 | condition = var.data_integrator.machines == null || length(var.data_integrator.machines) <= 1 129 | error_message = "Machine count should be at most 1" 130 | } 131 | validation { 132 | condition = var.data_integrator.units == 1 133 | error_message = "Units count should be 1" 134 | } 135 | validation { 136 | condition = ( 137 | lookup(var.data_integrator.config, "database-name", "") != "" 138 | && contains(["default", "admin"], lookup(var.data_integrator.config, "extra-user-roles", "admin")) 139 | ) 140 | error_message = "data-integrator config must contain a non-empty 'database-name' and 'extra-user-roles' must be either 'default' or 'admin'." 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /terraform/product/replica_set/README.md: -------------------------------------------------------------------------------- 1 | ## Requirements 2 | 3 | | Name | Version | 4 | |------|---------| 5 | | [terraform](#requirement\_terraform) | >= 1.6 | 6 | | [juju](#requirement\_juju) | ~> 1.0 | 7 | 8 | ## Providers 9 | 10 | | Name | Version | 11 | |------|---------| 12 | | [juju](#provider\_juju) | 0.23.1 | 13 | 14 | ## Modules 15 | 16 | | Name | Source | Version | 17 | |------|--------|---------| 18 | | [mongodb\_k8s](#module\_mongodb\_k8s) | ../../charm/replica_set | n/a | 19 | 20 | ## Resources 21 | 22 | | Name | Type | 23 | |------|------| 24 | | [juju_application.data_integrator](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 25 | | [juju_application.s3_integrator](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 26 | | [juju_application.self-signed-certificates](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 27 | | [juju_integration.mongodb_data_cross_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 28 | | [juju_integration.mongodb_data_same_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 29 | | [juju_integration.mongodb_s3_cross_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 30 | | [juju_integration.mongodb_s3_same_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 31 | | [juju_integration.mongodb_tls_cross_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 32 | | [juju_integration.mongodb_tls_same_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 33 | | [juju_offer.mongodb_client_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 34 | | [juju_offer.s3_integrator_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 35 | | [juju_offer.tls_provider_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 36 | 37 | ## Inputs 38 | 39 | | Name | Description | Type | Default | Required | 40 | |------|-------------|------|---------|:--------:| 41 | | [data\_integrator](#input\_data\_integrator) | Configuration for the data-integrator |
object({
app_name = optional(string, "data-integrator")
model_uuid = string
config = optional(map(string), { "database-name" : "test", "extra-user-roles" : "admin" })
channel = optional(string, "latest/edge")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 1)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
})
| n/a | yes | 42 | | [mongodb\_k8s](#input\_mongodb\_k8s) | MongoDB app definition |
object({
app_name = optional(string, "mongodb-k8s")
model_uuid = string
config = optional(map(string), { "role" : "replication" })
channel = optional(string, "6/stable")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 3)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
})
| n/a | yes | 43 | | [s3\_integrator](#input\_s3\_integrator) | Configuration for the backup integrator |
object({
app_name = optional(string, "s3-integrator")
model_uuid = string
config = map(string)
channel = optional(string, "latest/edge")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 1)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
})
| n/a | yes | 44 | | [self\_signed\_certificates](#input\_self\_signed\_certificates) | Configuration for the self-signed-certificates app |
object({
app_name = optional(string, "self-signed-certificates")
model_uuid = string
config = optional(map(string), { "ca-common-name" : "CA" })
channel = optional(string, "latest/edge")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 1)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
})
| n/a | yes | 45 | 46 | ## Outputs 47 | 48 | | Name | Description | 49 | |------|-------------| 50 | | [app\_names](#output\_app\_names) | Names of of all deployed applications. | 51 | | [offers](#output\_offers) | List of offers URLs. | 52 | | [provides](#output\_provides) | Map of all "provides" endpoints | 53 | | [requires](#output\_requires) | Map of all "requires" endpoints | 54 | -------------------------------------------------------------------------------- /terraform/product/sharded/README.md: -------------------------------------------------------------------------------- 1 | ## Requirements 2 | 3 | | Name | Version | 4 | |------|---------| 5 | | [terraform](#requirement\_terraform) | >= 1.6 | 6 | | [juju](#requirement\_juju) | ~> 1.0 | 7 | 8 | ## Providers 9 | 10 | | Name | Version | 11 | |------|---------| 12 | | [juju](#provider\_juju) | 0.23.1 | 13 | 14 | ## Modules 15 | 16 | | Name | Source | Version | 17 | |------|--------|---------| 18 | | [mongodb\_k8s](#module\_mongodb\_k8s) | ../../charm/sharded | n/a | 19 | 20 | ## Resources 21 | 22 | | Name | Type | 23 | |------|------| 24 | | [juju_application.data_integrator](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 25 | | [juju_application.mongos_k8s](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 26 | | [juju_application.s3_integrator](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 27 | | [juju_application.self-signed-certificates](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application) | resource | 28 | | [juju_integration.config_server_mongos_cross_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 29 | | [juju_integration.config_server_mongos_same_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 30 | | [juju_integration.mongos_data_integrator_same_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 31 | | [juju_integration.s3_config_server_cross_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 32 | | [juju_integration.s3_config_server_same_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 33 | | [juju_integration.tls_mongo_cross_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 34 | | [juju_integration.tls_mongo_same_model_integration](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/integration) | resource | 35 | | [juju_offer.config_server_mongos_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 36 | | [juju_offer.s3_integrator_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 37 | | [juju_offer.tls_provider_offer](https://registry.terraform.io/providers/juju/juju/latest/docs/resources/offer) | resource | 38 | 39 | ## Inputs 40 | 41 | | Name | Description | Type | Default | Required | 42 | |------|-------------|------|---------|:--------:| 43 | | [config\_server](#input\_config\_server) | Config server app definition |
object({
app_name = optional(string, "config-server")
model_uuid = string
config = optional(map(string), { "role" : "config-server" })
channel = optional(string, "6/stable")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 3)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
})
| n/a | yes | 44 | | [data\_integrator](#input\_data\_integrator) | Configuration for the data-integrator |
object({
app_name = optional(string, "data-integrator")
model_uuid = string
config = optional(map(string), { "database-name" : "test", "extra-user-roles" : "admin" })
channel = optional(string, "latest/edge")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 1)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
})
| n/a | yes | 45 | | [mongos\_k8s](#input\_mongos\_k8s) | Configuration for mongos |
object({
app_name = optional(string, "mongos-k8s")
model_uuid = string
config = optional(map(string), {})
channel = optional(string, "6/stable")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
})
| n/a | yes | 46 | | [s3\_integrator](#input\_s3\_integrator) | Configuration for the backup integrator |
object({
app_name = optional(string, "s3-integrator")
model_uuid = string
config = map(string)
channel = optional(string, "latest/edge")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 1)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
})
| n/a | yes | 47 | | [self\_signed\_certificates](#input\_self\_signed\_certificates) | Configuration for the self-signed-certificates app |
object({
app_name = optional(string, "self-signed-certificates")
model_uuid = string
config = optional(map(string), { "ca-common-name" : "CA" })
channel = optional(string, "latest/edge")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 1)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
})
| n/a | yes | 48 | | [shards](#input\_shards) | Shard apps |
list(object({
app_name = string
model_uuid = string
config = optional(map(string), { "role" : "shard" })
channel = optional(string, "6/stable")
base = optional(string, "ubuntu@22.04")
revision = optional(string, null)
units = optional(number, 3)
constraints = optional(string, "arch=amd64")
machines = optional(set(string), null)
storage = optional(map(string), {})
endpoint_bindings = optional(set(map(string)), [])
}))
| `[]` | no | 49 | 50 | ## Outputs 51 | 52 | | Name | Description | 53 | |------|-------------| 54 | | [app\_names](#output\_app\_names) | Names of of all deployed applications. | 55 | | [offers](#output\_offers) | List of offers URLs. | 56 | | [provides](#output\_provides) | Map of all "provides" endpoints | 57 | | [requires](#output\_requires) | Map of all "requires" endpoints | 58 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | name: Tests 4 | 5 | concurrency: 6 | group: "${{ github.workflow }}-${{ github.ref }}" 7 | cancel-in-progress: true 8 | 9 | on: 10 | pull_request: 11 | schedule: 12 | - cron: "53 0 * * *" # Daily at 00:53 UTC 13 | # Triggered on push to branch "main" by .github/workflows/release.yaml 14 | workflow_call: 15 | outputs: 16 | artifact-prefix: 17 | description: build_charm.yaml `artifact-prefix` output 18 | value: ${{ jobs.build.outputs.artifact-prefix }} 19 | 20 | jobs: 21 | lint: 22 | name: Lint 23 | uses: canonical/data-platform-workflows/.github/workflows/lint.yaml@v35.0.2 24 | 25 | lib-check: 26 | name: Check libraries 27 | runs-on: ubuntu-latest 28 | timeout-minutes: 5 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | - run: | 35 | # Workaround for https://github.com/canonical/charmcraft/issues/1389#issuecomment-1880921728 36 | touch requirements.txt 37 | - name: Check libs 38 | uses: canonical/charming-actions/check-libraries@2.6.2 39 | with: 40 | credentials: ${{ secrets.CHARMHUB_TOKEN }} 41 | github-token: ${{ secrets.GITHUB_TOKEN }} 42 | use-labels: false 43 | fail-build: ${{ github.event_name == 'pull_request' }} 44 | 45 | terraform-rs-test: 46 | name: Terraform - Validation of replica-set product module 47 | continue-on-error: true 48 | runs-on: ubuntu-22.04 49 | timeout-minutes: 120 50 | steps: 51 | - name: Checkout repo 52 | uses: actions/checkout@v4 53 | with: 54 | fetch-depth: 0 55 | 56 | - name: (GitHub hosted) Free up disk space 57 | run: | 58 | printf '\nDisk usage before cleanup\n' 59 | df --human-readable 60 | # Based on https://github.com/actions/runner-images/issues/2840#issuecomment-790492173 61 | rm -r /opt/hostedtoolcache/ 62 | printf '\nDisk usage after cleanup\n' 63 | df --human-readable 64 | 65 | - name: (self hosted) Disk usage 66 | run: df --human-readable 67 | 68 | - name: Install terraform snap 69 | run: | 70 | sudo snap install terraform --channel=latest/stable --classic 71 | 72 | - name: Lint / format / validate TF modules 73 | run: | 74 | pushd ./terraform 75 | for dir in charm/replica_set product/replica_set; do 76 | (cd "${dir}" && terraform init && terraform fmt && terraform validate) 77 | done 78 | popd 79 | 80 | - name: run checks - prepare 81 | run: | 82 | sudo snap install juju --channel=3.6 83 | 84 | - name: microk8s setup 85 | run: | 86 | sudo snap install microk8s --channel=1.27-strict 87 | sudo usermod -a -G snap_microk8s "$(whoami)" 88 | mkdir -p ~/.kube 89 | sudo chown -R "$(whoami)" ~/.kube 90 | newgrp snap_microk8s 91 | sudo microk8s enable dns storage ingress 92 | 93 | - name: Juju setup 94 | run: | 95 | mkdir -p ~/.local/share/juju 96 | sudo --user "$USER" juju bootstrap 'microk8s' --config model-logs-size=10G 97 | juju model-defaults logging-config='=INFO; unit=DEBUG' 98 | juju add-model test 99 | 100 | - name: Terraform deploy - replica-set product module 101 | run: | 102 | MODEL=$(juju show-model test | grep "model-uuid" | awk '{split($0, a, ": "); print a[2]}') 103 | pushd ./terraform/product/replica_set/ 104 | terraform apply \ 105 | -var="mongodb_k8s={\"model_uuid\": \"$MODEL\"}" \ 106 | -var="self_signed_certificates={\"model_uuid\": \"$MODEL\"}" \ 107 | -var="s3_integrator={\"model_uuid\": \"$MODEL\", \"config\": {\"bucket\": \"test\"}}" \ 108 | -var="data_integrator={\"model_uuid\": \"$MODEL\"}" \ 109 | -auto-approve 110 | popd 111 | 112 | - name: Wait for juju deployment 113 | run: | 114 | # TODO - remove this when juju wait-for starts reporting the up to date status 115 | until timeout 2m juju wait-for model test --query='life=="alive" && status=="available"' 116 | do 117 | echo "Retrying in 5 seconds..." 118 | juju status -m test 119 | sleep 5 120 | done 121 | 122 | terraform-sharding-test: 123 | name: Terraform - Validation of sharded cluster product module 124 | continue-on-error: true 125 | runs-on: ubuntu-22.04 126 | timeout-minutes: 120 127 | steps: 128 | - name: Checkout repo 129 | uses: actions/checkout@v4 130 | with: 131 | fetch-depth: 0 132 | 133 | - name: (GitHub hosted) Free up disk space 134 | run: | 135 | printf '\nDisk usage before cleanup\n' 136 | df --human-readable 137 | # Based on https://github.com/actions/runner-images/issues/2840#issuecomment-790492173 138 | rm -r /opt/hostedtoolcache/ 139 | printf '\nDisk usage after cleanup\n' 140 | df --human-readable 141 | 142 | - name: (self hosted) Disk usage 143 | run: df --human-readable 144 | 145 | - name: Install terraform snap 146 | run: | 147 | sudo snap install terraform --channel=latest/stable --classic 148 | 149 | - name: Lint / format / validate TF modules 150 | run: | 151 | pushd ./terraform 152 | for dir in charm/replica_set charm/sharded product/replica_set product/sharded; do 153 | (cd "${dir}" && terraform init && terraform fmt && terraform validate) 154 | done 155 | popd 156 | 157 | - name: run checks - prepare 158 | run: | 159 | sudo snap install juju --channel=3.6 160 | 161 | - name: microk8s setup 162 | run: | 163 | sudo snap install microk8s --channel=1.27-strict 164 | sudo usermod -a -G snap_microk8s "$(whoami)" 165 | mkdir -p ~/.kube 166 | sudo chown -R "$(whoami)" ~/.kube 167 | newgrp snap_microk8s 168 | sudo microk8s enable dns storage ingress 169 | 170 | - name: Juju setup 171 | run: | 172 | mkdir -p ~/.local/share/juju 173 | sudo --user "$USER" juju bootstrap 'microk8s' --config model-logs-size=10G 174 | juju model-defaults logging-config='=INFO; unit=DEBUG' 175 | juju add-model test 176 | 177 | - name: Terraform deploy - sharded cluster product module 178 | run: | 179 | MODEL=$(juju show-model test | grep "model-uuid" | awk '{split($0, a, ": "); print a[2]}') 180 | pushd ./terraform/product/sharded/ 181 | terraform apply \ 182 | -var="config_server={\"model_uuid\": \"$MODEL\"}" \ 183 | -var="shards=[{\"app_name\": \"shard-one\", \"model_uuid\": \"$MODEL\"},{\"app_name\": \"shard-two\", \"model_uuid\": \"$MODEL\"}]" \ 184 | -var="mongos_k8s={\"model_uuid\": \"$MODEL\"}" \ 185 | -var="config_server={\"model_uuid\": \"$MODEL\"}" \ 186 | -var="self_signed_certificates={\"model_uuid\": \"$MODEL\"}" \ 187 | -var="s3_integrator={\"model_uuid\": \"$MODEL\", \"config\": {\"bucket\": \"test\"}}" \ 188 | -var="data_integrator={\"model_uuid\": \"$MODEL\"}" \ 189 | -auto-approve 190 | popd 191 | 192 | - name: Wait for juju deployment 193 | run: | 194 | # TODO - remove this when juju wait-for starts reporting the up to date status 195 | until timeout 2m juju wait-for model test --query='life=="alive" && status=="available"' 196 | do 197 | echo "Retrying in 5 seconds..." 198 | juju status -m test 199 | sleep 5 200 | done 201 | 202 | build: 203 | strategy: 204 | matrix: 205 | path: 206 | - . 207 | name: Build charm | ${{ matrix.path }} 208 | uses: canonical/data-platform-workflows/.github/workflows/build_charm.yaml@v35.0.2 209 | with: 210 | path-to-charm-directory: ${{ matrix.path }} 211 | cache: false 212 | 213 | integration-test: 214 | name: Integration test charm 215 | needs: 216 | - lint 217 | - build 218 | uses: canonical/data-platform-workflows/.github/workflows/integration_test_charm.yaml@v35.0.2 219 | with: 220 | artifact-prefix: ${{ needs.build.outputs.artifact-prefix }} 221 | cloud: microk8s 222 | microk8s-snap-channel: 1.29-strict/stable # renovate: latest microk8s 223 | juju-agent-version: 3.6.1 # renovate: juju-agent-pin-minor 224 | _beta_allure_report: true 225 | permissions: 226 | contents: write # Needed for Allure Report beta 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Charmed MongoDB on Kubernetes 2 | [![CharmHub Badge](https://charmhub.io/mongodb-k8s/badge.svg)](https://charmhub.io/mongodb-k8s) 3 | [![Release to 6/edge](https://github.com/canonical/mongodb-k8s-operator/actions/workflows/release.yaml/badge.svg)](https://github.com/canonical/mongodb-k8s-operator/actions/workflows/release.yaml) 4 | [![Tests](https://github.com/canonical/mongodb-k8s-operator/actions/workflows/ci.yaml/badge.svg)](https://github.com/canonical/mongodb-k8s-operator/actions/workflows/ci.yaml) 5 | ## Overview 6 | 7 | The Charmed MongoDB Operator delivers automated operations management from [day 0 to day 2](https://codilime.com/glossary/day-0-day-1-day-2/#:~:text=Day%200%2C%20Day%201%2C%20and,just%20a%20daily%20operations%20routine.) on the [MongoDB Community Edition](https://github.com/mongodb/mongo) document database. It is an open source, end-to-end, production ready data platform on top of cloud native technologies. 8 | 9 | MongoDB is a popular NoSQL database application. It stores its data with JSON-like documents creating a flexible experience for users; with easy to use data aggregation for data analytics. It is a distributed database, so vertical and horizontal scaling come naturally. 10 | 11 | This operator charm deploys and operates MongoDB on Kubernetes. It offers features such as replication, TLS, password rotation, and easy to use integration with applications. The Charmed MongoDB Operator meets the need of deploying MongoDB in a structured and consistent manner while allowing the user flexibility in configuration. It simplifies deployment, scaling, configuration and management of MongoDB in production at scale in a reliable way. 12 | 13 | ## Requirements 14 | - at least 2GB of RAM. 15 | - at least 2 CPU threads per host. 16 | - For production deployment: at least 60GB of available storage on each host. 17 | - Access to the internet for downloading the charm. 18 | - Machine is running Ubuntu 22.04(jammy) or later. 19 | 20 | ## Config options 21 | auto-delete - `boolean`; When a relation is removed, auto-delete ensures that any relevant databases 22 | associated with the relation are also removed. Set with `juju config mongodb-k8s auto-delete=`. 23 | 24 | admin-password - `string`; The password for the database admin user. Set with `juju run-action mongodb-k8s/leader set-admin-password --wait` 25 | 26 | tls external key - `string`; TLS external key for encryption outside the cluster. Set with `juju run-action mongodb-k8s/0 set-tls-private-key "external-key=$(base64 -w0 external-key-0.pem)" --wait` 27 | 28 | tls internal key - `string`; TLS external key for encryption inside the cluster. Set with `juju run-action mongodb-k8s/0 set-tls-private-key "internal-key=$(base64 -w0 internal-key.pem)" --wait` 29 | 30 | ## Usage 31 | 32 | ### Basic Usage 33 | To deploy a single unit of MongoDB using its default configuration 34 | ```shell 35 | juju deploy ./mongodb-k8s_ubuntu-22.04-amd64.charm --resource mongodb-image=dataplatformoci/mongodb:5.0 36 | ``` 37 | 38 | It is customary to use MongoDB with replication. Hence usually more than one unit (preferably an odd number to prohibit a "split-brain" scenario) is deployed. To deploy MongoDB with multiple replicas, specify the number of desired units with the `-n` option. 39 | ```shell 40 | juju deploy ./mongodb-k8s_ubuntu-22.04-amd64.charm --resource mongodb-image=dataplatformoci/mongodb:5.0 -n 41 | ``` 42 | 43 | ### Replication 44 | #### Adding Replicas 45 | To add more replicas one can use the `juju scale-application` functionality i.e. 46 | ```shell 47 | juju scale-application mongodb-k8s -n 48 | ``` 49 | The implementation of `add-unit` allows the operator to add more than one unit, but functions internally by adding one replica at a time, as specified by the [constraints](https://www.mongodb.com/docs/manual/reference/command/replSetReconfig/#reconfiguration-can-add-or-remove-no-more-than-one-voting-member-at-a-time) of MongoDB. 50 | 51 | 52 | #### Removing Replicas 53 | Similarly to scale down the number of replicas the `juju scale-application` functionality may be used i.e. 54 | ```shell 55 | juju scale-application mongodb-k8s -n 56 | ``` 57 | The implementation of `remove-unit` allows the operator to remove more than one replica so long has the operator **does not remove a majority of the replicas**. The functionality of `remove-unit` functions by removing one replica at a time, as specified by the [constraints](https://www.mongodb.com/docs/manual/reference/command/replSetReconfig/#reconfiguration-can-add-or-remove-no-more-than-one-voting-member-at-a-time) of MongoDB. 58 | 59 | 60 | ## Relations 61 | 62 | Supported [relations](https://juju.is/docs/olm/relations): 63 | 64 | #### `mongodb_client` interface: 65 | 66 | Relations to applications are supported via the `mongodb_client` interface. To create a relation: 67 | 68 | ```shell 69 | juju relate mongodb-k8s application 70 | ``` 71 | 72 | To remove a relation: 73 | ```shell 74 | juju remove-relation mongodb-k8s application 75 | ``` 76 | 77 | #### `tls` interface: 78 | 79 | We have also supported TLS for the MongoDB k8s charm. To enable TLS: 80 | 81 | ```shell 82 | # deploy the TLS charm 83 | juju deploy tls-certificates-operator --channel=stable 84 | # add the necessary configurations for TLS 85 | juju config tls-certificates-operator generate-self-signed-certificates="true" ca-common-name="Test CA" 86 | # to enable TLS relate the two applications 87 | juju relate tls-certificates-operator mongodb-k8s 88 | ``` 89 | 90 | Updates to private keys for certificate signing requests (CSR) can be made via the `set-tls-private-key` action. Note passing keys to external/internal keys should *only be done with* `base64 -w0` *not* `cat`. With three replicas this schema should be followed: 91 | ```shell 92 | # generate shared internal key 93 | openssl genrsa -out internal-key.pem 3072 94 | # generate external keys for each unit 95 | openssl genrsa -out external-key-0.pem 3072 96 | openssl genrsa -out external-key-1.pem 3072 97 | openssl genrsa -out external-key-2.pem 3072 98 | # apply both private keys on each unit, shared internal key will be allied only on juju leader 99 | juju run-action mongodb-k8s /0 set-tls-private-key "external-key=$(base64 -w0 external-key-0.pem)" "internal-key=$(base64 -w0 internal-key.pem)" --wait 100 | juju run-action mongodb-k8s /1 set-tls-private-key "external-key=$(base64 -w0 external-key-1.pem)" "internal-key=$(base64 -w0 internal-key.pem)" --wait 101 | juju run-action mongodb-k8s /2 set-tls-private-key "external-key=$(base64 -w0 external-key-2.pem)" "internal-key=$(base64 -w0 internal-key.pem)" --wait 102 | 103 | # updates can also be done with auto-generated keys with 104 | juju run-action mongodb-k8s /0 set-tls-private-key --wait 105 | juju run-action mongodb-k8s /1 set-tls-private-key --wait 106 | juju run-action mongodb-k8s /2 set-tls-private-key --wait 107 | ``` 108 | 109 | To disable TLS remove the relation 110 | ```shell 111 | juju remove-relation mongodb-k8s tls-certificates-operator 112 | ``` 113 | 114 | Note: The TLS settings here are for self-signed-certificates which are not recommended for production clusters, the `tls-certificates-operator` charm offers a variety of configurations, read more on the TLS charm [here](https://charmhub.io/tls-certificates-operator) 115 | 116 | ### Password rotation 117 | #### Internal admin user 118 | The admin user is used internally by the Charmed MongoDB Operator, the `set-password` action can be used to rotate its password. 119 | ```shell 120 | # to set a specific password for the admin user 121 | juju run-action mongodb-k8s/leader set-password password= --wait 122 | 123 | # to randomly generate a password for the admin user 124 | juju run-action mongodb-k8s/leader set-password --wait 125 | ``` 126 | 127 | #### Related applications users 128 | To rotate the passwords of users created for related applications, the relation should be removed and related again. That process will generate a new user and password for the application. 129 | ```shell 130 | juju remove-relation application mongodb-k8s 131 | juju add-relation application mongodb-k8s 132 | ``` 133 | 134 | ## Security 135 | Security issues in the Charmed MongoDB Operator can be reported through [LaunchPad](https://wiki.ubuntu.com/DebuggingSecurity#How%20to%20File). Please do not file GitHub issues about security issues. 136 | 137 | 138 | ## Contributing 139 | 140 | Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines on enhancements to this charm following best practice guidelines, and [CONTRIBUTING.md](https://github.com/canonical/mongodb-operator/blob/main/CONTRIBUTING.md) for developer guidance. 141 | 142 | 143 | ## License 144 | The Charmed MongoDB Operator is free software, distributed under the Apache Software License, version 2.0. See [LICENSE](https://github.com/canonical/mongodb-operator/blob/main/LICENSE) for more information. 145 | 146 | The Charmed MongoDB Operator is free software, distributed under the Apache Software License, version 2.0. It [installs/operates/depends on] [MongoDB Community Version](https://github.com/mongodb/mongo), which is licensed under the Server Side Public License (SSPL) 147 | 148 | See [LICENSE](https://github.com/canonical/mongodb-operator/blob/main/LICENSE) for more information. 149 | 150 | ## Trademark notice 151 | MongoDB' is a trademark or registered trademark of MongoDB Inc. Other trademarks are property of their respective owners. 152 | -------------------------------------------------------------------------------- /tests/integration/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | import json 5 | import logging 6 | from pathlib import Path 7 | from typing import Any, Dict, List, Optional 8 | 9 | import yaml 10 | from dateutil.parser import parse 11 | from pymongo import MongoClient 12 | from pytest_operator.plugin import OpsTest 13 | from tenacity import Retrying, stop_after_delay, wait_fixed 14 | 15 | METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) 16 | APP_NAME = METADATA["name"] 17 | UNIT_IDS = [0, 1, 2] 18 | DEPLOYMENT_TIMEOUT = 2000 19 | 20 | SERIES = "jammy" 21 | 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | class Status: 27 | """Model class for status.""" 28 | 29 | def __init__(self, value: str, since: str, message: Optional[str] = None): 30 | self.value = value 31 | self.since = parse(since, ignoretz=True) 32 | self.message = message 33 | 34 | 35 | class Unit: 36 | """Model class for a Unit, with properties widely used.""" 37 | 38 | def __init__( 39 | self, 40 | id: int, 41 | name: str, 42 | ip: str, 43 | hostname: str, 44 | is_leader: bool, 45 | workload_status: Status, 46 | agent_status: Status, 47 | app_status: Status, 48 | ): 49 | self.id = id 50 | self.name = name 51 | self.ip = ip 52 | self.hostname = hostname 53 | self.is_leader = is_leader 54 | self.workload_status = workload_status 55 | self.agent_status = agent_status 56 | self.app_status = app_status 57 | 58 | def dump(self) -> Dict[str, Any]: 59 | """To json.""" 60 | result = {} 61 | for key, val in vars(self).items(): 62 | result[key] = vars(val) if isinstance(val, Status) else val 63 | return result 64 | 65 | 66 | async def get_app_name(ops_test: OpsTest, test_deployments: List[str] = []) -> str: 67 | """Returns the name of the cluster running MongoDB. 68 | 69 | This is important since not all deployments of the MongoDB charm have the application name 70 | "mongodb". 71 | 72 | Note: if multiple clusters are running MongoDB this will return the one first found. 73 | """ 74 | status = await ops_test.model.get_status() 75 | for app in ops_test.model.applications: 76 | # note that format of the charm field is not exactly "mongodb" but instead takes the form 77 | # of `local:focal/mongodb-6` 78 | if "mongodb" in status["applications"][app]["charm"]: 79 | logger.debug("Found mongodb app named '%s'", app) 80 | 81 | if app in test_deployments: 82 | logger.debug("mongodb app named '%s', was deployed by the test, not by user", app) 83 | continue 84 | 85 | return app 86 | 87 | return None 88 | 89 | 90 | async def check_or_scale_app(ops_test: OpsTest, user_app_name: str, required_units: int) -> None: 91 | """A helper function that scales existing cluster if necessary.""" 92 | # check if we need to scale 93 | current_units = len(ops_test.model.applications[user_app_name].units) 94 | 95 | count = required_units - current_units 96 | if required_units == current_units: 97 | return 98 | count = required_units - current_units 99 | await ops_test.model.applications[user_app_name].scale(scale_change=count) 100 | # TODO : Remove raise_on_error when we move to juju 3.5 (DPE-4996) 101 | await ops_test.model.wait_for_idle( 102 | apps=[user_app_name], status="active", raise_on_error=False, timeout=2000 103 | ) 104 | 105 | 106 | async def get_unit_hostname(ops_test: OpsTest, unit_id: int, app: str) -> str: 107 | """Get the hostname of a specific unit.""" 108 | _, hostname, _ = await ops_test.juju("ssh", f"{app}/{unit_id}", "hostname") 109 | return hostname.strip() 110 | 111 | 112 | async def get_raw_application(ops_test: OpsTest, app: str) -> Dict[str, Any]: 113 | """Get raw application details.""" 114 | ret_code, stdout, stderr = await ops_test.juju( 115 | *f"status --model {ops_test.model.info.name} {app} --format=json".split() 116 | ) 117 | if ret_code != 0: 118 | logger.error(f"Invalid return [{ret_code=}]: {stderr=}") 119 | raise Exception(f"[{ret_code=}] {stderr=}") 120 | return json.loads(stdout)["applications"][app] 121 | 122 | 123 | async def get_application_units(ops_test: OpsTest, app: str) -> List[Unit]: 124 | """Get fully detailed units of an application.""" 125 | # Juju incorrectly reports the IP addresses after the network is restored this is reported as a 126 | # bug here: https://github.com/juju/python-libjuju/issues/738. Once this bug is resolved use of 127 | # `get_unit_ip` should be replaced with `.public_address` 128 | raw_app = await get_raw_application(ops_test, app) 129 | units = [] 130 | for u_name, unit in raw_app["units"].items(): 131 | unit_id = int(u_name.split("/")[-1]) 132 | if not unit.get("address", False): 133 | # unit not ready yet... 134 | continue 135 | 136 | unit = Unit( 137 | id=unit_id, 138 | name=u_name.replace("/", "-"), 139 | ip=unit["address"], 140 | hostname=await get_unit_hostname(ops_test, unit_id, app), 141 | is_leader=unit.get("leader", False), 142 | workload_status=Status( 143 | value=unit["workload-status"]["current"], 144 | since=unit["workload-status"]["since"], 145 | message=unit["workload-status"].get("message"), 146 | ), 147 | agent_status=Status( 148 | value=unit["juju-status"]["current"], 149 | since=unit["juju-status"]["since"], 150 | ), 151 | app_status=Status( 152 | value=raw_app["application-status"]["current"], 153 | since=raw_app["application-status"]["since"], 154 | message=raw_app["application-status"].get("message"), 155 | ), 156 | ) 157 | 158 | units.append(unit) 159 | 160 | return units 161 | 162 | 163 | async def check_all_units_blocked_with_status( 164 | ops_test: OpsTest, db_app_name: str, status: Optional[str] 165 | ) -> None: 166 | # this is necessary because ops_model.units does not update the unit statuses 167 | for unit in await get_application_units(ops_test, db_app_name): 168 | assert ( 169 | unit.workload_status.value == "blocked" 170 | ), f"expected unit {unit.name} to be in blocked state. Got {unit.workload_status.value}" 171 | if status: 172 | # We can have extra info but we care for the most important status 173 | assert ( 174 | status in unit.workload_status.message 175 | ), f"expected {unit.name} status message to be `{status}`. Got `{unit.workload_status.message}`" 176 | 177 | 178 | async def wait_for_mongodb_units_blocked( 179 | ops_test: OpsTest, db_app_name: str, status: Optional[str] = None, timeout=20 180 | ) -> None: 181 | """Waits for units of MongoDB to be in the blocked state. 182 | 183 | This is necessary because the MongoDB app can report a different status than the units. 184 | """ 185 | hook_interval_key = "update-status-hook-interval" 186 | try: 187 | old_interval = (await ops_test.model.get_config())[hook_interval_key] 188 | await ops_test.model.set_config({hook_interval_key: "1m"}) 189 | for attempt in Retrying(stop=stop_after_delay(timeout), wait=wait_fixed(1), reraise=True): 190 | with attempt: 191 | await check_all_units_blocked_with_status(ops_test, db_app_name, status) 192 | finally: 193 | await ops_test.model.set_config({hook_interval_key: old_interval}) 194 | 195 | 196 | async def get_address_of_unit(ops_test: OpsTest, unit_id: int, app_name: str = APP_NAME) -> str: 197 | """Retrieves the address of the unit based on provided id.""" 198 | status = await ops_test.model.get_status() 199 | return status["applications"][app_name]["units"][f"{app_name}/{unit_id}"]["address"] 200 | 201 | 202 | async def get_direct_mongo_client( 203 | ops_test: OpsTest, 204 | app_name: str, 205 | mongos: bool = False, 206 | ) -> MongoClient: 207 | """Returns a direct mongodb client potentially passing over some of the units.""" 208 | port = "27018" 209 | mongodb_name = app_name or await get_app_name(ops_test, APP_NAME) 210 | 211 | for unit in ops_test.model.applications[mongodb_name].units: 212 | if unit.workload_status == "active": 213 | url = await mongodb_uri( 214 | ops_test, 215 | [int(unit.name.split("/")[1])], 216 | app_name=mongodb_name, 217 | port=port, 218 | ) 219 | return MongoClient(url, directConnection=True) 220 | assert False, "No fitting unit could be found" 221 | 222 | 223 | async def get_password( 224 | ops_test: OpsTest, 225 | unit_id: int = 0, 226 | username: str = "operator", 227 | app_name: str = APP_NAME, 228 | ) -> str: 229 | """Use the charm action to retrieve the password from provided unit. 230 | 231 | Returns: 232 | String with the password stored on the peer relation databag. 233 | """ 234 | action = await ops_test.model.units.get(f"{app_name}/{unit_id}").run_action( 235 | "get-password", **{"username": username} 236 | ) 237 | action = await action.wait() 238 | return action.results["password"] 239 | 240 | 241 | async def mongodb_uri( 242 | ops_test: OpsTest, 243 | unit_ids: list[int] | None = None, 244 | port: str = "27017", 245 | app_name: str = APP_NAME, 246 | username: str = "operator", 247 | ) -> str: 248 | if unit_ids is None: 249 | unit_ids = range(0, len(ops_test.model.applications[app_name].units)) 250 | 251 | addresses = [await get_address_of_unit(ops_test, unit_id, app_name) for unit_id in unit_ids] 252 | hosts = [f"{host}:{port}" for host in addresses] 253 | hosts = ",".join(hosts) 254 | 255 | password = await get_password(ops_test, 0, username=username, app_name=app_name) 256 | 257 | return f"mongodb://{username}:{password}@{hosts}/admin" 258 | 259 | 260 | def get_cluster_shards(mongos_client: MongoClient) -> set: 261 | """Returns a set of the shard members.""" 262 | shard_list = mongos_client.admin.command("listShards") 263 | curr_members = [member["host"].split("/")[0] for member in shard_list["shards"]] 264 | return set(curr_members) 265 | 266 | 267 | def has_correct_shards(mongos_client: MongoClient, expected_shards: list[str]) -> bool: 268 | """Returns true if the cluster config has the expected shards.""" 269 | shard_names = get_cluster_shards(mongos_client) 270 | return shard_names == set(expected_shards) 271 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2024 Canonical Ltd. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------