diff --git a/.github/workflows/auto-update-libs-k8s-worker.yaml b/.github/workflows/auto-update-libs-k8s-worker.yaml index 1d186001..79dd44fa 100644 --- a/.github/workflows/auto-update-libs-k8s-worker.yaml +++ b/.github/workflows/auto-update-libs-k8s-worker.yaml @@ -1,4 +1,4 @@ -name: Auto-update K8s-worker charm libraries +name: Auto-update K8s charm libraries on: schedule: @@ -9,4 +9,4 @@ jobs: uses: canonical/operator-workflows/.github/workflows/auto_update_charm_libs.yaml@main secrets: inherit with: - working-directory: ./charms/k8s-worker/ \ No newline at end of file + working-directory: ./charms/worker/k8s diff --git a/.github/workflows/auto-update-libs-k8s.yaml b/.github/workflows/auto-update-libs-k8s.yaml deleted file mode 100644 index fabc1dd3..00000000 --- a/.github/workflows/auto-update-libs-k8s.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: Auto-update K8s charm libraries - -on: - schedule: - - cron: "0 1 * * *" - -jobs: - auto-update-libs: - uses: canonical/operator-workflows/.github/workflows/auto_update_charm_libs.yaml@main - secrets: inherit - with: - working-directory: ./charms/k8s/ \ No newline at end of file diff --git a/.github/workflows/build-charm.yaml b/.github/workflows/build-charm.yaml index 5498010d..bdbe77da 100644 --- a/.github/workflows/build-charm.yaml +++ b/.github/workflows/build-charm.yaml @@ -24,7 +24,7 @@ jobs: - uses: canonical/setup-lxd@v0.1.1 - name: Extract charm name working-directory: ${{ inputs.working-directory }} - run: echo "CHARM_NAME=$([ -f metadata.yaml ] && yq '.name' metadata.yaml || echo UNKNOWN)" >> $GITHUB_ENV + run: echo "CHARM_NAME=$([ -f charmcraft.yaml ] && yq '.name' charmcraft.yaml || echo UNKNOWN)" >> $GITHUB_ENV - name: Pack charm if: ${{ env.CHARM_NAME != 'UNKNOWN' && !cancelled() }} working-directory: ${{ inputs.working-directory }}/${{ matrix.path }} @@ -34,7 +34,7 @@ jobs: echo "CHARM_FILE=$(ls ${{env.CHARM_NAME}}_*.charm)" >> $GITHUB_ENV - name: Upload charm artifact if: ${{ env.CHARM_FILE != '' && !cancelled() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.CHARM_NAME }}-charm path: ${{ inputs.working-directory }}/${{ env.CHARM_FILE }} diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 70aba32d..5b2261ea 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -8,8 +8,8 @@ jobs: strategy: matrix: path: - - "./charms/k8s/" - - "./charms/k8s-worker/" + - "./charms/worker/k8s/" + - "./charms/worker/" uses: ./.github/workflows/build-charm.yaml with: working-directory: ${{ matrix.path }} diff --git a/.github/workflows/publish-k8s-worker.yaml b/.github/workflows/publish-k8s-worker.yaml index e13f5e8e..24b98fba 100644 --- a/.github/workflows/publish-k8s-worker.yaml +++ b/.github/workflows/publish-k8s-worker.yaml @@ -12,4 +12,5 @@ jobs: secrets: inherit with: channel: latest/edge - working-directory: ./charms/k8s-worker/ \ No newline at end of file + working-directory: ./charms/worker/k8s/ + tag-prefix: k8s-worker \ No newline at end of file diff --git a/.github/workflows/publish-k8s.yaml b/.github/workflows/publish-k8s.yaml index 72928d36..c87869ab 100644 --- a/.github/workflows/publish-k8s.yaml +++ b/.github/workflows/publish-k8s.yaml @@ -12,4 +12,5 @@ jobs: secrets: inherit with: channel: latest/edge - working-directory: ./charms/k8s/ \ No newline at end of file + working-directory: ./charms/k8s/ + tag-prefix: k8s \ No newline at end of file diff --git a/.github/workflows/tests-k8s-worker.yaml b/.github/workflows/tests-k8s-worker.yaml deleted file mode 100644 index 138be18c..00000000 --- a/.github/workflows/tests-k8s-worker.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: Tests K8s Worker - -on: - pull_request: - -jobs: - unit-tests: - uses: canonical/operator-workflows/.github/workflows/test.yaml@main - secrets: inherit - with: - self-hosted-runner: false - working-directory: ./charms/k8s-worker/ \ No newline at end of file diff --git a/.github/workflows/tests-k8s.yaml b/.github/workflows/tests-k8s.yaml index 7793577f..2555b038 100644 --- a/.github/workflows/tests-k8s.yaml +++ b/.github/workflows/tests-k8s.yaml @@ -9,4 +9,4 @@ jobs: secrets: inherit with: self-hosted-runner: false - working-directory: ./charms/k8s/ \ No newline at end of file + working-directory: ./charms/worker/k8s/ \ No newline at end of file diff --git a/charms/CONTRIBUTION.md b/charms/CONTRIBUTION.md new file mode 100644 index 00000000..8b571d08 --- /dev/null +++ b/charms/CONTRIBUTION.md @@ -0,0 +1,69 @@ +# Contributing + +## Structure of the charms + +The `k8s` and `k8s-worker` charms are noticeably tucked into one-another. + +``` +└── worker + ├── charmcraft.yaml + ├── requirements.txt + └── k8s + ├── charmcraft.yaml + ├── lib + │ └── charms/... + ├── requirements.txt + └── src + └── charm.py +``` + +While unfamiliar to some charm developers, this lets both charms share the exact same `src` folder. This is accomplished by using the `parts.charm.charm-entrypoint` value in the `worker` directory set to `k8s/src/charm.py`. + +### What's unique + +The unique parts of the charm are what are in each charm's top-level directory: + +``` +charmcraft.yaml +config.yaml +actions.yaml +metadata.yaml +requirements.yaml +``` + +In order to exclude the `k8s` exclusive components from the `k8s-worker` charm, charmcraft will read the `worker/.jujuignore` file to determine what to leave out of the final charm. + +### What's not + +The shared portions of each charm are within `worker/k8s` (except for the above mentioned exclusions). This includes shared libraries from `worker/k8s/lib`, shared source from `worker/k8s/src`, shared python dependencies from `worker/k8s/requirements.txt` + +### How to distinguish which charm code should engage + +The charm can distinguish whether it's a `control-plane` or `worker` unit by using `self.is_worker` or `self.is_control_plane` by querying its metadata. + +### Why two charms? + +Much of the charm's behavior will be identical. They will employ many of the same relations, many of the same resources, configure the same snap, and use many of the same configuration options. One might therefore assume the two should be 1 charm. History with Charmed Kubernetes has proven that having 2 charms split between control-plane and worker has advantages when a relation is split across `requires` and `provides`. + +### Why not use a charm library? + +Sharing code between a charm library is a really reasonable idea, there are limitations that a charm library presents: +* limited to a single file +* PRs where the library changes doesn't reflect in the secondary charm +* updating a second charm isn't immediate + - must upload to charmhub, then download into the secondary charms + +### How to use two charms in the same code base: + +In cases where the charms should diverge the behavior, use a runtime switch to make the decision + +```python +if self.is_control_plane: + # do control-plane only thing + ... +# do more common things +... +if self.is_worker: + # do worker only thing + ... +``` diff --git a/charms/k8s-worker/charmcraft.yaml b/charms/k8s-worker/charmcraft.yaml deleted file mode 100644 index df6fdcb5..00000000 --- a/charms/k8s-worker/charmcraft.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# 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" - run-on: - - name: ubuntu - channel: "22.04" diff --git a/charms/k8s-worker/metadata.yaml b/charms/k8s-worker/metadata.yaml deleted file mode 100644 index 054b8b58..00000000 --- a/charms/k8s-worker/metadata.yaml +++ /dev/null @@ -1,27 +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. - -name: k8s-worker -display-name: Kubernetes Worker -summary: A machine charm for a K8s Worker -docs: https://discourse.charmhub.io -issues: https://github.com/canonical/k8s-operator/issues -maintainers: - - https://launchpad.net/~containers -source: https://github.com/canonical/k8s-operator - -assumes: - - juju >= 3.1 - -description: | - A machine charm which operates a Kubernetes worker. - - This charm installs and operates a Kubernetes worker via the k8s snap. It exposes - relations to co-operate with other kubernetes components - - This charm provides the following running components: - * kube-proxy - * kubelet - * containerd diff --git a/charms/k8s-worker/requirements.txt b/charms/k8s-worker/requirements.txt deleted file mode 100644 index aaa16b15..00000000 --- a/charms/k8s-worker/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -ops >= 2.2.0 diff --git a/charms/k8s-worker/src/charm.py b/charms/k8s-worker/src/charm.py deleted file mode 100755 index 5fb37216..00000000 --- a/charms/k8s-worker/src/charm.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2024 Canonical Ltd. -# See LICENSE file for licensing details. - -# Learn more at: https://juju.is/docs/sdk - -"""K8s-worker Charm. - -A machine charm which operates a Kubernetes worker. - -This charm installs and operates a Kubernetes worker via the k8s snap. It exposes -relations to co-operate with other kubernetes components. -""" - - -import logging - -import ops - -# Log messages can be retrieved using juju debug-log -logger = logging.getLogger(__name__) - -VALID_LOG_LEVELS = ["info", "debug", "warning", "error", "critical"] - - -class K8sWorkerCharm(ops.CharmBase): - """Charm the service.""" - - def __init__(self, *args): - """Construct. - - Args: - args: Arguments passed to the CharmBase parent constructor. - """ - super().__init__(*args) - self.framework.observe(self.on.update_status, self._on_update_status) - - def _on_update_status(self, _event: ops.UpdateStatusEvent): - """Handle update-status event. - - Args: - _event: event triggering the handler. - """ - self.unit.status = ops.ActiveStatus("Ready") - - -if __name__ == "__main__": # pragma: nocover - ops.main.main(K8sWorkerCharm) diff --git a/charms/k8s-worker/tests/unit/test_base.py b/charms/k8s-worker/tests/unit/test_base.py deleted file mode 100644 index 4d5b2bb8..00000000 --- a/charms/k8s-worker/tests/unit/test_base.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2024 Canonical Ltd. -# See LICENSE file for licensing details. - -# Learn more about testing at: https://juju.is/docs/sdk/testing - -# pylint: disable=duplicate-code,missing-function-docstring -"""Unit tests.""" - - -import ops -import ops.testing -import pytest - -from charm import K8sWorkerCharm - - -@pytest.fixture() -def harness(): - harness = ops.testing.Harness(K8sWorkerCharm) - harness.begin() - yield harness - harness.cleanup() - - -def test_config_changed_invalid(harness): - # Trigger a config-changed event with an unknown-config option - with pytest.raises(ValueError): - harness.update_config({"unknown-config": "foobar"}) - - -def test_update_status(harness): - harness.charm.on.update_status.emit() - assert harness.model.unit.status == ops.ActiveStatus("Ready") diff --git a/charms/k8s-worker/tox.ini b/charms/k8s-worker/tox.ini deleted file mode 100644 index a892e808..00000000 --- a/charms/k8s-worker/tox.ini +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright 2024 Canonical Ltd. -# See LICENSE file for licensing details. - -[tox] -skipsdist=True -skip_missing_interpreters = True -envlist = lint, unit, static, coverage-report - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -setenv = - PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} - PYTHONBREAKPOINT=ipdb.set_trace - PY_COLORS=1 -passenv = - PYTHONPATH - CHARM_BUILD_DIR - MODEL_SETTINGS - -[testenv:format] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} - black {[vars]all_path} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - codespell - flake8<6.0.0 - flake8-builtins - flake8-copyright<6.0.0 - flake8-docstrings>=1.6.0 - flake8-docstrings-complete>=1.0.3 - flake8-test-docs>=1.0 - mypy - pep8-naming - pydocstyle>=2.10 - pylint - pyproject-flake8<6.0.0 - pytest - pytest-asyncio - pytest-operator - requests - types-PyYAML - types-requests - -r{toxinidir}/requirements.txt -commands = - pydocstyle {[vars]src_path} - codespell {toxinidir} --skip {toxinidir}/.git --skip {toxinidir}/.tox \ - --skip {toxinidir}/build --skip {toxinidir}/lib --skip {toxinidir}/venv \ - --skip {toxinidir}/.mypy_cache --skip {toxinidir}/icon.svg - # pflake8 wrapper supports config from pyproject.toml - pflake8 {[vars]all_path} --ignore=W503 - isort --check-only --diff {[vars]all_path} - black --check --diff {[vars]all_path} - mypy {[vars]all_path} - pylint {[vars]all_path} - -[testenv:unit] -description = Run unit tests -deps = - coverage[toml] - pytest - -r{toxinidir}/requirements.txt -commands = - coverage run --source={[vars]src_path} \ - -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs} - coverage report - -[testenv:coverage-report] -description = Create test coverage report -deps = - coverage[toml] - pytest - -r{toxinidir}/requirements.txt -commands = - coverage report - -[testenv:static] -description = Run static analysis tests -deps = - bandit[toml] - -r{toxinidir}/requirements.txt -commands = - bandit -c {toxinidir}/pyproject.toml -r {[vars]src_path} {[vars]tst_path} - -[testenv:integration] -description = Run integration tests -deps = - juju - pytest - pytest-asyncio - pytest-operator - -r{toxinidir}/requirements.txt -commands = - pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} - -[testenv:src-docs] -allowlist_externals=sh -setenv = - PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} -description = Generate documentation for src -deps = - lazydocs - -r{toxinidir}/requirements.txt -commands = - ; can't run lazydocs directly due to needing to run it on src/* which produces an invocation error in tox - sh generate-src-docs.sh diff --git a/charms/k8s/charmcraft.yaml b/charms/k8s/charmcraft.yaml deleted file mode 100644 index c53dd3bf..00000000 --- a/charms/k8s/charmcraft.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# 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" - run-on: - - name: ubuntu - channel: "22.04" -config: - options: - channel: - default: edge - type: string - description: | - Snap channel to install k8s snap from -parts: - charm: - build-packages: [git] diff --git a/charms/k8s/metadata.yaml b/charms/k8s/metadata.yaml deleted file mode 100644 index 2e8d8390..00000000 --- a/charms/k8s/metadata.yaml +++ /dev/null @@ -1,40 +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. - -name: k8s -display-name: Kubernetes -summary: A machine charm for K8s -docs: https://discourse.charmhub.io -issues: https://github.com/canonical/k8s-operator/issues -maintainers: - - https://launchpad.net/~containers -source: https://github.com/canonical/k8s-operator - -assumes: - - juju >= 3.1 - -description: | - A machine charm which operates a complete Kubernetes cluster. - - This charm installs and operates a Kubernetes cluster via the k8s snap. It exposes - relations to co-operate with other kubernetes components such as optional CNIs, - optional cloud-providers, optional schedulers, external backing stores, and external - certificate storage. - - This charm provides the following running components: - * kube-apiserver - * kube-scheduler - * kube-controller-manager - * kube-proxy - * kubelet - * containerd - - This charm can optionally disable the following components: - * A Kubernetes Backing Store - * A Kubernetes CNI - -peers: - cluster: - interface: cluster diff --git a/charms/k8s/tests/unit/__init__.py b/charms/k8s/tests/unit/__init__.py deleted file mode 100644 index e3979c0f..00000000 --- a/charms/k8s/tests/unit/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright 2024 Canonical Ltd. -# See LICENSE file for licensing details. diff --git a/charms/worker/.jujuignore b/charms/worker/.jujuignore new file mode 100644 index 00000000..e8302501 --- /dev/null +++ b/charms/worker/.jujuignore @@ -0,0 +1,11 @@ +k8s/venv +k8s/tox.ini +k8s/tests/ +k8s/charmcraft.yaml +k8s/actions.yaml +k8s/config.yaml +k8s/.coverage +k8s/.tox +k8s/.mypy_cache +k8s/lxd-profile.yaml +__pycache__/ \ No newline at end of file diff --git a/charms/worker/README.md b/charms/worker/README.md new file mode 100644 index 00000000..e76944bc --- /dev/null +++ b/charms/worker/README.md @@ -0,0 +1,19 @@ +# K8s Worker Charm + +This defines a charm named `k8s-worker`, an application which deploys the `k8s` snap providing an opinioned distribution of Canonical Kubernetes within juju which is extensible, manage-able, and observable via the juju eco-system. The units deployed by this charm operate multiple kubernetes binaries maintained by Canonical (`kubelet`, `kube-proxy`, `kube-scheduler`, `kube-controller-manager`, and `kube-apiserver`) + +## Deploy +Deploy the charm from the charmhub store with a single command to have a single node cluster. + +```sh +juju deploy k8s +juju deploy k8s-worker +juju relate k8s k8s-worker:cluster +``` + +## Adding Units +Adding more units to the kubernetes cluster is accomplished by adding more units. + +```sh +juju add-unit k8s-worker +``` diff --git a/charms/worker/charmcraft.yaml b/charms/worker/charmcraft.yaml new file mode 100644 index 00000000..aecd6372 --- /dev/null +++ b/charms/worker/charmcraft.yaml @@ -0,0 +1,69 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +# This file configures Charmcraft. +# See https://juju.is/docs/sdk/charmcraft-config for guidance. + +name: k8s-worker +title: Kubernetes Worker +summary: A machine charm for a K8s Worker +description: | + A machine charm which operates a Kubernetes worker. + + This charm installs and operates a Kubernetes worker via the k8s snap. It exposes + relations to co-operate with other kubernetes components + + This charm provides the following running components: + * kube-proxy + * kubelet + * containerd +links: + contact: https://launchpad.net/~containers + documentation: https://discourse.charmhub.io + issues: + - https://github.com/canonical/k8s-operator/issues + source: + - https://github.com/canonical/k8s-operator + +assumes: + - juju >= 3.1 + +type: charm +bases: + - build-on: + - name: ubuntu + channel: "20.04" + architectures: [amd64] + run-on: + - name: ubuntu + channel: "20.04" + architectures: [amd64] + - name: ubuntu + channel: "22.04" + architectures: [amd64] +config: + options: + channel: + default: edge + type: string + description: Snap channel of the k8s snap +parts: + charm: + build-packages: [git] + charm-entrypoint: k8s/src/charm.py + lib: + # move the ./k8s/lib path to ./lib since + # charmcraft assumes it to be there once the charm runs + after: [charm] + plugin: nil + source: ./ + override-prime: | + rm -rf $CRAFT_PRIME/lib + mv $CRAFT_PRIME/k8s/lib $CRAFT_PRIME/lib + +requires: + cluster: + interface: k8s-cluster + # interface to connect with the k8s charm to provide + # authentication token via a secret in order to cluster + # this machine as a worker unit. + # juju integrate k8s:k8s-cluster k8s-worker:cluster \ No newline at end of file diff --git a/charms/worker/k8s/.jujuignore b/charms/worker/k8s/.jujuignore new file mode 100644 index 00000000..4b478d61 --- /dev/null +++ b/charms/worker/k8s/.jujuignore @@ -0,0 +1,8 @@ +venv +tox.ini +tests/ +pyproject.toml +.coverage +.tox +.mypy_cache +__pycache__/ \ No newline at end of file diff --git a/charms/worker/k8s/README.md b/charms/worker/k8s/README.md new file mode 100644 index 00000000..4d60a8a6 --- /dev/null +++ b/charms/worker/k8s/README.md @@ -0,0 +1,20 @@ +# K8s Charm + +This defines a charm named `k8s`, an application which deploys the `k8s` snap providing an opinioned distribution of Canonical Kubernetes within juju which is extensible, manage-able, and observable via the juju eco-system. The units deployed by this charm operate multiple kubernetes binaries maintained by Canonical (`kubelet`, `kube-proxy`, `kube-scheduler`, `kube-controller-manager`, and `kube-apiserver`) + +## Deploy +Deploy the charm from the charmhub store with a single command to have a single node cluster. + +```sh +juju deploy k8s +``` + +## Adding Units +Adding more units to the kubernetes cluster is accomplished by adding more units. + +```sh +juju add-unit k8s +``` + +## Configuring +The charm's `charmcraft.yaml` defines options for every node in the kubernetes cluster exp \ No newline at end of file diff --git a/charms/worker/k8s/charmcraft.yaml b/charms/worker/k8s/charmcraft.yaml new file mode 100644 index 00000000..27b9ebff --- /dev/null +++ b/charms/worker/k8s/charmcraft.yaml @@ -0,0 +1,68 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +# This file configures Charmcraft. +# See https://juju.is/docs/sdk/charmcraft-config for guidance. + +name: k8s +title: Kubernetes +summary: A machine charm for K8s +description: |- + A machine charm which operates a complete Kubernetes cluster. + + This charm installs and operates a Kubernetes cluster via the k8s snap. It exposes + relations to co-operate with other kubernetes components such as optional CNIs, + optional cloud-providers, optional schedulers, external backing stores, and external + certificate storage. + + This charm provides the following running components: + * kube-apiserver + * kube-scheduler + * kube-controller-manager + * kube-proxy + * kubelet + * containerd + + This charm can optionally disable the following components: + * A Kubernetes Backing Store + * A Kubernetes CNI +links: + contact: https://launchpad.net/~containers + documentation: https://discourse.charmhub.io + issues: + - https://github.com/canonical/k8s-operator/issues + source: + - https://github.com/canonical/k8s-operator + +assumes: + - juju >= 3.1 + +type: charm +bases: + - build-on: + - name: ubuntu + channel: "20.04" + architectures: [amd64] + run-on: + - name: ubuntu + channel: "20.04" + architectures: [amd64] + - name: ubuntu + channel: "22.04" + architectures: [amd64] +config: + options: + channel: + default: edge + type: string + description: Snap channel of the k8s snap +parts: + charm: + build-packages: [git] + +peers: + cluster: + interface: cluster + +provides: + k8s-cluster: + interface: k8s-cluster \ No newline at end of file diff --git a/charms/k8s/lib/charms/k8s/v0/k8sd_api_manager.py b/charms/worker/k8s/lib/charms/k8s/v0/k8sd_api_manager.py similarity index 100% rename from charms/k8s/lib/charms/k8s/v0/k8sd_api_manager.py rename to charms/worker/k8s/lib/charms/k8s/v0/k8sd_api_manager.py diff --git a/charms/k8s/lib/charms/operator_libs_linux/v2/snap.py b/charms/worker/k8s/lib/charms/operator_libs_linux/v2/snap.py similarity index 100% rename from charms/k8s/lib/charms/operator_libs_linux/v2/snap.py rename to charms/worker/k8s/lib/charms/operator_libs_linux/v2/snap.py diff --git a/charms/k8s/lxd-profile.yaml b/charms/worker/k8s/lxd-profile.yaml similarity index 100% rename from charms/k8s/lxd-profile.yaml rename to charms/worker/k8s/lxd-profile.yaml diff --git a/charms/k8s/pyproject.toml b/charms/worker/k8s/pyproject.toml similarity index 98% rename from charms/k8s/pyproject.toml rename to charms/worker/k8s/pyproject.toml index 7765baf7..fa352edc 100644 --- a/charms/k8s/pyproject.toml +++ b/charms/worker/k8s/pyproject.toml @@ -79,4 +79,4 @@ max-complexity = 10 [tool.codespell] skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" [tool.pyright] -extraPaths = ["./lib"] +extraPaths = ["./lib"] \ No newline at end of file diff --git a/charms/k8s/requirements.txt b/charms/worker/k8s/requirements.txt similarity index 100% rename from charms/k8s/requirements.txt rename to charms/worker/k8s/requirements.txt diff --git a/charms/k8s/src/charm.py b/charms/worker/k8s/src/charm.py similarity index 83% rename from charms/k8s/src/charm.py rename to charms/worker/k8s/src/charm.py index ea5740eb..8b39c11e 100755 --- a/charms/k8s/src/charm.py +++ b/charms/worker/k8s/src/charm.py @@ -41,7 +41,12 @@ class K8sCharm(ops.CharmBase): - """A charm for managing a K8s cluster via the k8s snap.""" + """A charm for managing a K8s cluster via the k8s snap. + + Attrs: + is_worker: true if this is a worker charm unit + is_control_plane: true if this is a control-plane charm unit + """ def __init__(self, *args): """Initialise the K8s charm. @@ -57,8 +62,14 @@ def __init__(self, *args): self.reconciler = Reconciler(self, self._reconcile) + self.is_worker = self.meta.name == "k8s-worker" self.framework.observe(self.on.update_status, self._on_update_status) + @property + def is_control_plane(self) -> bool: + """Returns true if the unit is not a worker.""" + return not self.is_worker + @on_error(WaitingStatus("Failed to apply snap requirements"), subprocess.CalledProcessError) def _apply_snap_requirements(self): """Apply necessary snap requirements for the k8s snap. @@ -99,15 +110,13 @@ def _bootstrap_k8s_snap(self): # TODO: Make port (and address) configurable. self.api_manager.bootstrap_k8s_snap(name, f"{str(address)}:6400") - def _create_cluster_tokens(self): - """Create tokens for the units in the peer cluster relation.""" - if not self.unit.is_leader(): - return - - relation = self.model.get_relation("cluster") - if not relation: - return + def _distribute_cluster_tokens(self, relation, _role): + """Distribute role based tokens as secrets on a relation. + Args: + relation: The relation for which to create tokens + _role: "worker" or "control-plane" role + """ units = {u for u in relation.units if u.name != self.unit.name} app_databag = relation.data.get(self.model.app, {}) @@ -122,6 +131,16 @@ def _create_cluster_tokens(self): secret.grant(relation, unit=unit) relation.data[self.app][unit.name] = secret.id or "" + def _create_cluster_tokens(self): + """Create tokens for the units in the cluster and k8s-cluster relations.""" + if not self.unit.is_leader() or not self.is_control_plane: + return + + if peer := self.model.get_relation("cluster"): + self._distribute_cluster_tokens(peer, "control-plane") + + # TODO handle requesting cluster tokens for workers + @on_error( WaitingStatus("Waiting for enable components"), InvalidResponseError, K8sdConnectionError ) @@ -159,7 +178,7 @@ def _install_k8s_snap(self): channel = self.config["channel"] k8s_snap.ensure(SnapState.Latest, channel=channel) - @on_error(WaitingStatus("Waiting for Cluster token"), TypeError) + @on_error(WaitingStatus("Waiting for Cluster token"), TypeError, K8sdConnectionError) def _join_cluster(self): """Retrieve the join token from secret databag and join the cluster.""" if self.api_manager.is_cluster_bootstrapped(): @@ -180,7 +199,7 @@ def _reconcile(self, _): """Reconcile state change events.""" self._install_k8s_snap() self._apply_snap_requirements() - if self.unit.is_leader(): + if self.unit.is_leader() and self.is_control_plane: self._bootstrap_k8s_snap() self._enable_components() self._create_cluster_tokens() @@ -195,18 +214,25 @@ def _reconcile(self, _): ) def _update_status(self): """Check k8s snap status.""" + if self.is_worker: + # TODO: replace with code to confirm that the worker + # is part of the joined cluster + return if self.api_manager.is_cluster_ready(): if version := self._get_snap_version(): self.unit.set_workload_version(version) else: status.add(ops.WaitingStatus("Waiting for k8s to be ready.")) - def _on_update_status(self, _): + def _on_update_status(self, _event: ops.UpdateStatusEvent): """Handle update-status event.""" if not self.reconciler.stored.reconciled: return - with status.context(self.unit): - self._update_status() + try: + with status.context(self.unit): + self._update_status() + except status.ReconcilerError: + log.exception("Can't to update_status") if __name__ == "__main__": # pragma: nocover diff --git a/charms/k8s-worker/tests/unit/__init__.py b/charms/worker/k8s/tests/unit/__init__.py similarity index 100% rename from charms/k8s-worker/tests/unit/__init__.py rename to charms/worker/k8s/tests/unit/__init__.py diff --git a/charms/k8s/tests/unit/test_base.py b/charms/worker/k8s/tests/unit/test_base.py similarity index 61% rename from charms/k8s/tests/unit/test_base.py rename to charms/worker/k8s/tests/unit/test_base.py index df3dbffe..fcb11ca1 100644 --- a/charms/k8s/tests/unit/test_base.py +++ b/charms/worker/k8s/tests/unit/test_base.py @@ -14,10 +14,11 @@ from charm import K8sCharm -@pytest.fixture() -def harness(): +@pytest.fixture(params=["worker", "control-plane"]) +def harness(request): harness = ops.testing.Harness(K8sCharm) harness.begin() + harness.charm.is_worker = request.param == "worker" yield harness harness.cleanup() @@ -29,5 +30,9 @@ def test_config_changed_invalid(harness): def test_update_status(harness): + harness.charm.reconciler.stored.reconciled = True # Pretended to be reconciled harness.charm.on.update_status.emit() - assert harness.model.unit.status == ops.BlockedStatus("Failed to install k8s snap.") + if harness.charm.is_control_plane: + assert harness.model.unit.status == ops.WaitingStatus("Cluster not yet ready") + else: + assert harness.model.unit.status == ops.ActiveStatus("Ready") diff --git a/charms/k8s/tests/unit/test_k8sd_api_manager.py b/charms/worker/k8s/tests/unit/test_k8sd_api_manager.py similarity index 100% rename from charms/k8s/tests/unit/test_k8sd_api_manager.py rename to charms/worker/k8s/tests/unit/test_k8sd_api_manager.py diff --git a/charms/k8s/tox.ini b/charms/worker/k8s/tox.ini similarity index 100% rename from charms/k8s/tox.ini rename to charms/worker/k8s/tox.ini diff --git a/charms/worker/lxd-profile.yaml b/charms/worker/lxd-profile.yaml new file mode 100644 index 00000000..fcb130dd --- /dev/null +++ b/charms/worker/lxd-profile.yaml @@ -0,0 +1,30 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +description: "LXD profile for Canonical Kubernetes" +config: + boot.autostart: "true" + linux.kernel_modules: ip_vs,ip_vs_rr,ip_vs_wrr,ip_vs_sh,ip_tables,ip6_tables,netlink_diag,nf_nat,overlay,br_netfilter + raw.lxc: | + lxc.apparmor.profile=unconfined + lxc.mount.auto=proc:rw sys:rw cgroup:rw + lxc.cgroup.devices.allow=a + lxc.cap.drop= + security.nesting: "true" + security.privileged: "true" +devices: + aadisable: + path: /sys/module/nf_conntrack/parameters/hashsize + source: /sys/module/nf_conntrack/parameters/hashsize + type: disk + aadisable2: + path: /dev/kmsg + source: /dev/kmsg + type: unix-char + aadisable3: + path: /sys/fs/bpf + source: /sys/fs/bpf + type: disk + aadisable4: + path: /proc/sys/net/netfilter/nf_conntrack_max + source: /proc/sys/net/netfilter/nf_conntrack_max + type: disk diff --git a/charms/k8s-worker/pyproject.toml b/charms/worker/pyproject.toml similarity index 73% rename from charms/k8s-worker/pyproject.toml rename to charms/worker/pyproject.toml index ef4ea7de..fa352edc 100644 --- a/charms/k8s-worker/pyproject.toml +++ b/charms/worker/pyproject.toml @@ -1,5 +1,6 @@ [tool.bandit] -exclude_dirs = ["/venv/"] +exclude_dirs = ["/venv/", "tests"] +skips = ["B404","B603"] [tool.bandit.assert_used] skips = ["*/*test.py", "*/test_*.py", "*tests/*.py"] @@ -24,7 +25,9 @@ exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] select = ["E", "W", "F", "C", "N", "R", "D", "H"] # Ignore W503, E501 because using black creates errors with this # Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107"] +# Ignore N805 first argument should be named self. Pydantic validators do not comply. +ignore = ["W503", "E501", "D107", "N805"] + # D100, D101, D102, D103: Ignore missing docstrings in tests per-file-ignores = ["tests/*:D100,D101,D102,D103,D104,D205,D212,D415"] docstring-convention = "google" @@ -39,7 +42,11 @@ explicit_package_bases = true namespace_packages = true [tool.pylint] -disable = "wrong-import-order,redefined-outer-name" +# Ignore too-few-public-methods due to pydantic models +# Ignore no-self-argument due to pydantic validators +disable = "wrong-import-order,redefined-outer-name,too-few-public-methods,no-self-argument,fixme" +# Ignore Pydantic check: https://github.com/pydantic/pydantic/issues/1961 +extension-pkg-whitelist = "pydantic" # wokeignore:rule=whitelist [tool.pytest.ini_options] minversion = "6.0" @@ -71,3 +78,5 @@ max-complexity = 10 [tool.codespell] skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" +[tool.pyright] +extraPaths = ["./lib"] \ No newline at end of file diff --git a/charms/worker/requirements.txt b/charms/worker/requirements.txt new file mode 100644 index 00000000..fa105ff3 --- /dev/null +++ b/charms/worker/requirements.txt @@ -0,0 +1,2 @@ +-r k8s/requirements.txt + diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 9292ce3b..13909904 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -68,7 +68,7 @@ class Charm: @property def metadata(self) -> dict: """Charm Metadata.""" - return yaml.safe_load((self.path / "metadata.yaml").read_text()) + return yaml.safe_load((self.path / "charmcraft.yaml").read_text()) @property def app_name(self) -> str: @@ -122,8 +122,10 @@ async def resolve(self, charm_files: List[str]) -> Path: self._charmfile, *_ = filter(lambda s: s.name.startswith(header), potentials) log.info("For %s found charmfile %s", self.app_name, self._charmfile) except ValueError: - log.info("For %s build charmfile", self.app_name) - self._charmfile = await self.ops_test.build_charm(self.path) + log.warning("No pre-built charm is available, let's build it") + if self._charmfile is None: + log.info("For %s build charmfile", self.app_name) + self._charmfile = await self.ops_test.build_charm(self.path) if self._charmfile is None: raise FileNotFoundError(f"{self.app_name}_*.charm not found") return self._charmfile.resolve() @@ -180,8 +182,8 @@ async def deploy_model( async def kubernetes_cluster(request: pytest.FixtureRequest, ops_test: OpsTest): """Deploy local kubernetes charms.""" model = "main" - charm_names = ("k8s", "k8s-worker") - charms = [Charm(ops_test, Path("charms") / p) for p in charm_names] + charm_path = ("worker/k8s", "worker") + charms = [Charm(ops_test, Path("charms") / p) for p in charm_path] charm_files = await asyncio.gather( *[charm.resolve(request.config.option.charm_files) for charm in charms] ) diff --git a/tox.ini b/tox.ini index 71e280df..aa71d61e 100644 --- a/tox.ini +++ b/tox.ini @@ -28,8 +28,7 @@ deps = commands = isort {[vars]all_path} black {[vars]all_path} - tox -c {toxinidir}/charms/k8s -e format - tox -c {toxinidir}/charms/k8s-worker -e format + tox -c {toxinidir}/charms/worker/k8s -e format [testenv:lint] @@ -65,15 +64,13 @@ commands = black --check --diff {[vars]all_path} mypy {[vars]all_path} pylint {[vars]all_path} - tox -c {toxinidir}/charms/k8s -e lint - tox -c {toxinidir}/charms/k8s-worker -e lint + tox -c {toxinidir}/charms/worker/k8s -e lint [testenv:unit] allowlist_externals = tox commands = - tox -c {toxinidir}/charms/k8s -e unit - tox -c {toxinidir}/charms/k8s-worker -e unit + tox -c {toxinidir}/charms/worker/k8s -e unit [testenv:static] description = Run static analysis tests