diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d03d0deb..3f679276 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,6 +40,5 @@ jobs: uses: canonical/charm-logdump-action@main if: failure() with: - # TEMPLATE-TODO: Replace the application name - app: operator-template + app: kratos-external-idp-integrator model: testing diff --git a/.github/workflows/on_pull_request.yaml b/.github/workflows/on_pull_request.yaml index ac5e2199..7e5db9fe 100644 --- a/.github/workflows/on_pull_request.yaml +++ b/.github/workflows/on_pull_request.yaml @@ -1,8 +1,36 @@ -name: Tests +name: On Pull Request + +# On pull_request, we: +# * always run lint checks +# * always run tests +# * always publish to charmhub at latest/edge/branchname + on: pull_request: jobs: - run-tests: + lint: + name: Lint + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install dependencies + run: python3 -m pip install tox + + - name: Run linters + run: tox -e lint + + tests: name: Run Tests - uses: ./.github/workflows/ci.yaml + needs: + - lint + uses: ./.github/workflows/tests.yaml + + # publish runs in parallel with tests, as we always publish in this situation + publish-charm: + name: Publish Charm + uses: ./.github/workflows/publish.yaml + secrets: + CHARMCRAFT_CREDENTIALS: ${{ secrets.CHARMCRAFT_CREDENTIALS }} diff --git a/.github/workflows/on_push.yaml b/.github/workflows/on_push.yaml index 1c0b4f90..d1594f52 100644 --- a/.github/workflows/on_push.yaml +++ b/.github/workflows/on_push.yaml @@ -1,12 +1,29 @@ -name: Tests +name: On Push + +# On push to a "special" branch, we: +# * always publish to charmhub at latest/edge/branchname +# * always run tests +# where a "special" branch is one of main/master or track/**, as +# by convention these branches are the source for a corresponding +# charmhub edge channel. + on: push: branches: - - master - main - track/** jobs: - run-tests: + tests: name: Run Tests - uses: ./.github/workflows/ci.yaml \ No newline at end of file + uses: ./.github/workflows/tests.yaml + secrets: + charmcraft-credentials: ${{ secrets.CHARMCRAFT_CREDENTIALS }} + + # publish runs in series with tests, and only publishes if tests passes + publish-charm: + name: Publish Charm + needs: tests + uses: ./.github/workflows/publish.yaml + secrets: + CHARMCRAFT_CREDENTIALS: ${{ secrets.CHARMCRAFT_CREDENTIALS }} diff --git a/.github/workflows/on_schedule.yaml b/.github/workflows/on_schedule.yaml new file mode 100644 index 00000000..e12b5fe8 --- /dev/null +++ b/.github/workflows/on_schedule.yaml @@ -0,0 +1,18 @@ +on: + schedule: + - cron: '0 8 * * MON' + +jobs: + update-charm-libs: + name: Update Charm Libraries + uses: ./.github/workflows/update_libs.yaml + + update-deps: + name: Update Dependencies + uses: ./.github/workflows/renovate.yaml + + tests: + name: Run Tests + uses: ./.github/workflows/tests.yaml + secrets: + charmcraft-credentials: ${{ secrets.CHARMCRAFT_CREDENTIALS }} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..354ef46a --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,61 @@ +# reusable workflow for publishing all charms in this repo +name: Publish + +on: + workflow_call: + inputs: + source_branch: + description: Github branch from this repo to publish. If blank, will use the default branch + default: '' + required: false + type: string + secrets: + CHARMCRAFT_CREDENTIALS: + required: true + + workflow_dispatch: + inputs: + destination_channel: + description: CharmHub channel to publish to + required: false + default: 'latest/edge' + type: string + source_branch: + description: Github branch from this repo to publish. If blank, will use the default branch + required: false + default: '' + type: string + +jobs: + publish-charm: + name: Publish Charm + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch }} + + - name: Select charmhub channel + uses: canonical/charming-actions/channel@2.1.0 + id: select-channel + if: ${{ inputs.destination_channel == '' }} + + # Combine inputs from different sources to a single canonical value so later steps don't + # need logic for picking the right one + - name: Parse and combine inputs + id: parse-inputs + run: | + # destination_channel + destination_channel="${{ inputs.destination_channel || steps.select-channel.outputs.name }}" + echo "setting output of destination_channel=$destination_channel" + echo "::set-output name=destination_channel::$destination_channel" + + - name: Upload charm to charmhub + uses: canonical/charming-actions/upload-charm@2.1.0 + with: + credentials: ${{ secrets.CHARMCRAFT_CREDENTIALS }} + github-token: ${{ secrets.GITHUB_TOKEN }} + channel: ${{ steps.parse-inputs.outputs.destination_channel }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..17d595e0 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,26 @@ +# reusable workflow triggered manually +name: Release charm to other tracks and channels + +on: + workflow_dispatch: + inputs: + destination-channel: + description: 'Destination Channel' + required: true + origin-channel: + description: 'Origin Channel' + required: true + +jobs: + promote-charm: + name: Promote charm + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Release charm to channel + uses: canonical/charming-actions/release-charm@2.1.0 + with: + credentials: ${{ secrets.CHARMCRAFT_CREDENTIALS }} + github-token: ${{ secrets.GITHUB_TOKEN }} + destination-channel: ${{ github.event.inputs.destination-channel }} + origin-channel: ${{ github.event.inputs.origin-channel }} diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml new file mode 100644 index 00000000..618c315a --- /dev/null +++ b/.github/workflows/renovate.yaml @@ -0,0 +1,21 @@ +# workflow for checking package versions and opening PRs to bump +name: Renovate +on: + workflow_dispatch: + workflow_call: + +jobs: + renovate: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: List + run: ls -la + + - name: Self-hosted Renovate + uses: renovatebot/github-action@v32.236.0 + with: + configurationFile: renovate-config.js + token: ${{ github.token }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..9575a951 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,45 @@ +name: Tests +on: + workflow_dispatch: + workflow_call: + +jobs: + unit-test: + name: Unit tests + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install dependencies + run: python -m pip install tox + + - name: Run tests + run: tox -e unit + + integration-test-microk8s: + name: Integration tests (microk8s) + runs-on: ubuntu-22.04 + needs: + - unit-test + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@main + with: + provider: microk8s + channel: 1.25/stable + bootstrap-options: '--agent-version=2.9.34' + + - name: Run integration tests + # set a predictable model name so it can be consumed by charm-logdump-action + run: tox -e integration -- --model testing + + - name: Dump logs + uses: canonical/charm-logdump-action@main + if: failure() + with: + app: kratos + model: testing diff --git a/.github/workflows/update_libs.yaml b/.github/workflows/update_libs.yaml new file mode 100644 index 00000000..c3225e0b --- /dev/null +++ b/.github/workflows/update_libs.yaml @@ -0,0 +1,54 @@ +# reusable workflow for checking library versions and opening PRs to bump +name: Update Charm Libraries + +on: + workflow_call: + inputs: + charmcraft_channel: + description: Channel from which to install charmcraft + default: 'latest/candidate' + required: false + type: string + secrets: + CHARMCRAFT_CREDENTIALS: + required: true + workflow_dispatch: + inputs: + charmcraft_channel: + description: Channel from which to install charmcraft + default: 'latest/candidate' + required: false + type: string + +jobs: + update-libs: + name: Update charm libraries + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - run: sudo snap install charmcraft --channel ${{ inputs.charmcraft_channel }} --classic + + - name: update charm libs + run: charmcraft fetch-lib + env: + CHARMCRAFT_AUTH: ${{ secrets.CHARMCRAFT_CREDENTIALS }} + + - name: Create Pull Request + id: create-pull-request + uses: peter-evans/create-pull-request@v4 + with: + title: '(Automated) Update Charm Libs' + body: 'Update charm libs' + commit-message: 'Update charm libs' + signoff: false + delete-branch: true + branch: 'automated-update-charm-libs' + + - name: Print Created Pull Request + if: ${{ steps.create-pull-request.outputs.pull-request-number }} + run: | + echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" + echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef9acdcf..d3f15b23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,11 +5,9 @@ This document explains the processes and practices recommended for contributing enhancements to this operator. - - - Generally, before developing enhancements to this charm, you should consider [opening an issue - ](https://github.com/canonical/operator-template/issues) explaining your use case. -- If you would like to chat with us about your use-cases or proposed implementation, you can reach + ](https://github.com/canonical/kratos-external-idp-integrator/issues) explaining your use case. +- If you would like to chat with us about charm development, you can reach us at [Canonical Mattermost public channel](https://chat.charmhub.io/charmhub/channels/charm-dev) or [Discourse](https://discourse.charmhub.io/). - Familiarising yourself with the [Charmed Operator Framework](https://juju.is/docs/sdk) library @@ -50,20 +48,17 @@ charmcraft pack ### Deploy - - ```bash # Create a model juju add-model dev # Enable DEBUG logging juju model-config logging-config="=INFO;unit=DEBUG" # Deploy the charm -juju deploy ./template-operator_ubuntu-20.04-amd64.charm \ +juju deploy ./kratos-external-idp-integrator_ubuntu-22.04-amd64.charm \ --resource httpbin-image=kennethreitz/httpbin \ ``` ## Canonical Contributor Agreement - -Canonical welcomes contributions to the Charmed Template Operator. Please check out our [contributor agreement](https://ubuntu.com/legal/contributors) if you're interested in contributing to the solution. +Canonical welcomes contributions to the Charmed Kratos IdP Integrator Operator. Please check out our [contributor agreement](https://ubuntu.com/legal/contributors) if you're interested in contributing to the solution. diff --git a/README.md b/README.md index 7959ce32..76bbc336 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,7 @@ TODO: Include a link to the default image your charm uses ## Contributing - - Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines on enhancements to this charm following best practice guidelines, and -[CONTRIBUTING.md](https://github.com///blob/main/CONTRIBUTING.md) for developer +[CONTRIBUTING.md](https://github.com/canonical/kratos-external-idp-integrator/blob/main/CONTRIBUTING.md) for developer guidance. diff --git a/actions.yaml b/actions.yaml index 9f06d90f..b1445114 100644 --- a/actions.yaml +++ b/actions.yaml @@ -1,16 +1,2 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -# -# TEMPLATE-TODO: change this example to suit your needs. -# If you don't need actions, you can remove the file entirely. -# It ties in to the example _on_fortune_action handler in src/charm.py -# -# Learn more about actions at: https://juju.is/docs/sdk/actions - -fortune: - description: Returns a pithy phrase. - params: - fail: - description: "Fail with this message" - type: string - default: "" diff --git a/charmcraft.yaml b/charmcraft.yaml index e109b8b2..d37ba37e 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -5,7 +5,7 @@ type: charm bases: - build-on: - name: "ubuntu" - channel: "20.04" + channel: "22.04" run-on: - name: "ubuntu" - channel: "20.04" + channel: "22.04" diff --git a/config.yaml b/config.yaml index 77e1248c..b1445114 100644 --- a/config.yaml +++ b/config.yaml @@ -1,14 +1,2 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -# -# TEMPLATE-TODO: change this example to suit your needs. -# If you don't need a config, you can remove the file entirely. -# It ties in to the example _on_config_changed handler in src/charm.py -# -# Learn more about config at: https://juju.is/docs/sdk/config - -options: - thing: - default: 🎁 - description: A thing used by the charm. - type: string diff --git a/metadata.yaml b/metadata.yaml index 504761fe..f7e0e1b2 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,23 +1,8 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -# For a complete list of supported options, see: -# https://juju.is/docs/sdk/metadata-reference -name: template-operator +name: kratos-external-idp-integrator description: | - TEMPLATE-TODO: fill out the charm's description + Charm used to integrate Charmed Kratos with external IdPs summary: | - TEMPLATE-TODO: fill out the charm's summary - -# TEMPLATE-TODO: replace with containers for your workload (delete for non-k8s) -containers: - httpbin: - resource: httpbin-image - -# TEMPLATE-TODO: each container defined above must specify an oci-image resource -resources: - httpbin-image: - type: oci-image - description: OCI image for httpbin (kennethreitz/httpbin) - # Included for simplicity in integration tests - upstream-source: kennethreitz/httpbin + Charm used to integrate Charmed Kratos with external IdPs diff --git a/src/charm.py b/src/charm.py index 27fbdf50..dc10a07e 100755 --- a/src/charm.py +++ b/src/charm.py @@ -17,88 +17,19 @@ from ops.charm import CharmBase from ops.framework import StoredState from ops.main import main -from ops.model import ActiveStatus logger = logging.getLogger(__name__) -class OperatorTemplateCharm(CharmBase): +class KratosIdpIntegratorCharm(CharmBase): """Charm the service.""" _stored = StoredState() def __init__(self, *args): 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) - self.framework.observe(self.on.fortune_action, self._on_fortune_action) self._stored.set_default(things=[]) - def _on_httpbin_pebble_ready(self, event): - """Define and start a workload using the Pebble API. - - TEMPLATE-TODO: change this example to suit your needs. - You'll need to specify the right entrypoint and environment - configuration for your specific workload. Tip: you can see the - standard entrypoint of an existing container using docker inspect - - Learn more about Pebble layers at https://github.com/canonical/pebble - """ - # Get a reference the container attribute on the PebbleReadyEvent - container = event.workload - # Define an initial Pebble layer configuration - pebble_layer = { - "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": {"thing": self.model.config["thing"]}, - } - }, - } - # Add initial Pebble config layer using the Pebble API - container.add_layer("httpbin", pebble_layer, combine=True) - # Autostart any services that were defined with startup: enabled - container.autostart() - # Learn more about statuses in the SDK docs: - # https://juju.is/docs/sdk/constructs#heading--statuses - self.unit.status = ActiveStatus() - - def _on_config_changed(self, _): - """Just an example to show how to deal with changed configuration. - - TEMPLATE-TODO: change this example to suit your needs. - If you don't need to handle config, you can remove this method, - the hook created in __init__.py for it, the corresponding test, - and the config.py file. - - Learn more about config at https://juju.is/docs/sdk/config - """ - current = self.config["thing"] - if current not in self._stored.things: - logger.debug("found a new thing: %r", current) - self._stored.things.append(current) - - def _on_fortune_action(self, event): - """Just an example to show how to receive actions. - - TEMPLATE-TODO: change this example to suit your needs. - If you don't need to handle actions, you can remove this method, - the hook created in __init__.py for it, the corresponding test, - and the actions.py file. - - Learn more about actions at https://juju.is/docs/sdk/actions - """ - fail = event.params["fail"] - if fail: - event.fail(fail) - else: - event.set_results({"fortune": "A bug in the code is worth two in the documentation."}) - if __name__ == "__main__": - main(OperatorTemplateCharm) + main(KratosIdpIntegratorCharm) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 072b5197..8494fc0c 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -4,7 +4,6 @@ import logging -import urllib.request from pathlib import Path import pytest @@ -25,8 +24,7 @@ async def test_build_and_deploy(ops_test: OpsTest): """ # build and deploy charm from local source folder charm = await ops_test.build_charm(".") - resources = {"httpbin-image": METADATA["resources"]["httpbin-image"]["upstream-source"]} - await ops_test.model.deploy(charm, resources=resources, application_name=APP_NAME) + await ops_test.model.deploy(charm, application_name=APP_NAME) # issuing dummy update_status just to trigger an event async with ops_test.fast_forward(): @@ -37,15 +35,3 @@ async def test_build_and_deploy(ops_test: OpsTest): timeout=1000, ) assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active" - - -@pytest.mark.abort_on_fail -async def test_application_is_up(ops_test: OpsTest): - status = await ops_test.model.get_status() # noqa: F821 - address = status["applications"][APP_NAME]["units"][f"{APP_NAME}/0"]["address"] - - url = f"http://{address}" - - logger.info("querying app address: %s", url) - response = urllib.request.urlopen(url, data=None, timeout=2.0) - assert response.code == 200 diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 34ddfa24..a871ef27 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -4,64 +4,14 @@ # Learn more about testing at: https://juju.is/docs/sdk/testing import unittest -from unittest.mock import Mock -from ops.model import ActiveStatus from ops.testing import Harness -from charm import OperatorTemplateCharm +from charm import KratosIdpIntegratorCharm class TestCharm(unittest.TestCase): def setUp(self): - self.harness = Harness(OperatorTemplateCharm) + self.harness = Harness(KratosIdpIntegratorCharm) self.addCleanup(self.harness.cleanup) self.harness.begin() - - def test_config_changed(self): - self.assertEqual(list(self.harness.charm._stored.things), []) - self.harness.update_config({"thing": "foo"}) - self.assertEqual(list(self.harness.charm._stored.things), ["foo"]) - - def test_action(self): - # the harness doesn't (yet!) help much with actions themselves - action_event = Mock(params={"fail": ""}) - self.harness.charm._on_fortune_action(action_event) - - self.assertTrue(action_event.set_results.called) - - def test_action_fail(self): - action_event = Mock(params={"fail": "fail this"}) - self.harness.charm._on_fortune_action(action_event) - - self.assertEqual(action_event.fail.call_args, [("fail this",)]) - - def test_httpbin_pebble_ready(self): - # Check the initial Pebble plan is empty - initial_plan = self.harness.get_container_pebble_plan("httpbin") - self.assertEqual(initial_plan.to_yaml(), "{}\n") - # 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": {"thing": "🎁"}, - } - }, - } - # Get the httpbin container from the model - container = self.harness.model.unit.get_container("httpbin") - # Emit the PebbleReadyEvent carrying the httpbin container - self.harness.charm.on.httpbin_pebble_ready.emit(container) - # 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, ActiveStatus())