├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── enhancement_proposal.yml ├── pull_request_template.md └── workflows │ ├── auto_update_libs.yaml │ ├── bot_pr_approval.yaml │ ├── comment.yaml │ ├── comment_contributing.yaml │ ├── integration_test.yaml │ ├── integration_test_juju3.yaml │ ├── promote_charm.yaml │ ├── publish_charm.yaml │ └── test.yaml ├── .gitignore ├── .jujuignore ├── .licenserc.yaml ├── .trivyignore ├── .woke.yaml ├── .wokeignore ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── actions.yaml ├── charmcraft.yaml ├── config.yaml ├── docs ├── changelog.md ├── explanation │ └── charm-architecture.md ├── how-to │ ├── configure-hostname.md │ ├── configure-initial-settings.md │ ├── configure-object-storage.md │ ├── contribute.md │ ├── enable-antispam.md │ ├── enable-waf.md │ ├── index.md │ ├── install-plugins.md │ ├── install-themes.md │ ├── integrate-with-cos.md │ ├── redeploy.md │ ├── retrieve-initial-credentials.md │ ├── rotate-secrets.md │ └── upgrade.md ├── index.md ├── reference │ ├── actions.md │ ├── configurations.md │ ├── external-access.md │ ├── plugins.md │ ├── relation-endpoints.md │ └── themes.md └── tutorial.md ├── icon.svg ├── lib └── charms │ ├── data_platform_libs │ └── v0 │ │ └── data_interfaces.py │ ├── grafana_k8s │ └── v0 │ │ └── grafana_dashboard.py │ ├── loki_k8s │ └── v0 │ │ └── loki_push_api.py │ ├── nginx_ingress_integrator │ └── v0 │ │ └── nginx_route.py │ ├── observability_libs │ └── v0 │ │ └── juju_topology.py │ └── prometheus_k8s │ └── v0 │ └── prometheus_scrape.py ├── metadata.yaml ├── pyproject.toml ├── renovate.json ├── requirements.txt ├── src ├── charm.py ├── cos.py ├── exceptions.py ├── grafana_dashboards │ └── wordpress.json ├── loki_alert_rules │ └── .gitkeep ├── state.py └── types_.py ├── tests ├── __init__.py ├── conftest.py ├── integration │ ├── conftest.py │ ├── helper.py │ ├── pre_run_script.sh │ ├── pre_run_script_juju3.sh │ ├── requirements.txt │ ├── requirements_juju3.txt │ ├── test_addon.py │ ├── test_core.py │ ├── test_cos_grafana.py │ ├── test_cos_loki.py │ ├── test_cos_prometheus.py │ ├── test_external.py │ ├── test_ingress.py │ └── test_machine.py └── unit │ ├── __init__.py │ ├── conftest.py │ ├── test_charm.py │ └── wordpress_mock.py ├── tox.ini ├── trivy.yaml └── wordpress_rock ├── files ├── etc │ └── apache2 │ │ ├── apache2.conf │ │ ├── conf-available │ │ ├── docker-php-swift-proxy.conf │ │ └── docker-php.conf │ │ └── sites-available │ │ └── 000-default.conf └── usr │ └── bin │ ├── pruneaccesslogs │ └── pruneerrorlogs ├── patches └── openid.patch └── rockcraft.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | .* 3 | !files/docker-php.conf 4 | !files/docker-php-swift-proxy.conf 5 | !files/apache2.conf 6 | !files/000-default.conf 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["Type: Bug", "Status: Triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: > 8 | Thanks for taking the time to fill out this bug report! Before submitting your issue, please make 9 | sure you are using the latest version of the charm. If not, please switch to this image prior to 10 | posting your report to make sure it's not already solved. 11 | - type: textarea 12 | id: bug-description 13 | attributes: 14 | label: Bug Description 15 | description: > 16 | If applicable, add screenshots to help explain the problem you are facing. 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: reproduction 21 | attributes: 22 | label: To Reproduce 23 | description: > 24 | Please provide a step-by-step instruction of how to reproduce the behavior. 25 | placeholder: | 26 | 1. `juju deploy ...` 27 | 2. `juju relate ...` 28 | 3. `juju status --relations` 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: environment 33 | attributes: 34 | label: Environment 35 | description: > 36 | We need to know a bit more about the context in which you run the charm. 37 | - Are you running Juju locally, on lxd, in multipass or on some other platform? 38 | - What track and channel you deployed the charm from (i.e. `latest/edge` or similar). 39 | - Version of any applicable components, like the juju snap, the model controller, lxd, microk8s, and/or multipass. 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: logs 44 | attributes: 45 | label: Relevant log output 46 | description: > 47 | Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 48 | Fetch the logs using `juju debug-log --replay` and `kubectl logs ...`. Additional details available in the juju docs 49 | at https://juju.is/docs/olm/juju-logs 50 | render: shell 51 | validations: 52 | required: true 53 | - type: textarea 54 | id: additional-context 55 | attributes: 56 | label: Additional context 57 | 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement_proposal.yml: -------------------------------------------------------------------------------- 1 | name: Enhancement Proposal 2 | description: File an enhancement proposal 3 | labels: ["Type: Enhancement", "Status: Triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: > 8 | Thanks for taking the time to fill out this enhancement proposal! Before submitting your issue, please make 9 | sure there isn't already a prior issue concerning this. If there is, please join that discussion instead. 10 | - type: textarea 11 | id: enhancement-proposal 12 | attributes: 13 | label: Enhancement Proposal 14 | description: > 15 | Describe the enhancement you would like to see in as much detail as needed. 16 | validations: 17 | required: true 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Applicable spec: 2 | 3 | ### Overview 4 | 5 | 6 | 7 | ### Rationale 8 | 9 | 10 | 11 | ### Juju Events Changes 12 | 13 | 14 | 15 | ### Module Changes 16 | 17 | 18 | 19 | ### Library Changes 20 | 21 | 22 | 23 | ### Checklist 24 | 25 | - [ ] The [charm style guide](https://juju.is/docs/sdk/styleguide) was applied 26 | - [ ] The [contributing guide](https://github.com/canonical/is-charms-contributing-guide) was applied 27 | - [ ] The changes are compliant with [ISD054 - Managing Charm Complexity](https://discourse.charmhub.io/t/specification-isd014-managing-charm-complexity/11619) 28 | - [ ] The documentation for charmhub is updated. 29 | - [ ] The changelog `docs/changelog.md` is updated. 30 | - [ ] The PR is tagged with appropriate label (`urgent`, `trivial`, `complex`) 31 | - [ ] The docs/changelog.md is updated with user-relevant changes in the format of [keep a changelog v1.1.0](https://keepachangelog.com/en/1.1.0/) 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/auto_update_libs.yaml: -------------------------------------------------------------------------------- 1 | name: Auto-update charm libraries 2 | 3 | on: 4 | schedule: 5 | - cron: "0 1 * * *" 6 | 7 | jobs: 8 | auto-update-libs: 9 | uses: canonical/operator-workflows/.github/workflows/auto_update_charm_libs.yaml@main 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/bot_pr_approval.yaml: -------------------------------------------------------------------------------- 1 | name: Provide approval for bot PRs 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | bot_pr_approval: 8 | uses: canonical/operator-workflows/.github/workflows/bot_pr_approval.yaml@main 9 | secrets: inherit 10 | -------------------------------------------------------------------------------- /.github/workflows/comment.yaml: -------------------------------------------------------------------------------- 1 | name: Comment on the pull request 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Tests"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | comment-on-pr: 11 | uses: canonical/operator-workflows/.github/workflows/comment.yaml@main 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/comment_contributing.yaml: -------------------------------------------------------------------------------- 1 | name: Comment on the pull request 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | branches: 8 | - 'track/**' 9 | 10 | jobs: 11 | comment-on-pr: 12 | uses: canonical/operator-workflows/.github/workflows/comment_contributing.yaml@main 13 | secrets: inherit 14 | -------------------------------------------------------------------------------- /.github/workflows/integration_test.yaml: -------------------------------------------------------------------------------- 1 | name: integration-tests 2 | 3 | on: 4 | pull_request: 5 | workflow_call: 6 | 7 | jobs: 8 | integration-test: 9 | name: Integration Test 10 | uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main 11 | secrets: inherit 12 | with: 13 | extra-arguments: >- 14 | --openstack-rc=${GITHUB_WORKSPACE}/openrc 15 | --kube-config=${GITHUB_WORKSPACE}/kube-config 16 | --screenshot-dir=/tmp 17 | modules: '["test_addon", "test_core", "test_external", "test_ingress"]' 18 | pre-run-script: | 19 | -c "sudo microk8s enable hostpath-storage 20 | sudo microk8s kubectl -n kube-system rollout status -w deployment/hostpath-provisioner 21 | sudo microk8s config > ${GITHUB_WORKSPACE}/kube-config 22 | chmod +x tests/integration/pre_run_script.sh 23 | ./tests/integration/pre_run_script.sh" 24 | setup-devstack-swift: true 25 | trivy-image-config: ./trivy.yaml 26 | channel: 1.31/stable 27 | -------------------------------------------------------------------------------- /.github/workflows/integration_test_juju3.yaml: -------------------------------------------------------------------------------- 1 | name: integration-tests-juju3 2 | 3 | on: 4 | pull_request: 5 | workflow_call: 6 | 7 | jobs: 8 | integration-test-juju3: 9 | name: Integration Test on Juju3 10 | uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main 11 | secrets: inherit 12 | with: 13 | extra-arguments: >- 14 | --openstack-rc=${GITHUB_WORKSPACE}/openrc 15 | --kube-config=${GITHUB_WORKSPACE}/kube-config 16 | --screenshot-dir=/tmp 17 | juju-channel: 3/stable 18 | channel: 1.29-strict/stable 19 | modules: '["test_addon", "test_core", "test_cos_grafana", "test_cos_loki", "test_cos_prometheus"]' 20 | pre-run-script: | 21 | -c "sudo microk8s enable hostpath-storage 22 | sudo microk8s kubectl -n kube-system rollout status -w deployment/hostpath-provisioner 23 | sudo microk8s config > ${GITHUB_WORKSPACE}/kube-config 24 | chmod +x tests/integration/pre_run_script_juju3.sh 25 | ./tests/integration/pre_run_script_juju3.sh" 26 | test-tox-env: "integration-juju3" 27 | setup-devstack-swift: true 28 | trivy-image-config: ./trivy.yaml 29 | -------------------------------------------------------------------------------- /.github/workflows/promote_charm.yaml: -------------------------------------------------------------------------------- 1 | name: Promote charm 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | origin-channel: 7 | type: choice 8 | description: 'Origin Channel' 9 | options: 10 | - latest/edge 11 | destination-channel: 12 | type: choice 13 | description: 'Destination Channel' 14 | options: 15 | - latest/stable 16 | secrets: 17 | CHARMHUB_TOKEN: 18 | required: true 19 | 20 | jobs: 21 | promote-charm: 22 | uses: canonical/operator-workflows/.github/workflows/promote_charm.yaml@main 23 | with: 24 | origin-channel: ${{ github.event.inputs.origin-channel }} 25 | destination-channel: ${{ github.event.inputs.destination-channel }} 26 | secrets: inherit 27 | -------------------------------------------------------------------------------- /.github/workflows/publish_charm.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to edge 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - track/* 8 | 9 | jobs: 10 | publish-to-edge: 11 | uses: canonical/operator-workflows/.github/workflows/publish_charm.yaml@main 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | unit-tests: 8 | uses: canonical/operator-workflows/.github/workflows/test.yaml@main 9 | secrets: inherit 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | *.charm 3 | *.swp 4 | .tox 5 | .coverage 6 | __pycache__ 7 | **/*.rock 8 | -------------------------------------------------------------------------------- /.jujuignore: -------------------------------------------------------------------------------- 1 | /env 2 | /image 3 | *.py[cod] 4 | *.charm 5 | image-builder 6 | Dockerfile 7 | Makefile 8 | -------------------------------------------------------------------------------- /.licenserc.yaml: -------------------------------------------------------------------------------- 1 | header: 2 | license: 3 | spdx-id: Apache-2.0 4 | copyright-owner: Canonical Ltd. 5 | content: | 6 | Copyright [year] [owner] 7 | See LICENSE file for licensing details. 8 | pattern: | 9 | Copyright \d{4} Canonical Ltd. 10 | See LICENSE file for licensing details. 11 | paths: 12 | - '**' 13 | paths-ignore: 14 | - '.github/**' 15 | - '**/.gitkeep' 16 | - '**/*.cfg' 17 | - '**/*.conf' 18 | - '**/*.j2' 19 | - '**/*.json' 20 | - '**/*.md' 21 | - '**/*.rule' 22 | - '**/*.tmpl' 23 | - '**/*.txt' 24 | - '.codespellignore' 25 | - '.dockerignore' 26 | - '.flake8' 27 | - '.jujuignore' 28 | - '.gitignore' 29 | - '.licenserc.yaml' 30 | - '.trivyignore' 31 | - '.woke.yaml' 32 | - '.woke.yml' 33 | - 'CODEOWNERS' 34 | - 'icon.svg' 35 | - 'LICENSE' 36 | - 'trivy.yaml' 37 | - 'zap_rules.tsv' 38 | - 'lib/**' 39 | - 'wordpress_rock/patches/**' 40 | comment: on-failure 41 | -------------------------------------------------------------------------------- /.trivyignore: -------------------------------------------------------------------------------- 1 | CVE-2022-24775 2 | CVE-2022-29248 3 | CVE-2022-31042 4 | CVE-2022-31043 5 | CVE-2022-31090 6 | CVE-2022-31091 7 | CVE-2023-24539 8 | CVE-2023-24540 9 | CVE-2023-29197 10 | CVE-2023-29400 11 | CVE-2023-29403 12 | CVE-2023-39325 13 | CVE-2023-45283 14 | CVE-2023-45287 15 | CVE-2023-45288 16 | CVE-2024-24790 17 | CVE-2024-34156 18 | # golang 19 | CVE-2024-45337 20 | CVE-2024-45338 21 | CVE-2025-22869 22 | -------------------------------------------------------------------------------- /.woke.yaml: -------------------------------------------------------------------------------- 1 | ignore_files: 2 | - lib/charms/nginx_ingress_integrator/v0/nginx_route.py 3 | -------------------------------------------------------------------------------- /.wokeignore: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | # # Ignore charm libs 4 | lib 5 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @canonical/is-charms 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | This document explains the processes and practices recommended for contributing enhancements to the 4 | WordPress operator. 5 | 6 | ## Overview 7 | 8 | - Generally, before developing enhancements to this charm, you should consider [opening an issue 9 | ](https://github.com/canonical/wordpress-k8s-operator/issues) explaining your use case. 10 | - If you would like to chat with us about your use-cases or proposed implementation, you can reach 11 | us at [Canonical Matrix public channel](https://matrix.to/#/#charmhub-charmdev:ubuntu.com) 12 | or [Discourse](https://discourse.charmhub.io/). 13 | - Familiarizing yourself with the [Juju documentation](https://canonical-juju.readthedocs-hosted.com/en/latest/user/howto/manage-charms/) 14 | will help you a lot when working on new features or bug fixes. 15 | - All enhancements require review before being merged. Code review typically examines 16 | - code quality 17 | - test coverage 18 | - user experience for Juju operators of this charm. 19 | - Please help us out in ensuring easy to review branches by rebasing your pull request branch onto 20 | the `main` branch. This also avoids merge commits and creates a linear Git commit history. 21 | - For further information on contributing, please refer to our 22 | [Contributing Guide](https://github.com/canonical/is-charms-contributing-guide). 23 | 24 | ## Canonical contributor agreement 25 | 26 | Canonical welcomes contributions to the WordPress Operator. Please check out our 27 | [contributor agreement](https://ubuntu.com/legal/contributors) if you're interested in contributing 28 | to the solution. 29 | 30 | ## Develop 31 | 32 | To build and deploy the `wordpress-k8s` charm from source follow the steps below. 33 | 34 | ### OCI image build and upload 35 | 36 | Use [Rockcraft](https://documentation.ubuntu.com/rockcraft/en/latest/) to create an 37 | OCI image for the WordPress app, and then upload the image to a [MicroK8s](https://microk8s.io/docs) registry, 38 | which stores OCI archives so they can be downloaded and deployed. 39 | 40 | Enable MicroK8S registry: 41 | 42 | ```bash 43 | microk8s enable registry 44 | ``` 45 | 46 | The following commands pack the OCI image and push it into 47 | the MicroK8s registry: 48 | 49 | ```bash 50 | cd 51 | rockcraft pack 52 | skopeo --insecure-policy copy --dest-tls-verify=false oci-archive:wordpress_1.0_amd64.rock docker://localhost:32000/wordpress:latest 53 | ``` 54 | 55 | ### Build the charm 56 | 57 | Build the charm locally using Charmcraft. It should output a `.charm` file. 58 | 59 | ```bash 60 | charmcraft pack 61 | ``` 62 | 63 | ### Deploy the charm 64 | 65 | Deploy the locally built WordPress charm with the following command. 66 | 67 | ```bash 68 | juju deploy ./wordpress-k8s_ubuntu-22.04-amd64.charm \ 69 | --resource wordpress-image=localhost:32000/wordpress:latest 70 | ``` 71 | 72 | You should now be able to see your local WordPress charm progress through the stages of the 73 | deployment through `juju status --watch 2s`. 74 | 75 | ### Test 76 | 77 | This project uses `tox` for managing test environments. There are some pre-configured environments 78 | that can be used for linting and formatting code when you're preparing contributions to the charm: 79 | 80 | * `tox`: Runs all of the basic checks (`lint`, `unit`, `static`, and `coverage-report`). 81 | * `tox -e fmt`: Runs formatting using `black` and `isort`. 82 | * `tox -e lint`: Runs a range of static code analysis to check the code. 83 | * `tox -e static`: Runs other checks such as `bandit` for security issues. 84 | * `tox -e unit`: Runs the unit tests. 85 | * `tox -e integration`: Runs the integration tests. 86 | -------------------------------------------------------------------------------- /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 2022 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CharmHub Badge](https://charmhub.io/wordpress-k8s/badge.svg)](https://charmhub.io/wordpress-k8s) 2 | [![Publish to edge](https://github.com/canonical/wordpress-k8s-operator/actions/workflows/publish_charm.yaml/badge.svg)](https://github.com/canonical/wordpress-k8s-operator/actions/workflows/publish_charm.yaml) 3 | [![Promote charm](https://github.com/canonical/wordpress-k8s-operator/actions/workflows/promote_charm.yaml/badge.svg)](https://github.com/canonical/wordpress-k8s-operator/actions/workflows/promote_charm.yaml) 4 | [![Discourse Status](https://img.shields.io/discourse/status?server=https%3A%2F%2Fdiscourse.charmhub.io&style=flat&label=CharmHub%20Discourse)](https://discourse.charmhub.io) 5 | 6 | # WordPress operator 7 | 8 | A [Juju](https://juju.is/) [charm](https://juju.is/docs/olm/charmed-operators) deploying and managing WordPress on Kubernetes. [WordPress](https://wordpress.com) is the world's most popular website builder, and it's free and open-source. 9 | 10 | This charm simplifies the deployment and operations of WordPress on Kubernetes, 11 | including scaling the number of instances, integration with SSO, 12 | access to OpenStack Swift object storage for redundant file storage and more. 13 | It allows for deployment on many different Kubernetes platforms, 14 | from [MicroK8s](https://microk8s.io/) to [Charmed Kubernetes](https://ubuntu.com/kubernetes) 15 | to public cloud Kubernetes offerings. 16 | 17 | As such, the charm makes it straightforward for those looking to take control of their own content management system while simplifying operations, 18 | and gives them the freedom to deploy on the Kubernetes platform of their choice. 19 | 20 | For DevOps or SRE teams this charm will make operating WordPress straightforward through Juju's clean interface. 21 | It will allow deployment into multiple environments for testing of changes, 22 | and supports scaling out for enterprise deployments. 23 | 24 | For information about how to deploy, integrate, and manage this charm, see the Official [wordpress-k8s-operator Documentation](https://charmhub.io/wordpress-k8s/docs). 25 | 26 | ## Get started 27 | 28 | To begin, refer to the [Getting Started](https://charmhub.io/wordpress-k8s/docs/tutorial) tutorial for step-by-step instructions. 29 | 30 | ### Basic operations 31 | 32 | The following actions are available for the charm: 33 | - get-initial-password 34 | - rotate-wordpress-secrets 35 | 36 | You can find more information about supported actions in [the Charmhub documentation](https://charmhub.io/wordpress-k8s/actions). 37 | 38 | ## Integrations 39 | 40 | Deployment of WordPress requires a relational database. The integration with the mysql interface is required by the wordpress-k8s charm for which `mysql-k8s` charm can be deployed as follows: 41 | 42 | ``` 43 | juju deploy mysql-k8s --trust 44 | # 'database' interface is required since mysql-k8s charm provides multiple compatible interfaces 45 | juju integrate wordpress-k8s mysql-k8s:database 46 | ``` 47 | 48 | Apart from this required integration, the charm can be integrated with other Juju charms and services as well. You can find the full list of integrations in [the Charmhub documentation](https://charmhub.io/wordpress-k8s/integrations). 49 | 50 | ## Learn more 51 | 52 | - [Read more](https://charmhub.io/wordpress-k8s/docs) 53 | - [Developer documentation](https://codex.wordpress.org/Developer_Documentation) 54 | - [Official webpage](https://wordpress.com) 55 | - [Troubleshooting](https://matrix.to/#/#charmhub-charmdev:ubuntu.com) 56 | 57 | ## Project and community 58 | 59 | The WordPress Operator is a member of the Ubuntu family. 60 | It's an open source project that warmly welcomes community projects, contributions, suggestions, fixes and constructive feedback. 61 | 62 | - [Code of conduct](https://ubuntu.com/community/code-of-conduct) 63 | - [Get support](https://discourse.charmhub.io/) 64 | - [Contribute](https://charmhub.io/wordpress-k8s/docs/how-to-contribute) 65 | - [Matrix](https://matrix.to/#/#charmhub-charmdev:ubuntu.com) 66 | 67 | -------------------------------------------------------------------------------- /actions.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | get-initial-password: 4 | description: > 5 | Retrieve auto-generated initial password for accessing WordPress admin 6 | account. The password is set once during deployment. If the wordpress-k8s charm is configured 7 | with `initial_settings` parameters containing `admin_password`, this action has no effect. 8 | rotate-wordpress-secrets: 9 | description: > 10 | Invalidate user sessions by rotating the following secrets: 11 | auth_key, auth_salt, logged_in_key, logged_in_salt, nonce_key, nonce_salt, secure_auth_key, 12 | secure_auth_salt. 13 | Users will be forced to log in again. This might be useful under security breach circumstances. 14 | update-database: 15 | description: > 16 | After upgrading WordPress to a new version it is typically necessary to run 'wp core update-db' 17 | to migrate the database schema. This action does exactly that. 18 | params: 19 | dry-run: 20 | type: boolean 21 | description: Runs the 'wp core update-db --dry-run' command. 22 | -------------------------------------------------------------------------------- /charmcraft.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | type: charm 4 | bases: 5 | - build-on: 6 | - name: "ubuntu" 7 | channel: "22.04" 8 | run-on: 9 | - name: "ubuntu" 10 | channel: "22.04" 11 | parts: 12 | charm: 13 | # Tell charmcraft to not use requirements.txt 14 | charm-requirements: [] 15 | charm-python-packages: 16 | - ops 17 | - requests 18 | - ops-lib-mysql 19 | charm-binary-python-packages: 20 | - mysql-connector-python==9.1.0 21 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | options: 4 | blog_hostname: 5 | type: string 6 | description: > 7 | Hostname for accessing WordPress, if ingress relation is active. Defaults to the application 8 | name. 9 | default: "" 10 | initial_settings: 11 | type: string 12 | description: > 13 | YAML formatted WordPress configuration. It is used only 14 | during initial deployment. Changing it at later stage has no effect. 15 | If set to non empty string required keys are: 16 | 17 | user_name: admin_username 18 | admin_email: name@example.com 19 | 20 | Optionally you can also provide 21 | 22 | admin_password: # autogenerated if not set 23 | 24 | If admin_password is not provided it will be automatically generated 25 | and stored on the operator pod in the /root directory. 26 | default: | 27 | user_name: admin 28 | admin_email: devnull@example.com 29 | plugins: 30 | type: string 31 | description: | 32 | Plugin slugs of plugins to be installed, separated by comma. Including or excluding 33 | a default plugin here will have no effect. 34 | default: "" 35 | themes: 36 | type: string 37 | description: | 38 | Theme slugs of themes to be installed, separated by comma. Including or excluding 39 | a default theme here will have no effect. 40 | default: "" 41 | wp_plugin_akismet_key: 42 | type: string 43 | description: Akismet key. If empty, akismet will not be automatically enabled 44 | default: "" 45 | wp_plugin_openid_team_map: 46 | type: string 47 | description: > 48 | Launchpad teams and corresponding access levels, for use with the openid plugins. 49 | 50 | Valid WordPress access levels are: administrator, editor, author, contributor, subscriber 51 | 52 | If empty, OpenID will not be enabled. 53 | 54 | Format is key=value pairs (where key is the Launchpad team, and value is 55 | the WordPress role) - commas separate multiple pairs. 56 | 57 | Example format: 58 | 59 | "site-sysadmins=administrator,site-editors=editor,site-executives=editor" 60 | default: "" 61 | wp_plugin_openstack-objectstorage_config: 62 | type: string 63 | description: | 64 | YAML dictionary with keys named after WordPress settings and the desired values. 65 | Please note that the settings will be reset to values provided every time hooks run. 66 | It is important to note that for multi-unit deployments, the `openstack-objectstorage-k8s` 67 | plugin must be enabled to sync data across WordPress applications. Furthermore, object ACLs 68 | must be configured beforehand to be accessible by public. See openstack 69 | documentation(https://docs.openstack.org/swift/latest/overview_acl.html) for more detail. 70 | 71 | ``` 72 | auth-url: authentication URL to openstack. Example: http://10.100.115.2/identity/v3 73 | bucket: name of the bucket for WordPress. Example: WordPress 74 | copy-to-swift: Value ‘1’ or ‘0’ denoting true, false respectively on whether to 75 | copy the local data to swift. Example: 1 76 | domain: OpenStack Project domain ID. Example: Default 77 | object-prefix: Object path prefix. Example: wp-content/uploads/ 78 | password: OpenStack password. Example: openstack_secret_password 79 | region: OpenStack region. Example: RegionOne 80 | remove-local-file: Value ‘1’ or ‘0’ denoting true, false respectively on whether to remove local 81 | file. Example: 0 82 | serve-from-swift: Value ‘1’ or ‘0’ denoting true, false respectively on whether to serve the 83 | contents file directly from swift. If set, media URLs to path $WORDPRESS_IP/wp-content/uploads/ 84 | will be proxied to $OPENSTACK_IP/{account}/{container}/{object-prefix}/. Example: 1 85 | swift-url: OpenStack Swift URL. 86 | example: http://10.100.115.2:8080/v3/AUTH_1d449b4237d3499dabd95210c33ca150 87 | tenant: OpenStack tenant name. Example: demo 88 | username: OpenStack username. Example: demo 89 | ``` 90 | default: "" 91 | use_nginx_ingress_modsec: 92 | type: boolean 93 | default: true 94 | description: > 95 | Boolean value denoting whether modsec based WAF should be enabled. Applied if ingress 96 | relation is available. 97 | health_check_timeout_seconds: 98 | type: int 99 | default: 5 100 | description: > 101 | This setting specifies the duration, in seconds, that pebble will wait for a WordPress health check to complete 102 | before timing out. Use this setting to adjust the timeout based on expected system performance and conditions 103 | upload_max_filesize: 104 | type: string 105 | default: 2M 106 | description: > 107 | The maximum size of an uploaded file. https://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize 108 | post_max_size: 109 | type: string 110 | default: 8M 111 | description: > 112 | Sets max size of post data allowed. https://www.php.net/manual/en/ini.core.php#ini.post-max-size 113 | max_execution_time: 114 | type: int 115 | default: 30 116 | description: > 117 | This sets the maximum time in seconds a script is allowed to run before it is terminated by the parser. 118 | https://www.php.net/manual/en/info.configuration.php#ini.max-execution-time 119 | max_input_time: 120 | type: int 121 | default: -1 122 | description: > 123 | This sets the maximum time in seconds a script is allowed to parse input data, like POST and GET. 124 | https://www.php.net/manual/en/info.configuration.php#ini.max-input-time 125 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 6 | 7 | Each revision is versioned by the date of the revision. 8 | 9 | ### 2025-05-29 10 | 11 | - docs: Fix issues found with local audit for style guide checks. 12 | 13 | ### 2025-03-11 14 | 15 | - fix: use supported YoastSEO plugin version. 16 | - docs: fix charm architecture diagram (separate charm containers boundary) 17 | 18 | ### 2025-03-10 19 | 20 | - Add charm architecture diagram. 21 | - Add changelog for tracking user-relevant changes. 22 | -------------------------------------------------------------------------------- /docs/explanation/charm-architecture.md: -------------------------------------------------------------------------------- 1 | # Charm architecture 2 | 3 | The wordpress-k8s charm aims to provide core functionalities of WordPress with horizontally 4 | scalable architecture, leveraging its flexible capabilities enhanced by plugins. Operational 5 | capabilities are enhanced through integration with the 6 | Canonical Observability Stack ([COS](https://charmhub.io/topics/canonical-observability-stack/)) 7 | charms. 8 | 9 | 10 | ## Containers 11 | 12 | The core component of wordpress-k8s charm consists of a wordpress-k8s main workload container with an Apache Prometheus exporter. The services inside the container are driven by 13 | Pebble, a lightweight API-driven process supervisor that controls the lifecycle of a service. 14 | Learn more about Pebble and its layer configurations [in the Pebble documentation](https://github.com/canonical/pebble). 15 | 16 | ```mermaid 17 | C4Context 18 | title Component diagram for WordPress Charm 19 | 20 | Container_Boundary(wordpress, "WordPress") { 21 | Component(apache-server, "Apache server", "", "Serves the WordPress application") 22 | Component(pebble, "Pebble", "", "Starts the WordPress server and app") 23 | 24 | Rel(pebble, apache-server, "") 25 | } 26 | 27 | Container_Boundary(charm, "WordPress Operator") { 28 | Component(charm, "WordPress Operator", "", "WordPress Operator (charm)") 29 | 30 | Rel(pebble, charm, "") 31 | } 32 | ``` 33 | 34 | ### WordPress 35 | 36 | This container runs the main workload of the charm. The OCI image is custom built and includes 37 | the [WordPress CLI](https://make.wordpress.org/cli/handbook/), Apache server and default WordPress plugins and themes. By 38 | default, Apache server accepts all the web traffic on port 80 and redirects the requests to 39 | WordPress PHP index file, handled by the default `x-httpd-php` handler. The configuration of the 40 | Apache server redirects can be found in 41 | [`wordpress_rock/files/etc/apache2`](https://github.com/canonical/wordpress-k8s-operator/blob/main/wordpress_rock/files/etc/apache2) 42 | folder. 43 | 44 | WordPress, by default, stores uploaded content files locally at `/wp-content/uploads` directory. 45 | To make the content accessible across WordPress replication servers, a swift-proxy is added to 46 | enable content storage on OpenStack Swift through the use of 47 | [`wp_plugin_openstack-objectstorage_config` configuration parameter](https://charmhub.io/wordpress-k8s/configurations#wp_plugin_openstack-objectstorage_config). Swift proxy settings can be found 48 | in [`docker-php-swift-proxy.conf`](https://github.com/canonical/wordpress-k8s-operator/blob/main/wordpress_rock/files/etc/apache2/conf-available/docker-php-swift-proxy.conf) 49 | in the repository. The settings are dynamically modified during runtime when the 50 | `wp_plugin_openstack-objectstorage_config` parameter is configured. 51 | 52 | In order to enable monitoring of Apache server status, redirection to WordPress PHP for route 53 | `/server-status` is overridden in 54 | [`apache2.conf`](https://github.com/canonical/wordpress-k8s-operator/blob/main/wordpress_rock/files/etc/apache2/apache2.conf). 55 | `/server-status` endpoint is accessed by `apache-exporter` service to convert and re-expose with 56 | open metrics compliant format for integration with `prometheus_scrape` interface. 57 | 58 | When a logging relation is joined, a promtail application is started via Pebble which starts 59 | pushing Apache server logs to Loki. The configurations for Apache have been set up to stream logs 60 | to both `access.log`, `error.log` files and container logs in 61 | [`000-default.conf`](https://github.com/canonical/wordpress-k8s-operator/blob/main/wordpress_rock/files/etc/apache2/sites-available/000-default.conf). 62 | These files are essential for promtail to read and push latest logs to Loki periodically. 63 | 64 | ### Charm 65 | 66 | This container is the main point of contact with the Juju controller. It communicates with Juju to 67 | run necessary charm code defined by the main `src/charm.py`. The source code is copied to the 68 | `/var/lib/juju/agents/unit-UNIT_NAME/charm` directory. 69 | 70 | ## OCI image 71 | 72 | The wordpress-image is custom built to include a default set of plugins and themes. The list of 73 | plugins and themes can be found at the reference section of the 74 | [documentation](https://charmhub.io/wordpress-k8s/docs/reference-plugins). Since WordPress is 75 | an application running on PHP, required libraries and dependencies are installed during the build 76 | process. 77 | 78 | WordPress application installation is done at runtime during database connection setup. This can 79 | happen during database relation changed, database relation joined or database config changed 80 | events. 81 | To facilitate the WordPress installation process, 82 | [WordPress CLI](https://make.wordpress.org/cli/handbook/guides/installing/) is embedded in the OCI 83 | image during the build step. The latest CLI PHP archive file from source is used. 84 | 85 | Currently, WordPress version 6.4.3 is used alongside Ubuntu 20.04 base image. The Ubuntu base image 86 | hasn't yet been upgraded to 22.04 due to an unsupported PHP version 8 for 87 | `wordpress-launchpad-integration` plugin (which currently supports PHP version 7). All other plugins and themes use 88 | the latest stable version by default, downloaded from the source. 89 | 90 | ## Integrations 91 | 92 | See [Relation endpoints](../reference/relation-endpoints.md). 93 | 94 | ### Peer relations 95 | 96 | When deploying multiple replications of the wordpress-k8s charm, peer relations are set up to 97 | ensure synchronization of data among replications. Namely, secrets and admin credentials are shared 98 | among peers. See more about the secrets in the `rotate-wordpress-secrets` action in the 99 | [Actions tab](https://charmhub.io/wordpress-k8s/actions#rotate-wordpress-secrets). 100 | 101 | ## Juju events 102 | 103 | Juju events allow progression of the charm in its lifecycle and encapsulates part of the execution 104 | context of a charm. Below is the list of observed events for `wordpress-k8s charm` with how the charm 105 | reacts to the event. For more information about the charm’s lifecycle in general, refer to the 106 | charm’s life [documentation](https://canonical-juju.readthedocs-hosted.com/en/3.6/user/reference/hook/). 107 | 108 | ### `start` 109 | 110 | This event marks the charm’s state as started. The charm’s running state must be persisted by the 111 | charm in its own state. See the documentation on the 112 | [start event](https://canonical-juju.readthedocs-hosted.com/en/3.6/user/reference/hook/#start). 113 | 114 | ### `uploads_storage_attached` 115 | 116 | This event marks the charm’s storage availability. The name of the event derived from the name of 117 | the storage noted in the `metadata.yaml` configuration under "storage" key. 118 | `containers.wordpress.mounts.storage` and `storage.uploads` section. The storage filesystem maps to 119 | `/var/www/html/wp-content/uploads` directory of the WordPress application, which is used to store 120 | uploaded content from the WordPress user. 121 | 122 | ### `leader_elected` 123 | 124 | This event is fired when Juju elects a leader unit among the replica peers. The wordpress-k8s charm 125 | then responds by setting up secrets and sharing them with peers through peer relation databag if 126 | not already set. 127 | 128 | ### `config-changed` 129 | 130 | The wordPress-k8s charm reacts to any configuration change and runs reconciliation between the current 131 | state and the desired state. See the list of 132 | [configurations](https://charmhub.io/wordpress-k8s/configure). 133 | 134 | ### `wordpress_pebble_ready` 135 | 136 | When this event is fired, wordpress-k8s charm installs, configures and starts Apache server for 137 | WordPress through Pebble if the storage is available. Configurations that are set dynamically 138 | include database connection and secrets used by the WordPress application. Dynamic configurations 139 | are modified in `wp-config.php` file and the changes are pushed through Pebble. 140 | 141 | ### `apache_prometheus_exporter_pebble_ready` 142 | 143 | This event signals that the `apache_prometheus_exporter` container is ready in the pod. Apache 144 | Prometheus exporter service is then started through Pebble. 145 | 146 | ### `wordpress-replica_relation_changed` 147 | 148 | When any of the relation is changed, wordpress-k8s charm must run reconciliation between the 149 | current state and the desired state with new relation data to synchronize the replication 150 | instances. The reconciliation process is divided into 3 distinct steps: core, theme and plugin 151 | reconciliation. Core reconciliation sets up the necessary WordPress application configuration: 152 | secrets and database connection. Theme and Plugin respectively reconcile between currently 153 | installed themes and plugins with the incoming list of themes and plugins. 154 | 155 | ### `upgrade-charm` 156 | 157 | The `upgrade-charm` event is fired on the upgrade charm command `juju refresh wordpress-k8s`. The command sets up 158 | secrets in peer-relation databag for upgraded deployment of WordPress if it was not already set. 159 | 160 | ## Charm code overview 161 | 162 | The `src/charm.py` is the default entry point for a charm and has the `WordpressCharm` Python class which inherits from CharmBase. 163 | 164 | CharmBase is the base class from which all Charms are formed, defined by [Ops](https://juju.is/docs/sdk/ops) (Python framework for developing charms). 165 | 166 | > See more in the Juju docs: [Charm](https://canonical-juju.readthedocs-hosted.com/en/3.6/user/reference/charm/). 167 | 168 | The `__init__` method guarantees that the charm observes all events relevant to its operation and handles them. 169 | 170 | -------------------------------------------------------------------------------- /docs/how-to/configure-hostname.md: -------------------------------------------------------------------------------- 1 | # How to configure hostname 2 | 3 | Configure the WordPress hostname with the `blog_hostname` configuration: 4 | 5 | ``` 6 | juju config wordpress-k8s blog_hostname= 7 | ``` 8 | Check that the configuration was updated with: 9 | 10 | ```bash 11 | juju config wordpress-k8s | grep -A 6 blog_hostname 12 | ``` 13 | 14 | The `value:` label should list ``. -------------------------------------------------------------------------------- /docs/how-to/configure-initial-settings.md: -------------------------------------------------------------------------------- 1 | # How to configure initial settings 2 | 3 | [note] 4 | This only works when setting up WordPress initially, before the database 5 | relation setup. Changing the value afterwards has no effect. 6 | [/note] 7 | 8 | By providing configuration value for `initial_settings` at deployment, you can tweak a few 9 | WordPress settings. For detailed information on configurable parameters, please refer to the 10 | [reference guide](https://charmhub.io/wordpress-k8s/docs/reference-configurations). 11 | 12 | ``` 13 | WORDPRESS_SETTINGS=$(cat << EOF 14 | user_name: admin 15 | admin_email: admin@testing.com 16 | admin_password: 17 | EOF 18 | ) 19 | juju deploy wordpress-k8s --config initial_settings=$WORDPRESS_SETTINGS 20 | ``` 21 | 22 | You can verify your initial WordPress settings by navigating to ​​the general settings page in 23 | WordPress (`http:///wp-admin/options-general.php`). 24 | 25 | You can also pass in the wordpress-k8s `configuration.yaml` file with the parameters above. See how 26 | to pass in a configuration file in the 27 | [Juju documentation](https://juju.is/docs/olm/manage-applications#heading--configure-an-application-during-deployment). -------------------------------------------------------------------------------- /docs/how-to/configure-object-storage.md: -------------------------------------------------------------------------------- 1 | # How to configure object storage 2 | 3 | Object storage configuration is required for the `wordpress-k8s` charm to work with 4 | multi-unit deployments. 5 | 6 | ### Prerequisites 7 | 8 | Follow the instructions on installing OpenStack from the OpenStack 9 | [documentation](https://docs.openstack.org/install-guide/). For testing purposes, you can install 10 | [DevStack](https://docs.openstack.org/devstack/latest/). 11 | 12 | After a successful installation, you should be able to see the `openrc` file at the location of 13 | installation. Source `openrc` and load the credentials with: 14 | 15 | ```bash 16 | source openrc && printenv | grep OS_ 17 | ``` 18 | 19 | The output of the command above should look something similar to the following. 20 | 21 | ``` 22 | export OS_CACERT= 23 | export OS_PROJECT_NAME=demo 24 | export OS_TENANT_NAME=demo 25 | export OS_USERNAME=demo 26 | export OS_PASSWORD= 27 | export OS_REGION_NAME=RegionOne 28 | export OS_IDENTITY_API_VERSION=3 29 | export OS_AUTH_TYPE=password 30 | export OS_AUTH_URL=http:///identity 31 | export OS_USER_DOMAIN_ID=default 32 | export OS_PROJECT_DOMAIN_ID=default 33 | export OS_VOLUME_API_VERSION=3 34 | ``` 35 | 36 | ### Configure the OpenStack object storage plugin 37 | 38 | To configure Swift storage for `wordpress-k8s`, copy and paste the following yaml content and adjust 39 | the values accordingly. 40 | 41 | ``` 42 | auth-url: http:///identity/v3 43 | bucket: WordPress 44 | copy-to-swift: 1 45 | domain: Default 46 | object-prefix: wp-content/uploads/ 47 | password: 48 | region: RegionOne 49 | remove-local-file: 0 50 | serve-from-swift: 1 51 | swift-url: swift_auth_url 52 | tenant: demo 53 | username: demo 54 | ``` 55 | 56 | [note] 57 | The `swift-url` value can be obtained by running `swift auth`. The value should look 58 | something like http://:8080/v3/AUTH_1d449b4237d3499dabd95210c33ca150, exported under 59 | OS_STORAGE_URL key. 60 | [/note] 61 | 62 | You can then configure the `wordpress-k8s` charm using the yaml contents above. 63 | 64 | ```bash 65 | juju config wordpress-k8s wp_plugin_openstack-objectstorage_config="$(cat )" 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/how-to/contribute.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Our documentation is hosted on the [Charmhub forum](https://discourse.charmhub.io/t/wordpress-documentation-overview/4052) to enable collaboration. Please use the "Help us improve this documentation" links on each documentation page to either directly change something you see that's wrong, ask a question, or make a suggestion about a potential change via the comments section. 4 | 5 | Our documentation is also available alongside the [source code on GitHub](https://github.com/canonical/wordpress-k8s-operator/). 6 | You may open a pull request with your documentation changes, or you can 7 | [file a bug](https://github.com/canonical/wordpress-k8s-operator/issues) to provide constructive feedback or suggestions. 8 | 9 | See [CONTRIBUTING.md](https://github.com/canonical/wordpress-k8s-operator/blob/main/CONTRIBUTING.md) 10 | for information on contributing to the source code. -------------------------------------------------------------------------------- /docs/how-to/enable-antispam.md: -------------------------------------------------------------------------------- 1 | # How to enable antispam (Akismet) 2 | 3 | Obtain an API key for Akismet by visiting the [Askimet official webpage](https://akismet.com/) 4 | and following the instructions. 5 | 6 | Using your key, enable the Akismet plugin with: 7 | 8 | ``` 9 | juju config wordpress-k8s wp_plugin_akismet_key= 10 | ``` 11 | 12 | The Akismet plugin should automatically be active after running the configuration. -------------------------------------------------------------------------------- /docs/how-to/enable-waf.md: -------------------------------------------------------------------------------- 1 | # How to enable WAF 2 | 3 | This step will walk you through making your WordPress application secure using Modsecurity 3.0 4 | Web Application Firewall. 5 | 6 | ### Prerequisites 7 | 8 | Deploy and relate the [nginx-ingress-integrator](https://charmhub.io/nginx-ingress-integrator) charm. 9 | 10 | ``` 11 | juju deploy nginx-ingress-integrator 12 | juju integrate wordpress-k8s nginx-ingress-integrator 13 | ``` 14 | 15 | ### Enable Modsecurity 3.0 WAF 16 | 17 | [note] 18 | This feature is only available for 19 | [nginx-ingress-integrator](https://charmhub.io/nginx-ingress-integrator) charm. 20 | [/note] 21 | 22 | The modsecurity WAF is enabled by default. 23 | 24 | To check if WAF is enabled, run `kubectl describe wordpress-k8s-ingress -m ` 25 | where `` is the name of the model that your WordPress app is deployed on. For the 26 | model name `wordpress-tutorial`, this command should output something like: 27 | 28 | ``` 29 | Name: wordpress-k8s-ingress 30 | Labels: app.juju.is/created-by=nginx-ingress-integrator 31 | Namespace: wordpress-tutorial 32 | Address: 33 | Ingress Class: 34 | Default backend: 35 | Rules: 36 | Host Path Backends 37 | ---- ---- -------- 38 | wordpress-k8s 39 | / wordpress-k8s-service:80 40 | Annotations: nginx.ingress.kubernetes.io/enable-modsecurity: true 41 | nginx.ingress.kubernetes.io/enable-owasp-modsecurity-crs: true 42 | nginx.ingress.kubernetes.io/modsecurity-snippet: 43 | SecRuleEngine On 44 | SecAction "id:900130,phase:1,nolog,pass,t:none,setvar:tx.crs_exclusions_wordpress=1" 45 | 46 | Include /etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf 47 | nginx.ingress.kubernetes.io/proxy-body-size: 20m 48 | nginx.ingress.kubernetes.io/rewrite-target: / 49 | nginx.ingress.kubernetes.io/ssl-redirect: false 50 | Events: 51 | ``` 52 | 53 | Note the `nginx.ingress.kubernetes.io/enable-modsecurity: true` annotation. 54 | -------------------------------------------------------------------------------- /docs/how-to/index.md: -------------------------------------------------------------------------------- 1 | # How-to guides 2 | 3 | The following guides cover key processes and common tasks for managing 4 | and using the WordPress charm. 5 | 6 | ## Initial setup 7 | * [Retrieve initial credentials] 8 | * [Configure initial settings] 9 | 10 | ## Basic operations 11 | * [Configure hostname] 12 | * [Configure object storage] 13 | * [Install plugins] 14 | * [Install themes] 15 | * [Integrate with COS] 16 | 17 | ## Security 18 | * [Enable antispam] 19 | * [Enable WAF] 20 | * [Rotate secrets] 21 | 22 | ## Upgrade and redeployment 23 | * [Upgrade] 24 | * [Redeploy] 25 | 26 | ## Development 27 | * [Contribute] 28 | 29 | 30 | [Retrieve initial credentials]: retrieve-initial-credentials.md 31 | [Configure initial settings]: configure-initial-settings.md 32 | [Integrate with COS]: integrate-with-cos.md 33 | [Configure hostname]: configure-hostname.md 34 | [Install plugins]: install-plugins.md 35 | [Install themes]: install-themes.md 36 | [Configure object storage]: configure-object-storage.md 37 | [Enable antispam]: enable-antispam.md 38 | [Enable WAF]: enable-waf.md 39 | [Rotate secrets]: rotate-secrets.md 40 | [Upgrade WordPress charm]: upgrade.md 41 | [Redeploy]: redeploy.md 42 | [Contribute]: contribute.md -------------------------------------------------------------------------------- /docs/how-to/install-plugins.md: -------------------------------------------------------------------------------- 1 | # How to install plugins 2 | 3 | Start by locating the plugin from the WordPress [plugins page](https://wordpress.org/plugins/). 4 | Once you’ve located the plugin, the plugin slug is the name of the plugin from the URL of the 5 | selected theme page. For example, for `https://wordpress.org/plugins/akismet/` the plugin slug is 6 | “akismet” after the “/plugins/” path in the URL. 7 | 8 | You can now install the plugin using the plugin slug with `juju config`: 9 | 10 | ``` 11 | juju config wordpress-k8s plugins= 12 | ``` 13 | 14 | To install multiple plugins at once, append more plugins separated by a comma. 15 | 16 | ``` 17 | juju config wordpress-k8s plugins=, 18 | ``` 19 | 20 | Once the configuration is complete, you can navigate to `http:///wp-admin/plugins.php` to 21 | verify your new plugin installation. -------------------------------------------------------------------------------- /docs/how-to/install-themes.md: -------------------------------------------------------------------------------- 1 | # How to install themes 2 | 3 | Start by locating the theme from the WordPress themes page. Once you’ve located the theme, the 4 | theme slug is the name of the theme from the URL of the selected theme page. For example, for 5 | https://wordpress.org/themes/twentytwentytwo/ the plugin slug is “twentytwentytwo” after the 6 | `/themes/` path in the URL. 7 | 8 | You can now install the theme using the theme slug with `juju config`: 9 | 10 | ``` 11 | juju config wordpress-k8s themes= 12 | ``` 13 | 14 | To install multiple themes at once, append more themes separated by a comma. 15 | 16 | ``` 17 | juju config wordpress-k8s themes=, 18 | ``` 19 | 20 | Once the configuration is complete, you can navigate to `http:///wp-admin/themes.php` to 21 | verify your new theme installation. -------------------------------------------------------------------------------- /docs/how-to/integrate-with-cos.md: -------------------------------------------------------------------------------- 1 | # How to integrate with COS 2 | 3 | ## Integrate with Prometheus K8s operator 4 | 5 | Deploy and relate [prometheus-k8s](https://charmhub.io/prometheus-k8s) charm with wordpress-k8s 6 | charm through the `metrics-endpoint` relation via `prometheus_scrape` interface. Prometheus should 7 | start scraping the metrics exposed at `:9117/metrics` endpoint. 8 | 9 | ``` 10 | juju deploy prometheus-k8s 11 | juju integrate wordpress-k8s prometheus-k8s 12 | ``` 13 | 14 | ## Integrate with Loki K8s operator 15 | 16 | Deploy and relate [loki-k8s](https://charmhub.io/loki-k8s) charm with wordpress-k8s charm through 17 | the `logging` relation via `loki_push_api` interface. Promtail worker should spawn and start pushing 18 | Apache access logs and error logs to Loki. 19 | 20 | ``` 21 | juju deploy loki-k8s 22 | juju integrate wordpress-k8s loki-k8s 23 | ``` 24 | 25 | ## Integrate with Grafana K8s operator 26 | 27 | In order for the Grafana dashboard to function properly, Grafana should be able to connect to 28 | Prometheus and Loki as its datasource. Deploy and relate the `prometheus-k8s` and `loki-k8s` 29 | charms with [grafana-k8s](https://charmhub.io/grafana-k8s) charm through the `grafana-source` integration. 30 | 31 | Note that the integration `grafana-source` has to be explicitly stated since `prometheus-k8s` and 32 | `grafana-k8s` share multiple interfaces. 33 | 34 | ``` 35 | juju deploy grafana-k8s 36 | juju integrate prometheus-k8s:grafana-source grafana-k8s:grafana-source 37 | juju integrate loki-k8s:grafana-source grafana-k8s:grafana-source 38 | ``` 39 | 40 | Then, the `wordpress-k8s` charm can be related with Grafana using the `grafana-dashboard` relation with 41 | `grafana_dashboard` interface. 42 | 43 | ``` 44 | juju integrate wordpress-k8s grafana-k8s 45 | ``` 46 | 47 | To access the Grafana dashboard for the WordPress charm, run the `get-admin-password` action 48 | to obtain credentials for admin access. 49 | 50 | ``` 51 | juju run grafana-k8s/0 get-admin-password 52 | ``` 53 | 54 | Log into the Grafana dashboard by visiting `http://:3000`. Navigate to 55 | `http://:3000/dashboards` and access the WordPress dashboard named **Wordpress Operator 56 | Overview**. 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/how-to/redeploy.md: -------------------------------------------------------------------------------- 1 | # How to redeploy 2 | 3 | This guide provides the necessary steps for migrating an existing WordPress 4 | instance to a new charm instance. 5 | 6 | ## Migrate database 7 | 8 | Follow the instructions 9 | in [the MySQL charm migration guide](https://charmhub.io/mysql/docs/h-migrate-cluster-via-restore) 10 | to migrate the content of the WordPress MySQL database. 11 | 12 | ## Migrate media files 13 | 14 | ### Media files stored in Kubernetes storage 15 | 16 | If your media files are stored in Kubernetes 17 | storage (`wp_plugin_openstack-objectstorage_config` is not configured), use the 18 | following steps to migrate your files: 19 | 20 | 1. Use the `juju scp` command to transfer files from 21 | the `/var/www/html/wp-content/uploads` directory of the old `wordpress` 22 | container to a local directory. 23 | 2. Use the `juju scp` command again to copy these files from the local 24 | directory to the `/var/www/html/wp-content/uploads` directory in the new 25 | WordPress charm instance's `wordpress` container. 26 | 27 | ### Media files stored in object storage 28 | 29 | If your media files are stored in object storage and 30 | the `wp_plugin_openstack-objectstorage_config` is not configured, you have two 31 | options: 32 | 33 | 1. Provide the new WordPress charm instance with the same credentials and 34 | connection information for the object storage. This allows the new instance 35 | to automatically access the existing files. 36 | 2. Use tools like [rclone](https://rclone.org) to copy files from the old 37 | storage bucket to a new bucket for the new deployment. -------------------------------------------------------------------------------- /docs/how-to/retrieve-initial-credentials.md: -------------------------------------------------------------------------------- 1 | # How to retrieve initial credentials 2 | 3 | Run the following command to get the initial admin password that can be used to login at 4 | `http:///wp-login.php`. 5 | 6 | ``` 7 | juju run wordpress-k8s/0 get-initial-password 8 | ``` 9 | 10 | The output of the action should look something similar to the following: 11 | 12 | ``` 13 | unit-wordpress-k8s-0: 14 | UnitId: wordpress-k8s/0 15 | id: "10" 16 | results: 17 | password: 18 | status: completed 19 | ``` 20 | 21 | The password should look something like: `XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`. 22 | 23 | You can use the password to log in to the admin account in `http:///wp-admin.php`. 24 | 25 | [note] 26 | If the `admin_password` value was passed in the `initial_settings` configuration, the 27 | password from this action is invalid. 28 | [/note] 29 | -------------------------------------------------------------------------------- /docs/how-to/rotate-secrets.md: -------------------------------------------------------------------------------- 1 | # How to rotate secrets 2 | 3 | To securely update all the WordPress secrets, run the following action. 4 | 5 | ``` 6 | juju run wordpress-k8s/0 rotate-wordpress-secrets 7 | ``` 8 | 9 | This action will force users to be logged out. All sessions and cookies will be invalidated. 10 | -------------------------------------------------------------------------------- /docs/how-to/upgrade.md: -------------------------------------------------------------------------------- 1 | # How to upgrade 2 | 3 | Before updating the charm you need to back up the database using 4 | the MySQL charm's `create-backup` action. 5 | 6 | ```bash 7 | juju run mysql/leader create-backup 8 | ``` 9 | 10 | Additional information can be found about backing up in 11 | [the MySQL documentation](https://charmhub.io/mysql/docs/h-create-and-list-backups). 12 | 13 | Then you can upgrade the WordPress charm: 14 | 15 | ``` 16 | juju refresh wordpress-k8s 17 | ``` 18 | 19 | After upgrading the WordPress charm you need to update the database schema: 20 | 21 | ``` 22 | juju run wordpress-k8s/0 update-database 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # WordPress operator 2 | 3 | A [Juju](https://juju.is/) [charm](https://juju.is/docs/olm/charmed-operators) deploying and managing WordPress on Kubernetes. [WordPress](https://wordpress.com/) is the world's most popular website builder, and it's free and open-source. 4 | 5 | This charm simplifies initial deployment and operations of WordPress on Kubernetes, including scaling the number of instances, integration with SSO, access to OpenStack Swift object storage for redundant file storage, and more. It allows for deployment on many different Kubernetes platforms, from [MicroK8s](https://microk8s.io/) to [Charmed Kubernetes](https://ubuntu.com/kubernetes) to public cloud Kubernetes offerings. 6 | 7 | This charm will make operating WordPress straightforward for DevOps or SRE teams through Juju's clean interface. It will allow deployment into multiple environments to test changes and support scaling out for enterprise deployments. 8 | 9 | ## In this documentation 10 | 11 | | | | 12 | |--|--| 13 | | [Tutorials](https://charmhub.io/wordpress-k8s/docs/tutorials-getting-started)
Get started - a hands-on introduction to using the Charmed WordPress operator for new users
| [How-to guides](https://charmhub.io/wordpress-k8s/docs/how-to-retrieve-initial-credentials)
Step-by-step guides covering key operations and common tasks | 14 | | [Reference](https://charmhub.io/wordpress-k8s/docs/reference-actions)
Technical information - specifications, APIs, architecture | [Explanation](https://charmhub.io/wordpress-k8s/docs/explanation-overview)
Concepts - discussion and clarification of key topics | 15 | 16 | ## Contributing to this documentation 17 | 18 | Documentation is an important part of this project, and we take the same open-source approach to the documentation as the code. As such, we welcome community contributions, suggestions, and constructive feedback on our documentation. See [How to contribute](https://charmhub.io/wordpress-k8s/docs/how-to-contribute) for more information. 19 | 20 | If there's a particular area of documentation that you'd like to see that's missing, please [file a bug](https://github.com/canonical/wordpress-k8s-operator/issues). 21 | 22 | ## Project and community 23 | 24 | The WordPress Operator is a member of the Ubuntu family. It's an open-source project that warmly welcomes community projects, contributions, suggestions, fixes, and constructive feedback. 25 | 26 | - [Code of conduct](https://ubuntu.com/community/code-of-conduct) 27 | - [Get support](https://discourse.charmhub.io/) 28 | - [Join our online chat](https://matrix.to/#/#charmhub-charmdev:ubuntu.com) 29 | - [Contribute](https://github.com/canonical/wordpress-k8s-operator/blob/main/CONTRIBUTING.md) 30 | 31 | Thinking about using the WordPress Operator for your next project? [Get in touch](https://matrix.to/#/#charmhub-charmdev:ubuntu.com)! 32 | 33 | # Contents 34 | 35 | 1. [Tutorial](tutorial.md) 36 | 1. [How-to](how-to) 37 | 1. [Retrieve initial credentials](how-to/retrieve-initial-credentials.md) 38 | 1. [Configure initial settings](how-to/configure-initial-settings.md) 39 | 1. [Configure hostname](how-to/configure-hostname.md) 40 | 1. [Configure object storage](how-to/configure-object-storage.md) 41 | 1. [Install plugins](how-to/install-plugins.md) 42 | 1. [Install themes](how-to/install-themes.md) 43 | 1. [Integrate with COS](how-to/integrate-with-cos.md) 44 | 1. [Enable antispam](how-to/enable-antispam.md) 45 | 1. [Enable WAF](how-to/enable-waf.md) 46 | 1. [Rotate secrets](how-to/rotate-secrets.md) 47 | 1. [Upgrade](how-to/upgrade.md) 48 | 1. [Redeploy](how-to/redeploy.md) 49 | 1. [Contribute](how-to/contribute.md) 50 | 1. [Reference](reference) 51 | 1. [Actions](reference/actions.md) 52 | 1. [Configurations](reference/configurations.md) 53 | 1. [Relation endpoints](reference/relation-endpoints.md) 54 | 1. [Plugins](reference/plugins.md) 55 | 1. [Themes](reference/themes.md) 56 | 1. [Explanation](explanation) 57 | 1. [Charm architecture](explanation/charm-architecture.md) 58 | 1. [Changelog](changelog.md) 59 | -------------------------------------------------------------------------------- /docs/reference/actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | See [Actions](https://charmhub.io/wordpress-k8s/actions). 4 | 5 | [note] 6 | Read more about actions in the Juju docs: [Action](https://canonical-juju.readthedocs-hosted.com/en/3.6/user/reference/action/) 7 | [/note] -------------------------------------------------------------------------------- /docs/reference/configurations.md: -------------------------------------------------------------------------------- 1 | # Configurations 2 | 3 | See [Configurations](https://charmhub.io/wordpress-k8s/configure). 4 | 5 | [note] 6 | Read more about configurations in the Juju docs: [Configuration](https://canonical-juju.readthedocs-hosted.com/en/3.6/user/reference/configuration/) 7 | [/note] -------------------------------------------------------------------------------- /docs/reference/external-access.md: -------------------------------------------------------------------------------- 1 | # External access requirements 2 | 3 | The WordPress charm may need to connect to external services and resources for 4 | certain functionalities. 5 | 6 | ## OpenStack object storage 7 | 8 | When activated, the WordPress instance will need to connect to configured 9 | OpenStack object storage for uploading and retrieval of media and assets. 10 | 11 | ## Akismet spam protection plugin 12 | 13 | The Akismet spam protection plugin, when enabled via the `wp_plugin_akismet_key` 14 | configuration, requires internet access to connect with 15 | the [Akismet API server](https://akismet.com/support/general/connection-issues/). 16 | This connection is essential for verifying and managing spam content. 17 | 18 | ## Installing additional plugins or themes 19 | 20 | For the installation of additional plugins or themes, the WordPress instance 21 | must access the main WordPress site to download installation files. Some plugins 22 | or themes might also need internet access to operate correctly after they are 23 | installed. 24 | -------------------------------------------------------------------------------- /docs/reference/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | By default, the following WordPress plugins are installed with the latest version during the OCI 4 | image build time. If the plugins are installed during runtime with 5 | `juju config wordpress-k8s plugins=`, the plugin will also be installed to its latest 6 | version by default and may cause version differences between pods. 7 | The `wordpress-k8s` charm supports multi-unit deployments. Therefore, installing plugins through UI 8 | has been disabled and can only be installed through the plugins configuration. Please see 9 | [Configurations](https://charmhub.io/wordpress-k8s/configure) section for more 10 | information. 11 | 12 | _\*The descriptions of the following plugins are taken from the WordPress plugin pages._ 13 | 14 | - [404page](https://wordpress.org/plugins/404page/): Custom 404 error page creator using the 15 | WordPress Page Editor. 16 | - [akismet](https://wordpress.org/plugins/akismet/): Comment and contact form submissions spam 17 | checker for malicious content prevention. 18 | - [all-in-one-event-calendar](https://wordpress.org/plugins/all-in-one-event-calendar/): Most 19 | advanced website calendar Responsive calendar system available for WordPress. 20 | - [coschedule-by-todaymade](https://wordpress.org/plugins/coschedule-by-todaymade/): Schedulable 21 | calendar with remote synchronization service. 22 | - [elementor](https://wordpress.org/plugins/elementor/): Intuitive visual website builder platform 23 | for WordPress. 24 | - [essential-addons-for-elementor-lite](https://wordpress.org/plugins/essential-addons-for-elementor-lite/): 25 | Addons for website builder Elementor. 26 | - [favicon-by-realfavicongenerator](https://wordpress.org/plugins/favicon-by-realfavicongenerator/): 27 | Favicon generator for desktop browsers, iPhone/iPad, Android devices, Windows 8 tablets and more. 28 | - [feedwordpress](https://wordpress.org/plugins/feedwordpress/): Atom/RSS aggregator for WordPress by 29 | syndicating content from selected feeds to WordPress weblog. 30 | - [genesis-columns-advanced](https://wordpress.org/plugins/genesis-columns-advanced/): Shortcode 31 | generator for every column configurations available with the column classes provided by the 32 | Genesis Framework. 33 | - [line-break-shortcode](https://wordpress.org/plugins/line-break-shortcode/): Shortcode [br] enabler 34 | for line breaks that will not be filtered out by TinyMCE. 35 | - [no-category-base-wpml](https://wordpress.org/plugins/no-category-base-wpml/): Mandatory 36 | ‘Category Base’ from category permalinks remover. 37 | - [openid](https://wordpress.org/plugins/openid/): Authenticator that allows users to authenticate to 38 | websites without having to create a new password using OpenID standard. 39 | - [openstack-objectstorage-k8s](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/openstack-objectstorage-k8s): 40 | Automatic image, video, document and other media storage provider using Openstack Swift. 41 | - [powerpress](https://wordpress.org/plugins/powerpress/): Podcast manager, enabling podcast 42 | management directly from your WordPress website. 43 | - [post-grid](https://wordpress.org/plugins/post-grid/): Fully customizable post grid layout 44 | builder. 45 | - [redirection](https://wordpress.org/plugins/redirection/): 301 redirect, 404 error tracker and 46 | manager. 47 | - [relative-image-urls](https://wordpress.org/plugins/relative-image-urls/): Relative URL enabler 48 | that overrides WordPress’s absolute URL to file. 49 | - [safe-svg](https://wordpress.org/plugins/safe-svg/): SVG uploader with SVG/XML vulnerability 50 | sanitizer. 51 | - [show-current-template](https://wordpress.org/plugins/show-current-template/): A tool bar indicator 52 | showing the current template file name, the current theme name and included template files’ name. 53 | - [simple-301-redirects](https://wordpress.org/plugins/simple-301-redirects/): Requests 301 54 | redirection enabler. 55 | - [simple-custom-css](https://wordpress.org/plugins/simple-custom-css/): Plugin and Theme default 56 | styles CSS overrider. 57 | - [so-widgets-bundle](https://wordpress.org/plugins/so-widgets-bundle/): Widgets bundle containing 58 | responsive elements for building website pages. 59 | - [svg-support](https://wordpress.org/plugins/svg-support/): Media library SVG uploader and 60 | enabler. 61 | - [syntaxhighlighter](https://wordpress.org/plugins/syntaxhighlighter/): Code syntax highlighter 62 | without losing formatting. 63 | - [wordpress-importer](https://wordpress.org/plugins/wordpress-importer/): A WordPress export file 64 | importer, importing the following content: posts, pages, comments, comment meta, custom fields, 65 | post meta, categories, tags and terms from custom taxonomies and term meta, authors. 66 | - [wordpress-launchpad-integration](https://git.launchpad.net/~canonical-sysadmins/wordpress-launchpad-integration/+git/wordpress-launchpad-integration): 67 | WordPress authenticator using Launchpad's OpenID provider. 68 | - [wordpress-teams-integration](https://git.launchpad.net/~canonical-sysadmins/wordpress-teams-integration/+git/wordpress-teams-integration): 69 | This plugin implements OpenID teams in Wordpress. 70 | - [wp-mastodon-share](https://wordpress.org/plugins/wp-mastodon-share/): Post sharing plugin to share 71 | a post to a user’s Mastodon instance. 72 | - [wp-markdown](https://wordpress.org/plugins/wp-markdown/): Plugin to enable writing posts (of any 73 | post type) using the Markdown syntax. 74 | - [wp-polls](https://wordpress.org/plugins/wp-polls/): Poll creator with customization via templates 75 | and css styles. 76 | - [wp-font-awesome](https://wordpress.org/plugins/wp-font-awesome/): Shortcode handlers to allow 77 | embedding Font Awesome icon a website. 78 | - [wp-lightbox-2](https://wordpress.org/plugins/wp-lightbox-2/): Responsive lightbox effects for 79 | website images and also creating lightbox effects for album/gallery photos on a WordPress blog. 80 | - [wp-statistics](https://wordpress.org/plugins/wp-statistics/): GDPR compliant website statistics 81 | tool. 82 | - [xubuntu-team-members](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-plugin-xubuntu-team-members): 83 | Adds the role "Xubuntu Team member" 84 | -------------------------------------------------------------------------------- /docs/reference/relation-endpoints.md: -------------------------------------------------------------------------------- 1 | # Relation endpoints 2 | 3 | See [Integrations](https://charmhub.io/wordpress-k8s/integrations). 4 | 5 | ### Database 6 | 7 | _Interface_: mysql_client 8 | _Supported charms_: [Charmed MySQL](https://charmhub.io/mysql), [Charmed MySQL-K8s](https://charmhub.io/mysql-k8s) 9 | 10 | The `database` endpoint can be integrated with MySQL based charms, providing long term storage for WordPress. 11 | The database relation connects wordpress-k8s with charms that support the `mysql_client` interface on port 3306 12 | in the database side. 13 | 14 | Example database integrate command: 15 | ``` 16 | juju integrate wordpress-k8s:database mysql-k8s:database 17 | ``` 18 | 19 | ### Grafana dashboard 20 | 21 | _Interface_: grafana-dashboard 22 | _Supported charms_: [grafana-k8s](https://charmhub.io/grafana-k8s) 23 | 24 | Grafana dashboard is a part of the COS relation to enhance observability. 25 | The relation enables quick dashboard access already tailored to fit the needs of 26 | operators to monitor the charm. The template for the Grafana dashboard for the 27 | `wordpress-k8s` charm can be found at `/src/grafana_dashboards/wordpress.json`. 28 | In the Grafana UI, it can be found as “WordPress 29 | Operator Overview” under the General section of the dashboard browser (`/dashboards`). Modifications 30 | to the dashboard can be made but will not be persisted upon restart or redeployment of the charm. 31 | 32 | The `wordpress-k8s` charm 33 | satisfies the `grafana_dashboard` interface by providing the pre-made dashboard template to the 34 | Grafana relation data bag under the "dashboards" key. Requires Prometheus datasource to be already 35 | integrated with Grafana. 36 | 37 | Grafana-Prometheus integrate command: 38 | ``` 39 | juju integrate grafana-k8s:grafana-source prometheus-k8s:grafana-source 40 | ``` 41 | Grafana-dashboard integrate command: 42 | ``` 43 | juju integrate wordpress-k8s grafana-dashboard 44 | ``` 45 | 46 | ### Ingress 47 | 48 | _Interface_: ingress 49 | _Supported charms_: [nginx-ingress-integrator](https://charmhub.io/nginx-ingress-integrator) 50 | 51 | Ingress manages external http/https access to services in a Kubernetes cluster. 52 | The `ingress` relation through [nginx-ingress-integrator](https://charmhub.io/nginx-ingress-integrator) 53 | charm enables additional `blog_hostname` and `use_nginx_ingress_modesec` configurations that 54 | provide capabilities such as ModSecurity enabled 55 | Web Application Firewall ([WAF](https://docs.nginx.com/nginx-waf/)). 56 | 57 | Note that the 58 | Kubernetes cluster must already have an nginx ingress controller deployed. Documentation to 59 | enable ingress in MicroK8s can be found [here](https://microk8s.io/docs/addon-ingress). 60 | 61 | Example ingress integrate command: 62 | ``` 63 | juju integrate wordpress-k8s nginx-ingress-integrator 64 | ``` 65 | 66 | ### Logging 67 | 68 | _Interface_: loki_push_api 69 | _Supported charms_: [loki-k8s](https://charmhub.io/loki-k8s) 70 | 71 | The `logging` relation is a part of the COS relation to enhance logging observability. 72 | Logging relation through the `loki_push_api` interface installs and runs promtail which ships the 73 | contents of local logs found at `/var/log/apache2/access.log` and `/var/log/apache2/error.log` to Loki. 74 | This can then be queried through the Loki API or easily visualized through Grafana. Learn more about COS 75 | [here](https://charmhub.io/topics/canonical-observability-stack). 76 | 77 | Logging-endpoint integrate command: 78 | ``` 79 | juju integrate wordpress-k8s loki-k8s 80 | ``` 81 | 82 | ### Metrics endpoint 83 | 84 | _Interface_: [prometheus_scrape](https://charmhub.io/interfaces/prometheus_scrape-v0) 85 | _Supported charms_: [prometheus-k8s](https://charmhub.io/prometheus-k8s) 86 | 87 | The `metrics-endpoint` relation allows scraping the `/metrics` endpoint provided by `apache-exporter` sidecar 88 | on port 9117, which provides apache metrics from apache’s `/server-status` route. This internal 89 | apache’s `/server-status` route is not exposed and can only be accessed from within the same 90 | Kubernetes pod. The metrics are exposed in the [open metrics format](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#data-model) and will only be scraped by Prometheus once the relation becomes active. For more 91 | information about the metrics exposed, please refer to the [apache-exporter documentation](https://github.com/Lusitaniae/apache_exporter#collectors). 92 | 93 | Metrics-endpoint integrate command: 94 | ``` 95 | juju integrate wordpress-k8s prometheus-k8s 96 | ``` 97 | -------------------------------------------------------------------------------- /docs/reference/themes.md: -------------------------------------------------------------------------------- 1 | # Themes 2 | 3 | By default, the following WordPress themes are installed with the latest version from source. 4 | 5 | - [launchpad](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-launchpad) 6 | - [light-wordpress-theme](https://git.launchpad.net/~canonical-sysadmins/ubuntu-community-webthemes/+git/light-wordpress-theme) 7 | - [mscom](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-mscom) 8 | - [thematic](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-thematic) 9 | - [twentyeleven](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-twentyeleven) 10 | - [twentytwenty](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-twentyeleven) 11 | - [twentytwentyone](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-twentyeleven) 12 | - [twentytwentytwo](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-twentyeleven) 13 | - [ubuntu-cloud-website](https://git.launchpad.net/~canonical-sysadmins/ubuntu-cloud-website/+git/ubuntu-cloud-website) 14 | - [ubuntu-community-wordpress-theme/ubuntu-community](https://git.launchpad.net/~canonical-sysadmins/ubuntu-community-wordpress-theme/+git/ubuntu-community-wordpress-theme) 15 | - [ubuntu-community/ubuntu-community](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-community) 16 | - [ubuntu-fi](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-fi) 17 | - [ubuntu-light](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-light) 18 | - [ubuntustudio-wp/ubuntustudio-wp](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntustudio-wp) 19 | - [xubuntu-website/xubuntu-eighteen](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-xubuntu-website) 20 | - [xubuntu-website/xubuntu-fifteen](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-xubuntu-website) 21 | - [xubuntu-website/xubuntu-fourteen](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-xubuntu-website) 22 | - [xubuntu-website/xubuntu-thirteen](https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-xubuntu-website) -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # Deploy the WordPress charm for the first time 2 | 3 | The `wordpress-k8s` charm helps deploy a horizontally scalable WordPress application with ease and 4 | also helps operate the charm by liaising with the Canonical Observability Stack (COS). This 5 | tutorial will walk you through each step of deployment to get a basic WordPress deployment. 6 | 7 | ## What you'll need 8 | - A working station, e.g., a laptop, with AMD64 architecture. 9 | - Juju 3 installed and bootstrapped to a MicroK8s controller. You can accomplish this process by using a Multipass VM as outlined in this guide: [Set up your test environment](https://canonical-juju.readthedocs-hosted.com/en/latest/user/howto/manage-your-deployment/manage-your-deployment-environment/#set-things-up) 10 | 11 | For more information about how to install Juju, see [Get started with Juju](https://canonical-juju.readthedocs-hosted.com/en/3.6/user/tutorial/). 12 | 13 | ## What you'll do 14 | 15 | - Deploy the [WordPress K8s charm](https://charmhub.io/wordpress-k8s) 16 | - [Deploy and integrate database](#deploy-and-integrate-database) 17 | - [Get admin credentials](#get-admin-credentials) 18 | 19 | ## Set up the environment 20 | 21 | To be able to work inside the Multipass VM first you need to log in with the following command: 22 | ```bash 23 | multipass shell my-juju-vm 24 | ``` 25 | 26 | [note] 27 | If you're working locally, you don't need to do this step. 28 | [/note] 29 | 30 | To manage resources effectively and to separate this tutorial's workload from 31 | your usual work, create a new model in the MicroK8s controller using the following command: 32 | 33 | 34 | ``` 35 | juju add-model wordpress-tutorial 36 | ``` 37 | 38 | ## Deploy WordPress K8s charm 39 | 40 | Deployment of WordPress requires a relational database. The integration with the 41 | `mysql` [interface](https://juju.is/docs/sdk/integration) is required by the wordpress-k8s 42 | charm and hence, [`mysql-k8s`](https://charmhub.io/mysql-k8s) charm will be used. 43 | 44 | Start off by deploying the WordPress charm. By default it will deploy the latest stable release of 45 | the `wordpress-k8s` charm. 46 | 47 | ``` 48 | juju deploy wordpress-k8s 49 | ``` 50 | 51 | ## Deploy and integrate database 52 | 53 | The following commands deploy the mysql-k8s charm and integrate it with the wordpress-k8s charm. 54 | 55 | ``` 56 | juju deploy mysql-k8s --trust 57 | juju integrate wordpress-k8s mysql-k8s:database 58 | ``` 59 | The `database` interface is required since `mysql-k8s` charm provides multiple compatible interfaces. 60 | 61 | Run `juju status` to see the current status of the deployment. The output should be similar to the following: 62 | 63 | ``` 64 | Model Controller Cloud/Region Version SLA Timestamp 65 | wordpress-tutorial microk8s-localhost microk8s/localhost 3.5.3 unsupported 18:48:09Z 66 | 67 | App Version Status Scale Charm Channel Rev Address Exposed Message 68 | mysql-k8s 8.0.37-0ubuntu0.22.04.3 active 1 mysql-k8s 8.0/stable 180 10.152.183.254 no 69 | wordpress-k8s 6.4.3 active 1 wordpress-k8s latest/stable 87 10.152.183.56 no 70 | 71 | Unit Workload Agent Address Ports Message 72 | mysql-k8s/0* active idle 10.1.200.163 Primary 73 | wordpress-k8s/0* active idle 10.1.200.161 74 | ``` 75 | 76 | The deployment finishes when the status shows "Active" for both the WordPress and MySQL charms. 77 | 78 | ## Get admin credentials 79 | 80 | Now that we have an active deployment, let’s access the WordPress 81 | application by accessing the IP of a `wordpress-k8s` unit. To start managing WordPress as an 82 | administrator, you need to get the credentials for the admin account. 83 | 84 | By running the `get-initial-password` action on a `wordpress-k8s` unit, Juju will read and fetch the 85 | admin credentials setup for you. You can use the following command below. 86 | 87 | ``` 88 | juju run wordpress-k8s/0 get-initial-password 89 | ``` 90 | 91 | The result should look something similar to the contents below: 92 | 93 | ``` 94 | Running operation 1 with 1 task 95 | - task 2 on unit-wordpress-k8s-0 96 | 97 | Waiting for task 2... 98 | password: 99 | 100 | ``` 101 | 102 | Password should look something like: `XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`. 103 | 104 | [note] 105 | If you are using Multipass VM for this tutorial, you will need to route the IP from Multipass. To do this first get the IP of the Multipass VM. 106 | Outside the Multipass VM run: 107 | ``` 108 | multipass info my-juju-vm 109 | ``` 110 | The IP you see here will be called in this example. 111 | 112 | Then route: 113 | ``` 114 | sudo ip route add via 115 | ``` 116 | [/note] 117 | 118 | 119 | You can now access your WordPress application at `http:///wp-login.php` and log in with the admin username and password from the previous action. 120 | 121 | 122 | ## Clean up the environment 123 | 124 | Congratulations! You have successfully deployed the WordPress charm, added a database, and accessed the application. 125 | 126 | You can clean up your environment by following this guide: 127 | [Tear down your test environment](https://canonical-juju.readthedocs-hosted.com/en/3.6/user/howto/manage-your-deployment/manage-your-deployment-environment/#tear-things-down) 128 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 31 | 33 | 37 | 41 | 42 | 46 | 51 | 57 | 62 | 67 | 73 | 74 | 78 | 83 | 89 | 94 | 99 | 105 | 106 | 109 | 114 | 120 | 121 | 122 | 126 | 130 | 131 | 136 | 137 | 163 | 166 | 170 | 174 | 178 | 182 | 183 | 185 | 186 | 188 | image/svg+xml 189 | 191 | 192 | 193 | 194 | 195 | 201 | 207 | 211 | 214 | 216 | 222 | 223 | 224 | 225 | 230 | 301 | 302 | -------------------------------------------------------------------------------- /lib/charms/observability_libs/v0/juju_topology.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | """## Overview. 4 | 5 | This document explains how to use the `JujuTopology` class to 6 | create and consume topology information from Juju in a consistent manner. 7 | 8 | The goal of the Juju topology is to uniquely identify a piece 9 | of software running across any of your Juju-managed deployments. 10 | This is achieved by combining the following four elements: 11 | 12 | - Model name 13 | - Model UUID 14 | - Application name 15 | - Unit identifier 16 | 17 | 18 | For a more in-depth description of the concept, as well as a 19 | walk-through of it's use-case in observability, see 20 | [this blog post](https://juju.is/blog/model-driven-observability-part-2-juju-topology-metrics) 21 | on the Juju blog. 22 | 23 | ## Library Usage 24 | 25 | This library may be used to create and consume `JujuTopology` objects. 26 | The `JujuTopology` class provides three ways to create instances: 27 | 28 | ### Using the `from_charm` method 29 | 30 | Enables instantiation by supplying the charm as an argument. When 31 | creating topology objects for the current charm, this is the recommended 32 | approach. 33 | 34 | ```python 35 | topology = JujuTopology.from_charm(self) 36 | ``` 37 | 38 | ### Using the `from_dict` method 39 | 40 | Allows for instantion using a dictionary of relation data, like the 41 | `scrape_metadata` from Prometheus or the labels of an alert rule. When 42 | creating topology objects for remote charms, this is the recommended 43 | approach. 44 | 45 | ```python 46 | scrape_metadata = json.loads(relation.data[relation.app].get("scrape_metadata", "{}")) 47 | topology = JujuTopology.from_dict(scrape_metadata) 48 | ``` 49 | 50 | ### Using the class constructor 51 | 52 | Enables instantiation using whatever values you want. While this 53 | is useful in some very specific cases, this is almost certainly not 54 | what you are looking for as setting these values manually may 55 | result in observability metrics which do not uniquely identify a 56 | charm in order to provide accurate usage reporting, alerting, 57 | horizontal scaling, or other use cases. 58 | 59 | ```python 60 | topology = JujuTopology( 61 | model="some-juju-model", 62 | model_uuid="00000000-0000-0000-0000-000000000001", 63 | application="fancy-juju-application", 64 | unit="fancy-juju-application/0", 65 | charm_name="fancy-juju-application-k8s", 66 | ) 67 | ``` 68 | 69 | """ 70 | from collections import OrderedDict 71 | from typing import Dict, List, Optional 72 | from uuid import UUID 73 | 74 | # The unique Charmhub library identifier, never change it 75 | LIBID = "bced1658f20f49d28b88f61f83c2d232" 76 | 77 | LIBAPI = 0 78 | LIBPATCH = 6 79 | 80 | 81 | class InvalidUUIDError(Exception): 82 | """Invalid UUID was provided.""" 83 | 84 | def __init__(self, uuid: str): 85 | self.message = "'{}' is not a valid UUID.".format(uuid) 86 | super().__init__(self.message) 87 | 88 | 89 | class JujuTopology: 90 | """JujuTopology is used for storing, generating and formatting juju topology information. 91 | 92 | DEPRECATED: This class is deprecated. Use `pip install cosl` and 93 | `from cosl.juju_topology import JujuTopology` instead. 94 | """ 95 | 96 | def __init__( 97 | self, 98 | model: str, 99 | model_uuid: str, 100 | application: str, 101 | unit: Optional[str] = None, 102 | charm_name: Optional[str] = None, 103 | ): 104 | """Build a JujuTopology object. 105 | 106 | A `JujuTopology` object is used for storing and transforming 107 | Juju topology information. This information is used to 108 | annotate Prometheus scrape jobs and alert rules. Such 109 | annotation when applied to scrape jobs helps in identifying 110 | the source of the scrapped metrics. On the other hand when 111 | applied to alert rules topology information ensures that 112 | evaluation of alert expressions is restricted to the source 113 | (charm) from which the alert rules were obtained. 114 | 115 | Args: 116 | model: a string name of the Juju model 117 | model_uuid: a globally unique string identifier for the Juju model 118 | application: an application name as a string 119 | unit: a unit name as a string 120 | charm_name: name of charm as a string 121 | """ 122 | if not self.is_valid_uuid(model_uuid): 123 | raise InvalidUUIDError(model_uuid) 124 | 125 | self._model = model 126 | self._model_uuid = model_uuid 127 | self._application = application 128 | self._charm_name = charm_name 129 | self._unit = unit 130 | 131 | def is_valid_uuid(self, uuid): 132 | """Validate the supplied UUID against the Juju Model UUID pattern. 133 | 134 | Args: 135 | uuid: string that needs to be checked if it is valid v4 UUID. 136 | 137 | Returns: 138 | True if parameter is a valid v4 UUID, False otherwise. 139 | """ 140 | try: 141 | return str(UUID(uuid, version=4)) == uuid 142 | except (ValueError, TypeError): 143 | return False 144 | 145 | @classmethod 146 | def from_charm(cls, charm): 147 | """Creates a JujuTopology instance by using the model data available on a charm object. 148 | 149 | Args: 150 | charm: a `CharmBase` object for which the `JujuTopology` will be constructed 151 | Returns: 152 | a `JujuTopology` object. 153 | """ 154 | return cls( 155 | model=charm.model.name, 156 | model_uuid=charm.model.uuid, 157 | application=charm.model.app.name, 158 | unit=charm.model.unit.name, 159 | charm_name=charm.meta.name, 160 | ) 161 | 162 | @classmethod 163 | def from_dict(cls, data: dict): 164 | """Factory method for creating `JujuTopology` children from a dictionary. 165 | 166 | Args: 167 | data: a dictionary with five keys providing topology information. The keys are 168 | - "model" 169 | - "model_uuid" 170 | - "application" 171 | - "unit" 172 | - "charm_name" 173 | `unit` and `charm_name` may be empty, but will result in more limited 174 | labels. However, this allows us to support charms without workloads. 175 | 176 | Returns: 177 | a `JujuTopology` object. 178 | """ 179 | return cls( 180 | model=data["model"], 181 | model_uuid=data["model_uuid"], 182 | application=data["application"], 183 | unit=data.get("unit", ""), 184 | charm_name=data.get("charm_name", ""), 185 | ) 186 | 187 | def as_dict( 188 | self, 189 | *, 190 | remapped_keys: Optional[Dict[str, str]] = None, 191 | excluded_keys: Optional[List[str]] = None, 192 | ) -> OrderedDict: 193 | """Format the topology information into an ordered dict. 194 | 195 | Keeping the dictionary ordered is important to be able to 196 | compare dicts without having to resort to deep comparisons. 197 | 198 | Args: 199 | remapped_keys: A dictionary mapping old key names to new key names, 200 | which will be substituted when invoked. 201 | excluded_keys: A list of key names to exclude from the returned dict. 202 | uuid_length: The length to crop the UUID to. 203 | """ 204 | ret = OrderedDict( 205 | [ 206 | ("model", self.model), 207 | ("model_uuid", self.model_uuid), 208 | ("application", self.application), 209 | ("unit", self.unit), 210 | ("charm_name", self.charm_name), 211 | ] 212 | ) 213 | if excluded_keys: 214 | ret = OrderedDict({k: v for k, v in ret.items() if k not in excluded_keys}) 215 | 216 | if remapped_keys: 217 | ret = OrderedDict( 218 | (remapped_keys.get(k), v) if remapped_keys.get(k) else (k, v) for k, v in ret.items() # type: ignore 219 | ) 220 | 221 | return ret 222 | 223 | @property 224 | def identifier(self) -> str: 225 | """Format the topology information into a terse string. 226 | 227 | This crops the model UUID, making it unsuitable for comparisons against 228 | anything but other identifiers. Mainly to be used as a display name or file 229 | name where long strings might become an issue. 230 | 231 | >>> JujuTopology( \ 232 | model = "a-model", \ 233 | model_uuid = "00000000-0000-4000-8000-000000000000", \ 234 | application = "some-app", \ 235 | unit = "some-app/1" \ 236 | ).identifier 237 | 'a-model_00000000_some-app' 238 | """ 239 | parts = self.as_dict( 240 | excluded_keys=["unit", "charm_name"], 241 | ) 242 | 243 | parts["model_uuid"] = self.model_uuid_short 244 | values = parts.values() 245 | 246 | return "_".join([str(val) for val in values]).replace("/", "_") 247 | 248 | @property 249 | def label_matcher_dict(self) -> Dict[str, str]: 250 | """Format the topology information into a dict with keys having 'juju_' as prefix. 251 | 252 | Relabelled topology never includes the unit as it would then only match 253 | the leader unit (ie. the unit that produced the dict). 254 | """ 255 | items = self.as_dict( 256 | remapped_keys={"charm_name": "charm"}, 257 | excluded_keys=["unit"], 258 | ).items() 259 | 260 | return {"juju_{}".format(key): value for key, value in items if value} 261 | 262 | @property 263 | def label_matchers(self) -> str: 264 | """Format the topology information into a promql/logql label matcher string. 265 | 266 | Topology label matchers should never include the unit as it 267 | would then only match the leader unit (ie. the unit that 268 | produced the matchers). 269 | """ 270 | items = self.label_matcher_dict.items() 271 | return ", ".join(['{}="{}"'.format(key, value) for key, value in items if value]) 272 | 273 | @property 274 | def model(self) -> str: 275 | """Getter for the juju model value.""" 276 | return self._model 277 | 278 | @property 279 | def model_uuid(self) -> str: 280 | """Getter for the juju model uuid value.""" 281 | return self._model_uuid 282 | 283 | @property 284 | def model_uuid_short(self) -> str: 285 | """Getter for the juju model value, truncated to the first eight letters.""" 286 | return self._model_uuid[:8] 287 | 288 | @property 289 | def application(self) -> str: 290 | """Getter for the juju application value.""" 291 | return self._application 292 | 293 | @property 294 | def charm_name(self) -> Optional[str]: 295 | """Getter for the juju charm name value.""" 296 | return self._charm_name 297 | 298 | @property 299 | def unit(self) -> Optional[str]: 300 | """Getter for the juju unit value.""" 301 | return self._unit 302 | -------------------------------------------------------------------------------- /metadata.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | name: wordpress-k8s 4 | display-name: WordPress 5 | summary: WordPress is an OSS to create a beautiful website. 6 | description: | 7 | A [Juju](https://juju.is/) [charm](https://juju.is/docs/olm/charmed-operators) deploying and managing 8 | WordPress on Kubernetes. [WordPress](https://wordpress.com/) is the world's most popular website 9 | builder, and it's free and open-source. 10 | 11 | This charm simplifies initial deployment and operations of WordPress on Kubernetes, including scaling 12 | the number of instances, integration with SSO, access to OpenStack Swift object storage for redundant 13 | file storage, and more. It allows for deployment on many different Kubernetes platforms, from 14 | [MicroK8s](https://microk8s.io/) to [Charmed Kubernetes](https://ubuntu.com/kubernetes) to public 15 | cloud Kubernetes offerings. 16 | 17 | This charm will make operating WordPress straightforward for DevOps or SRE teams through Juju's clean 18 | interface. It will allow deployment into multiple environments to test changes and support scaling out 19 | for enterprise deployments. 20 | docs: https://discourse.charmhub.io/t/wordpress-documentation-overview/4052 21 | maintainers: 22 | - https://launchpad.net/~canonical-is-devops 23 | issues: https://github.com/canonical/wordpress-k8s-operator/issues 24 | source: https://github.com/canonical/wordpress-k8s-operator 25 | tags: 26 | - applications 27 | - blog 28 | assumes: 29 | - k8s-api 30 | 31 | containers: 32 | wordpress: 33 | resource: wordpress-image 34 | mounts: 35 | - storage: uploads 36 | location: /var/www/html/wp-content/uploads 37 | 38 | storage: 39 | uploads: 40 | type: filesystem 41 | location: /var/www/html/wp-content/uploads 42 | 43 | peers: 44 | wordpress-replica: 45 | interface: wordpress-replica 46 | 47 | resources: 48 | wordpress-image: 49 | type: oci-image 50 | description: OCI image for wordpress 51 | 52 | provides: 53 | metrics-endpoint: 54 | interface: prometheus_scrape 55 | grafana-dashboard: 56 | interface: grafana_dashboard 57 | 58 | requires: 59 | database: 60 | interface: mysql_client 61 | nginx-route: 62 | interface: nginx-route 63 | limit: 1 64 | logging: 65 | interface: loki_push_api 66 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | [tool.bandit] 4 | exclude_dirs = ["/venv/"] 5 | [tool.bandit.assert_used] 6 | skips = ["*/integration/helper.py", "*/*test*"] 7 | 8 | # Testing tools configuration 9 | [tool.coverage.run] 10 | branch = true 11 | 12 | [tool.coverage.report] 13 | fail_under = 90 14 | show_missing = true 15 | 16 | # Formatting tools configuration 17 | [tool.black] 18 | line-length = 99 19 | target-version = ["py38"] 20 | 21 | [tool.isort] 22 | line_length = 99 23 | profile = "black" 24 | 25 | # Linting tools configuration 26 | [tool.flake8] 27 | max-line-length = 99 28 | max-doc-length = 99 29 | max-complexity = 10 30 | exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] 31 | select = ["E", "W", "F", "C", "N", "R", "D", "H"] 32 | # Ignore W503, E501 because using black creates errors with this 33 | ignore = ["W503", "E501"] 34 | # Ignore D104 Missing docstring in public package __init__ 35 | # DCO020, DCO030: Ignore missing args in docstring in mocks 36 | per-file-ignores = [ 37 | "tests/unit/__init__.py:D104", 38 | "tests/**:DCO020,DCO030,DCO050,DCO060,D205,D212" 39 | ] 40 | docstring-convention = "google" 41 | # Check for properly formatted copyright header in each file 42 | copyright-check = "True" 43 | copyright-author = "Canonical Ltd." 44 | copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" 45 | 46 | [tool.pytest.ini_options] 47 | markers = [ 48 | "slow: marks slow and not very important tests", 49 | "requires_secret: mark tests that require external secrets" 50 | ] 51 | 52 | [tool.mypy] 53 | ignore_missing_imports = true 54 | allow_redefinition = true 55 | plugins = ["pydantic.mypy"] 56 | 57 | [tool.pylint.'MESSAGES CONTROL'] 58 | disable = "too-few-public-methods,too-many-arguments,too-many-lines,line-too-long,fixme" 59 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "regexManagers": [ 7 | { 8 | "fileMatch": ["(^|/)rockcraft.yaml$"], 9 | "description": "Update base image references", 10 | "matchStringsStrategy": "any", 11 | "matchStrings": ["# renovate: build-base:\\s+(?[^:]*):(?[^\\s@]*)(@(?sha256:[0-9a-f]*))?", 12 | "# renovate: base:\\s+(?[^:]*):(?[^\\s@]*)(@(?sha256:[0-9a-f]*))?"], 13 | "datasourceTemplate": "docker", 14 | "versioningTemplate": "ubuntu" 15 | } 16 | ], 17 | "packageRules": [ 18 | { 19 | "enabled": true, 20 | "matchDatasources": [ 21 | "docker" 22 | ], 23 | "pinDigests": true 24 | }, 25 | { 26 | "matchFiles": ["rockcraft.yaml"], 27 | "matchUpdateTypes": ["major", "minor", "patch"], 28 | "enabled": false 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mysql-connector-python 2 | ops==2.21.0 3 | requests==2.32.3 4 | pydantic>=1,<2 5 | -------------------------------------------------------------------------------- /src/cos.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2024 Canonical Ltd. 4 | # See LICENSE file for licensing details. 5 | 6 | """COS integration for WordPress charm.""" 7 | from typing import Dict, List, TypedDict 8 | 9 | from charms.loki_k8s.v0.loki_push_api import LogProxyConsumer 10 | from ops.pebble import Check, Layer, Service 11 | 12 | 13 | class PrometheusStaticConfig(TypedDict, total=False): 14 | """Configuration parameters for prometheus metrics endpoint scraping. 15 | 16 | For more information, see: 17 | https://prometheus.io/docs/prometheus/latest/configuration/configuration/#static_config 18 | 19 | Attrs: 20 | targets: list of hosts to scrape, e.g. "*:8080", every unit's port 8080 21 | labels: labels assigned to all metrics scraped from the targets. 22 | """ 23 | 24 | targets: List[str] 25 | labels: Dict[str, str] 26 | 27 | 28 | class PrometheusMetricsJob(TypedDict, total=False): 29 | """Configuration parameters for prometheus metrics scraping job. 30 | 31 | For more information, see: 32 | https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config 33 | 34 | Attrs: 35 | metrics_path: The HTTP resource path on which to fetch metrics from targets. 36 | static_configs: List of labeled statically configured targets for this job. 37 | """ 38 | 39 | metrics_path: str 40 | static_configs: List[PrometheusStaticConfig] 41 | 42 | 43 | APACHE_PROMETHEUS_SCRAPE_PORT = "9117" 44 | _APACHE_EXPORTER_PEBBLE_SERVICE = Service( 45 | name="apache-exporter", 46 | raw={ 47 | "override": "replace", 48 | "summary": "Apache Exporter", 49 | "command": "apache_exporter", 50 | "startup": "enabled", 51 | }, 52 | ) 53 | _APACHE_EXPORTER_PEBBLE_CHECK = Check( 54 | name="apache-exporter-up", 55 | raw={ 56 | "override": "replace", 57 | "level": "alive", 58 | "http": {"url": f"http://localhost:{APACHE_PROMETHEUS_SCRAPE_PORT}/metrics"}, 59 | }, 60 | ) 61 | PROM_EXPORTER_PEBBLE_CONFIG = Layer( 62 | { 63 | "summary": "Apache prometheus exporter", 64 | "description": "Prometheus exporter for apache", 65 | "services": { 66 | _APACHE_EXPORTER_PEBBLE_SERVICE.name: _APACHE_EXPORTER_PEBBLE_SERVICE.to_dict() 67 | }, 68 | "checks": {_APACHE_EXPORTER_PEBBLE_CHECK.name: _APACHE_EXPORTER_PEBBLE_CHECK.to_dict()}, 69 | } 70 | ) 71 | 72 | APACHE_LOG_PATHS = [ 73 | "/var/log/apache2/access.*.log", 74 | "/var/log/apache2/error.*.log", 75 | ] 76 | 77 | REQUEST_DURATION_MICROSECONDS_BUCKETS = [ 78 | 10000, 79 | 25000, 80 | 50000, 81 | 100000, 82 | 200000, 83 | 300000, 84 | 400000, 85 | 500000, 86 | 750000, 87 | 1000000, 88 | 1500000, 89 | 2000000, 90 | 2500000, 91 | 5000000, 92 | 10000000, 93 | ] 94 | 95 | 96 | class ApacheLogProxyConsumer(LogProxyConsumer): 97 | """Extends LogProxyConsumer to add a metrics pipeline to promtail.""" 98 | 99 | def _scrape_configs(self) -> dict: 100 | """Generate the scrape_configs section of the Promtail config file. 101 | 102 | Returns: 103 | A dict representing the `scrape_configs` section. 104 | """ 105 | scrape_configs = super()._scrape_configs() 106 | scrape_configs["scrape_configs"].append( 107 | { 108 | "job_name": "access_log_exporter", 109 | "static_configs": [{"labels": {"__path__": "/var/log/apache2/access.*.log"}}], 110 | "pipeline_stages": [ 111 | { 112 | "logfmt": { 113 | "mapping": { 114 | "request_duration_microseconds": "request_duration_microseconds", 115 | "content_type": "content_type", 116 | "path": "path", 117 | } 118 | } 119 | }, 120 | {"labels": {"content_type": "content_type", "path": "path"}}, 121 | { 122 | "match": { 123 | "selector": '{path=~"^/server-status.*$"}', 124 | "action": "drop", 125 | } 126 | }, 127 | {"labeldrop": ["filename", "path"]}, 128 | { 129 | "metrics": { 130 | "request_duration_microseconds": { 131 | "type": "Histogram", 132 | "source": "request_duration_microseconds", 133 | "prefix": "apache_access_log_", 134 | "config": {"buckets": REQUEST_DURATION_MICROSECONDS_BUCKETS}, 135 | } 136 | } 137 | }, 138 | {"drop": {"expression": ".*"}}, 139 | ], 140 | } 141 | ) 142 | return scrape_configs 143 | -------------------------------------------------------------------------------- /src/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """User-defined exceptions used by WordPress charm.""" 5 | import ops.model 6 | 7 | __all__ = [ 8 | "WordPressStatusException", 9 | "WordPressBlockedStatusException", 10 | "WordPressWaitingStatusException", 11 | "WordPressMaintenanceStatusException", 12 | "WordPressInstallError", 13 | ] 14 | 15 | 16 | # This exception is used to signal the early termination of a reconciliation process. 17 | # The early termination can be caused by many things like relation is not ready or config is not 18 | # updated, and may turn the charm into waiting or block state. They are inevitable in the early 19 | # stage of the charm's lifecycle, thus this is not an error (N818), same for all the subclasses. 20 | class WordPressStatusException(Exception): # noqa: N818 21 | """Exception to signal an early termination of the reconciliation. 22 | 23 | ``status`` represents the status change comes with the early termination. 24 | Do not instantiate this class directly, use subclass instead. 25 | """ 26 | 27 | _status_class = ops.model.StatusBase 28 | 29 | def __init__(self, message: str): 30 | """Initialize the instance. 31 | 32 | Args: 33 | message: A message explaining the reason for given exception. 34 | 35 | Raises: 36 | TypeError: if same base class is used to instantiate base class. 37 | """ 38 | # Using type is necessary to check types between subclasses and superclass. 39 | # pylint: disable=unidiomatic-typecheck 40 | if type(self) is WordPressStatusException: 41 | raise TypeError("Instantiating a base class: WordPressStatusException") 42 | super().__init__(message) 43 | self.status = self._status_class(message) 44 | 45 | 46 | class WordPressBlockedStatusException(WordPressStatusException): # noqa: N818 47 | """Same as :exc:`exceptions.WordPressStatusException`.""" 48 | 49 | _status_class = ops.model.BlockedStatus 50 | 51 | 52 | class WordPressWaitingStatusException(WordPressStatusException): # noqa: N818 53 | """Same as :exc:`exceptions.WordPressStatusException`.""" 54 | 55 | _status_class = ops.model.WaitingStatus 56 | 57 | 58 | class WordPressMaintenanceStatusException(WordPressStatusException): # noqa: N818 59 | """Same as :exc:`exceptions.WordPressStatusException`.""" 60 | 61 | _status_class = ops.model.MaintenanceStatus 62 | 63 | 64 | class WordPressInstallError(Exception): 65 | """Exception for unrecoverable errors during WordPress installation.""" 66 | -------------------------------------------------------------------------------- /src/loki_alert_rules/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canonical/wordpress-k8s-operator/2a5ce7b65cb83a5b5ff2a3194838a21d0a3b522a/src/loki_alert_rules/.gitkeep -------------------------------------------------------------------------------- /src/state.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Wordpress charm state.""" 5 | import dataclasses 6 | import logging 7 | import os 8 | import typing 9 | 10 | import ops 11 | 12 | # pylint: disable=no-name-in-module 13 | from pydantic import BaseModel, HttpUrl, ValidationError 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class CharmConfigInvalidError(Exception): 19 | """Exception raised when a charm configuration is found to be invalid. 20 | 21 | Attributes: 22 | msg: Explanation of the error. 23 | """ 24 | 25 | def __init__(self, msg: str): 26 | """Initialize a new instance of the CharmConfigInvalidError exception. 27 | 28 | Args: 29 | msg: Explanation of the error. 30 | """ 31 | self.msg = msg 32 | 33 | 34 | class ProxyConfig(BaseModel): 35 | """Configuration for external access through proxy. 36 | 37 | Attributes: 38 | http_proxy: The http proxy URL. 39 | https_proxy: The https proxy URL. 40 | no_proxy: Comma separated list of hostnames to bypass proxy. 41 | """ 42 | 43 | http_proxy: typing.Optional[HttpUrl] 44 | https_proxy: typing.Optional[HttpUrl] 45 | no_proxy: typing.Optional[str] 46 | 47 | @classmethod 48 | def from_env(cls) -> typing.Optional["ProxyConfig"]: 49 | """Instantiate ProxyConfig from juju charm environment. 50 | 51 | Returns: 52 | ProxyConfig if proxy configuration is provided, None otherwise. 53 | """ 54 | http_proxy = os.environ.get("JUJU_CHARM_HTTP_PROXY") 55 | https_proxy = os.environ.get("JUJU_CHARM_HTTPS_PROXY") 56 | no_proxy = os.environ.get("JUJU_CHARM_NO_PROXY") 57 | 58 | if not http_proxy and not https_proxy: 59 | return None 60 | 61 | return cls( 62 | http_proxy=http_proxy if http_proxy else None, 63 | https_proxy=https_proxy if https_proxy else None, 64 | no_proxy=no_proxy, 65 | ) 66 | 67 | 68 | @dataclasses.dataclass(frozen=True) 69 | class State: 70 | """The Wordpress k8s operator charm state. 71 | 72 | Attributes: 73 | proxy_config: Proxy configuration to access Jenkins upstream through. 74 | """ 75 | 76 | proxy_config: typing.Optional[ProxyConfig] 77 | 78 | @classmethod 79 | def from_charm(cls, _: ops.CharmBase) -> "State": 80 | """Initialize the state from charm. 81 | 82 | Returns: 83 | Current state of the charm. 84 | 85 | Raises: 86 | CharmConfigInvalidError: if invalid state values were encountered. 87 | """ 88 | try: 89 | proxy_config = ProxyConfig.from_env() 90 | except ValidationError as exc: 91 | logger.error("Invalid juju model proxy configuration, %s", exc) 92 | raise CharmConfigInvalidError("Invalid model proxy configuration.") from exc 93 | 94 | return cls( 95 | proxy_config=proxy_config, 96 | ) 97 | -------------------------------------------------------------------------------- /src/types_.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Module for commonly used internal types in WordPress charm.""" 5 | 6 | from typing import Any, NamedTuple, Optional, Union 7 | 8 | 9 | class CommandExecResult(NamedTuple): 10 | """Result of executed command from WordPress container. 11 | 12 | Attrs: 13 | return_code: exit code from executed command. 14 | stdout: standard output from the executed command. 15 | stderr: standard error output from the executed command. 16 | """ 17 | 18 | return_code: int 19 | stdout: Union[str, bytes] 20 | stderr: Union[str, bytes, None] 21 | 22 | 23 | class ExecResult(NamedTuple): 24 | """Wrapper for executed command result from WordPress container. 25 | 26 | Attrs: 27 | success: True if command successful, else False. 28 | result: returned value from execution command, parsed in desired format. 29 | message: error message output of executed command. 30 | """ 31 | 32 | success: bool 33 | result: Any 34 | message: str 35 | 36 | 37 | class DatabaseConfig(NamedTuple): 38 | """Configuration values required to connect to database. 39 | 40 | Attrs: 41 | hostname: The hostname under which the database is being served. 42 | port: The port which the database is listening on. 43 | database: The name of the database to connect to. 44 | username: The username to use to authenticate to the database. 45 | password: The password to use to authenticat to the database. 46 | """ 47 | 48 | hostname: Optional[str] 49 | port: Optional[int] 50 | database: Optional[str] 51 | username: Optional[str] 52 | password: Optional[str] 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Tests module.""" 5 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Fixtures for Wordpress charm tests.""" 5 | 6 | import pytest 7 | 8 | 9 | def pytest_addoption(parser: pytest.Parser): 10 | """Parse additional pytest options. 11 | 12 | Args: 13 | parser: pytest command line parser. 14 | """ 15 | # --openstack-rc points to an openstack credential file in the "rc" file style 16 | # Here's an example of that file 17 | # $ echo ~/openrc 18 | # export OS_REGION_NAME=RegionOne 19 | # export OS_PROJECT_DOMAIN_ID=default 20 | # export OS_AUTH_URL=http://10.0.0.1/identity 21 | # export OS_TENANT_NAME=demo 22 | # export OS_USER_DOMAIN_ID=default 23 | # export OS_USERNAME=demo 24 | # export OS_VOLUME_API_VERSION=3 25 | # export OS_AUTH_TYPE=password 26 | # export OS_PROJECT_NAME=demo 27 | # export OS_PASSWORD=nomoresecret 28 | # export OS_IDENTITY_API_VERSION=3 29 | parser.addoption("--openstack-rc", action="store", default="") 30 | # Akismet API key for testing the Akismet plugin 31 | parser.addoption("--akismet-api-key", action="store", default="") 32 | # OpenID username and password for testing the OpenID plugin 33 | parser.addoption("--openid-username", action="store", default="") 34 | parser.addoption("--openid-password", action="store", default="") 35 | # Launchpad team for the launchpad OpenID account 36 | parser.addoption("--launchpad-team", action="store", default="") 37 | # Kubernetes cluster configuration file 38 | parser.addoption("--kube-config", action="store", default="") 39 | # Config WordPress with a mysql database deployed as a pod in kubernetes instead of mysql charm 40 | parser.addoption("--test-db-from-config", action="store_true", default=False) 41 | # Number of WordPress units should the test deploy 42 | parser.addoption("--num-units", action="store", type=int, default=1) 43 | # A directory to store screenshots generated by test_upgrade 44 | parser.addoption("--screenshot-dir", action="store", default="") 45 | # WordPress docker image built for the WordPress charm. 46 | parser.addoption("--wordpress-image", action="store") 47 | # Pre-build charm file 48 | parser.addoption("--charm-file", action="store") 49 | -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Fixtures for the wordpress integration tests.""" 5 | 6 | import configparser 7 | import json 8 | import re 9 | import secrets 10 | from pathlib import Path 11 | from typing import AsyncGenerator, Dict, Optional 12 | 13 | import pytest 14 | import pytest_asyncio 15 | import swiftclient 16 | import swiftclient.exceptions 17 | import swiftclient.service 18 | from juju.controller import Controller 19 | from juju.model import Model 20 | from pytest import Config 21 | from pytest_operator.plugin import OpsTest 22 | 23 | from tests.integration.helper import WordpressApp 24 | 25 | 26 | @pytest.fixture(scope="module") 27 | def model(ops_test: OpsTest) -> Model: 28 | """Return the juju model object created by pytest-operator.""" 29 | model = ops_test.model 30 | assert model 31 | return model 32 | 33 | 34 | @pytest.fixture(scope="module", name="kube_config") 35 | def kube_config_fixture(pytestconfig: Config): 36 | """The Kubernetes cluster configuration file.""" 37 | kube_config = pytestconfig.getoption("--kube-config") 38 | assert kube_config, ( 39 | "The Kubernetes config file path should not be empty, " 40 | "please include it in the --kube-config parameter" 41 | ) 42 | return kube_config 43 | 44 | 45 | @pytest_asyncio.fixture(scope="module", name="machine_controller") 46 | async def machine_controller_fixture() -> AsyncGenerator[Controller, None]: 47 | """The lxd controller.""" 48 | controller = Controller() 49 | await controller.connect_controller("localhost") 50 | 51 | yield controller 52 | 53 | await controller.disconnect() 54 | 55 | 56 | @pytest_asyncio.fixture(scope="module", name="machine_model") 57 | async def machine_model_fixture(machine_controller: Controller) -> AsyncGenerator[Model, None]: 58 | """The machine model for jenkins agent machine charm.""" 59 | machine_model_name = f"mysql-machine-{secrets.token_hex(2)}" 60 | model = await machine_controller.add_model(machine_model_name) 61 | 62 | yield model 63 | 64 | await model.disconnect() 65 | 66 | 67 | @pytest_asyncio.fixture(scope="module", name="wordpress") 68 | async def wordpress_fixture( 69 | pytestconfig: Config, ops_test: OpsTest, model: Model, kube_config: str 70 | ) -> WordpressApp: 71 | """Prepare the wordpress charm for integration tests.""" 72 | exit_code, _, _ = await ops_test.juju("model-config", "logging-config==INFO;unit=DEBUG") 73 | assert exit_code == 0 74 | charm = pytestconfig.getoption("--charm-file") 75 | charm_dir = Path(__file__).parent.parent.parent 76 | if not charm: 77 | charm = await ops_test.build_charm(charm_dir) 78 | else: 79 | charm = Path(charm).absolute() 80 | wordpress_image = pytestconfig.getoption("--wordpress-image") 81 | if not wordpress_image: 82 | raise ValueError("--wordpress-image is required to run integration test") 83 | app = await model.deploy( 84 | charm, 85 | resources={ 86 | "wordpress-image": wordpress_image, 87 | }, 88 | num_units=1, 89 | series="focal", 90 | ) 91 | await model.wait_for_idle(status="blocked", apps=[app.name], timeout=30 * 60) 92 | return WordpressApp(app, ops_test=ops_test, kube_config=kube_config) 93 | 94 | 95 | @pytest_asyncio.fixture(scope="module") 96 | async def prepare_mysql(ops_test: OpsTest, wordpress: WordpressApp, model: Model): 97 | """Deploy and relate the mysql-k8s charm for integration tests.""" 98 | app = await model.deploy("mysql-k8s", channel="8.0/stable", trust=True) 99 | await model.wait_for_idle(status="active", apps=[app.name], timeout=30 * 60) 100 | await model.relate(f"{wordpress.name}:database", f"{app.name}:database") 101 | await model.wait_for_idle( 102 | status="active", apps=[app.name, wordpress.name], timeout=40 * 60, idle_period=30 103 | ) 104 | 105 | 106 | @pytest_asyncio.fixture(scope="module") 107 | async def prepare_machine_mysql( 108 | wordpress: WordpressApp, machine_controller: Controller, machine_model: Model, model: Model 109 | ): 110 | """Deploy and relate the mysql-k8s charm for integration tests.""" 111 | await machine_model.deploy("mysql", channel="8.0/edge", trust=True) 112 | await machine_model.create_offer("mysql:database") 113 | await machine_model.wait_for_idle(status="active", apps=["mysql"], timeout=30 * 60) 114 | await model.relate( 115 | f"{wordpress.name}:database", 116 | f"{machine_controller.controller_name}:admin/{machine_model.name}.mysql", 117 | ) 118 | 119 | 120 | @pytest.fixture(scope="module", name="openstack_environment") 121 | def openstack_environment_fixture(pytestconfig: Config): 122 | """Parse the openstack rc style configuration file from the --openstack-rc argument. 123 | 124 | Returns: a dictionary of environment variables and values, or None if --openstack-rc isn't 125 | provided. 126 | """ 127 | rc_file = pytestconfig.getoption("--openstack-rc") 128 | if not rc_file: 129 | raise ValueError("--openstack-rc is required to run this test") 130 | with open(rc_file, encoding="utf-8") as rc_fo: 131 | rc_file = rc_fo.read() 132 | rc_file = re.sub("^export ", "", rc_file, flags=re.MULTILINE) 133 | openstack_conf = configparser.ConfigParser() 134 | openstack_conf.read_string("[DEFAULT]\n" + rc_file) 135 | return {k.upper(): v for k, v in openstack_conf["DEFAULT"].items()} 136 | 137 | 138 | @pytest.fixture(scope="module", name="swift_conn") 139 | def swift_conn_fixture(openstack_environment) -> Optional[swiftclient.Connection]: 140 | """Create a swift connection client.""" 141 | return swiftclient.Connection( 142 | authurl=openstack_environment["OS_AUTH_URL"], 143 | auth_version="3", 144 | user=openstack_environment["OS_USERNAME"], 145 | key=openstack_environment["OS_PASSWORD"], 146 | os_options={ 147 | "user_domain_name": openstack_environment["OS_USER_DOMAIN_ID"], 148 | "project_domain_name": openstack_environment["OS_PROJECT_DOMAIN_ID"], 149 | "project_name": openstack_environment["OS_PROJECT_NAME"], 150 | }, 151 | ) 152 | 153 | 154 | @pytest.fixture(scope="module", name="swift_config") 155 | def swift_config_fixture( 156 | ops_test: OpsTest, 157 | swift_conn: swiftclient.Connection, 158 | openstack_environment: Dict[str, str], 159 | ) -> Dict[str, str]: 160 | """Create a swift config dict that can be used for wp_plugin_openstack-objectstorage_config.""" 161 | swift_service = swiftclient.service.SwiftService( 162 | options={ 163 | "auth_version": "3", 164 | "os_auth_url": openstack_environment["OS_AUTH_URL"], 165 | "os_username": openstack_environment["OS_USERNAME"], 166 | "os_password": openstack_environment["OS_PASSWORD"], 167 | "os_project_name": openstack_environment["OS_PROJECT_NAME"], 168 | "os_project_domain_name": openstack_environment["OS_PROJECT_DOMAIN_ID"], 169 | } 170 | ) 171 | container = f"wordpress_{ops_test.model_name}" 172 | # if the container exists, remove the container 173 | swift_service.delete(container=container) 174 | # create a swift container for our test 175 | swift_conn.put_container(container) 176 | # change container ACL to allow us getting an object by HTTP request without any authentication 177 | # the swift server will act as a static HTTP server after this 178 | swift_service.post(container=container, options={"read_acl": ".r:*,.rlistings"}) 179 | 180 | return { 181 | "auth-url": openstack_environment["OS_AUTH_URL"] + "/v3", 182 | "bucket": container, 183 | "password": openstack_environment["OS_PASSWORD"], 184 | "object-prefix": "wp-content/uploads/", 185 | "region": openstack_environment["OS_REGION_NAME"], 186 | "tenant": openstack_environment["OS_PROJECT_NAME"], 187 | "domain": openstack_environment["OS_PROJECT_DOMAIN_ID"], 188 | "swift-url": swift_conn.url, 189 | "username": openstack_environment["OS_USERNAME"], 190 | "copy-to-swift": "1", 191 | "serve-from-swift": "1", 192 | "remove-local-file": "0", 193 | } 194 | 195 | 196 | @pytest_asyncio.fixture(scope="module") 197 | async def prepare_swift(wordpress: WordpressApp, swift_config: Dict[str, str]): 198 | """Configure the wordpress charm to use openstack swift object storage.""" 199 | await wordpress.set_config( 200 | {"wp_plugin_openstack-objectstorage_config": json.dumps(swift_config)} 201 | ) 202 | await wordpress.model.wait_for_idle(status="active", apps=[wordpress.name], timeout=30 * 60) 203 | 204 | 205 | @pytest_asyncio.fixture(scope="module") 206 | async def prepare_nginx_ingress(wordpress: WordpressApp, prepare_mysql): 207 | """Deploy and relate nginx-ingress-integrator charm for integration tests.""" 208 | await wordpress.model.deploy( 209 | "nginx-ingress-integrator", channel="latest/edge", series="focal", revision=133, trust=True 210 | ) 211 | await wordpress.model.wait_for_idle(apps=["nginx-ingress-integrator"], timeout=30 * 60) 212 | await wordpress.model.relate(f"{wordpress.name}:nginx-route", "nginx-ingress-integrator") 213 | await wordpress.model.wait_for_idle(status="active") 214 | 215 | 216 | @pytest_asyncio.fixture(scope="module") 217 | async def prepare_prometheus(wordpress: WordpressApp, prepare_mysql): 218 | """Deploy and relate prometheus-k8s charm for integration tests.""" 219 | prometheus = await wordpress.model.deploy("prometheus-k8s", channel="1/stable", trust=True) 220 | await wordpress.model.wait_for_idle( 221 | status="active", apps=[prometheus.name], raise_on_error=False, timeout=30 * 60 222 | ) 223 | await wordpress.model.relate(f"{wordpress.name}:metrics-endpoint", prometheus.name) 224 | await wordpress.model.wait_for_idle( 225 | status="active", 226 | apps=[prometheus.name, wordpress.name], 227 | timeout=20 * 60, 228 | raise_on_error=False, 229 | ) 230 | 231 | 232 | @pytest_asyncio.fixture(scope="module") 233 | async def prepare_loki(wordpress: WordpressApp, prepare_mysql): 234 | """Deploy and relate loki-k8s charm for integration tests.""" 235 | loki = await wordpress.model.deploy("loki-k8s", channel="1/stable", trust=True) 236 | await wordpress.model.wait_for_idle(apps=[loki.name], status="active", timeout=20 * 60) 237 | await wordpress.model.relate(f"{wordpress.name}:logging", loki.name) 238 | await wordpress.model.wait_for_idle( 239 | apps=[loki.name, wordpress.name], status="active", timeout=40 * 60 240 | ) 241 | -------------------------------------------------------------------------------- /tests/integration/pre_run_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2024 Canonical Ltd. 4 | # See LICENSE file for licensing details. 5 | 6 | # Pre-run script for integration test operator-workflows action. 7 | # https://github.com/canonical/operator-workflows/blob/main/.github/workflows/integration_test.yaml 8 | 9 | # Jenkins machine agent charm is deployed on lxd and Jenkins-k8s server charm is deployed on 10 | # microk8s. 11 | 12 | TESTING_MODEL="$(juju switch)" 13 | 14 | # lxd should be install and init by a previous step in integration test action. 15 | echo "bootstrapping lxd juju controller" 16 | sg microk8s -c "microk8s status --wait-ready" 17 | sg microk8s -c "juju bootstrap localhost localhost" 18 | 19 | echo "Switching to testing model" 20 | sg microk8s -c "juju switch $TESTING_MODEL" 21 | -------------------------------------------------------------------------------- /tests/integration/pre_run_script_juju3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2024 Canonical Ltd. 4 | # See LICENSE file for licensing details. 5 | 6 | # Pre-run script for integration test operator-workflows action. 7 | # https://github.com/canonical/operator-workflows/blob/main/.github/workflows/integration_test.yaml 8 | 9 | # Jenkins machine agent charm is deployed on lxd and Jenkins-k8s server charm is deployed on 10 | # microk8s. 11 | 12 | TESTING_MODEL="$(juju switch)" 13 | 14 | # lxd should be install and init by a previous step in integration test action. 15 | echo "bootstrapping lxd juju controller" 16 | sg snap_microk8s -c "microk8s status --wait-ready" 17 | sg snap_microk8s -c "juju bootstrap localhost localhost" 18 | 19 | echo "Switching to testing model" 20 | sg snap_microk8s -c "juju switch $TESTING_MODEL" 21 | -------------------------------------------------------------------------------- /tests/integration/requirements.txt: -------------------------------------------------------------------------------- 1 | cosl 2 | juju>=2.9,<3 3 | kubernetes>=25.3,<26 4 | pillow 5 | pytest==8.1.1 6 | pytest-cov 7 | pytest-operator 8 | python-keystoneclient 9 | python-swiftclient 10 | requests 11 | types-PyYAML 12 | types-requests 13 | websockets<14 -------------------------------------------------------------------------------- /tests/integration/requirements_juju3.txt: -------------------------------------------------------------------------------- 1 | cosl 2 | juju==3.6.1.1 3 | kubernetes>=25.3,<26 4 | macaroonbakery==1.3.1 5 | pillow 6 | pytest==8.1.1 7 | pytest-cov 8 | pytest-operator 9 | python-keystoneclient 10 | python-swiftclient 11 | requests 12 | types-PyYAML 13 | types-requests 14 | -------------------------------------------------------------------------------- /tests/integration/test_addon.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Integration tests for WordPress charm COS addon management.""" 5 | 6 | 7 | from typing import List, Set 8 | 9 | import pytest 10 | 11 | from charm import WordpressCharm 12 | from tests.integration.helper import WordpressApp 13 | 14 | 15 | @pytest.mark.usefixtures("prepare_mysql", "prepare_swift") 16 | async def test_wordpress_install_uninstall_themes(wordpress: WordpressApp): 17 | """ 18 | arrange: after WordPress charm has been deployed and db relation established. 19 | act: change themes setting in config. 20 | assert: themes should be installed and uninstalled accordingly. 21 | """ 22 | theme_change_list: List[Set[str]] = [ 23 | {"twentyfifteen", "classic"}, 24 | {"tt1-blocks", "twentyfifteen"}, 25 | {"tt1-blocks"}, 26 | {"twentyeleven"}, 27 | set(), 28 | ] 29 | for themes in theme_change_list: 30 | await wordpress.set_config({"themes": ",".join(themes)}) 31 | await wordpress.model.wait_for_idle(status="active", apps=[wordpress.name]) 32 | 33 | for wordpress_client in await wordpress.client_for_units(): 34 | expected_themes = themes 35 | expected_themes.update(WordpressCharm._WORDPRESS_DEFAULT_THEMES) 36 | actual_themes = set(wordpress_client.list_themes()) 37 | assert ( 38 | expected_themes == actual_themes 39 | ), f"theme installed {themes} should match themes setting in config" 40 | 41 | 42 | @pytest.mark.usefixtures("prepare_mysql", "prepare_swift") 43 | async def test_wordpress_theme_installation_error(wordpress: WordpressApp): 44 | """ 45 | arrange: after WordPress charm has been deployed and db relation established. 46 | act: install a nonexistent theme. 47 | assert: charm should switch to blocked state and the reason should be included in the status 48 | message. 49 | """ 50 | invalid_theme = "invalid-theme-sgkeahrgalejr" 51 | await wordpress.set_config({"themes": invalid_theme}) 52 | await wordpress.wait_for_wordpress_idle() 53 | 54 | for unit in wordpress.get_units(): 55 | assert ( 56 | unit.workload_status == "blocked" 57 | ), "status should be 'blocked' since the theme in themes config does not exist" 58 | 59 | assert ( 60 | invalid_theme in unit.workload_status_message 61 | ), "status message should contain the reason why it's blocked" 62 | 63 | await wordpress.set_config({"themes": ""}) 64 | await wordpress.wait_for_wordpress_idle(status="active") 65 | 66 | 67 | @pytest.mark.usefixtures("prepare_mysql", "prepare_swift") 68 | async def test_wordpress_install_uninstall_plugins(wordpress: WordpressApp): 69 | """ 70 | arrange: after WordPress charm has been deployed and db relation established. 71 | act: change plugins setting in config. 72 | assert: plugins should be installed and uninstalled accordingly. 73 | """ 74 | plugin_change_list: List[Set[str]] = [ 75 | {"classic-editor", "classic-widgets"}, 76 | {"classic-editor"}, 77 | {"classic-widgets"}, 78 | set(), 79 | ] 80 | for plugins in plugin_change_list: 81 | await wordpress.set_config({"plugins": ",".join(plugins)}) 82 | await wordpress.wait_for_wordpress_idle(status="active") 83 | 84 | for wordpress_client in await wordpress.client_for_units(): 85 | expected_plugins = plugins 86 | expected_plugins.update(WordpressCharm._WORDPRESS_DEFAULT_PLUGINS) 87 | actual_plugins = set(wordpress_client.list_plugins()) 88 | assert ( 89 | expected_plugins == actual_plugins 90 | ), f"plugin installed {plugins} should match plugins setting in config" 91 | 92 | 93 | @pytest.mark.usefixtures("prepare_mysql", "prepare_swift") 94 | async def test_wordpress_plugin_installation_error(wordpress: WordpressApp): 95 | """ 96 | arrange: after WordPress charm has been deployed and db relation established. 97 | act: install a nonexistent plugin. 98 | assert: charm should switch to blocked state and the reason should be included in the status 99 | message. 100 | """ 101 | invalid_plugin = "invalid-plugin-sgkeahrgalejr" 102 | await wordpress.set_config({"plugins": invalid_plugin}) 103 | await wordpress.wait_for_wordpress_idle() 104 | 105 | for unit in wordpress.get_units(): 106 | assert ( 107 | unit.workload_status == "blocked" 108 | ), "status should be 'blocked' since the plugin in plugins config does not exist" 109 | 110 | assert ( 111 | invalid_plugin in unit.workload_status_message 112 | ), "status message should contain the reason why it's blocked" 113 | 114 | await wordpress.set_config({"plugins": ""}) 115 | await wordpress.wait_for_wordpress_idle(status="active") 116 | -------------------------------------------------------------------------------- /tests/integration/test_core.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Integration tests for WordPress charm core functionality.""" 5 | 6 | import io 7 | import json 8 | import secrets 9 | import urllib.parse 10 | 11 | import PIL.Image 12 | import pytest 13 | import requests 14 | from pytest_operator.plugin import OpsTest 15 | 16 | from tests.integration.helper import WordpressApp, WordpressClient 17 | 18 | 19 | @pytest.mark.usefixtures("prepare_mysql") 20 | @pytest.mark.abort_on_fail 21 | async def test_wordpress_up(wordpress: WordpressApp, ops_test: OpsTest): 22 | """ 23 | arrange: after WordPress charm has been deployed and db relation established. 24 | act: test wordpress server is up. 25 | assert: wordpress service is up. 26 | """ 27 | await wordpress.model.wait_for_idle(status="active") 28 | for unit_ip in await wordpress.get_unit_ips(): 29 | assert requests.get(f"http://{unit_ip}", timeout=10).status_code == 200 30 | 31 | 32 | @pytest.mark.usefixtures("prepare_mysql", "prepare_swift") 33 | async def test_wordpress_functionality(wordpress: WordpressApp): 34 | """ 35 | arrange: after WordPress charm has been deployed and db relation established. 36 | act: test WordPress basic functionality (login, post, comment). 37 | assert: WordPress works normally as a blog site. 38 | """ 39 | for unit_ip in await wordpress.get_unit_ips(): 40 | WordpressClient.run_wordpress_functionality_test( 41 | host=unit_ip, 42 | admin_username="admin", 43 | admin_password=await wordpress.get_default_admin_password(), 44 | ) 45 | 46 | 47 | @pytest.mark.usefixtures("prepare_mysql") 48 | async def test_change_upload_limit(wordpress: WordpressApp): 49 | """ 50 | arrange: after WordPress charm has been deployed and db relation established. 51 | act: change upload limit related settings. 52 | assert: upload limit change should be reflected in the upload page. 53 | """ 54 | await wordpress.set_config({"upload_max_filesize": "16M"}) 55 | await wordpress.model.wait_for_idle(status="active") 56 | password = await wordpress.get_default_admin_password() 57 | for unit_ip in await wordpress.get_unit_ips(): 58 | wordpress_client = WordpressClient( 59 | host=unit_ip, 60 | username="admin", 61 | password=password, 62 | is_admin=True, 63 | ) 64 | text = wordpress_client.get_post(f"http://{unit_ip}/wp-admin/upload.php") 65 | # upload limit = min(upload_max_filesize, post_max_size) 66 | assert "Maximum upload file size: 8 MB" in text 67 | await wordpress.set_config({"post_max_size": "16M"}) 68 | await wordpress.model.wait_for_idle(status="active") 69 | for unit_ip in await wordpress.get_unit_ips(): 70 | wordpress_client = WordpressClient( 71 | host=unit_ip, 72 | username="admin", 73 | password=password, 74 | is_admin=True, 75 | ) 76 | text = wordpress_client.get_post(f"http://{unit_ip}/wp-admin/upload.php") 77 | assert "Maximum upload file size: 16 MB" in text 78 | 79 | 80 | @pytest.mark.usefixtures("prepare_mysql", "prepare_swift") 81 | async def test_openstack_object_storage_plugin( 82 | wordpress: WordpressApp, 83 | swift_conn, 84 | ): 85 | """ 86 | arrange: after charm deployed, db relation established and openstack swift server ready. 87 | act: update charm configuration for openstack object storage plugin. 88 | assert: openstack object storage plugin should be installed after the config update and 89 | WordPress openstack swift object storage integration should be set up properly. 90 | After openstack swift plugin activated, an image file uploaded to one unit through 91 | WordPress media uploader should be accessible from all units. 92 | """ 93 | container = await wordpress.get_swift_bucket() 94 | for idx, unit_ip in enumerate(await wordpress.get_unit_ips()): 95 | image = PIL.Image.new("RGB", (500, 500), color=(idx, 0, 0)) 96 | nonce = secrets.token_hex(8) 97 | filename = f"{nonce}.{unit_ip}.{idx}.jpg" 98 | image_buf = io.BytesIO() 99 | image.save(image_buf, format="jpeg") 100 | image = image_buf.getvalue() 101 | wordpress_client = WordpressClient( 102 | host=unit_ip, 103 | username="admin", 104 | password=await wordpress.get_default_admin_password(), 105 | is_admin=True, 106 | ) 107 | image_urls = wordpress_client.upload_media(filename=filename, content=image)["urls"] 108 | swift_object_list = [ 109 | o["name"] for o in swift_conn.get_container(container, full_listing=True)[1] 110 | ] 111 | assert any( 112 | nonce in f for f in swift_object_list 113 | ), "media files uploaded should be stored in swift object storage" 114 | source_url = min(image_urls, key=len) 115 | for image_url in image_urls: 116 | assert ( 117 | requests.get(image_url, timeout=10).status_code == 200 118 | ), "the original image and resized images should be accessible from the WordPress site" 119 | for host in await wordpress.get_unit_ips(): 120 | url_components = list(urllib.parse.urlsplit(source_url)) 121 | url_components[1] = host 122 | url = urllib.parse.urlunsplit(url_components) 123 | assert ( 124 | requests.get(url, timeout=10).content == image 125 | ), "image downloaded from WordPress should match the image uploaded" 126 | 127 | 128 | @pytest.mark.usefixtures("prepare_mysql", "prepare_swift") 129 | async def test_apache_config(wordpress: WordpressApp, ops_test: OpsTest): 130 | """ 131 | arrange: after WordPress charm has been deployed and db relation established. 132 | act: update the config to trigger a new reconciliation. 133 | assert: apache config test works properly and prevents the restart of the server. 134 | """ 135 | await wordpress.set_config( 136 | {"initial_settings": json.dumps({"user_name": "foo", "admin_email": "bar@example.com"})} 137 | ) 138 | await wordpress.wait_for_wordpress_idle() 139 | exit_code, stdout, _ = await ops_test.juju("debug-log", "--replay") 140 | assert exit_code == 0 141 | assert "Apache config docker-php-swift-proxy is enabled" in stdout 142 | assert "Conf docker-php-swift-proxy already enabled" not in stdout 143 | 144 | 145 | @pytest.mark.usefixtures("prepare_mysql") 146 | async def test_uploads_owner(wordpress: WordpressApp, ops_test: OpsTest): 147 | """ 148 | arrange: after WordPress charm has been deployed and db relation established. 149 | act: get uploads directory owner 150 | assert: uploads belongs to wordpress user. 151 | """ 152 | cmd = [ 153 | "juju", 154 | "ssh", 155 | f"{wordpress.app.name}/0", 156 | "stat", 157 | '--printf="%u"', 158 | "/var/www/html/wp-content/uploads", 159 | ] 160 | 161 | retcode, stdout, _ = await ops_test.run(*cmd) 162 | assert retcode == 0 163 | assert "584792" == stdout.strip() 164 | -------------------------------------------------------------------------------- /tests/integration/test_cos_grafana.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | # pylint: disable=protected-access,too-many-locals 5 | 6 | """Integration tests for WordPress charm COS integration.""" 7 | 8 | import functools 9 | 10 | import pytest 11 | import requests 12 | from juju.action import Action 13 | from juju.client._definitions import FullStatus 14 | 15 | from tests.integration.helper import WordpressApp, wait_for 16 | 17 | 18 | def dashboard_exist(loggedin_session: requests.Session, unit_address: str): 19 | """Checks if the WordPress dashboard is registered in Grafana. 20 | 21 | Args: 22 | loggedin_session: Requests session that's authorized to make API calls. 23 | unit_address: Grafana unit address. 24 | 25 | Returns: 26 | True if all dashboard is found. False otherwise. 27 | """ 28 | dashboards = loggedin_session.get( 29 | f"http://{unit_address}:3000/api/search", 30 | timeout=10, 31 | params={"query": "Wordpress Operator Overview"}, 32 | ).json() 33 | return len(dashboards) 34 | 35 | 36 | @pytest.mark.usefixtures("prepare_mysql") 37 | async def test_grafana_integration( 38 | wordpress: WordpressApp, 39 | ): 40 | """ 41 | arrange: after WordPress charm has been deployed and relations established among cos. 42 | act: grafana charm joins relation 43 | assert: grafana wordpress dashboard can be found 44 | """ 45 | grafana = await wordpress.model.deploy("grafana-k8s", channel="1/stable", trust=True) 46 | await wordpress.model.wait_for_idle(status="active", apps=["grafana-k8s"], timeout=20 * 60) 47 | await wordpress.model.add_relation("wordpress-k8s:grafana-dashboard", "grafana-k8s") 48 | await wordpress.model.wait_for_idle( 49 | status="active", apps=["grafana-k8s", "wordpress-k8s"], timeout=30 * 60 50 | ) 51 | action: Action = await grafana.units[0].run_action("get-admin-password") 52 | await action.wait() 53 | password = action.results["admin-password"] 54 | 55 | status: FullStatus = await wordpress.model.get_status(filters=[grafana.name]) 56 | for unit in status.applications[grafana.name].units.values(): 57 | sess = requests.session() 58 | sess.post( 59 | f"http://{unit.address}:3000/login", 60 | json={ 61 | "user": "admin", 62 | "password": password, 63 | }, 64 | ).raise_for_status() 65 | await wait_for( 66 | functools.partial(dashboard_exist, loggedin_session=sess, unit_address=unit.address), 67 | timeout=60 * 20, 68 | ) 69 | -------------------------------------------------------------------------------- /tests/integration/test_cos_loki.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | # pylint: disable=protected-access,too-many-locals 5 | 6 | """Integration tests for WordPress charm COS integration.""" 7 | 8 | import functools 9 | from typing import Iterable 10 | 11 | import kubernetes 12 | import pytest 13 | import requests 14 | from juju.client._definitions import FullStatus 15 | 16 | from tests.integration.helper import WordpressApp, wait_for 17 | 18 | 19 | def log_files_exist(unit_address: str, application_name: str, filenames: Iterable[str]) -> bool: 20 | """Returns whether log filenames exist in Loki logs query. 21 | 22 | Args: 23 | unit_address: Loki unit ip address. 24 | application_name: Application name to query logs for. 25 | filenames: Expected filenames to be present in logs collected by Loki. 26 | 27 | Returns: 28 | True if log files with logs exists. False otherwise. 29 | """ 30 | series = requests.get(f"http://{unit_address}:3100/loki/api/v1/series", timeout=10).text 31 | assert application_name in series 32 | log_query = requests.get( 33 | f"http://{unit_address}:3100/loki/api/v1/query", 34 | timeout=10, 35 | params={"query": f'{{juju_application="{application_name}"}}'}, 36 | ).json() 37 | 38 | return len(log_query["data"]["result"]) != 0 39 | 40 | 41 | @pytest.mark.abort_on_fail 42 | @pytest.mark.usefixtures("prepare_mysql", "prepare_loki") 43 | async def test_loki_integration( 44 | wordpress: WordpressApp, 45 | kube_config: str, 46 | ): 47 | """ 48 | arrange: after WordPress charm has been deployed and relations established. 49 | act: loki charm joins relation 50 | assert: loki joins relation successfully, logs are being output to container and to files for 51 | loki to scrape. 52 | """ 53 | status: FullStatus = await wordpress.model.get_status(filters=["loki-k8s"]) 54 | for unit in status.applications["loki-k8s"].units.values(): 55 | await wait_for( 56 | functools.partial( 57 | log_files_exist, 58 | unit.address, 59 | wordpress.name, 60 | ("/var/log/apache2/error.*.log", "/var/log/apache2/access.*.log"), 61 | ), 62 | timeout=10 * 60, 63 | ) 64 | kubernetes.config.load_kube_config(config_file=kube_config) 65 | kube_core_client = kubernetes.client.CoreV1Api() 66 | 67 | kube_log = kube_core_client.read_namespaced_pod_log( 68 | name=f"{wordpress.name}-0", namespace=wordpress.model.name, container="wordpress" 69 | ) 70 | assert kube_log 71 | -------------------------------------------------------------------------------- /tests/integration/test_cos_prometheus.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | # pylint: disable=protected-access,too-many-locals 5 | 6 | """Integration tests for WordPress charm COS integration.""" 7 | 8 | import pytest 9 | import requests 10 | from juju.client._definitions import FullStatus 11 | 12 | from cos import APACHE_PROMETHEUS_SCRAPE_PORT 13 | from tests.integration.helper import WordpressApp 14 | 15 | 16 | @pytest.mark.abort_on_fail 17 | @pytest.mark.usefixtures("prepare_mysql", "prepare_prometheus") 18 | async def test_prometheus_integration( 19 | wordpress: WordpressApp, 20 | ): 21 | """ 22 | arrange: none. 23 | act: deploy the WordPress charm and relations established with prometheus. 24 | assert: prometheus metrics endpoint for prometheus is active and prometheus has active scrape 25 | targets. 26 | """ 27 | for unit_ip in await wordpress.get_unit_ips(): 28 | res = requests.get(f"http://{unit_ip}:{APACHE_PROMETHEUS_SCRAPE_PORT}", timeout=10) 29 | assert res.status_code == 200 30 | status: FullStatus = await wordpress.model.get_status(filters=["prometheus-k8s"]) 31 | for unit in status.applications["prometheus-k8s"].units.values(): 32 | query_targets = requests.get( 33 | f"http://{unit.address}:9090/api/v1/targets", timeout=10 34 | ).json() 35 | assert len(query_targets["data"]["activeTargets"]) 36 | -------------------------------------------------------------------------------- /tests/integration/test_external.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Integration tests for WordPress charm external service integration.""" 5 | 6 | import secrets 7 | 8 | import pytest 9 | from pytest import Config 10 | 11 | from tests.integration.helper import WordpressApp, WordpressClient 12 | 13 | 14 | @pytest.mark.requires_secret 15 | @pytest.mark.usefixtures("prepare_mysql", "prepare_swift") 16 | async def test_akismet_plugin( 17 | wordpress: WordpressApp, 18 | pytestconfig: Config, 19 | ): 20 | """ 21 | arrange: after WordPress charm has been deployed, db relation established. 22 | act: update charm configuration for Akismet plugin. 23 | assert: Akismet plugin should be activated and spam detection function should be working. 24 | """ 25 | akismet_api_key = pytestconfig.getoption("--akismet-api-key") 26 | if not akismet_api_key: 27 | raise ValueError("--akismet-api-key is required for running this test") 28 | 29 | await wordpress.set_config({"wp_plugin_akismet_key": akismet_api_key}) 30 | await wordpress.wait_for_wordpress_idle(status="active") 31 | 32 | for wordpress_client in await wordpress.client_for_units(): 33 | post = wordpress_client.create_post(secrets.token_hex(8), secrets.token_hex(8)) 34 | wordpress_client.create_comment( 35 | post_id=post["id"], post_link=post["link"], content="akismet-guaranteed-spam" 36 | ) 37 | wordpress_client.create_comment( 38 | post_id=post["id"], post_link=post["link"], content="test comment" 39 | ) 40 | assert ( 41 | len(wordpress_client.list_comments(status="spam", post_id=post["id"])) == 1 42 | ), "Akismet plugin should move the triggered spam comment to the spam section" 43 | assert ( 44 | len(wordpress_client.list_comments(post_id=post["id"])) == 1 45 | ), "Akismet plugin should keep the normal comment" 46 | 47 | 48 | @pytest.mark.requires_secret 49 | @pytest.mark.usefixtures("prepare_mysql") 50 | async def test_openid_plugin( 51 | wordpress: WordpressApp, 52 | pytestconfig: Config, 53 | ): 54 | """ 55 | arrange: after WordPress charm has been deployed, db relation established. 56 | act: update charm configuration for OpenID plugin. 57 | assert: A WordPress user should be created with correct roles according to the config. 58 | """ 59 | openid_username = pytestconfig.getoption("--openid-username") 60 | if not openid_username: 61 | raise ValueError("--openid-username is required for running this test") 62 | openid_password = pytestconfig.getoption("--openid-password") 63 | if not openid_password: 64 | raise ValueError("--openid-password is required for running this test") 65 | launchpad_team = pytestconfig.getoption("--launchpad-team") 66 | if not launchpad_team: 67 | raise ValueError("--launchpad-team is required for running this test") 68 | await wordpress.set_config({"wp_plugin_openid_team_map": f"{launchpad_team}=administrator"}) 69 | await wordpress.wait_for_wordpress_idle(status="active") 70 | 71 | for idx, unit_ip in enumerate(await wordpress.get_unit_ips()): 72 | # wordpress-teams-integration has a bug causing desired roles not to be assigned to 73 | # the user when first-time login. Login twice by creating the WordPressClient client twice 74 | # for the very first time. 75 | for attempt in range(2 if idx == 0 else 1): 76 | try: 77 | wordpress_client = WordpressClient( 78 | host=unit_ip, 79 | username=openid_username, 80 | password=openid_password, 81 | is_admin=True, 82 | use_launchpad_login=True, 83 | ) 84 | except AssertionError: 85 | if attempt == 0: 86 | continue 87 | raise 88 | assert ( 89 | "administrator" in wordpress_client.list_roles() 90 | ), "An launchpad OpenID account should be associated with the WordPress admin user" 91 | -------------------------------------------------------------------------------- /tests/integration/test_ingress.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Integration tests for WordPress charm ingress integration.""" 5 | 6 | import kubernetes 7 | import pytest 8 | import requests 9 | 10 | from tests.integration.helper import WordpressApp 11 | 12 | 13 | @pytest.mark.usefixtures("prepare_mysql", "prepare_nginx_ingress", "prepare_swift") 14 | async def test_ingress(wordpress: WordpressApp): 15 | """ 16 | arrange: after WordPress charm has been deployed and db relation established. 17 | act: deploy the nginx-ingress-integrator charm and create the relation between ingress charm 18 | and WordPress charm. 19 | assert: A Kubernetes ingress should be created and the ingress should accept HTTPS connections. 20 | """ 21 | response = requests.get("http://127.0.0.1", headers={"Host": wordpress.name}, timeout=5) 22 | assert ( 23 | response.status_code == 200 and "wordpress" in response.text.lower() 24 | ), "Ingress should accept requests to WordPress and return correct contents" 25 | 26 | new_hostname = "wordpress.test" 27 | await wordpress.set_config({"blog_hostname": new_hostname}) 28 | await wordpress.model.wait_for_idle(status="active") 29 | response = requests.get( 30 | "https://127.0.0.1", headers={"Host": new_hostname}, timeout=5, verify=False 31 | ) # nosec 32 | assert ( 33 | response.status_code == 200 and "wordpress" in response.text.lower() 34 | ), "Ingress should update the server name indication based routing after blog_hostname updated" 35 | 36 | 37 | @pytest.mark.usefixtures("prepare_mysql", "prepare_nginx_ingress", "prepare_swift") 38 | async def test_ingress_modsecurity( 39 | wordpress: WordpressApp, 40 | kube_config: str, 41 | ): 42 | """ 43 | arrange: WordPress charm is running and Nginx ingress integrator deployed and related to it. 44 | act: update the use_nginx_ingress_modsec WordPress charm config. 45 | assert: A Kubernetes ingress modsecurity should be enabled and proper rules should be set up 46 | for WordPress. 47 | """ 48 | await wordpress.set_config({"use_nginx_ingress_modsec": "true"}) 49 | await wordpress.model.wait_for_idle(status="active") 50 | 51 | kubernetes.config.load_kube_config(config_file=kube_config) 52 | kube = kubernetes.client.NetworkingV1Api() 53 | 54 | def get_ingress_annotation(): 55 | """Get ingress annotations from kubernetes. 56 | 57 | Returns: 58 | Nginx ingress annotations. 59 | """ 60 | ingress_list = kube.list_namespaced_ingress(namespace=wordpress.model.name).items 61 | return ingress_list[0].metadata.annotations 62 | 63 | ingress_annotations = get_ingress_annotation() 64 | assert ingress_annotations["nginx.ingress.kubernetes.io/enable-modsecurity"] == "true" 65 | assert ( 66 | ingress_annotations["nginx.ingress.kubernetes.io/enable-owasp-modsecurity-crs"] == "true" 67 | ) 68 | assert ( 69 | 'SecAction "id:900130,phase:1,nolog,pass,t:none,setvar:tx.crs_exclusions_wordpress=1"\n' 70 | in ingress_annotations["nginx.ingress.kubernetes.io/modsecurity-snippet"] 71 | ) 72 | -------------------------------------------------------------------------------- /tests/integration/test_machine.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Integration tests for WordPress charm core functionality with mysql machine charm.""" 5 | 6 | import functools 7 | 8 | import pytest 9 | from helper import get_mysql_primary_unit, wait_for 10 | from juju.application import Application 11 | from juju.model import Model 12 | 13 | from tests.integration.helper import WordpressApp 14 | 15 | 16 | @pytest.mark.usefixtures("prepare_machine_mysql") 17 | async def test_database_endpoints_changed(machine_model: Model, wordpress: WordpressApp): 18 | """ 19 | arrange: given related mysql charm with 3 units. 20 | act: when the leader mysql unit is removed and hence the endpoints changed event fired. 21 | assert: the WordPress correctly connects to the newly elected leader endpoint. 22 | """ 23 | model: Model = wordpress.model 24 | mysql: Application = machine_model.applications["mysql"] 25 | await mysql.add_unit(2) 26 | await machine_model.wait_for_idle(["mysql"], timeout=30 * 60) 27 | await model.wait_for_idle(["wordpress-k8s"]) 28 | 29 | leader = await get_mysql_primary_unit(mysql.units) 30 | assert leader, "No leader unit found." 31 | await mysql.destroy_unit(leader.name) 32 | await machine_model.wait_for_idle(["mysql"], timeout=30 * 60, idle_period=30) 33 | await model.wait_for_idle(["wordpress-k8s"]) 34 | 35 | leader = await wait_for(functools.partial(get_mysql_primary_unit, mysql.units)) 36 | 37 | assert ( 38 | await leader.get_public_address() in await wordpress.get_wordpress_config() 39 | ), "MySQL leader unit IP not found." 40 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | -------------------------------------------------------------------------------- /tests/unit/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """Fixtures for WordPress charm unit tests.""" 5 | 6 | import typing 7 | import unittest 8 | import unittest.mock 9 | 10 | import ops.pebble 11 | import ops.testing 12 | import pytest 13 | 14 | from charm import WordpressCharm 15 | from tests.unit.wordpress_mock import WordpressPatch 16 | 17 | 18 | @pytest.fixture(scope="function", name="patch") 19 | def patch_fixture(): 20 | """Enable WordPress patch system, used in combine with :class:`ops.testing.Harness`. 21 | 22 | Yields: 23 | The instance of :class:`tests.unit.wordpress_mock.WordpressPatch`, which can be used to 24 | inspect the WordPress mocking system (mocking db, mocking file system, etc). 25 | """ 26 | patch = WordpressPatch() 27 | patch.start() 28 | yield patch 29 | patch.stop() 30 | 31 | 32 | @pytest.fixture(scope="function", name="harness") 33 | def harness_fixture(patch: WordpressPatch): # pylint: disable=unused-argument 34 | """Enable ops test framework harness.""" 35 | harness = ops.testing.Harness(WordpressCharm) 36 | yield harness 37 | harness.cleanup() 38 | 39 | 40 | @pytest.fixture(scope="function", name="app_name") 41 | def app_name_fixture(): 42 | """The name of the charm application.""" 43 | return "wordpress-k8s" 44 | 45 | 46 | @pytest.fixture(scope="function", name="setup_replica_consensus") 47 | def setup_replica_consensus_fixture(harness: ops.testing.Harness, app_name: str): 48 | """Returns a function that can be used to set up peer relation. 49 | 50 | After calling the yielded function, the replica consensus including WordPress salt keys and 51 | secrets will be populated. The unit will become a leader unit in this process. 52 | """ 53 | 54 | def _setup_replica_consensus(): 55 | """Function to set up peer relation. See fixture docstring for more information. 56 | 57 | Returns: 58 | Relation data for WordPress peers. Includes WordPress salt keys and secrets. 59 | """ 60 | replica_relation_id = harness.add_relation("wordpress-replica", app_name) 61 | harness.add_storage("uploads") 62 | harness.set_leader() 63 | harness.begin_with_initial_hooks() 64 | harness.framework.reemit() 65 | consensus = harness.get_relation_data(replica_relation_id, app_name) 66 | return consensus 67 | 68 | return _setup_replica_consensus 69 | 70 | 71 | @pytest.fixture(scope="function", name="example_database_host_port") 72 | def example_database_host_port_fixture(): 73 | """An example database connection host and port tuple.""" 74 | return ("test_database_host", "3306") 75 | 76 | 77 | @pytest.fixture(scope="function", name="example_database_info") 78 | def example_database_info_fixture(example_database_host_port: typing.Tuple[str, str]): 79 | """An example database connection info from mysql_client interface.""" 80 | return { 81 | "endpoints": ":".join(example_database_host_port), 82 | "database": "test_database_name", 83 | "username": "test_database_user", 84 | "password": "test_database_password", 85 | } 86 | 87 | 88 | @pytest.fixture(scope="function", name="example_invalid_database_info") 89 | def example_invalid_database_info_fixture(): 90 | """An example database connection info from mysql_client interface.""" 91 | return { 92 | "endpoints": "test_database_host:1234", 93 | "database": "test_database_name", 94 | "username": "test_database_user", 95 | "password": "test_database_password", 96 | } 97 | 98 | 99 | @pytest.fixture(scope="function", name="example_database_info_no_port") 100 | def example_database_info_no_port_fixture(): 101 | """An example database connection info from mysql_client interface.""" 102 | return { 103 | "endpoints": "test_database_host", 104 | "database": "test_database_name", 105 | "username": "test_database_user", 106 | "password": "test_database_password", 107 | } 108 | 109 | 110 | @pytest.fixture(scope="function", name="example_database_info_no_port_diff_host") 111 | def example_database_info_no_port_diff_host_fixture(): 112 | """An example database connection info from mysql_client interface.""" 113 | return { 114 | "endpoints": "test_database_host2", 115 | "database": "test_database_name", 116 | "username": "test_database_user", 117 | "password": "test_database_password", 118 | } 119 | 120 | 121 | @pytest.fixture(scope="function", name="example_database_info_connection_error") 122 | def example_database_info_connection_error_fixture(): 123 | """An example database connection info from mysql_client interface.""" 124 | return { 125 | "endpoints": "a", 126 | "database": "b", 127 | "username": "c", 128 | "password": "d", 129 | } 130 | 131 | 132 | @pytest.fixture(scope="function") 133 | def setup_database_relation( 134 | harness: ops.testing.Harness, example_database_info: typing.Dict[str, str] 135 | ): 136 | """Returns a function that can be used to set up database relation. 137 | 138 | After calling the yielded function, a database relation will be set up. example_database_info 139 | will be used as the relation data. Return a tuple of relation id and the relation data. 140 | """ 141 | 142 | def _setup_database_relation(): 143 | """Function to set up database relation. See fixture docstring for more information. 144 | 145 | Returns: 146 | Tuple of relation id and relation data. 147 | """ 148 | db_relation_id = harness.add_relation("database", "mysql") 149 | harness.add_relation_unit(db_relation_id, "mysql/0") 150 | harness.update_relation_data(db_relation_id, "mysql", example_database_info) 151 | return db_relation_id, example_database_info 152 | 153 | return _setup_database_relation 154 | 155 | 156 | @pytest.fixture(scope="function", name="setup_database_relation_no_port") 157 | def setup_database_relation_no_port_fixture( 158 | harness: ops.testing.Harness, example_database_info_no_port: typing.Dict[str, str] 159 | ): 160 | """Returns a function that can be used to set up database relation. 161 | 162 | After calling the yielded function, a database relation will be set up. example_database_info 163 | will be used as the relation data. Return a tuple of relation id and the relation data. 164 | """ 165 | 166 | def _setup_database_relation(): 167 | """Function to set up database relation. See fixture docstring for more information. 168 | 169 | Returns: 170 | Tuple of relation id and relation data. 171 | """ 172 | db_relation_id = harness.add_relation("database", "mysql") 173 | harness.add_relation_unit(db_relation_id, "mysql/0") 174 | harness.update_relation_data(db_relation_id, "mysql", example_database_info_no_port) 175 | return db_relation_id, example_database_info_no_port 176 | 177 | return _setup_database_relation 178 | 179 | 180 | @pytest.fixture(scope="function") 181 | def setup_database_relation_invalid_port( 182 | harness: ops.testing.Harness, example_invalid_database_info: typing.Dict[str, str] 183 | ): 184 | """Returns a function that can be used to set up database relation with a non 3306 port. 185 | 186 | After calling the yielded function, a database relation will be set up. example_database_info 187 | will be used as the relation data. Return a tuple of relation id and the relation data. 188 | """ 189 | 190 | def _setup_database_relation(): 191 | """Function to set up database relation. See fixture docstring for more information. 192 | 193 | Returns: 194 | Tuple of relation id and relation data. 195 | """ 196 | db_relation_id = harness.add_relation("database", "mysql") 197 | harness.add_relation_unit(db_relation_id, "mysql/0") 198 | harness.update_relation_data(db_relation_id, "mysql", example_invalid_database_info) 199 | return db_relation_id, example_invalid_database_info 200 | 201 | return _setup_database_relation 202 | 203 | 204 | @pytest.fixture(scope="function") 205 | def setup_database_relation_connection_error( 206 | harness: ops.testing.Harness, example_database_info_connection_error: typing.Dict[str, str] 207 | ): 208 | """Returns a function that can be used to set up database relation with a non 3306 port. 209 | 210 | After calling the yielded function, a database relation will be set up. 211 | example_database_info_connection_error will be used as the relation data. 212 | Return a tuple of relation id and the relation data. 213 | """ 214 | 215 | def _setup_database_relation(): 216 | """Function to set up database relation. See fixture docstring for more information. 217 | 218 | Returns: 219 | Tuple of relation id and relation data. 220 | """ 221 | db_relation_id = harness.add_relation("database", "mysql") 222 | harness.add_relation_unit(db_relation_id, "mysql/0") 223 | harness.update_relation_data( 224 | db_relation_id, "mysql", example_database_info_connection_error 225 | ) 226 | return db_relation_id, example_database_info_connection_error 227 | 228 | return _setup_database_relation 229 | 230 | 231 | @pytest.fixture(scope="function") 232 | def action_event_mock(): 233 | """Creates a mock object for :class:`ops.charm.ActionEvent`.""" 234 | event_mock = unittest.mock.MagicMock() 235 | event_mock.set_results = unittest.mock.MagicMock() 236 | event_mock.fail = unittest.mock.MagicMock() 237 | return event_mock 238 | 239 | 240 | @pytest.fixture(scope="function") 241 | def run_standard_plugin_test( 242 | patch: WordpressPatch, 243 | harness: ops.testing.Harness, 244 | setup_replica_consensus: typing.Callable[[], dict], 245 | setup_database_relation_no_port: typing.Callable[[], typing.Tuple[int, dict]], 246 | ): 247 | """Returns a function that can be used to perform some general test for different plugins.""" 248 | 249 | def _run_standard_plugin_test( 250 | plugin: str, 251 | plugin_config: typing.Dict[str, str], 252 | excepted_options: typing.Dict[str, typing.Any], 253 | excepted_options_after_removed: typing.Optional[typing.Dict[str, str]] = None, 254 | additional_check_after_install: typing.Optional[typing.Callable] = None, 255 | ): 256 | """Function to perform standard plugins test. 257 | 258 | Args: 259 | plugin: Name of WordPress standard plugin to test. 260 | plugin_config: Configurable parameters for WordPress plugins. See config.yaml for 261 | configuration details. 262 | excepted_options: Expected configurations of a given plugin. 263 | excepted_options_after_removed: Remaining options after plugin deactivation. 264 | additional_check_after_install: Callback to additional checks to perform after 265 | installation. 266 | """ 267 | plugin_config_keys = list(plugin_config.keys()) 268 | harness.set_can_connect(harness.model.unit.containers["wordpress"], True) 269 | setup_replica_consensus() 270 | _, db_info = setup_database_relation_no_port() 271 | patch.database.prepare_database( 272 | host=db_info["endpoints"], 273 | database=db_info["database"], 274 | user=db_info["username"], 275 | password=db_info["password"], 276 | ) 277 | 278 | harness.update_config(plugin_config) 279 | 280 | database_instance = patch.database.get_wordpress_database( 281 | host="test_database_host", database="test_database_name" 282 | ) 283 | assert database_instance 284 | assert ( 285 | database_instance.activated_plugins == {plugin} 286 | if isinstance(plugin, str) 287 | else set(plugin) 288 | ), f"{plugin} should be activated after {plugin_config_keys} being set" 289 | assert ( 290 | database_instance.options == excepted_options 291 | ), f"options of plugin {plugin} should be set correctly" 292 | 293 | if additional_check_after_install is not None: 294 | additional_check_after_install() 295 | 296 | harness.update_config({k: "" for k in plugin_config}) 297 | assert ( 298 | database_instance.activated_plugins == set() 299 | ), f"{plugin} should be deactivated after {plugin_config_keys} being reset" 300 | assert ( 301 | database_instance.options == {} 302 | if excepted_options_after_removed is None 303 | else excepted_options_after_removed 304 | ), f"{plugin} options should be removed after {plugin_config_keys} being reset" 305 | 306 | return _run_standard_plugin_test 307 | 308 | 309 | @pytest.fixture(scope="function") 310 | def attach_storage( 311 | patch: WordpressPatch, 312 | ): 313 | """Attach the "upload" storage to the mock container.""" 314 | patch.container.fs["/proc/mounts"] = "/var/www/html/wp-content/uploads" 315 | yield 316 | patch.container.fs["/proc/mounts"] = "" 317 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | [tox] 4 | skipsdist=True 5 | envlist = lint, unit, static, coverage-report 6 | skip_missing_interpreters = True 7 | 8 | [vars] 9 | src_path = {toxinidir}/src/ 10 | tst_path = {toxinidir}/tests/ 11 | all_path = {[vars]src_path} {[vars]tst_path} 12 | 13 | [testenv] 14 | setenv = 15 | PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} 16 | PYTHONBREAKPOINT=ipdb.set_trace 17 | PY_COLORS=1 18 | passenv = 19 | PYTHONPATH 20 | CHARM_BUILD_DIR 21 | MODEL_SETTINGS 22 | allowlist_externals = docker 23 | 24 | [testenv:fmt] 25 | description = Apply coding style standards to code 26 | deps = 27 | black 28 | isort 29 | commands = 30 | isort {[vars]all_path} 31 | black {[vars]all_path} 32 | 33 | [testenv:lint] 34 | description = Check code against coding style standards 35 | deps = 36 | black 37 | codespell 38 | flake8<6.0.0 39 | flake8-builtins 40 | flake8-copyright 41 | flake8-docstrings>=1.6.0 42 | flake8-docstrings-complete>=1.0.4 43 | flake8-test-docs 44 | isort 45 | mypy 46 | pep8-naming 47 | pydocstyle 48 | pylint 49 | pyproject-flake8<6.0.0 50 | pytest_operator 51 | -r{toxinidir}/requirements.txt 52 | -r{toxinidir}/tests/integration/requirements.txt 53 | commands = 54 | codespell {toxinidir} \ 55 | --skip {toxinidir}/.git \ 56 | --skip {toxinidir}/.tox \ 57 | --skip {toxinidir}/build \ 58 | --skip {toxinidir}/lib \ 59 | --skip {toxinidir}/venv \ 60 | --skip {toxinidir}/.mypy_cache 61 | # pflake8 wrapper supports config from pyproject.toml 62 | pflake8 {[vars]all_path} 63 | isort --check-only --diff {[vars]all_path} 64 | black --check --diff {[vars]all_path} 65 | mypy {[vars]all_path} 66 | pydocstyle {[vars]src_path} 67 | pylint {[vars]all_path} 68 | 69 | [testenv:static] 70 | description = Run static analysis tests 71 | deps = 72 | bandit[toml] 73 | toml 74 | -r{toxinidir}/requirements.txt 75 | commands = 76 | bandit -c {toxinidir}/pyproject.toml -r {[vars]src_path} {[vars]tst_path} 77 | 78 | [testenv:unit] 79 | description = Run unit tests 80 | deps = 81 | cosl 82 | coverage[toml] 83 | pytest 84 | -r{toxinidir}/requirements.txt 85 | commands = 86 | coverage run --source={[vars]src_path} \ 87 | -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs} 88 | coverage report 89 | 90 | [testenv:coverage-report] 91 | description = Create test coverage report 92 | deps = 93 | coverage[toml] 94 | pytest 95 | -r{toxinidir}/requirements.txt 96 | commands = 97 | coverage report 98 | 99 | [testenv:integration] 100 | description = Run integration tests 101 | deps = 102 | websockets<14.0 103 | -r{toxinidir}/requirements.txt 104 | -r{toxinidir}/tests/integration/requirements.txt 105 | commands = 106 | pytest {[vars]tst_path} -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} 107 | 108 | [testenv:integration-juju3] 109 | description = Run integration tests using Juju 3 110 | deps = 111 | -r{toxinidir}/requirements.txt 112 | -r{toxinidir}/tests/integration/requirements_juju3.txt 113 | commands = 114 | pytest {[vars]tst_path} -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} 115 | -------------------------------------------------------------------------------- /trivy.yaml: -------------------------------------------------------------------------------- 1 | skip-files: 2 | - /etc/ssl/private/ssl-cert-snakeoil.key 3 | -------------------------------------------------------------------------------- /wordpress_rock/files/etc/apache2/apache2.conf: -------------------------------------------------------------------------------- 1 | # This is the main Apache server configuration file. It contains the 2 | # configuration directives that give the server its instructions. 3 | # See http://httpd.apache.org/docs/2.4/ for detailed information about 4 | # the directives and /usr/share/doc/apache2/README.Debian about Debian specific 5 | # hints. 6 | # 7 | # 8 | # Summary of how the Apache 2 configuration works in Debian: 9 | # The Apache 2 web server configuration in Debian is quite different to 10 | # upstream's suggested way to configure the web server. This is because Debian's 11 | # default Apache2 installation attempts to make adding and removing modules, 12 | # virtual hosts, and extra configuration directives as flexible as possible, in 13 | # order to make automating the changes and administering the server as easy as 14 | # possible. 15 | 16 | # It is split into several files forming the configuration hierarchy outlined 17 | # below, all located in the /etc/apache2/ directory: 18 | # 19 | # /etc/apache2/ 20 | # |-- apache2.conf 21 | # | `-- ports.conf 22 | # |-- mods-enabled 23 | # | |-- *.load 24 | # | `-- *.conf 25 | # |-- conf-enabled 26 | # | `-- *.conf 27 | # `-- sites-enabled 28 | # `-- *.conf 29 | # 30 | # 31 | # * apache2.conf is the main configuration file (this file). It puts the pieces 32 | # together by including all remaining configuration files when starting up the 33 | # web server. 34 | # 35 | # * ports.conf is always included from the main configuration file. It is 36 | # supposed to determine listening ports for incoming connections which can be 37 | # customized anytime. 38 | # 39 | # * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/ 40 | # directories contain particular configuration snippets which manage modules, 41 | # global configuration fragments, or virtual host configurations, 42 | # respectively. 43 | # 44 | # They are activated by symlinking available configuration files from their 45 | # respective *-available/ counterparts. These should be managed by using our 46 | # helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See 47 | # their respective man pages for detailed information. 48 | # 49 | # * The binary is called apache2. Due to the use of environment variables, in 50 | # the default configuration, apache2 needs to be started/stopped with 51 | # /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not 52 | # work with the default configuration. 53 | 54 | 55 | # Global configuration 56 | # 57 | 58 | # 59 | # ServerRoot: The top of the directory tree under which the server's 60 | # configuration, error, and log files are kept. 61 | # 62 | # NOTE! If you intend to place this on an NFS (or otherwise network) 63 | # mounted filesystem then please read the Mutex documentation (available 64 | # at ); 65 | # you will save yourself a lot of trouble. 66 | # 67 | # Do NOT add a slash at the end of the directory path. 68 | # 69 | #ServerRoot "/etc/apache2" 70 | 71 | # 72 | # The accept serialization lock file MUST BE STORED ON A LOCAL DISK. 73 | # 74 | #Mutex file:${APACHE_LOCK_DIR} default 75 | 76 | # 77 | # The directory where shm and other runtime files will be stored. 78 | # 79 | 80 | DefaultRuntimeDir ${APACHE_RUN_DIR} 81 | 82 | # 83 | # PidFile: The file in which the server should record its process 84 | # identification number when it starts. 85 | # This needs to be set in /etc/apache2/envvars 86 | # 87 | PidFile ${APACHE_PID_FILE} 88 | 89 | # 90 | # Timeout: The number of seconds before receives and sends time out. 91 | # 92 | Timeout 300 93 | 94 | # 95 | # KeepAlive: Whether or not to allow persistent connections (more than 96 | # one request per connection). Set to "Off" to deactivate. 97 | # 98 | KeepAlive On 99 | 100 | # 101 | # MaxKeepAliveRequests: The maximum number of requests to allow 102 | # during a persistent connection. Set to 0 to allow an unlimited amount. 103 | # We recommend you leave this number high, for maximum performance. 104 | # 105 | MaxKeepAliveRequests 100 106 | 107 | # 108 | # KeepAliveTimeout: Number of seconds to wait for the next request from the 109 | # same client on the same connection. 110 | # 111 | KeepAliveTimeout 5 112 | 113 | 114 | # These need to be set in /etc/apache2/envvars 115 | User ${APACHE_RUN_USER} 116 | Group ${APACHE_RUN_GROUP} 117 | 118 | # 119 | # HostnameLookups: Log the names of clients or just their IP addresses 120 | # e.g., www.apache.org (on) or 204.62.129.132 (off). 121 | # The default is off because it'd be overall better for the net if people 122 | # had to knowingly turn this feature on, since enabling it means that 123 | # each client request will result in AT LEAST one lookup request to the 124 | # nameserver. 125 | # 126 | HostnameLookups Off 127 | 128 | # ErrorLog: The location of the error log file. 129 | # If you do not specify an ErrorLog directive within a 130 | # container, error messages relating to that virtual host will be 131 | # logged here. If you *do* define an error logfile for a 132 | # container, that host's errors will be logged there and not here. 133 | 134 | # send error logs to ${APACHE_LOG_DIR}/error.%Y-%m-%d-%H-%M-%S.log and rotate when size limits are reached 135 | # prune old log files with pruneerrorlogs 136 | # `rotatelogs -e` echos logs through to stdout, which will be redirected to stderr (kubernetes container logs) 137 | ErrorLog "|$ /usr/bin/rotatelogs -e -p /usr/bin/pruneerrorlogs ${APACHE_LOG_DIR}/error.%Y-%m-%d-%H-%M-%S.log 256M 1>&2" 138 | 139 | # 140 | # LogLevel: Control the severity of messages logged to the error_log. 141 | # Available values: trace8, ..., trace1, debug, info, notice, warn, 142 | # error, crit, alert, emerg. 143 | # It is also possible to configure the log level for particular modules, e.g. 144 | # "LogLevel info ssl:warn" 145 | # 146 | LogLevel warn 147 | 148 | # Include module configuration: 149 | IncludeOptional mods-enabled/*.load 150 | IncludeOptional mods-enabled/*.conf 151 | 152 | # Include list of ports to listen on 153 | Include ports.conf 154 | 155 | 156 | # Sets the default security model of the Apache2 HTTPD server. It does 157 | # not allow access to the root filesystem outside of /usr/share and /var/www. 158 | # The former is used by web applications packaged in Debian, 159 | # the latter may be used for local directories served by the web server. If 160 | # your system is serving content from a sub-directory in /srv you must allow 161 | # access here, or in any related virtual host. 162 | 163 | Options FollowSymLinks 164 | AllowOverride None 165 | Require all denied 166 | 167 | 168 | 169 | AllowOverride None 170 | Require all granted 171 | 172 | 173 | 174 | Options Indexes FollowSymLinks 175 | AllowOverride None 176 | Require all granted 177 | 178 | 179 | # 2023/02/14 180 | # To allow /server-status route access from within the pod for apache prometheus exporter access. 181 | # Required for prometheus integration. 182 | 183 | SetHandler server-status 184 | Allow from localhost 185 | 186 | 187 | # 188 | # Options Indexes FollowSymLinks 189 | # AllowOverride None 190 | # Require all granted 191 | # 192 | 193 | 194 | 195 | 196 | # AccessFileName: The name of the file to look for in each directory 197 | # for additional configuration directives. See also the AllowOverride 198 | # directive. 199 | # 200 | AccessFileName .htaccess 201 | 202 | # 203 | # The following lines prevent .htaccess and .htpasswd files from being 204 | # viewed by Web clients. 205 | # 206 | 207 | Require all denied 208 | 209 | 210 | 211 | # 212 | # The following directives define some format nicknames for use with 213 | # a CustomLog directive. 214 | # 215 | # These deviate from the Common Log Format definitions in that they use %O 216 | # (the actual bytes sent including headers) instead of %b (the size of the 217 | # requested file), because the latter makes it impossible to detect partial 218 | # requests. 219 | # 220 | # Note that the use of %{X-Forwarded-For}i instead of %h is not recommended. 221 | # Use mod_remoteip instead. 222 | # 223 | LogFormat "client_ip=%a time=\"%{%Y-%m-%dT%H:%M:%S}t.%{usec_frac}t%{%z}t\" method=%m path=\"%U\" query=\"%q\" status=%>s referrer=\"%{Referer}i\" user_agent=\"%{User-agent}i\" content_type=\"%{Content-Type}o\" response_size=%b request_duration_microseconds=%D hostname=%V" logfmt 224 | LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined 225 | LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined 226 | LogFormat "%h %l %u %t \"%r\" %>s %O" common 227 | LogFormat "%{Referer}i -> %U" referer 228 | LogFormat "%{User-agent}i" agent 229 | 230 | # Include of directories ignores editors' and dpkg's backup files, 231 | # see README.Debian for details. 232 | 233 | # Include generic snippets of statements 234 | IncludeOptional conf-enabled/*.conf 235 | 236 | # Include the virtual host configurations: 237 | IncludeOptional sites-enabled/*.conf 238 | -------------------------------------------------------------------------------- /wordpress_rock/files/etc/apache2/conf-available/docker-php-swift-proxy.conf: -------------------------------------------------------------------------------- 1 | SSLProxyEngine on 2 | PassEnv SWIFT_URL 3 | ProxyPass /wp-content/uploads/ ${SWIFT_URL} 4 | ProxyPassReverse /wp-content/uploads/ ${SWIFT_URL} 5 | Timeout 300 6 | -------------------------------------------------------------------------------- /wordpress_rock/files/etc/apache2/conf-available/docker-php.conf: -------------------------------------------------------------------------------- 1 | 2 | SetHandler application/x-httpd-php 3 | 4 | 5 | 6 | Header Set Cache-Control "max-age=0, no-store" 7 | 8 | 9 | DirectoryIndex disabled 10 | DirectoryIndex index.php index.html 11 | 12 | 13 | Options -Indexes 14 | AllowOverride All 15 | RewriteEngine On 16 | RewriteBase / 17 | RewriteRule ^index\.php$ - [L] 18 | RewriteCond %{REQUEST_FILENAME} !-f 19 | RewriteCond %{REQUEST_FILENAME} !-d 20 | # 2023/02/14 21 | # To allow apache's server-status route to be handled by apache rather than being forwarded 22 | # to WordPress server. Required for prometheus integration. 23 | RewriteCond %{REQUEST_URI} !server-status 24 | RewriteRule . /index.php [L] 25 | 26 | -------------------------------------------------------------------------------- /wordpress_rock/files/etc/apache2/sites-available/000-default.conf: -------------------------------------------------------------------------------- 1 | 2 | # The ServerName directive sets the request scheme, hostname and port that 3 | # the server uses to identify itself. This is used when creating 4 | # redirection URLs. In the context of virtual hosts, the ServerName 5 | # specifies what hostname must appear in the request's Host: header to 6 | # match this virtual host. For the default virtual host (this file) this 7 | # value is not decisive as it is used as a last resort host regardless. 8 | # However, you must set it for any further virtual host explicitly. 9 | #ServerName www.example.com 10 | 11 | ServerAdmin webmaster@localhost 12 | DocumentRoot /var/www/html 13 | 14 | # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, 15 | # error, crit, alert, emerg. 16 | # It is also possible to configure the loglevel for particular 17 | # modules, e.g. 18 | #LogLevel info ssl:warn 19 | 20 | # send access logs to ${APACHE_LOG_DIR}/access.%Y-%m-%d-%H-%M-%S.log and rotate when size limits are reached 21 | # prune old log files with pruneaccesslogs 22 | CustomLog "| /usr/bin/rotatelogs -p /usr/bin/pruneaccesslogs ${APACHE_LOG_DIR}/access.%Y-%m-%d-%H-%M-%S.log 256M" logfmt 23 | 24 | # send access logs to stdout (kubernetes container logs) as well 25 | CustomLog /dev/stdout logfmt 26 | 27 | # For most configuration files from conf-available/, which are 28 | # enabled or disabled at a global level, it is possible to 29 | # include a line for only one particular virtual host. For example the 30 | # following line enables the CGI configuration for this host only 31 | # after it has been globally disabled with "a2disconf". 32 | #Include conf-available/serve-cgi-bin.conf 33 | 34 | 35 | # vim: syntax=apache ts=4 sw=4 sts=4 sr noet 36 | -------------------------------------------------------------------------------- /wordpress_rock/files/usr/bin/pruneaccesslogs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2024 Canonical Ltd. 3 | # See LICENSE file for licensing details. 4 | 5 | # shellcheck disable=SC2012,SC2046,SC2086 6 | 7 | log_pattern="/var/log/apache2/access.*.log" 8 | max_size=$((4*1024*1024*1024)) 9 | 10 | while [ $(du -cb $log_pattern | tail -n 1 | cut -f1) -gt $max_size ]; do 11 | oldest=$(ls $log_pattern | sort | head -n 1) 12 | if [ -n "$oldest" ]; then 13 | rm -f "$oldest" 14 | else 15 | break 16 | fi 17 | done 18 | -------------------------------------------------------------------------------- /wordpress_rock/files/usr/bin/pruneerrorlogs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2024 Canonical Ltd. 3 | # See LICENSE file for licensing details. 4 | 5 | # shellcheck disable=SC2012,SC2046,SC2086 6 | 7 | log_pattern="/var/log/apache2/error.*.log" 8 | max_size=$((4*1024*1024*1024)) 9 | 10 | while [ $(du -cb $log_pattern | tail -n 1 | cut -f1) -gt $max_size ]; do 11 | oldest=$(ls $log_pattern | sort | head -n 1) 12 | if [ -n "$oldest" ]; then 13 | rm -f "$oldest" 14 | else 15 | break 16 | fi 17 | done 18 | -------------------------------------------------------------------------------- /wordpress_rock/patches/openid.patch: -------------------------------------------------------------------------------- 1 | diff --git a/openid/common.php b/openid/common.php 2 | index df7737f..f8bbf07 100644 3 | --- a/openid/common.php 4 | +++ b/openid/common.php 5 | @@ -760,6 +760,12 @@ function openid_page( $message, $title = '' ) { 6 | if ( ( $wp_locale ) && ( 'rtl' == $wp_locale->text_direction ) ) { 7 | wp_admin_css( 'login-rtl', true ); 8 | } 9 | + if ( function_exists( 'wp_scripts' ) ) { 10 | + wp_scripts(); 11 | + } 12 | + if ( function_exists( 'wp_styles' ) ) { 13 | + wp_styles(); 14 | + } 15 | 16 | do_action( 'admin_head' ); 17 | do_action( 'openid_page_head' ); 18 | -------------------------------------------------------------------------------- /wordpress_rock/rockcraft.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | name: wordpress 5 | summary: Wordpress rock 6 | description: Wordpress OCI image for the Wordpress charm 7 | base: ubuntu@24.04 8 | run-user: _daemon_ 9 | license: Apache-2.0 10 | version: "1.0" 11 | platforms: 12 | amd64: 13 | parts: 14 | apache2: 15 | plugin: dump 16 | source: files 17 | build-packages: 18 | - apache2 19 | - php 20 | - rsync 21 | overlay-packages: 22 | - apache2 23 | - libapache2-mod-php 24 | - libgmp-dev 25 | - php 26 | - php-curl 27 | - php-gd 28 | - php-gmp 29 | - php-mysql 30 | - php-symfony-yaml 31 | - php-xml 32 | - pwgen 33 | - python3 34 | - python3-yaml 35 | - ca-certificates 36 | build-environment: 37 | # Required to source $CRAFT_OVERLAY/etc/apache2/envvars 38 | - APACHE_CONFDIR: /etc/apache2 39 | - IMAGE_RUN_USER: _daemon_ 40 | - IMAGE_RUN_GROUP: _daemon_ 41 | - IMAGE_RUN_USER_ID: 584792 42 | - IMAGE_RUN_GROUP_ID: 584792 43 | overlay-script: | 44 | craftctl default 45 | sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' $CRAFT_OVERLAY/etc/apache2/envvars 46 | sed -ri 's/\{APACHE_RUN_(USER|GROUP):=.+\}/\{APACHE_RUN_\1:=_daemon_\}/' $CRAFT_OVERLAY/etc/apache2/envvars 47 | . $CRAFT_OVERLAY/etc/apache2/envvars 48 | for dir in "$CRAFT_OVERLAY$APACHE_LOCK_DIR" "$CRAFT_OVERLAY$APACHE_RUN_DIR" "$CRAFT_OVERLAY$APACHE_LOG_DIR"; 49 | do 50 | rm -rvf "$dir"; 51 | mkdir -p "$dir"; 52 | chown "$IMAGE_RUN_USER_ID:$IMAGE_RUN_GROUP_ID" "$dir"; 53 | chmod u=rwx,g=rx,o=rx "$dir"; 54 | done 55 | chown -R --no-dereference "$IMAGE_RUN_USER_ID:$IMAGE_RUN_GROUP_ID" "$CRAFT_OVERLAY$APACHE_LOG_DIR" 56 | ln -sfT ../../../dev/stdout "$CRAFT_OVERLAY$APACHE_LOG_DIR/other_vhosts_access.log" 57 | rsync -abP $CRAFT_PART_SRC/etc/apache2/ $CRAFT_OVERLAY/etc/apache2 58 | 59 | # Enable apache2 modules 60 | chroot $CRAFT_OVERLAY /bin/sh -x <<'EOF' 61 | a2enconf docker-php 62 | a2enmod headers 63 | a2enmod mpm_prefork 64 | a2enmod proxy 65 | a2enmod proxy_http 66 | a2enmod rewrite 67 | a2enmod ssl 68 | EOF 69 | apache-exporter: 70 | plugin: go 71 | build-snaps: 72 | - go/1.22/stable 73 | source: https://github.com/Lusitaniae/apache_exporter.git 74 | source-type: git 75 | source-tag: v1.0.10 76 | source-depth: 1 77 | wordpress: 78 | after: 79 | - apache2 80 | plugin: nil 81 | build-environment: 82 | - WP_VERSION: 6.8.1 83 | build-packages: 84 | - curl 85 | override-build: | 86 | curl -sSL --create-dirs https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o wp 87 | chmod +x wp 88 | 89 | mkdir -p wordpress_install_dir 90 | (cd wordpress_install_dir; $CRAFT_PART_BUILD/wp core download --version=${WP_VERSION} --allow-root) 91 | 92 | cp -R . $CRAFT_PART_INSTALL 93 | organize: 94 | wordpress_install_dir: /var/www/html 95 | wp: /usr/local/bin/wp 96 | # Wordpress plugins 97 | get-wordpress-plugins: 98 | source: . 99 | plugin: nil 100 | after: 101 | - wordpress 102 | build-packages: 103 | - curl 104 | - unzip 105 | build-environment: 106 | - WP_PLUGINS: >- 107 | 404page 108 | all-in-one-event-calendar 109 | coschedule-by-todaymade 110 | elementor 111 | essential-addons-for-elementor-lite 112 | favicon-by-realfavicongenerator 113 | feedwordpress 114 | genesis-columns-advanced 115 | line-break-shortcode 116 | no-category-base-wpml 117 | post-grid 118 | powerpress 119 | redirection 120 | relative-image-urls 121 | rel-publisher 122 | safe-svg 123 | show-current-template 124 | simple-301-redirects 125 | simple-custom-css 126 | so-widgets-bundle 127 | svg-support 128 | syntaxhighlighter 129 | wordpress-importer 130 | wp-font-awesome 131 | wp-lightbox-2 132 | wp-markdown 133 | wp-mastodon-share 134 | wp-polls 135 | wp-statistics 136 | override-build: | 137 | for plugin in $WP_PLUGINS; 138 | do 139 | curl -sSL "https://downloads.wordpress.org/plugin/${plugin}.latest-stable.zip" -o "${plugin}.zip" 140 | unzip -q "${plugin}.zip" 141 | rm "${plugin}.zip" 142 | done 143 | curl -sSL "https://downloads.wordpress.org/plugin/openid.3.6.1.zip" -o "openid.zip" 144 | unzip -q "openid.zip" 145 | rm "openid.zip" 146 | git apply $CRAFT_PART_SRC/patches/openid.patch 147 | # Latest YoastSEO does not support 5.9.3 version of WordPress. 148 | curl -sSL "https://downloads.wordpress.org/plugin/wordpress-seo.18.9.zip" -o "wordpress-seo.zip" 149 | unzip "wordpress-seo.zip" 150 | rm "wordpress-seo.zip" 151 | cp -R . $CRAFT_PART_INSTALL 152 | organize: 153 | "*": /var/www/html/wp-content/plugins/ 154 | ## Plugins fetched via git 155 | get-wordpress-launchpad-integration: 156 | after: 157 | - get-wordpress-plugins 158 | plugin: dump 159 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress-launchpad-integration/+git/wordpress-launchpad-integration 160 | source-type: git 161 | organize: 162 | "*": /var/www/html/wp-content/plugins/wordpress-launchpad-integration/ 163 | get-wordpress-teams-integration: 164 | after: 165 | - get-wordpress-plugins 166 | plugin: dump 167 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress-teams-integration/+git/wordpress-teams-integration 168 | source-type: git 169 | organize: 170 | "*": /var/www/html/wp-content/plugins/wordpress-teams-integration/ 171 | get-openstack-objectstorage-k8s: 172 | after: 173 | - get-wordpress-plugins 174 | plugin: dump 175 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/openstack-objectstorage-k8s 176 | source-type: git 177 | organize: 178 | "*": /var/www/html/wp-content/plugins/openstack-objectstorage-k8s/ 179 | get-wp-plugin-xubuntu-team-members: 180 | after: 181 | - get-wordpress-plugins 182 | plugin: dump 183 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-plugin-xubuntu-team-members 184 | source-type: git 185 | organize: 186 | "*": /var/www/html/wp-content/plugins/xubuntu-team-members/ 187 | # Wordpress themes 188 | get-light-wordpress-theme: 189 | after: 190 | - wordpress 191 | plugin: dump 192 | source: https://git.launchpad.net/~canonical-sysadmins/ubuntu-community-webthemes/+git/light-wordpress-theme 193 | source-type: git 194 | organize: 195 | "*": /var/www/html/wp-content/themes/light-wordpress-theme/ 196 | get-wp-theme-mscom: 197 | after: 198 | - wordpress 199 | plugin: dump 200 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-mscom 201 | source-type: git 202 | organize: 203 | "*": /var/www/html/wp-content/themes/mscom/ 204 | get-wp-theme-thematic: 205 | after: 206 | - wordpress 207 | plugin: dump 208 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-thematic 209 | source-type: git 210 | organize: 211 | "*": /var/www/html/wp-content/themes/thematic/ 212 | get-ubuntu-cloud-website: 213 | after: 214 | - wordpress 215 | plugin: dump 216 | source: https://git.launchpad.net/~canonical-sysadmins/ubuntu-cloud-website/+git/ubuntu-cloud-website 217 | source-type: git 218 | organize: 219 | "*": /var/www/html/wp-content/themes/ubuntu-cloud-website/ 220 | get-wp-theme-ubuntu-community: 221 | after: 222 | - wordpress 223 | plugin: dump 224 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-community 225 | source-type: git 226 | organize: 227 | "*": /var/www/html/wp-content/themes/ubuntu-community/ 228 | get-ubuntu-community-wordpress-theme: 229 | after: 230 | - wordpress 231 | plugin: dump 232 | source: https://git.launchpad.net/~canonical-sysadmins/ubuntu-community-wordpress-theme/+git/ubuntu-community-wordpress-theme 233 | source-type: git 234 | organize: 235 | "*": /var/www/html/wp-content/themes/ubuntu-community-wordpress-theme/ 236 | get-wp-theme-ubuntu-fi: 237 | after: 238 | - wordpress 239 | plugin: dump 240 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-fi 241 | source-type: git 242 | organize: 243 | "*": /var/www/html/wp-content/themes/ubuntu-fi/ 244 | get-wp-theme-ubuntu-light: 245 | after: 246 | - wordpress 247 | plugin: dump 248 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-light 249 | source-type: git 250 | organize: 251 | "*": /var/www/html/wp-content/themes/ubuntu-light/ 252 | get-wp-theme-ubuntustudio-wp: 253 | after: 254 | - wordpress 255 | plugin: dump 256 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntustudio-wp 257 | source-type: git 258 | organize: 259 | "*": /var/www/html/wp-content/themes/ubuntustudio-wp/ 260 | get-wp-theme-launchpad: 261 | after: 262 | - wordpress 263 | plugin: dump 264 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-launchpad 265 | source-type: git 266 | organize: 267 | "*": /var/www/html/wp-content/themes/launchpad/ 268 | get-wp-theme-xubuntu-website: 269 | after: 270 | - wordpress 271 | plugin: dump 272 | source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-xubuntu-website 273 | source-type: git 274 | organize: 275 | "*": /var/www/html/wp-content/themes/xubuntu-website/ 276 | get-resource-centre: 277 | after: 278 | - wordpress 279 | plugin: nil 280 | build-packages: [bzr] 281 | build-environment: 282 | # bzr is unable to import system python package breezy 283 | - PYTHONPATH: "/usr/lib/python3/dist-packages:/usr/local/lib/python3.12/dist-packages" 284 | override-build: | 285 | pip3 install breezy launchpadlib --break-system-packages 286 | bzr branch lp:resource-centre 287 | cp -R . $CRAFT_PART_INSTALL 288 | organize: 289 | resource-centre: /var/www/html/wp-content/themes/resource-centre/ 290 | # Post-install configuration 291 | wordpress-configure: 292 | plugin: nil 293 | after: 294 | - get-wordpress-launchpad-integration 295 | - get-wordpress-teams-integration 296 | - get-openstack-objectstorage-k8s 297 | - get-wp-plugin-xubuntu-team-members 298 | - get-light-wordpress-theme 299 | - get-wp-theme-mscom 300 | - get-wp-theme-thematic 301 | - get-ubuntu-cloud-website 302 | - get-wp-theme-ubuntu-community 303 | - get-wp-theme-ubuntu-fi 304 | - get-wp-theme-ubuntu-light 305 | - get-wp-theme-ubuntustudio-wp 306 | - get-wp-theme-launchpad 307 | - get-wp-theme-xubuntu-website 308 | - get-resource-centre 309 | - get-ubuntu-community-wordpress-theme 310 | build-environment: 311 | - IMAGE_RUN_USER_ID: 584792 312 | - IMAGE_RUN_GROUP_ID: 584792 313 | override-prime: | 314 | craftctl default 315 | rm -rf **/.git 316 | chown $IMAGE_RUN_USER_ID:$IMAGE_RUN_GROUP_ID -R --no-dereference "$CRAFT_PRIME/var/www/html" 317 | --------------------------------------------------------------------------------