From 5df14a47b5ce37a461d657114c70e1e03e27e568 Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 12:02:31 +0200 Subject: [PATCH 01/17] Remove templated metadata.yaml and populate initial charmcraft yaml so that basic charm can be built --- charmcraft.yaml | 19 ++++++++++++++++--- metadata.yaml | 50 ------------------------------------------------- 2 files changed, 16 insertions(+), 53 deletions(-) delete mode 100644 metadata.yaml diff --git a/charmcraft.yaml b/charmcraft.yaml index df6fdcb..04d6a48 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -1,13 +1,26 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. -# This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. type: charm bases: - build-on: - name: ubuntu channel: "22.04" + architectures: [amd64] run-on: - name: ubuntu - channel: "22.04" + channel: "24.04" + architectures: [amd64] + +name: penpot +title: Penpot +description: | + A Juju charm deploying and managing Penpot on Kubernetes. Penpot is the + web-based open-source design tool that bridges the gap between designers + and developers. +summary: An operator deploying and managing Penpot. +links: + issues: https://github.com/canonical/penpot-operator/issues + source: https://github.com/canonical/penpot-operator + contact: + - https://launchpad.net/~canonical-is-devops diff --git a/metadata.yaml b/metadata.yaml deleted file mode 100644 index 19585a7..0000000 --- a/metadata.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2024 Canonical Ltd. -# See LICENSE file for licensing details. -# This file populates the Overview on Charmhub. -# See https://juju.is/docs/sdk/metadata-reference for a checklist and guidance. - -# The charm package name, no spaces (required) -# See https://juju.is/docs/sdk/naming#heading--naming-charms for guidance. -name: is-charms-template - -# The following metadata are human-readable and will be published prominently on Charmhub. - -# (Recommended) -display-name: Charm Template - -# (Required) -summary: A very short one-line summary of the charm. -docs: https://discourse.charmhub.io -issues: https://github.com/canonical/is-charms-template-repo/issues -maintainers: - - https://launchpad.net/~canonical-is-devops -source: https://github.com/canonical/is-charms-template-repo - -description: | - A single sentence that says what the charm is, concisely and memorably. - - A paragraph of one to three short sentences, that describe what the charm does. - - A third paragraph that explains what need the charm meets. - - Finally, a paragraph that describes whom the charm is useful for. - -# The containers and resources metadata apply to Kubernetes charms only. -# Remove them if not required. - -# Your workload’s containers. -containers: - httpbin: - resource: httpbin-image - -# This field populates the Resources tab on Charmhub. -resources: - # An OCI image resource for each container listed above. - # You may remove this if your charm will run without a workload sidecar container. - httpbin-image: - type: oci-image - description: OCI image for httpbin - # The upstream-source field is ignored by Juju. It is included here as a reference - # so the integration testing suite knows which image to deploy during testing. This field - # is also used by the 'canonical/charming-actions' Github action for automated releasing. - upstream-source: kennethreitz/httpbin From fd5afbba2f72801d2d5fdef5ac90b6eb7f2c51c3 Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 12:26:37 +0200 Subject: [PATCH 02/17] Updates for lint and unit tests to pass --- config.yaml | 4 --- src/charm.py | 75 ++++------------------------------------- tests/unit/test_base.py | 46 ++----------------------- 3 files changed, 9 insertions(+), 116 deletions(-) diff --git a/config.yaml b/config.yaml index dd4dcdd..2d47f78 100644 --- a/config.yaml +++ b/config.yaml @@ -1,9 +1,5 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. -# This file defines charm config options, and populates the Configure tab on Charmhub. -# If your charm does not require configuration options, delete this file entirely. -# -# See https://juju.is/docs/config for guidance. options: # An example config option to customise the log level of the workload diff --git a/src/charm.py b/src/charm.py index e9b21ed..6b2c8b4 100755 --- a/src/charm.py +++ b/src/charm.py @@ -17,7 +17,6 @@ import typing import ops -from ops import pebble # Log messages can be retrieved using juju debug-log logger = logging.getLogger(__name__) @@ -25,7 +24,7 @@ VALID_LOG_LEVELS = ["info", "debug", "warning", "error", "critical"] -class IsCharmsTemplateCharm(ops.CharmBase): +class PenpotCharm(ops.CharmBase): """Charm the service.""" def __init__(self, *args: typing.Any): @@ -35,83 +34,21 @@ def __init__(self, *args: typing.Any): args: Arguments passed to the CharmBase parent constructor. """ super().__init__(*args) - self.framework.observe(self.on.httpbin_pebble_ready, self._on_httpbin_pebble_ready) self.framework.observe(self.on.config_changed, self._on_config_changed) - def _on_httpbin_pebble_ready(self, event: ops.PebbleReadyEvent) -> None: - """Define and start a workload using the Pebble API. - - Change this example to suit your needs. You'll need to specify the right entrypoint and - environment configuration for your specific workload. - - Learn more about interacting with Pebble at at https://juju.is/docs/sdk/pebble. - - Args: - event: event triggering the handler. - """ - # Get a reference the container attribute on the PebbleReadyEvent - container = event.workload - # Add initial Pebble config layer using the Pebble API - container.add_layer("httpbin", self._pebble_layer, combine=True) - # Make Pebble reevaluate its plan, ensuring any services are started if enabled. - container.replan() - # Learn more about statuses in the SDK docs: - # https://juju.is/docs/sdk/constructs#heading--statuses - self.unit.status = ops.ActiveStatus() - - def _on_config_changed(self, event: ops.ConfigChangedEvent) -> None: - """Handle changed configuration. - - Change this example to suit your needs. If you don't need to handle config, you can remove - this method. - - Learn more about config at https://juju.is/docs/sdk/config - - Args: - event: event triggering the handler. - """ + def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None: + """Handle changed configuration.""" # Fetch the new config value - log_level = self.model.config["log-level"].lower() + log_level = str(self.model.config["log-level"]).lower() # Do some validation of the configuration option if log_level in VALID_LOG_LEVELS: # The config is good, so update the configuration of the workload - container = self.unit.get_container("httpbin") - # Verify that we can connect to the Pebble API in the workload container - if container.can_connect(): - # Push an updated layer with the new config - container.add_layer("httpbin", self._pebble_layer, combine=True) - container.replan() - - logger.debug("Log level for gunicorn changed to '%s'", log_level) - self.unit.status = ops.ActiveStatus() - else: - # We were unable to connect to the Pebble API, so we defer this event - event.defer() - self.unit.status = ops.WaitingStatus("waiting for Pebble API") + self.unit.status = ops.ActiveStatus() else: # In this case, the config option is bad, so block the charm and notify the operator. self.unit.status = ops.BlockedStatus("invalid log level: '{log_level}'") - @property - def _pebble_layer(self) -> pebble.LayerDict: - """Return a dictionary representing a Pebble layer.""" - return { - "summary": "httpbin layer", - "description": "pebble config layer for httpbin", - "services": { - "httpbin": { - "override": "replace", - "summary": "httpbin", - "command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent", - "startup": "enabled", - "environment": { - "GUNICORN_CMD_ARGS": f"--log-level {self.model.config['log-level']}" - }, - } - }, - } - if __name__ == "__main__": # pragma: nocover - ops.main.main(IsCharmsTemplateCharm) + ops.main.main(PenpotCharm) diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py index c1ce697..f694dff 100644 --- a/tests/unit/test_base.py +++ b/tests/unit/test_base.py @@ -11,7 +11,7 @@ import ops import ops.testing -from charm import IsCharmsTemplateCharm +from charm import PenpotCharm class TestCharm(unittest.TestCase): @@ -19,56 +19,16 @@ class TestCharm(unittest.TestCase): def setUp(self): """Set up the testing environment.""" - self.harness = ops.testing.Harness(IsCharmsTemplateCharm) + self.harness = ops.testing.Harness(PenpotCharm) self.addCleanup(self.harness.cleanup) self.harness.begin() - def test_httpbin_pebble_ready(self): - # Expected plan after Pebble ready with default config - expected_plan = { - "services": { - "httpbin": { - "override": "replace", - "summary": "httpbin", - "command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent", - "startup": "enabled", - "environment": {"GUNICORN_CMD_ARGS": "--log-level info"}, - } - }, - } - # Simulate the container coming up and emission of pebble-ready event - self.harness.container_pebble_ready("httpbin") - # Get the plan now we've run PebbleReady - updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict() - # Check we've got the plan we expected - self.assertEqual(expected_plan, updated_plan) - # Check the service was started - service = self.harness.model.unit.get_container("httpbin").get_service("httpbin") - self.assertTrue(service.is_running()) - # Ensure we set an ActiveStatus with no message - self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus()) - - def test_config_changed_valid_can_connect(self): - # Ensure the simulated Pebble API is reachable - self.harness.set_can_connect("httpbin", True) + def test_config_changed_valid(self): # Trigger a config-changed event with an updated value self.harness.update_config({"log-level": "debug"}) - # Get the plan now we've run PebbleReady - updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict() - updated_env = updated_plan["services"]["httpbin"]["environment"] - # Check the config change was effective - self.assertEqual(updated_env, {"GUNICORN_CMD_ARGS": "--log-level debug"}) self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus()) - def test_config_changed_valid_cannot_connect(self): - # Trigger a config-changed event with an updated value - self.harness.update_config({"log-level": "debug"}) - # Check the charm is in WaitingStatus - self.assertIsInstance(self.harness.model.unit.status, ops.WaitingStatus) - def test_config_changed_invalid(self): - # Ensure the simulated Pebble API is reachable - self.harness.set_can_connect("httpbin", True) # Trigger a config-changed event with an updated value self.harness.update_config({"log-level": "foobar"}) # Check the charm is in BlockedStatus From d0d6720826b586c53fd258545411f10899f9ed2c Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 12:33:07 +0200 Subject: [PATCH 03/17] Fix integration tests --- tests/integration/test_charm.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index f212ec1..8d51bad 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) -METADATA = yaml.safe_load(Path("./metadata.yaml").read_text(encoding="utf-8")) +CHARMCRAFT = yaml.safe_load(Path("./charmcraft.yaml").read_text(encoding="utf-8")) APP_NAME = METADATA["name"] @@ -26,12 +26,10 @@ async def test_build_and_deploy(ops_test: OpsTest, pytestconfig: pytest.Config): Assert on the unit status before any relations/configurations take place. """ # Deploy the charm and wait for active/idle status - charm = pytestconfig.getoption("--charm-file") - resources = {"httpbin-image": METADATA["resources"]["httpbin-image"]["upstream-source"]} assert ops_test.model await asyncio.gather( ops_test.model.deploy( - f"./{charm}", resources=resources, application_name=APP_NAME, series="jammy" + f"./{charm}", application_name=APP_NAME, series="noble" ), ops_test.model.wait_for_idle( apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 From 724b391862594141351c3c27b93786dfdf11ebd6 Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 12:38:16 +0200 Subject: [PATCH 04/17] Update variable name in integration tests --- tests/integration/test_charm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 8d51bad..47af992 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) CHARMCRAFT = yaml.safe_load(Path("./charmcraft.yaml").read_text(encoding="utf-8")) -APP_NAME = METADATA["name"] +APP_NAME = CHARMCRAFT["name"] @pytest.mark.abort_on_fail From 1a0be116c98de04458cc688c24bfa08b2e8927e3 Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 12:43:44 +0200 Subject: [PATCH 05/17] We need to define the charm file in integration test --- tests/integration/test_charm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 47af992..1b02316 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -25,6 +25,7 @@ async def test_build_and_deploy(ops_test: OpsTest, pytestconfig: pytest.Config): Assert on the unit status before any relations/configurations take place. """ + charm = pytestconfig.getoption("--charm-file") # Deploy the charm and wait for active/idle status assert ops_test.model await asyncio.gather( From fbbdb8353173b533b9bb7035fdd06bbb6dc3e996 Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 12:49:12 +0200 Subject: [PATCH 06/17] Update formatting --- tests/integration/test_charm.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 1b02316..1817341 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -29,9 +29,7 @@ async def test_build_and_deploy(ops_test: OpsTest, pytestconfig: pytest.Config): # Deploy the charm and wait for active/idle status assert ops_test.model await asyncio.gather( - ops_test.model.deploy( - f"./{charm}", application_name=APP_NAME, series="noble" - ), + ops_test.model.deploy(f"./{charm}", application_name=APP_NAME, series="noble"), ops_test.model.wait_for_idle( apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 ), From 6f7554b3eb8a992337fc50bfb8921f25c5c22c5a Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 13:30:37 +0200 Subject: [PATCH 07/17] Run integration test on juju 3.3 since we're deploying noble --- .github/workflows/integration_test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 2f4fe63..2117b07 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -8,6 +8,7 @@ jobs: uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main secrets: inherit with: + juju-channel: 3.3/stable load-test-enabled: false load-test-run-args: "-e LOAD_TEST_HOST=localhost" zap-before-command: "curl -H \"Host: indico.local\" http://localhost/bootstrap --data-raw 'csrf_token=00000000-0000-0000-0000-000000000000&first_name=admin&last_name=admin&email=admin%40admin.com&username=admin&password=lunarlobster&confirm_password=lunarlobster&affiliation=Canonical'" From 1f0aa9438c468107b2bb6f54c692d9228d263ec7 Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 16:27:54 +0200 Subject: [PATCH 08/17] Switch to canonical k8s which supports juju 3.3 --- .github/workflows/integration_test.yaml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 2117b07..f2eec89 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -5,19 +5,11 @@ on: jobs: integration-tests: - uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main + uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@add_support_for_canonical_k8s secrets: inherit with: juju-channel: 3.3/stable - load-test-enabled: false - load-test-run-args: "-e LOAD_TEST_HOST=localhost" - zap-before-command: "curl -H \"Host: indico.local\" http://localhost/bootstrap --data-raw 'csrf_token=00000000-0000-0000-0000-000000000000&first_name=admin&last_name=admin&email=admin%40admin.com&username=admin&password=lunarlobster&confirm_password=lunarlobster&affiliation=Canonical'" - zap-enabled: true - zap-cmd-options: '-T 60 -z "-addoninstall jython" --hook "/zap/wrk/tests/zap/hook.py"' - zap-target: localhost - zap-target-port: 80 - zap-rules-file-name: "zap_rules.tsv" - trivy-fs-enabled: true - trivy-image-config: "trivy.yaml" + provider: k8s self-hosted-runner: true self-hosted-runner-label: "edge" + use-canonical-k8s: true From e184732b224236e590c3de1bf2035c621b5a7ab0 Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 17:01:32 +0200 Subject: [PATCH 09/17] Use strictly confined microk8s --- .github/workflows/integration_test.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index f2eec89..aa33051 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -5,11 +5,10 @@ on: jobs: integration-tests: - uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@add_support_for_canonical_k8s + uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main secrets: inherit with: + channel: 1.28-strict/stable juju-channel: 3.3/stable - provider: k8s self-hosted-runner: true self-hosted-runner-label: "edge" - use-canonical-k8s: true From c08208916b3aeea16432f9549391464121e603d2 Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 17:12:54 +0200 Subject: [PATCH 10/17] Noble isn't supported on 3.3, bump to 3.4 --- .github/workflows/integration_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index aa33051..36a9954 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -9,6 +9,6 @@ jobs: secrets: inherit with: channel: 1.28-strict/stable - juju-channel: 3.3/stable + juju-channel: 3.4/stable self-hosted-runner: true self-hosted-runner-label: "edge" From 930ce8a931279e7e8066dcff7582e55eb32f872d Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 19 Jun 2024 17:55:08 +0200 Subject: [PATCH 11/17] Match pylibjuju version to juju version for noble support --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 202340f..8a5f784 100644 --- a/tox.ini +++ b/tox.ini @@ -100,8 +100,7 @@ commands = [testenv:integration] description = Run integration tests deps = - # Last compatible version with Juju 2.9 - juju==3.0.4 + juju==3.4.0.0 pytest pytest-asyncio pytest-operator From d53c393299e2083ce0dbae11ea877c3706853135 Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Thu, 20 Jun 2024 08:39:32 +0200 Subject: [PATCH 12/17] Remove explicit reference to noble in charm deploy --- tests/integration/test_charm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 1817341..0a8d427 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -29,7 +29,7 @@ async def test_build_and_deploy(ops_test: OpsTest, pytestconfig: pytest.Config): # Deploy the charm and wait for active/idle status assert ops_test.model await asyncio.gather( - ops_test.model.deploy(f"./{charm}", application_name=APP_NAME, series="noble"), + ops_test.model.deploy(f"./{charm}", application_name=APP_NAME), ops_test.model.wait_for_idle( apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 ), From 72a8de12f6d6502ef941bc92cda28c9ed9fce17e Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 26 Jun 2024 10:00:19 +0200 Subject: [PATCH 13/17] Initial README, with sections commented out that aren't yet true --- README.md | 56 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 5d8da8e..7bd1b72 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,48 @@ -# is-charms-template - -Charmhub package name: operator-template -More information: https://charmhub.io/is-charms-template +A Juju charm deploying and managing Penpot on Kubernetes. Penpot is the +web-based open-source design tool that bridges the gap between designers and +developers. -Describe your charm in one or two sentences. + -## Other resources +As such, the charm makes it easy for those looking to take control of their +own Penpot deployment while keeping operations sumple, and gives them the +freedom to deploy on the Kubernetes platform of their choice. - + -- [Read more](https://example.com) +## Project and community -- [Contributing](CONTRIBUTING.md) +The Penpot 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. +* [Code of conduct](https://ubuntu.com/community/code-of-conduct) +* [Get support](https://discourse.charmhub.io/) + +Thinking about using Penpot for your next project? [Get in touch](https://chat.charmhub.io/charmhub/channels/charm-dev)! -- See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms. +--- + From 8c579da9cd6920f26c13a55437b614e9baaaef4b Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Wed, 26 Jun 2024 10:28:41 +0200 Subject: [PATCH 14/17] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bd1b72..fcab197 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ developers. This charm simplifies initial deployment and "day N" operations of Penpot, such as scaling the number of instances, integration with external authentication providers, access to S3 for redundant file storage and more. It -allows for deployment on many different Kuberenetes platforms, from [MicroK8s](https://microk8s.io) to +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. --> From 48ff9d428c66c3efabe65a0b1f4650fbc034602f Mon Sep 17 00:00:00 2001 From: mthaddon Date: Wed, 26 Jun 2024 13:48:43 +0200 Subject: [PATCH 15/17] Include link to Penpot Co-authored-by: Mariyan Dimitrov --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fcab197..cd42b35 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Discourse Status](https://img.shields.io/discourse/status?server=https%3A%2F%2Fdiscourse.charmhub.io&style=flat&label=CharmHub%20Discourse)](https://discourse.charmhub.io) --> -A Juju charm deploying and managing Penpot on Kubernetes. Penpot is the +A Juju charm deploying and managing [Penpot](https://penpot.app) on Kubernetes. Penpot is the web-based open-source design tool that bridges the gap between designers and developers. From 4d52276d19439ac5efb71a2ddba6c978443b2d1c Mon Sep 17 00:00:00 2001 From: Tom Haddon Date: Thu, 27 Jun 2024 14:40:59 +0200 Subject: [PATCH 16/17] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cd42b35..a5534b3 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ offerings. --> As such, the charm makes it easy for those looking to take control of their -own Penpot deployment while keeping operations sumple, and gives them the +own Penpot deployment while keeping operations simple, and gives them the freedom to deploy on the Kubernetes platform of their choice. -A Juju charm deploying and managing [Penpot](https://penpot.app) on Kubernetes. Penpot is the +A Juju charm that deploys and manages [Penpot](https://penpot.app) on Kubernetes. Penpot is the web-based open-source design tool that bridges the gap between designers and developers.