diff --git a/.github/workflows/frontend_charm_analysis.yml b/.github/workflows/frontend_charm_analysis.yml index 751c14bef..707d6b287 100644 --- a/.github/workflows/frontend_charm_analysis.yml +++ b/.github/workflows/frontend_charm_analysis.yml @@ -4,13 +4,13 @@ on: branches: - main push: - branches: ["main"] + branches: ["main", "integration-test-fix"] tags: ["v*.*.*"] jobs: lint-report: name: Lint and format report - runs-on: ubuntu-22.04 + runs-on: [self-hosted, linux, large] steps: - uses: actions/checkout@v3 - name: Install tox @@ -23,7 +23,7 @@ jobs: static-analysis: name: Static analysis - runs-on: ubuntu-22.04 + runs-on: [self-hosted, linux] steps: - uses: actions/checkout@v3 - name: Install tox @@ -36,7 +36,7 @@ jobs: unit-tests-with-coverage: name: Unit tests - runs-on: ubuntu-22.04 + runs-on: [self-hosted, linux] steps: - uses: actions/checkout@v3 - name: Install tox diff --git a/.github/workflows/frontend_charm_integration_tests.yml b/.github/workflows/frontend_charm_integration_tests.yml index 80670c320..18ad95f6e 100644 --- a/.github/workflows/frontend_charm_integration_tests.yml +++ b/.github/workflows/frontend_charm_integration_tests.yml @@ -1,13 +1,17 @@ name: Run charm tests on: push: - branches: ["main"] + branches: ["main", "integration-test-fix"] tags: ["v*.*.*"] jobs: integration-test: + permissions: + contents: read + packages: read + name: Integration tests - runs-on: ubuntu-22.04 + runs-on: [self-hosted, linux, large] steps: - name: Checkout uses: actions/checkout@v3 @@ -16,9 +20,32 @@ jobs: with: channel: 5.13/stable - name: Setup operator environment - uses: charmed-kubernetes/actions-operator@main + uses: charmed-kubernetes/actions-operator@main # mz2/actions-operator@4fcb55b99e2a0f69a5910870d3639d203aeb87aa with: + channel: 1.27/stable # 1.26.4 provider: microk8s + juju-channel: 3.0/stable + # juju-classic-confinement: "false" + # microk8s-classic-confinement: "false" + charmcraft-channel: 2.x/stable + lxd-channel: 5.13/stable + - name: Adjust containerd template to accept ghcr.io images + run: | + sudo mkdir -p /var/snap/microk8s/current/args && + echo "" | sudo tee -a /var/snap/microk8s/current/args/containerd-template.toml && + echo '[plugins."io.containerd.grpc.v1.cri".registry.configs."ghcr.io".auth]' | sudo tee -a /var/snap/microk8s/current/args/containerd-template.toml && + echo ' username = "$GHCR_PACKAGE_API_USER"' | sudo tee -a /var/snap/microk8s/current/args/containerd-template.toml && + echo ' password = "$GHCR_PACKAGE_API_TOKEN"' | sudo tee -a /var/snap/microk8s/current/args/containerd-template.toml && + sudo systemctl restart snap.microk8s.daemon-containerd.service && + sudo microk8s.stop && + sleep 10 && + sudo microk8s.start && + sleep 10 && + sudo microk8s status --wait-ready && + sleep 30 + env: + GHCR_PACKAGE_API_USER: ${{ secrets.GHCR_PACKAGE_API_USER }} + GHCR_PACKAGE_API_TOKEN: ${{ secrets.GHCR_PACKAGE_API_TOKEN }} - name: Run integration tests run: tox -e integration - name: Archive Tested Charm @@ -43,3 +70,29 @@ jobs: defaults: run: working-directory: ./frontend/charm + + publish-charm: + name: Publish Charm + needs: integration-test + runs-on: [self-hosted, linux] + if: ${{ github.ref_name == 'main' }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install charmcraft + run: sudo snap install charmcraft --classic + - name: Fetch Tested Charm + uses: actions/download-artifact@v3 + with: + name: tested-charm + - name: Move charm in current directory + run: find ./ -name test-observer-frontend_ubuntu-22.04-amd64.charm -exec mv -t ./ {} \; + - name: Select Charmhub channel + uses: canonical/charming-actions/channel@2.2.2 + id: channel + - name: Upload charm to Charmhub + uses: canonical/charming-actions/upload-charm@2.2.2 + with: + credentials: "${{ secrets.CHARMHUB_AUTH_API }}" + github-token: "${{ secrets.GITHUB_TOKEN }}" + channel: "${{ steps.channel.outputs.name }}" diff --git a/.github/workflows/frontend_charm_promote.yml b/.github/workflows/frontend_charm_promote.yml new file mode 100644 index 000000000..a04866df9 --- /dev/null +++ b/.github/workflows/frontend_charm_promote.yml @@ -0,0 +1,43 @@ +name: Promote Charm + +on: + workflow_dispatch: + inputs: + promotion: + type: choice + description: Channel to promote from + options: + - edge -> beta + - beta -> candidate + - candidate -> stable + +jobs: + promote: + name: Promote Charm + runs-on: [self-hosted, linux] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set target channel + env: + PROMOTE_FROM: ${{ github.event.inputs.promotion }} + run: | + if [ "${PROMOTE_FROM}" == "edge -> beta" ]; then + echo "promote-from=edge" >> ${GITHUB_ENV} + echo "promote-to=beta" >> ${GITHUB_ENV} + elif [ "${PROMOTE_FROM}" == "beta -> candidate" ]; then + echo "promote-from=beta" >> ${GITHUB_ENV} + echo "promote-to=candidate" >> ${GITHUB_ENV} + elif [ "${PROMOTE_FROM}" == "candidate -> stable" ]; then + echo "promote-from=candidate" >> ${GITHUB_ENV} + echo "promote-to=stable" >> ${GITHUB_ENV} + fi + - name: Promote Charm + uses: canonical/charming-actions/release-charm@2.2.0 + with: + base-channel: 22.04 + credentials: ${{ secrets.CHARMHUB_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} + destination-channel: latest/${{ env.promote-to }} + origin-channel: latest/${{ env.promote-from }} + charmcraft-channel: latest/stable diff --git a/.github/workflows/public_backend_image.yml b/.github/workflows/public_backend_image.yml index 0e74e6430..5f1653a9c 100644 --- a/.github/workflows/public_backend_image.yml +++ b/.github/workflows/public_backend_image.yml @@ -9,7 +9,7 @@ env: jobs: build-and-push-backend-image: - runs-on: ubuntu-latest + runs-on: [self-hosted, linux] permissions: contents: read packages: write diff --git a/.github/workflows/publish_frontend_image.yml b/.github/workflows/publish_frontend_image.yml index 3b901b821..982edb457 100644 --- a/.github/workflows/publish_frontend_image.yml +++ b/.github/workflows/publish_frontend_image.yml @@ -9,7 +9,7 @@ env: jobs: build-and-push-frontend-image: - runs-on: ubuntu-latest + runs-on: [self-hosted, linux] permissions: contents: read packages: write diff --git a/.github/workflows/release_charms.yml b/.github/workflows/release_charms.yml index b344a8106..148b8b753 100644 --- a/.github/workflows/release_charms.yml +++ b/.github/workflows/release_charms.yml @@ -9,7 +9,7 @@ on: jobs: build: name: Release test-observer-api charm to edge - runs-on: ubuntu-latest + runs-on: [self-hosted, linux] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/.github/workflows/test_backend.yml b/.github/workflows/test_backend.yml index 223752b3b..a2718155a 100644 --- a/.github/workflows/test_backend.yml +++ b/.github/workflows/test_backend.yml @@ -6,7 +6,7 @@ concurrency: cancel-in-progress: true jobs: test: - runs-on: ubuntu-latest + runs-on: [self-hosted, linux, large] defaults: run: working-directory: backend diff --git a/.github/workflows/test_frontend.yml b/.github/workflows/test_frontend.yml index 5a99a692b..c2f76f534 100644 --- a/.github/workflows/test_frontend.yml +++ b/.github/workflows/test_frontend.yml @@ -6,7 +6,7 @@ concurrency: cancel-in-progress: true jobs: test: - runs-on: ubuntu-latest + runs-on: [self-hosted, linux, large] defaults: run: working-directory: frontend diff --git a/frontend/charm/metadata.yaml b/frontend/charm/metadata.yaml index 767c3d4c3..3a4857d29 100644 --- a/frontend/charm/metadata.yaml +++ b/frontend/charm/metadata.yaml @@ -21,4 +21,4 @@ resources: frontend-image: type: oci-image description: OCI image for test-observer-frontend - upstream-source: ghcr.io/canonical/test_observer/frontend:v0.0.10 + upstream-source: ghcr.io/canonical/test_observer/frontend:v0.0.10 \ No newline at end of file diff --git a/frontend/charm/src/charm.py b/frontend/charm/src/charm.py index f85192eaf..f9f997677 100755 --- a/frontend/charm/src/charm.py +++ b/frontend/charm/src/charm.py @@ -15,7 +15,6 @@ IngressPerAppRequirer, IngressPerAppRevokedEvent, ) -from ops.framework import StoredState from ops.model import ( ActiveStatus, BlockedStatus, @@ -30,8 +29,6 @@ class TestObserverFrontendCharm(ops.CharmBase): """The frontend charm operates serving the frontend through nginx.""" - _stored = StoredState() - def __init__(self, *args): super().__init__(*args) self.pebble_service_name = "test-observer-frontend" @@ -56,8 +53,6 @@ def __init__(self, *args): self.framework.observe(self.ingress.on.ready, self._on_ingress_ready) self.framework.observe(self.ingress.on.revoked, self._on_ingress_revoked) - self._stored.set_default(backend_hostname=None, backend_port=None) - def _on_frontend_pebble_ready(self, event: ops.PebbleReadyEvent): container = event.workload container.add_layer("frontend", self._pebble_layer, combine=True) @@ -98,20 +93,12 @@ def _config_is_valid(self, config) -> Tuple[bool, str]: def _on_rest_api_relation_update(self, event): api_hostname = event.relation.data[event.app].get("hostname") api_port = event.relation.data[event.app].get("port") - logger.debug(f"API hostname: {api_hostname} (app: {event.app})") - - if self.unit.is_leader(): - self._stored.backend_hostname = api_hostname - self._stored.backend_port = api_port - + logger.debug(f"API hostname: {api_hostname}, port: {api_port} (app: {event.app})") self._update_layer_and_restart(event) def _on_rest_api_relation_broken(self, event): - logger.debug("REST API relation broken -> removing backend hostname") - - if self.unit.is_leader(): - self._stored.backend_hostname = None - self._stored.backend_port = None + logger.debug("REST API relation broken") + self._update_layer_and_restart(event) def nginx_config(self, base_uri: str) -> str: """Return a config where the backend port `base_uri` is adjusted.""" @@ -136,30 +123,91 @@ def nginx_config(self, base_uri: str) -> str: }} """ + def nginx_503_config(self) -> str: + """Return a config for the situation when the backend is not yet available.""" + return """ + server { + listen 80 default_server; + server_name _; + return 503; + error_page 503 @maintenance; + + location @maintenance { + rewrite ^(.*)$ /503.html break; + root /usr/share/nginx/html; + } + } + """ + + def html_503(self) -> str: + """Return a 503 response page.""" + return """ + + + 503 Service Unavailable + + +

503 Service Unavailable

+

Backend not yet configured.

+ + + """ + def _update_layer_and_restart(self, event): self.unit.status = MaintenanceStatus(f"Updating {self.pebble_service_name} layer") + api_relation = self.model.relations["test-observer-rest-api"][0] + + if api_relation is None: + if self.container.can_connect(): + self.container.add_layer( + self.pebble_service_name, self._pebble_layer, combine=True + ) + self.container.push( + "/etc/nginx/sites-available/test-observer-frontend", + self.nginx_503_config(), + make_dirs=True, + ) + self.container.push( + "/usr/share/nginx/html/503.html", + self.html_503(), + make_dirs=True, + ) + self.container.restart(self.pebble_service_name) + self.unit.status = MaintenanceStatus( + "test-observer-rest-api relation not connected." + ) + else: + self.unit.status = WaitingStatus( + "Waiting for Pebble for API to set maintenance state" + ) + + return + + hostname = api_relation.data[api_relation.app]["hostname"] + port = api_relation.data[api_relation.app]["port"] + scheme = self.config["test-observer-api-scheme"] - hostname = self._stored.backend_hostname - port = self._stored.backend_port + + logger.info(f"Hostname: {hostname} (from relation: {hostname})") + logger.info(f"Port: {port} (from relation: {port})") if int(port) == 80 or int(port) == 443: base_uri = f"{scheme}{hostname}" else: base_uri = f"{scheme}{hostname}:{port}" - self.container.push( - "/etc/nginx/sites-available/test-observer-frontend", - self.nginx_config(base_uri=base_uri), - make_dirs=True, - ) - if self.container.can_connect(): self.container.add_layer(self.pebble_service_name, self._pebble_layer, combine=True) + self.container.push( + "/etc/nginx/sites-available/test-observer-frontend", + self.nginx_config(base_uri=base_uri), + make_dirs=True, + ) self.container.restart(self.pebble_service_name) self.unit.status = ActiveStatus() else: - self.unit.status = WaitingStatus("Waiting for Pebble for API") + self.unit.status = WaitingStatus("Waiting for Pebble for API to set available state") @property def _pebble_layer(self): diff --git a/frontend/charm/tests/integration/test_charm.py b/frontend/charm/tests/integration/test_charm.py index 10742f61b..77cc33f3a 100644 --- a/frontend/charm/tests/integration/test_charm.py +++ b/frontend/charm/tests/integration/test_charm.py @@ -17,8 +17,8 @@ @pytest.mark.abort_on_fail -async def test_build_and_deploy(ops_test: OpsTest): - """Build the charm-under-test and deploy it together with related charms. +async def test_build_and_deploy_without_backend(ops_test: OpsTest): + """Build the charm-under-test and deploy it together without related charms. Assert on the unit status before any relations/configurations take place. """ @@ -29,8 +29,7 @@ async def test_build_and_deploy(ops_test: OpsTest): ops_test.model.deploy(charm, resources=resources, application_name=APP_NAME), ops_test.model.wait_for_idle( apps=[APP_NAME], - status="active", - raise_on_blocked=True, - timeout=1000, + status="maintenance", + timeout=2000, ), ) diff --git a/frontend/charm/tox.ini b/frontend/charm/tox.ini index fb9bfa4f4..d205153ad 100644 --- a/frontend/charm/tox.ini +++ b/frontend/charm/tox.ini @@ -66,7 +66,7 @@ commands = description = Run integration tests deps = pytest - juju + juju<3.1 pytest-operator -r {tox_root}/requirements.txt commands =