├── .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 | [](https://charmhub.io/wordpress-k8s)
2 | [](https://github.com/canonical/wordpress-k8s-operator/actions/workflows/publish_charm.yaml)
3 | [](https://github.com/canonical/wordpress-k8s-operator/actions/workflows/promote_charm.yaml)
4 | [](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 |
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 |
--------------------------------------------------------------------------------