diff --git a/.github/workflows/metallb-workflow.yaml b/.github/workflows/metallb-workflow.yaml deleted file mode 100644 index 11fd080..0000000 --- a/.github/workflows/metallb-workflow.yaml +++ /dev/null @@ -1,89 +0,0 @@ -name: Test Suite for MetalLB - -on: - - pull_request - -jobs: - call-inclusive-naming-check: - name: Inclusive naming - uses: canonical-web-and-design/Inclusive-naming/.github/workflows/woke.yaml@main - with: - fail-on-error: "true" - - lint-unit: - name: Lint Unit - uses: charmed-kubernetes/workflows/.github/workflows/lint-unit.yaml@main - needs: - - call-inclusive-naming-check - with: - python: "['3.8', '3.9', '3.10']" - - integration-test-metallb: - runs-on: ubuntu-latest - name: Integration test - timeout-minutes: 30 - strategy: - matrix: - rbac: ["without RBAC", "with RBAC"] - channel: ["1.24/stable", "1.25/stable"] - steps: - - name: Check out code - uses: actions/checkout@v2 - - name: Setup operator environment - uses: charmed-kubernetes/actions-operator@main - with: - provider: microk8s - channel: ${{ matrix.channel }} - # RBAC is enabled by default; TODO: ideally, this would be an option of actions-operator - - name: Disable RBAC - if: ${{ matrix.rbac == 'without RBAC' }} - run: | - sg microk8s -c "microk8s disable rbac" - sg microk8s -c "microk8s status --wait-ready" - - name: Run test - env: - RBAC: ${{ matrix.rbac == 'with RBAC' }} - run: sg microk8s -c "tox -e integration -- --rbac=$RBAC" - - name: Setup Debug Artifact Collection - if: ${{ failure() }} - run: mkdir tmp - - name: Collect K8s Status - if: ${{ failure() }} - run: sudo microk8s.kubectl get all -A 2>&1 | tee tmp/microk8s-status-all.txt - - name: Collect Juju Status - if: ${{ failure() }} - run: sudo juju status 2>&1 | tee tmp/juju-status.txt - - name: Collect K8s Deployment details - if: ${{ failure() }} - run: sudo microk8s.kubectl describe deployments -A 2>&1 | tee tmp/microk8s-deployments.txt - - name: Collect K8s ReplicaSet details - if: ${{ failure() }} - run: sudo microk8s.kubectl describe replicasets -A 2>&1 | tee tmp/microk8s-replicasets.txt - - name: Collect K8s DaemonSet details - if: ${{ failure() }} - run: sudo microk8s.kubectl describe daemonsets -A 2>&1 | tee tmp/microk8s-daemonsets.txt - - name: Collect K8s pod logs - if: ${{ failure() }} - run: | - for pod in `sudo microk8s.kubectl get pods -n metallb-system | awk '{print$1}' | grep -v NAME`; do - echo "Pod logs for: $pod" - echo "----------------------------------" - sudo microk8s.kubectl logs $pod -n metallb-system 2>&1 | tee tmp/pod-$pod-logs.txt - echo - echo - done - - name: Collect microk8s snap logs - if: ${{ failure() }} - run: sudo snap logs -n 300 microk8s 2>&1 | tee tmp/snap-log-microk8s.txt - - name: Collect Juju logs for metallb-controller - if: ${{ failure() }} - run: sudo juju debug-log --replay --no-tail -i metallb-controller | tee tmp/unit-metallb-controller-0.log - - name: Collect Juju logs for metallb-speaker - if: ${{ failure() }} - run: sudo juju debug-log --replay --no-tail -i metallb-speaker | tee tmp/unit-metallb-speaker-0.log - - name: Upload debug artifacts - if: ${{ failure() }} - uses: actions/upload-artifact@v2 - with: - name: test-run-artifacts - path: tmp diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml new file mode 100644 index 0000000..668b789 --- /dev/null +++ b/.github/workflows/workflow.yaml @@ -0,0 +1,100 @@ +name: Test Suite +on: + push: + branches: + - main + pull_request: + +jobs: + call-inclusive-naming-check: + name: Inclusive Naming + uses: canonical-web-and-design/Inclusive-naming/.github/workflows/woke.yaml@main + with: + fail-on-error: "true" + + lint-unit: + name: Lint Unit + uses: charmed-kubernetes/workflows/.github/workflows/lint-unit.yaml@main + with: + python: "['3.8', '3.9', '3.10', '3.11']" + needs: + - call-inclusive-naming-check + + integration-test-metallb: + runs-on: ubuntu-latest + name: Integration test + timeout-minutes: 30 + strategy: + matrix: + rbac: [ "without RBAC", "with RBAC" ] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@main + with: + provider: microk8s + channel: 1.27-strict/stable + juju-channel: 3.1/stable + - name: Disable RBAC If Needed + if: ${{ matrix.rbac == 'without RBAC' }} + run: | + sudo microk8s disable rbac + sudo microk8s status --wait-ready + - name: Run test + run: sg snap_microk8s -c "tox -e integration" + - name: Setup Debug Artifact Collection + if: ${{ failure() }} + run: mkdir tmp + - name: Collect K8s Status + if: ${{ failure() }} + run: sudo microk8s.kubectl get all -A 2>&1 | tee tmp/microk8s-status-all.txt + - name: Collect Juju Status + if: ${{ failure() }} + run: sudo juju status 2>&1 | tee tmp/juju-status.txt + - name: Collect K8s Deployment details + if: ${{ failure() }} + run: sudo microk8s.kubectl describe deployments -A 2>&1 | tee tmp/microk8s-deployments.txt + - name: Collect K8s ReplicaSet details + if: ${{ failure() }} + run: sudo microk8s.kubectl describe replicasets -A 2>&1 | tee tmp/microk8s-replicasets.txt + - name: Collect K8s DaemonSet details + if: ${{ failure() }} + run: sudo microk8s.kubectl describe daemonsets -A 2>&1 | tee tmp/microk8s-daemonsets.txt + - name: Collect K8s ServiceAccount details + if: ${{ failure() }} + run: sudo microk8s.kubectl describe serviceaccounts -A 2>&1 | tee tmp/microk8s-serviceaccounts.txt + - name: Collect K8s Role details + if: ${{ failure() }} + run: sudo microk8s.kubectl describe roles -A 2>&1 | tee tmp/microk8s-roles.txt + - name: Collect K8s ClusterRole details + if: ${{ failure() }} + run: sudo microk8s.kubectl describe clusterroles 2>&1 | tee tmp/microk8s-clusterroles.txt + - name: Collect K8s RoleBinding details + if: ${{ failure() }} + run: sudo microk8s.kubectl describe rolebindings -A 2>&1 | tee tmp/microk8s-rolebindings.txt + - name: Collect K8s ClusterRoleBinding details + if: ${{ failure() }} + run: sudo microk8s.kubectl describe clusterrolebindings 2>&1 | tee tmp/microk8s-clusterrolebindings.txt + - name: Collect K8s pod logs + if: ${{ failure() }} + run: | + for pod in `sudo microk8s.kubectl get pods -n metallb-system | awk '{print$1}' | grep -v NAME`; do + echo "Pod logs for: $pod" + echo "----------------------------------" + sudo microk8s.kubectl logs $pod -n metallb-system 2>&1 | tee tmp/pod-$pod-logs.txt + echo + echo + done + - name: Collect microk8s snap logs + if: ${{ failure() }} + run: sudo snap logs -n 300 microk8s 2>&1 | tee tmp/snap-log-microk8s.txt + - name: Collect Juju logs + if: ${{ failure() }} + run: sudo juju debug-log --replay | tee tmp/juju.log + - name: Upload debug artifacts + if: ${{ failure() }} + uses: actions/upload-artifact@v2 + with: + name: test-run-artifacts + path: tmp diff --git a/.gitignore b/.gitignore index 8f05fc5..89b61c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,7 @@ -.history +venv/ build/ *.charm -charms/metallb-controller/.build/ -charms/metallb-speaker/.build/ -.tox -.coverage* -.stestr/ -__pycache__ -coverage -cover +.tox/ +.coverage +__pycache__/ +*.py[cod] diff --git a/.jujuignore b/.jujuignore deleted file mode 100644 index b75f70f..0000000 --- a/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/env -*.py[cod] -*.charm diff --git a/.wokeignore b/.wokeignore new file mode 100644 index 0000000..0479c90 --- /dev/null +++ b/.wokeignore @@ -0,0 +1 @@ +upstream/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..288f021 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +To make contributions to this charm, you'll need a working [development setup](https://juju.is/docs/sdk/dev-setup). + +You can create an environment for development with `tox`: + +```shell +tox devenv -e integration +source venv/bin/activate +``` + +## Testing + +This project uses `tox` for managing test environments. There are some pre-configured environments +that can be used for linting and formatting code when you're preparing contributions to the charm: + +```shell +tox run -e format # update your code according to linting rules +tox run -e lint # code style +tox run -e unit # unit tests +tox run -e integration # integration tests +tox # runs 'format', 'lint', and 'unit' environments +``` + +## Build the charm + +Build the charm in this git repository using: + +```shell +charmcraft pack +``` + + -[charms]: charms -[bundle]: bundle [charmcraft]: https://github.com/canonical/charmcraft/ [MicroK8s]: http://microk8s.io/ -[local overlay]: docs/local-overlay.yaml -[microbot-manifest]: docs/example-microbot-lb.yaml + diff --git a/bundle/README.md b/bundle/README.md deleted file mode 100644 index 423a584..0000000 --- a/bundle/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# MetalLB Bundle - -## Overview - -MetalLB offers a software network load balancing implementation that allows for -LoadBalancing services in Kubernetes. Upstream documentation for MetalLB can be -found at - -The official documentation for these charms and how to use them with Kubernetes -can be found at . diff --git a/bundle/bundle.yaml b/bundle/bundle.yaml deleted file mode 100644 index 59c0602..0000000 --- a/bundle/bundle.yaml +++ /dev/null @@ -1,11 +0,0 @@ -description: A charm bundle to deploy MetalLB in Kubernetes -bundle: kubernetes -applications: - metallb-controller: - charm: cs:~containers/metallb-controller - scale: 1 - options: - iprange: "192.168.1.88-192.168.1.89" - metallb-speaker: - charm: cs:~containers/metallb-speaker - scale: 1 diff --git a/bundle/tests/conftest.py b/bundle/tests/conftest.py deleted file mode 100644 index 5dc00da..0000000 --- a/bundle/tests/conftest.py +++ /dev/null @@ -1,115 +0,0 @@ -import asyncio -import logging -import yaml -from contextlib import asynccontextmanager -from distutils.util import strtobool -from pathlib import Path - -import pytest - - -log = logging.getLogger(__name__) - - -def pytest_addoption(parser): - parser.addoption( - "--rbac", - nargs="?", - type=strtobool, - default=False, - const=True, - help="Whether RBAC is enabled and should be tested", - ) - - -@pytest.fixture -def rbac(request): - return request.config.getoption("--rbac") - - -@pytest.fixture(scope="module") -def test_helpers(ops_test): - return TestHelpers(ops_test) - - -class TestHelpers: - def __init__(self, ops_test): - self.ops_test = ops_test - - async def kubectl(self, *cmd): - rc, stdout, stderr = await self.ops_test.run( - "kubectl", - "-n", - self.ops_test.model_name, - *cmd, - ) - assert rc == 0, f"Command 'kubectl {' '.join(cmd)}' failed:\n{stderr}" - return stdout.strip() - - async def pods_ready(self, label, count): - log.info(f"Waiting for {count} {label} pods to be ready") - for attempt in range(60): - status = await self.kubectl( - "get", - "pod", - "-l", - label, - "-o", - "jsonpath={.items[*].status.phase}", - ) - status = status.split() - log.info(f"Attempt: {attempt}/60 Status: {status}") - if status == ["Running"] * count: - return - else: - await asyncio.sleep(5) - else: - raise TimeoutError( - f"Timed out waiting for {count} {label} pods to be ready" - ) - - async def svc_ingress(self, svc_name): - log.info(f"Waiting for ingress address for {svc_name}") - for attempt in range(60): - ingress_address = await self.kubectl( - "get", - "svc", - svc_name, - "-o", - "jsonpath={.status.loadBalancer.ingress[0].ip}", - ) - log.info(f"Ingress address: {ingress_address}") - if ingress_address != "": - return ingress_address - else: - await asyncio.sleep(2) - else: - raise TimeoutError( - f"Timed out waiting for {svc_name} to have an ingress address" - ) - - async def metallb_ready(self): - # wait for operator pods to be ready - await self.pods_ready("operator.juju.is/target=application", 2) - # wait for workload pods to be ready - await self.pods_ready( - "app.kubernetes.io/name in (metallb-controller,metallb-speaker)", 2 - ) - - async def apply_rbac_operator_rules(self): - rbac_src_path = Path("./docs/rbac-permissions-operators.yaml") - rbac_dst_path = self.ops_test.tmp_path / "rbac.yaml" - rbac_rules = list(yaml.safe_load_all(rbac_src_path.read_text())) - for subject in rbac_rules[1]["subjects"]: - subject["namespace"] = self.ops_test.model_name - rbac_dst_path.write_text(yaml.safe_dump_all(rbac_rules)) - await self.kubectl("apply", "-f", str(rbac_dst_path)) - - @asynccontextmanager - async def deploy_microbot(self): - await self.kubectl("apply", "-f", "./docs/example-microbot-lb.yaml") - try: - await self.pods_ready("app=microbot-lb", 3) - yield - finally: - await self.kubectl("delete", "-f", "./docs/example-microbot-lb.yaml") diff --git a/bundle/tests/test_bundle.py b/bundle/tests/test_bundle.py deleted file mode 100644 index 810c7e0..0000000 --- a/bundle/tests/test_bundle.py +++ /dev/null @@ -1,113 +0,0 @@ -import logging -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Mapping -import yaml - -import aiohttp - - -log = logging.getLogger(__name__) - - -@dataclass -class CharmPath: - path: str - - @property - def metadata(self) -> Mapping[str, Any]: - return yaml.safe_load(Path(self.path, "metadata.yaml").open()) - - @property - def resources(self) -> Mapping[str, str]: - return { - name: rsc["upstream-source"] - for name, rsc in self.metadata["resources"].items() - } - - -async def test_build_and_deploy(ops_test, test_helpers, rbac): - rc, stdout, stderr = await ops_test.run( - "juju", "model-config", "-m", ops_test.model_full_name - ) - log.info(stdout) - # can't use the bundle because of: - # https://github.com/juju/python-libjuju/issues/472 - # can't use ops_test.build_charms() because of: - # https://github.com/canonical/charmcraft/issues/554 - controller = CharmPath("charms/metallb-controller") - speaker = CharmPath("charms/metallb-speaker") - controller_charm = await ops_test.build_charm(controller.path) - speaker_charm = await ops_test.build_charm(speaker.path) - controller = await ops_test.model.deploy( - controller_charm, - config={"iprange": "10.1.240.240-10.1.240.241"}, - resources=controller.resources, - series="jammy", - trust=True - ) - speaker = await ops_test.model.deploy( - speaker_charm, - resources=speaker.resources, - series="jammy", - trust=True - ) - - if rbac: - def units_in_error(expect_error): - def _predicate(): - no = "no " if not expect_error else "" - if not (controller.units and speaker.units): - s = "s" if not (controller.units or speaker.units) else "" - log.info(f"Waiting for {no}error: missing unit{s}") - return False - controller_status = controller.units[0].workload_status - speaker_status = speaker.units[0].workload_status - log.info( - f"Waiting for {no}error: {controller_status}, {speaker_status}" - ) - if expect_error: - # only error is allowed - return {"error"} == {controller_status, speaker_status} - else: - # no error is allowed - return {"error"} ^ {controller_status, speaker_status} - - return _predicate - - log.info("Testing RBAC failure and recovery") - # confirm units go to error if RBAC rules not in place - await ops_test.model.block_until( - units_in_error(True), timeout=5 * 60, wait_period=1 - ) - # confirm adding RBAC rules enables units to resolve - log.info("Applying RBAC rules and retrying hooks") - await test_helpers.apply_rbac_operator_rules() - await controller.units[0].resolved(retry=True) - await speaker.units[0].resolved(retry=True) - # NB: This only blocks until the units are not in error state, but they - # likely are in maintenance or executing instead. If we went straight to - # the wait_for_idle below, it would immediately fail due to the previous - # error states since the hooks haven't started the retry yet. - await ops_test.model.block_until( - units_in_error(False), timeout=5 * 60, wait_period=1 - ) - - await ops_test.model.wait_for_idle(wait_for_active=True, raise_on_blocked=True) - - -async def test_microbot_lb(ops_test, test_helpers): - # test metallb - log.info("Testing LB with microbot") - await test_helpers.metallb_ready() - async with test_helpers.deploy_microbot(): - svc_address = await test_helpers.svc_ingress("microbot-lb") - timeout = aiohttp.ClientTimeout(connect=10) - async with aiohttp.request("GET", f"http://{svc_address}", timeout=timeout) as resp: - assert resp.status == 200 - - for unit in ops_test.model.units.values(): - rc, stdout, stderr = await ops_test.run( - "juju", "show-status-log", "-m", ops_test.model_full_name, unit.name - ) - log.info(stdout) diff --git a/charmcraft.yaml b/charmcraft.yaml new file mode 100644 index 0000000..972a161 --- /dev/null +++ b/charmcraft.yaml @@ -0,0 +1,18 @@ +# Architectures based on supported arch's in upstream +# https://hub.docker.com/layers/bitnami/metallb-controller/0.13.10/images/sha256-d9bbb30d02d02ad499a8390105ebe5b94c5fd9086da9591f4e88b7855a9f5e46?context=explore +type: charm +bases: + - build-on: + - name: "ubuntu" + channel: "22.04" + architectures: ["amd64"] + run-on: + - name: "ubuntu" + channel: "22.04" + architectures: + - amd64 + - arm64 +parts: + charm: + prime: + - upstream/** \ No newline at end of file diff --git a/charms/metallb-controller/Pipfile b/charms/metallb-controller/Pipfile deleted file mode 100644 index dd31811..0000000 --- a/charms/metallb-controller/Pipfile +++ /dev/null @@ -1,18 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -mock = "*" -coverage = "*" -stestr = "*" -ipdb = "*" - -[packages] -kubernetes = "*" -oci-image = {git = "https://github.com/juju-solutions/resource-oci-image/"} -ops = "*" - -[requires] -python_version = "3.8" diff --git a/charms/metallb-controller/Pipfile.lock b/charms/metallb-controller/Pipfile.lock deleted file mode 100644 index b3cc8c6..0000000 --- a/charms/metallb-controller/Pipfile.lock +++ /dev/null @@ -1,603 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "6451d93cbe73cc9da721e59394adcecbf401816c7ddd16b83b73d5c4068b55e8" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.8" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "cachetools": { - "hashes": [ - "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757", - "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db" - ], - "markers": "python_version ~= '3.7'", - "version": "==5.2.0" - }, - "certifi": { - "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2022.6.15" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", - "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2.1.0" - }, - "google-auth": { - "hashes": [ - "sha256:3b2f9d2f436cc7c3b363d0ac66470f42fede249c3bafcc504e9f0bcbe983cff0", - "sha256:75b3977e7e22784607e074800048f44d6a56df589fb2abe58a11d4d20c97c314" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.9.0" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "kubernetes": { - "hashes": [ - "sha256:9900f12ae92007533247167d14cdee949cd8c7721f88b4a7da5f5351da3834cd", - "sha256:da19d58865cf903a8c7b9c3691a2e6315d583a98f0659964656dfdf645bf7e49" - ], - "index": "pypi", - "version": "==24.2.0" - }, - "oauthlib": { - "hashes": [ - "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2", - "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==3.2.0" - }, - "oci-image": { - "git": "https://github.com/juju-solutions/resource-oci-image/", - "ref": "fca2ff473e96db170811b81ffe70505ac70612e8" - }, - "ops": { - "hashes": [ - "sha256:1a73753a03d6816045d4a0b4942137e65d74a38da29fad975f7dfbd16e312b0d", - "sha256:ecd058b04445096bd48019c4013b982ac20fb5a1823a94f168b3f5d928a2f98c" - ], - "index": "pypi", - "version": "==1.5.0" - }, - "pyasn1": { - "hashes": [ - "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", - "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", - "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", - "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", - "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", - "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", - "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", - "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", - "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", - "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", - "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" - ], - "version": "==0.4.8" - }, - "pyasn1-modules": { - "hashes": [ - "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", - "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", - "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", - "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", - "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", - "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", - "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", - "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", - "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", - "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", - "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", - "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", - "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" - ], - "version": "==0.2.8" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==6.0" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.1" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", - "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.1" - }, - "rsa": { - "hashes": [ - "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", - "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==4.8" - }, - "setuptools": { - "hashes": [ - "sha256:16923d366ced322712c71ccb97164d07472abeecd13f3a6c283f6d5d26722793", - "sha256:db3b8e2f922b2a910a29804776c643ea609badb6a32c4bcc226fd4fd902cce65" - ], - "markers": "python_version >= '3.7'", - "version": "==63.1.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "urllib3": { - "hashes": [ - "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec", - "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.10" - }, - "websocket-client": { - "hashes": [ - "sha256:5d55652dc1d0b3c734f044337d929aaf83f4f9138816ec680c1aefefb4dc4877", - "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.3" - } - }, - "develop": { - "asttokens": { - "hashes": [ - "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c", - "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5" - ], - "version": "==2.0.5" - }, - "attrs": { - "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" - }, - "autopage": { - "hashes": [ - "sha256:01be3ee61bb714e9090fcc5c10f4cf546c396331c620c6ae50a2321b28ed3199", - "sha256:0fbf5efbe78d466a26753e1dee3272423a3adc989d6a778c700e89a3f8ff0d88" - ], - "markers": "python_version >= '3.6'", - "version": "==0.5.1" - }, - "backcall": { - "hashes": [ - "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", - "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" - ], - "version": "==0.2.0" - }, - "cliff": { - "hashes": [ - "sha256:045aee3f3c64471965d7ad507ce8474a4e2f20815fbb5405a770f8596a2a00a0", - "sha256:a21da482714b9f0b0e9bafaaf2f6a8b3b14161bb47f62e10e28d2fe4ff4b1626" - ], - "markers": "python_version >= '3.6'", - "version": "==3.10.1" - }, - "cmd2": { - "hashes": [ - "sha256:e6f49b0854b6aec2f20073bae99f1deede16c24b36fde682045d73c80c4cfb51", - "sha256:f3b0467daca18fca0dc7838de7726a72ab64127a018a377a86a6ed8ebfdbb25f" - ], - "markers": "python_version >= '3.6'", - "version": "==2.4.1" - }, - "coverage": { - "hashes": [ - "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749", - "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982", - "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3", - "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9", - "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428", - "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e", - "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c", - "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9", - "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264", - "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605", - "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397", - "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d", - "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c", - "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815", - "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068", - "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b", - "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4", - "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4", - "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3", - "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84", - "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83", - "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4", - "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8", - "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb", - "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d", - "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df", - "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6", - "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b", - "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72", - "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13", - "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df", - "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc", - "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6", - "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28", - "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b", - "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4", - "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad", - "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46", - "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3", - "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9", - "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54" - ], - "index": "pypi", - "version": "==6.4.1" - }, - "decorator": { - "hashes": [ - "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", - "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" - ], - "markers": "python_version >= '3.7'", - "version": "==5.1.1" - }, - "executing": { - "hashes": [ - "sha256:c6554e21c6b060590a6d3be4b82fb78f8f0194d809de5ea7df1c093763311501", - "sha256:d1eef132db1b83649a3905ca6dd8897f71ac6f8cac79a7e58a1a09cf137546c9" - ], - "version": "==0.8.3" - }, - "extras": { - "hashes": [ - "sha256:132e36de10b9c91d5d4cc620160a476e0468a88f16c9431817a6729611a81b4e", - "sha256:f689f08df47e2decf76aa6208c081306e7bd472630eb1ec8a875c67de2366e87" - ], - "version": "==1.0.0" - }, - "fixtures": { - "hashes": [ - "sha256:d2758826400d095b79666cf93a32a84f50ff8cd179831927efb48cd1e3ca7466" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" - }, - "future": { - "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.18.2" - }, - "ipdb": { - "hashes": [ - "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5" - ], - "index": "pypi", - "version": "==0.13.9" - }, - "ipython": { - "hashes": [ - "sha256:7ca74052a38fa25fe9bedf52da0be7d3fdd2fb027c3b778ea78dfe8c212937d1", - "sha256:f2db3a10254241d9b447232cec8b424847f338d9d36f9a577a6192c332a46abd" - ], - "markers": "python_version >= '3.7'", - "version": "==8.4.0" - }, - "jedi": { - "hashes": [ - "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d", - "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab" - ], - "markers": "python_version >= '3.6'", - "version": "==0.18.1" - }, - "matplotlib-inline": { - "hashes": [ - "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee", - "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c" - ], - "markers": "python_version >= '3.5'", - "version": "==0.1.3" - }, - "mock": { - "hashes": [ - "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62", - "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc" - ], - "index": "pypi", - "version": "==4.0.3" - }, - "parso": { - "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" - ], - "markers": "python_version >= '3.6'", - "version": "==0.8.3" - }, - "pbr": { - "hashes": [ - "sha256:e547125940bcc052856ded43be8e101f63828c2d94239ffbe2b327ba3d5ccf0a", - "sha256:e8dca2f4b43560edef58813969f52a56cef023146cbb8931626db80e6c1c4308" - ], - "markers": "python_version >= '2.6'", - "version": "==5.9.0" - }, - "pexpect": { - "hashes": [ - "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", - "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" - ], - "markers": "sys_platform != 'win32'", - "version": "==4.8.0" - }, - "pickleshare": { - "hashes": [ - "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", - "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" - ], - "version": "==0.7.5" - }, - "prettytable": { - "hashes": [ - "sha256:118eb54fd2794049b810893653b20952349df6d3bc1764e7facd8a18064fa9b0", - "sha256:d1c34d72ea2c0ffd6ce5958e71c428eb21a3d40bf3133afe319b24aeed5af407" - ], - "markers": "python_version >= '3.7'", - "version": "==3.3.0" - }, - "prompt-toolkit": { - "hashes": [ - "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0", - "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.30" - }, - "ptyprocess": { - "hashes": [ - "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", - "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" - ], - "version": "==0.7.0" - }, - "pure-eval": { - "hashes": [ - "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350", - "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3" - ], - "version": "==0.2.2" - }, - "pygments": { - "hashes": [ - "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb", - "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519" - ], - "markers": "python_version >= '3.6'", - "version": "==2.12.0" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "pyperclip": { - "hashes": [ - "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57" - ], - "version": "==1.8.2" - }, - "python-subunit": { - "hashes": [ - "sha256:042039928120fbf392e8c983d60f3d8ae1b88f90a9f8fd7188ddd9c26cad1e48", - "sha256:40f34660c3da3e513cf2e59498a87ef04ebe2b5fe144fa25d476e1f888b19659" - ], - "version": "==1.4.0" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==6.0" - }, - "setuptools": { - "hashes": [ - "sha256:16923d366ced322712c71ccb97164d07472abeecd13f3a6c283f6d5d26722793", - "sha256:db3b8e2f922b2a910a29804776c643ea609badb6a32c4bcc226fd4fd902cce65" - ], - "markers": "python_version >= '3.7'", - "version": "==63.1.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "stack-data": { - "hashes": [ - "sha256:77bec1402dcd0987e9022326473fdbcc767304892a533ed8c29888dacb7dddbc", - "sha256:aa1d52d14d09c7a9a12bb740e6bdfffe0f5e8f4f9218d85e7c73a8c37f7ae38d" - ], - "version": "==0.3.0" - }, - "stestr": { - "hashes": [ - "sha256:c23ee7ab441228d88365904a224fb8442da0c0289f901cf4a9422e929b7be9cd", - "sha256:de06fb51cf281ac720cdda9a73cb4e34d0cf50ffbf57166567ab37ccb2d02253" - ], - "index": "pypi", - "version": "==3.2.1" - }, - "stevedore": { - "hashes": [ - "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c", - "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335" - ], - "markers": "python_version >= '3.6'", - "version": "==3.5.0" - }, - "testtools": { - "hashes": [ - "sha256:57c13433d94f9ffde3be6534177d10fb0c1507cc499319128958ca91a65cb23f", - "sha256:798525999f053e4df4e352c0c198baeb9f5079f34bad5bd57a44e97a54fa0330" - ], - "markers": "python_version >= '3.5'", - "version": "==2.5.0" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '3.7'", - "version": "==0.10.2" - }, - "traitlets": { - "hashes": [ - "sha256:0bb9f1f9f017aa8ec187d8b1b2a7a6626a2a1d877116baba52a129bfa124f8e2", - "sha256:65fa18961659635933100db8ca120ef6220555286949774b9cfc106f941d1c7a" - ], - "markers": "python_version >= '3.7'", - "version": "==5.3.0" - }, - "voluptuous": { - "hashes": [ - "sha256:4b838b185f5951f2d6e8752b68fcf18bd7a9c26ded8f143f92d6d28f3921a3e6", - "sha256:e8d31c20601d6773cb14d4c0f42aee29c6821bbd1018039aac7ac5605b489723" - ], - "version": "==0.13.1" - }, - "wcwidth": { - "hashes": [ - "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", - "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" - ], - "version": "==0.2.5" - } - } -} diff --git a/charms/metallb-controller/README.md b/charms/metallb-controller/README.md deleted file mode 100644 index 215c1d3..0000000 --- a/charms/metallb-controller/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# MetalLB-Controller Charm - -## Overview - -MetalLB offers a software network load balancing implementation that allows for -LoadBalancing services in Kubernetes. Upstream documentation for MetalLB can be -found at - -The metallb-controller charm is meant to be used together with the metallb-speaker charm. -The official documentation for these charms and how to use them with Kubernetes -can be found at . diff --git a/charms/metallb-controller/charmcraft.yaml b/charms/metallb-controller/charmcraft.yaml deleted file mode 100644 index 3e5877b..0000000 --- a/charms/metallb-controller/charmcraft.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Architectures based on supported arch's in upstream -# https://hub.docker.com/layers/metallb/controller/v0.12/images/sha256-2787f87563065ee3461b6e24691bcdafd09d2166a4691800b58136d7d1701b65 -type: charm -bases: - - build-on: - - name: "ubuntu" - channel: "20.04" - architectures: ["amd64"] - run-on: - - name: "ubuntu" - channel: "20.04" - architectures: - - amd64 - - arm - - arm64 - - ppc64le - - s390x - - name: "ubuntu" - channel: "22.04" - architectures: - - amd64 - - arm - - arm64 - - ppc64le - - s390x diff --git a/charms/metallb-controller/config.yaml b/charms/metallb-controller/config.yaml deleted file mode 100644 index b9aeb59..0000000 --- a/charms/metallb-controller/config.yaml +++ /dev/null @@ -1,19 +0,0 @@ -options: - protocol: - type: string - default: 'layer2' - description: | - Type of configuration to use to announce service IPs. Upstream MetalLB supports - both Layer 2 and BGP configuration. This charm currently only support the - option 'layer2'. The layer 2 configuration works by responding to ARP requests - on your local network directly, to give the machine's MAC address to clients. - iprange: - type: string - default: "192.168.1.240-192.168.1.247" - description: | - For the Layer 2 Configuration only. This is the IP range from which MetalLB - will have control over and choose IPs from to distribute to kubernetes services - requesting an external IP of type Load Balancer. The ip range can be specified as - a range (i.e 192.168.1.240-192.168.1.247") or as a CIDR (i.e "192.168.1.240/29") - To be able to specify more than one ip pool, only the CIDR notation can be used - (i.e "192.168.1.88/31,192.168.1.240/30"). diff --git a/charms/metallb-controller/icon.png b/charms/metallb-controller/icon.png deleted file mode 100644 index f45263b..0000000 Binary files a/charms/metallb-controller/icon.png and /dev/null differ diff --git a/charms/metallb-controller/metadata.yaml b/charms/metallb-controller/metadata.yaml deleted file mode 100644 index 7cd95b8..0000000 --- a/charms/metallb-controller/metadata.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: metallb-controller -description: | - This charm deploys MetalLB controller in a Kubernetes model, which provides - a software defined load balancer. -docs: https://discourse.charmhub.io/t/metallb-controller-speaker/6320 -summary: | - MetalLB offers a software network load balancing implementation that allows for - LoadBalancing services in Kubernetes. It is a young open-source project that could - be charmed to integrate it easily with the Canonical suite of projects. Upstream - documentation can be found here : https://metallb.universe.tf/. - The controller is the cluster-wide controller that handles IP address assignments. - It must be deployed with its counterpart, metallb-speaker, which speaks the protocol - of your choice to make the services reachable. -series: - - kubernetes -tags: - - kubernetes - - metallb -deployment: - type: stateless -resources: - metallb-controller-image: - type: oci-image - description: upstream docker image for metallb-controller - upstream-source: 'quay.io/metallb/controller:v0.12' diff --git a/charms/metallb-controller/requirements.txt b/charms/metallb-controller/requirements.txt deleted file mode 100644 index e3acdcd..0000000 --- a/charms/metallb-controller/requirements.txt +++ /dev/null @@ -1,25 +0,0 @@ --i https://pypi.org/simple -aiohttp==3.6.2; python_version >= '3.6' -async-timeout==3.0.1; python_full_version >= '3.5.3' -attrs==20.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -cachetools==4.1.1; python_version ~= '3.5' -certifi==2020.6.20 -chardet==3.0.4 -oci-image>=1.0.0,<2.0.0 -google-auth==1.22.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -kubernetes==11.0.0 -multidict==4.7.6; python_version >= '3.5' -oauthlib==3.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -ops==0.10.0 -pyasn1-modules==0.2.8 -pyasn1==0.4.8 -python-dateutil==2.8.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -pyyaml==5.3.1 -requests-oauthlib==1.3.0 -requests==2.24.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -rsa==4.6; python_version >= '3.5' -six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -urllib3==1.25.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' -websocket-client==0.57.0 -yarl==1.6.0; python_version >= '3.5' diff --git a/charms/metallb-controller/src/charm.py b/charms/metallb-controller/src/charm.py deleted file mode 100755 index 3940c50..0000000 --- a/charms/metallb-controller/src/charm.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 -"""Controller component for the MetalLB bundle.""" - -import json -import logging -import os -from hashlib import md5 - -from oci_image import OCIImageResource, OCIImageResourceError - -from ops.charm import CharmBase -from ops.framework import StoredState -from ops.main import main -from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus - -import utils - -logger = logging.getLogger(__name__) - - -class MetalLBControllerCharm(CharmBase): - """MetalLB Controller Charm.""" - - _stored = StoredState() - - def __init__(self, *args): - """Charm initialization for events observation.""" - super().__init__(*args) - if not self.unit.is_leader(): - self.unit.status = ActiveStatus() - return - self.image = OCIImageResource(self, "metallb-controller-image") - self.framework.observe(self.on.install, self._on_start) - self.framework.observe(self.on.start, self._on_start) - self.framework.observe(self.on.leader_elected, self._on_start) - self.framework.observe(self.on.upgrade_charm, self._on_upgrade) - self.framework.observe(self.on.config_changed, self._on_config_changed) - self.framework.observe(self.on.remove, self._on_remove) - # -- initialize states -- - self._stored.set_default(k8s_objects_created=False) - self._stored.set_default(started=False) - self._stored.set_default(config_hash=self._config_hash()) - # -- base values -- - self._stored.set_default(namespace=os.environ["JUJU_MODEL_NAME"]) - - def _config_hash(self): - data = json.dumps( - { - "iprange": self.model.config["iprange"], - }, - sort_keys=True, - ) - return md5(data.encode("utf8")).hexdigest() - - def _on_start(self, event): - """Occurs upon install, start, upgrade, and possibly config changed.""" - if self._stored.started: - return - self.unit.status = MaintenanceStatus("Fetching image information") - try: - image_info = self.image.fetch() - except OCIImageResourceError: - logging.exception("An error occured while fetching the image info") - self.unit.status = BlockedStatus("Error fetching image information") - return - - if not self._stored.k8s_objects_created: - self.unit.status = MaintenanceStatus( - "Creating supplementary " "Kubernetes objects" - ) - utils.create_k8s_objects(self._stored.namespace) - self._stored.k8s_objects_created = True - - self.unit.status = MaintenanceStatus("Configuring pod") - self.set_pod_spec(image_info) - - self.unit.status = ActiveStatus() - self._stored.started = True - - def _on_upgrade(self, event): - """Occurs when new charm code or image info is available.""" - self._stored.started = False - self._on_start(event) - - def _on_config_changed(self, event): - if self.model.config["protocol"] != "layer2": - self.unit.status = BlockedStatus( - "Invalid protocol; " 'only "layer2" currently supported' - ) - return - current_config_hash = self._config_hash() - if current_config_hash != self._stored.config_hash: - self._stored.started = False - self._stored.config_hash = current_config_hash - self._on_start(event) - - def _on_remove(self, event): - """Remove of artifacts created by the K8s API.""" - self.unit.status = MaintenanceStatus( - "Removing supplementary " "Kubernetes objects" - ) - utils.remove_k8s_objects(self._stored.namespace) - self.unit.status = MaintenanceStatus("Removing pod") - self._stored.started = False - self._stored.k8s_objects_created = False - - def set_pod_spec(self, image_info): - """Set pod spec.""" - iprange = self.model.config["iprange"].split(",") - cm = "address-pools:\n- name: default\n protocol: layer2\n addresses:\n" - for range in iprange: - cm += " - " + range + "\n" - - self.model.pod.set_spec(utils.get_pod_spec(image_info, cm)) - - -if __name__ == "__main__": - main(MetalLBControllerCharm) diff --git a/charms/metallb-controller/src/utils.py b/charms/metallb-controller/src/utils.py deleted file mode 100644 index 345f990..0000000 --- a/charms/metallb-controller/src/utils.py +++ /dev/null @@ -1,336 +0,0 @@ -"""Kubernetes utils library.""" - -import logging -import os -import random -import string - -from kubernetes import client, config -from kubernetes.client.rest import ApiException - -logger = logging.getLogger(__name__) - - -def create_k8s_objects(namespace): - """Create all supplementary K8s objects.""" - if supports_policy_v1_beta(): - create_pod_security_policy_with_api(namespace=namespace) - else: - logging.info("Not creating PSP, doesn't support policy_v1_beta") - create_namespaced_role_with_api( - name="config-watcher", - namespace=namespace, - labels={"app": "metallb"}, - resources=["configmaps"], - verbs=["get", "list", "watch"], - ) - create_namespaced_role_binding_with_api( - name="config-watcher", - namespace=namespace, - labels={"app": "metallb"}, - subject_name="metallb-controller", - ) - - -def remove_k8s_objects(namespace): - """Remove all supplementary K8s objects.""" - if supports_policy_v1_beta(): - delete_pod_security_policy_with_api(name="controller") - else: - logging.info("Skipping PSP removal, doesn't support policy_v1_beta") - delete_namespaced_role_binding_with_api(name="config-watcher", namespace=namespace) - delete_namespaced_role_with_api(name="config-watcher", namespace=namespace) - - -def create_pod_security_policy_with_api(namespace): - """Create pod security policy.""" - # Using the API because of LP:1886694 - logging.info("Creating pod security policy with K8s API") - _load_kube_config() - - metadata = client.V1ObjectMeta( - namespace=namespace, name="controller", labels={"app": "metallb"} - ) - policy_spec = client.PolicyV1beta1PodSecurityPolicySpec( - allow_privilege_escalation=False, - default_allow_privilege_escalation=False, - fs_group=client.PolicyV1beta1FSGroupStrategyOptions( - ranges=[client.PolicyV1beta1IDRange(max=65535, min=1)], rule="MustRunAs" - ), - host_ipc=False, - host_network=False, - host_pid=False, - privileged=False, - read_only_root_filesystem=True, - required_drop_capabilities=["ALL"], - run_as_user=client.PolicyV1beta1RunAsUserStrategyOptions( - ranges=[client.PolicyV1beta1IDRange(max=65535, min=1)], rule="MustRunAs" - ), - se_linux=client.PolicyV1beta1SELinuxStrategyOptions( - rule="RunAsAny", - ), - supplemental_groups=client.PolicyV1beta1SupplementalGroupsStrategyOptions( - ranges=[client.PolicyV1beta1IDRange(max=65535, min=1)], rule="MustRunAs" - ), - volumes=["configMap", "secret", "emptyDir"], - ) - - body = client.PolicyV1beta1PodSecurityPolicy(metadata=metadata, spec=policy_spec) - - with client.ApiClient() as api_client: - api_instance = client.PolicyV1beta1Api(api_client) - try: - api_instance.create_pod_security_policy(body, pretty=True) - except ApiException as err: - if err.status == 409: - # ignore "already exists" errors so that we can recover from - # partially failed setups - return - else: - raise - - -def delete_pod_security_policy_with_api(name): - """Delete pod security policy.""" - logging.info('Deleting pod security policy named "controller" with K8s API') - _load_kube_config() - - body = client.V1DeleteOptions() - with client.ApiClient() as api_client: - api_instance = client.PolicyV1beta1Api(api_client) - try: - api_instance.delete_pod_security_policy(name=name, body=body, pretty=True) - except ApiException: - logging.exception( - "Exception when calling PolicyV1beta1Api" - "->delete_pod_security_policy." - ) - - -def create_namespaced_role_with_api( - name, namespace, labels, resources, verbs, api_groups=[""] -): - """Create namespaced role.""" - # Using API because of bug https://bugs.launchpad.net/juju/+bug/1896076 - logging.info("Creating namespaced role with K8s API") - _load_kube_config() - - body = client.V1Role( - metadata=client.V1ObjectMeta(name=name, namespace=namespace, labels=labels), - rules=[ - client.V1PolicyRule( - api_groups=api_groups, - resources=resources, - verbs=verbs, - ) - ], - ) - with client.ApiClient() as api_client: - api_instance = client.RbacAuthorizationV1Api(api_client) - try: - api_instance.create_namespaced_role(namespace, body, pretty=True) - except ApiException as err: - if err.status == 409: - # ignore "already exists" errors so that we can recover from - # partially failed setups - return - else: - raise - - -def delete_namespaced_role_with_api(name, namespace): - """Delete namespaced role.""" - logging.info("Deleting namespaced role with K8s API") - _load_kube_config() - - body = client.V1DeleteOptions() - with client.ApiClient() as api_client: - api_instance = client.RbacAuthorizationV1Api(api_client) - try: - api_instance.delete_namespaced_role( - name=name, namespace=namespace, body=body, pretty=True - ) - except ApiException: - logging.exception( - "Exception when calling RbacAuthorizationV1Api" - "->delete_namespaced_role." - ) - - -def create_namespaced_role_binding_with_api( - name, namespace, labels, subject_name, subject_kind="ServiceAccount" -): - """Bind namespaced role to subject.""" - # Using API because of bug https://bugs.launchpad.net/juju/+bug/1896076 - logging.info("Creating role binding with K8s API") - _load_kube_config() - - body = client.V1RoleBinding( - metadata=client.V1ObjectMeta(name=name, namespace=namespace, labels=labels), - role_ref=client.V1RoleRef( - api_group="rbac.authorization.k8s.io", - kind="Role", - name=name, - ), - subjects=[ - client.V1Subject(kind=subject_kind, name=subject_name), - ], - ) - with client.ApiClient() as api_client: - api_instance = client.RbacAuthorizationV1Api(api_client) - try: - api_instance.create_namespaced_role_binding(namespace, body, pretty=True) - except ApiException as err: - if err.status == 409: - # ignore "already exists" errors so that we can recover from - # partially failed setups - return - else: - raise - - -def delete_namespaced_role_binding_with_api(name, namespace): - """Delete namespaced role binding with K8s API.""" - logging.info("Deleting namespaced role binding with API") - _load_kube_config() - - body = client.V1DeleteOptions() - with client.ApiClient() as api_client: - api_instance = client.RbacAuthorizationV1Api(api_client) - try: - api_instance.delete_namespaced_role_binding( - name=name, namespace=namespace, body=body, pretty=True - ) - except ApiException: - logging.exception( - "Exception when calling RbacAuthorizationV1Api" - "->delete_namespaced_role_binding." - ) - - -def supports_policy_v1_beta(): - """Determine if k8s api supports PolicyV1/beta.""" - logging.info("Determine if k8s api supports PolicyV1/beta") - _load_kube_config() - - with client.ApiClient() as api_client: - api_instance = client.PolicyV1beta1Api(api_client) - try: - api_instance.get_api_resources() - except ApiException as err: - if err.status == 404: - return False - return True - - -def get_pod_spec(image_info, cm): - """Get pod spec.""" - policyv1_beta = supports_policy_v1_beta() - rules = [ - { - "apiGroups": [""], - "resources": ["services"], - "verbs": ["get", "list", "watch", "update"], - }, - { - "apiGroups": [""], - "resources": ["services/status"], - "verbs": ["update"], - }, - { - "apiGroups": [""], - "resources": ["events"], - "verbs": ["create", "patch"], - }, - { - "apiGroups": [""], - "resources": ["nodes"], - "verbs": ["list"], - }, - ] - if policyv1_beta: - logging.info("Appending PSP-related podspec rules, policyv1_beta supported") - rules.append( - { - "apiGroups": ["policy"], - "resourceNames": ["controller"], - "resources": ["podsecuritypolicies"], - "verbs": ["use"], - } - ) - else: - logging.info("Skipping PSP-related podspec rules, policyv1_beta not supported") - - spec = { - "version": 3, - "serviceAccount": { - "roles": [ - { - "global": True, - "rules": rules, - } - ], - }, - "containers": [ - { - "name": "controller", - "imageDetails": image_info, - "imagePullPolicy": "Always", - "ports": [ - { - "containerPort": 7472, - "protocol": "TCP", - "name": "monitoring", - } - ], - # TODO: add constraint fields once it exists in pod_spec - # bug : https://bugs.launchpad.net/juju/+bug/1893123 - # 'resources': { - # 'limits': { - # 'cpu': '100m', - # 'memory': '100Mi', - # } - # }, - "kubernetes": { - "securityContext": { - "privileged": False, - "runAsNonRoot": True, - "runAsUser": 65534, - "readOnlyRootFilesystem": True, - "capabilities": {"drop": ["ALL"]}, - }, - # fields do not exist in pod_spec - # 'TerminationGracePeriodSeconds': 0, - }, - } - ], - "service": { - "annotations": { - "prometheus.io/port": "7472", - "prometheus.io/scrape": "true", - } - }, - "configMaps": {"config": {"config": cm}}, - } - return spec - - -def _random_secret(length): - letters = string.ascii_letters - result_str = "".join(random.SystemRandom().choice(letters) for i in range(length)) - return result_str - - -def _load_kube_config(): - # TODO: Remove this workaround when bug LP:1892255 is fixed - from pathlib import Path - - os.environ.update( - dict( - e.split("=") - for e in Path("/proc/1/environ").read_text().split("\x00") - if "KUBERNETES_SERVICE" in e - ) - ) - # end workaround - config.load_incluster_config() diff --git a/charms/metallb-controller/tests/__init__.py b/charms/metallb-controller/tests/__init__.py deleted file mode 100644 index 600aceb..0000000 --- a/charms/metallb-controller/tests/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Init mocking for unit tests.""" - -import sys - -import mock - -sys.path.append("src") - -oci_image = mock.MagicMock() -sys.modules["oci_image"] = oci_image diff --git a/charms/metallb-controller/tests/test_charm.py b/charms/metallb-controller/tests/test_charm.py deleted file mode 100644 index 280c175..0000000 --- a/charms/metallb-controller/tests/test_charm.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Unit tests.""" - -import unittest -from unittest.mock import Mock, patch - -from charm import MetalLBControllerCharm - -from ops.testing import Harness - -from utils import get_pod_spec - - -class TestCharm(unittest.TestCase): - """MetalLB Controller Charm Unit Tests.""" - - @patch.dict("charm.os.environ", {"JUJU_MODEL_NAME": "unit-test-metallb"}) - def setUp(self): - """Test setup.""" - self.harness = Harness(MetalLBControllerCharm) - self.harness.set_leader(is_leader=True) - self.harness.begin() - - @patch.dict("charm.os.environ", {"JUJU_MODEL_NAME": "unit-test-metallb"}) - @patch("utils.create_namespaced_role_binding_with_api") - @patch("utils.create_namespaced_role_with_api") - @patch("utils.create_pod_security_policy_with_api") - @patch("utils.supports_policy_v1_beta") - def test_on_start_lt_1_25( - self, - supports_policy_v1_beta, - create_psp, - create_ns_role, - create_ns_role_binding, - ): - """Test installation < 1.25.0.""" - supports_policy_v1_beta.return_value = True - mock_pod_spec = self.harness.charm.set_pod_spec = Mock() - self.assertFalse(self.harness.charm._stored.started) - self.harness.charm.on.start.emit() - mock_pod_spec.assert_called_once() - create_psp.assert_called_once() - create_ns_role.assert_called_once() - create_ns_role_binding.assert_called_once() - self.assertTrue(self.harness.charm._stored.started) - - @patch.dict("charm.os.environ", {"JUJU_MODEL_NAME": "unit-test-metallb"}) - @patch("utils.create_namespaced_role_binding_with_api") - @patch("utils.create_namespaced_role_with_api") - @patch("utils.create_pod_security_policy_with_api") - @patch("utils.supports_policy_v1_beta") - def test_on_start_gte_1_25( - self, - supports_policy_v1_beta, - create_psp, - create_ns_role, - create_ns_role_binding, - ): - """Test installation >= 1.25.0.""" - supports_policy_v1_beta.return_value = False - mock_pod_spec = self.harness.charm.set_pod_spec = Mock() - self.assertFalse(self.harness.charm._stored.started) - self.harness.charm.on.start.emit() - mock_pod_spec.assert_called_once() - create_psp.assert_not_called() - create_ns_role.assert_called_once() - create_ns_role_binding.assert_called_once() - self.assertTrue(self.harness.charm._stored.started) - - @patch("utils.create_k8s_objects") - def test_config_changed(self, create_k8s_objects): - """Test update config upon change.""" - mock_pod_spec = self.harness.charm.set_pod_spec = Mock() - self.harness.charm._stored.started = True - self.harness.update_config({"iprange": "192.168.1.88-192.168.1.89"}) - mock_pod_spec.assert_called_once() - - @patch("utils.delete_namespaced_role_with_api") - @patch("utils.delete_namespaced_role_binding_with_api") - @patch("utils.delete_pod_security_policy_with_api") - @patch("utils.supports_policy_v1_beta") - def test_on_remove_lt_1_25( - self, - supports_policy_v1_beta, - delete_psp, - delete_ns_role_binding, - delete_ns_role, - ): - """Test remove hook < 1.25.0.""" - supports_policy_v1_beta.return_value = True - self.harness.charm.on.remove.emit() - delete_psp.assert_called_once() - delete_ns_role_binding.assert_called_once() - delete_ns_role.assert_called_once() - self.assertFalse(self.harness.charm._stored.started) - - @patch("utils.delete_namespaced_role_with_api") - @patch("utils.delete_namespaced_role_binding_with_api") - @patch("utils.delete_pod_security_policy_with_api") - @patch("utils.supports_policy_v1_beta") - def test_on_remove_gte_1_25( - self, - supports_policy_v1_beta, - delete_psp, - delete_ns_role_binding, - delete_ns_role, - ): - """Test remove hook >= 1.25.0.""" - supports_policy_v1_beta.return_value = False - self.harness.charm.on.remove.emit() - delete_psp.assert_not_called() - delete_ns_role_binding.assert_called_once() - delete_ns_role.assert_called_once() - self.assertFalse(self.harness.charm._stored.started) - - @patch("utils.supports_policy_v1_beta") - def test_get_pod_spec(self, supports_policy_v1_beta): - """Test pod spec.""" - psp_rule = { - "apiGroups": ["policy"], - "resourceNames": ["controller"], - "resources": ["podsecuritypolicies"], - "verbs": ["use"], - } - - supports_policy_v1_beta.return_value = True - spec = get_pod_spec("info", "cm") - rules = spec["serviceAccount"]["roles"][0]["rules"] - assert psp_rule in rules - - supports_policy_v1_beta.return_value = False - spec = get_pod_spec("info", "cm") - rules = spec["serviceAccount"]["roles"][0]["rules"] - assert psp_rule not in rules - - -if __name__ == "__main__": - unittest.main() diff --git a/charms/metallb-controller/tox.ini b/charms/metallb-controller/tox.ini deleted file mode 100644 index c7555c5..0000000 --- a/charms/metallb-controller/tox.ini +++ /dev/null @@ -1,62 +0,0 @@ -[tox] -skipsdist = True -envlist = unit, lint -sitepackages = False -skip_missing_interpreters = False - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}/src - CHARM_NAME = metallb-controller - -[testenv:unit] -commands = - pipenv install --dev --ignore-pipfile - coverage erase - stestr run --slowest --test-path=./tests --top-dir=./ - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report -deps = pipenv -setenv = - {[testenv]setenv} - PYTHON=coverage run - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - -[testenv:lint] -commands = - flake8 - black --check {toxinidir} -deps = - flake8 - flake8-docstrings - flake8-import-order - pep8-naming - flake8-colors - black - -[flake8] -ignore = - # line break after binary operator - W504, -exclude = - .git, - __pycache__, - .tox, - mod, - .history, - build, - .build, -max-line-length = 88 -max-complexity = 10 diff --git a/charms/metallb-speaker/Pipfile b/charms/metallb-speaker/Pipfile deleted file mode 100644 index dd31811..0000000 --- a/charms/metallb-speaker/Pipfile +++ /dev/null @@ -1,18 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -mock = "*" -coverage = "*" -stestr = "*" -ipdb = "*" - -[packages] -kubernetes = "*" -oci-image = {git = "https://github.com/juju-solutions/resource-oci-image/"} -ops = "*" - -[requires] -python_version = "3.8" diff --git a/charms/metallb-speaker/Pipfile.lock b/charms/metallb-speaker/Pipfile.lock deleted file mode 100644 index 42c9364..0000000 --- a/charms/metallb-speaker/Pipfile.lock +++ /dev/null @@ -1,606 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "6451d93cbe73cc9da721e59394adcecbf401816c7ddd16b83b73d5c4068b55e8" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.8" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "cachetools": { - "hashes": [ - "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757", - "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db" - ], - "markers": "python_version ~= '3.7'", - "version": "==5.2.0" - }, - "certifi": { - "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.6.15" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", - "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" - ], - "markers": "python_version >= '3.6'", - "version": "==2.1.0" - }, - "google-auth": { - "hashes": [ - "sha256:3b2f9d2f436cc7c3b363d0ac66470f42fede249c3bafcc504e9f0bcbe983cff0", - "sha256:75b3977e7e22784607e074800048f44d6a56df589fb2abe58a11d4d20c97c314" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.9.0" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "kubernetes": { - "hashes": [ - "sha256:9900f12ae92007533247167d14cdee949cd8c7721f88b4a7da5f5351da3834cd", - "sha256:da19d58865cf903a8c7b9c3691a2e6315d583a98f0659964656dfdf645bf7e49" - ], - "index": "pypi", - "version": "==24.2.0" - }, - "oauthlib": { - "hashes": [ - "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2", - "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe" - ], - "markers": "python_version >= '3.6'", - "version": "==3.2.0" - }, - "oci-image": { - "git": "https://github.com/juju-solutions/resource-oci-image/", - "hashes": [ - "sha256:3ed25bd96f1720c7336a1b4c4e32cf87694715b3b8fadbb919e6de6a18f59fb8" - ], - "ref": "fca2ff473e96db170811b81ffe70505ac70612e8" - }, - "ops": { - "hashes": [ - "sha256:1a73753a03d6816045d4a0b4942137e65d74a38da29fad975f7dfbd16e312b0d", - "sha256:ecd058b04445096bd48019c4013b982ac20fb5a1823a94f168b3f5d928a2f98c" - ], - "index": "pypi", - "version": "==1.5.0" - }, - "pyasn1": { - "hashes": [ - "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", - "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", - "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", - "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", - "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", - "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", - "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", - "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", - "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", - "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", - "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" - ], - "version": "==0.4.8" - }, - "pyasn1-modules": { - "hashes": [ - "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", - "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", - "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", - "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", - "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", - "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", - "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", - "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", - "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", - "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", - "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", - "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", - "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" - ], - "version": "==0.2.8" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.1" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", - "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.1" - }, - "rsa": { - "hashes": [ - "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", - "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb" - ], - "markers": "python_version >= '3.6'", - "version": "==4.8" - }, - "setuptools": { - "hashes": [ - "sha256:16923d366ced322712c71ccb97164d07472abeecd13f3a6c283f6d5d26722793", - "sha256:db3b8e2f922b2a910a29804776c643ea609badb6a32c4bcc226fd4fd902cce65" - ], - "markers": "python_version >= '3.7'", - "version": "==63.1.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "urllib3": { - "hashes": [ - "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec", - "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.10" - }, - "websocket-client": { - "hashes": [ - "sha256:5d55652dc1d0b3c734f044337d929aaf83f4f9138816ec680c1aefefb4dc4877", - "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.3" - } - }, - "develop": { - "asttokens": { - "hashes": [ - "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c", - "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5" - ], - "version": "==2.0.5" - }, - "attrs": { - "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" - }, - "autopage": { - "hashes": [ - "sha256:01be3ee61bb714e9090fcc5c10f4cf546c396331c620c6ae50a2321b28ed3199", - "sha256:0fbf5efbe78d466a26753e1dee3272423a3adc989d6a778c700e89a3f8ff0d88" - ], - "markers": "python_version >= '3.6'", - "version": "==0.5.1" - }, - "backcall": { - "hashes": [ - "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", - "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" - ], - "version": "==0.2.0" - }, - "cliff": { - "hashes": [ - "sha256:045aee3f3c64471965d7ad507ce8474a4e2f20815fbb5405a770f8596a2a00a0", - "sha256:a21da482714b9f0b0e9bafaaf2f6a8b3b14161bb47f62e10e28d2fe4ff4b1626" - ], - "markers": "python_version >= '3.6'", - "version": "==3.10.1" - }, - "cmd2": { - "hashes": [ - "sha256:e6f49b0854b6aec2f20073bae99f1deede16c24b36fde682045d73c80c4cfb51", - "sha256:f3b0467daca18fca0dc7838de7726a72ab64127a018a377a86a6ed8ebfdbb25f" - ], - "markers": "python_version >= '3.6'", - "version": "==2.4.1" - }, - "coverage": { - "hashes": [ - "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749", - "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982", - "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3", - "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9", - "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428", - "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e", - "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c", - "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9", - "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264", - "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605", - "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397", - "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d", - "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c", - "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815", - "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068", - "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b", - "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4", - "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4", - "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3", - "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84", - "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83", - "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4", - "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8", - "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb", - "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d", - "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df", - "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6", - "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b", - "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72", - "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13", - "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df", - "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc", - "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6", - "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28", - "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b", - "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4", - "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad", - "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46", - "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3", - "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9", - "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54" - ], - "index": "pypi", - "version": "==6.4.1" - }, - "decorator": { - "hashes": [ - "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", - "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" - ], - "markers": "python_version >= '3.7'", - "version": "==5.1.1" - }, - "executing": { - "hashes": [ - "sha256:c6554e21c6b060590a6d3be4b82fb78f8f0194d809de5ea7df1c093763311501", - "sha256:d1eef132db1b83649a3905ca6dd8897f71ac6f8cac79a7e58a1a09cf137546c9" - ], - "version": "==0.8.3" - }, - "extras": { - "hashes": [ - "sha256:132e36de10b9c91d5d4cc620160a476e0468a88f16c9431817a6729611a81b4e", - "sha256:f689f08df47e2decf76aa6208c081306e7bd472630eb1ec8a875c67de2366e87" - ], - "version": "==1.0.0" - }, - "fixtures": { - "hashes": [ - "sha256:d2758826400d095b79666cf93a32a84f50ff8cd179831927efb48cd1e3ca7466" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" - }, - "future": { - "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.18.2" - }, - "ipdb": { - "hashes": [ - "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5" - ], - "index": "pypi", - "version": "==0.13.9" - }, - "ipython": { - "hashes": [ - "sha256:7ca74052a38fa25fe9bedf52da0be7d3fdd2fb027c3b778ea78dfe8c212937d1", - "sha256:f2db3a10254241d9b447232cec8b424847f338d9d36f9a577a6192c332a46abd" - ], - "markers": "python_version >= '3.7'", - "version": "==8.4.0" - }, - "jedi": { - "hashes": [ - "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d", - "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab" - ], - "markers": "python_version >= '3.6'", - "version": "==0.18.1" - }, - "matplotlib-inline": { - "hashes": [ - "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee", - "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c" - ], - "markers": "python_version >= '3.5'", - "version": "==0.1.3" - }, - "mock": { - "hashes": [ - "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62", - "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc" - ], - "index": "pypi", - "version": "==4.0.3" - }, - "parso": { - "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" - ], - "markers": "python_version >= '3.6'", - "version": "==0.8.3" - }, - "pbr": { - "hashes": [ - "sha256:e547125940bcc052856ded43be8e101f63828c2d94239ffbe2b327ba3d5ccf0a", - "sha256:e8dca2f4b43560edef58813969f52a56cef023146cbb8931626db80e6c1c4308" - ], - "markers": "python_version >= '2.6'", - "version": "==5.9.0" - }, - "pexpect": { - "hashes": [ - "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", - "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" - ], - "markers": "sys_platform != 'win32'", - "version": "==4.8.0" - }, - "pickleshare": { - "hashes": [ - "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", - "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" - ], - "version": "==0.7.5" - }, - "prettytable": { - "hashes": [ - "sha256:118eb54fd2794049b810893653b20952349df6d3bc1764e7facd8a18064fa9b0", - "sha256:d1c34d72ea2c0ffd6ce5958e71c428eb21a3d40bf3133afe319b24aeed5af407" - ], - "markers": "python_version >= '3.7'", - "version": "==3.3.0" - }, - "prompt-toolkit": { - "hashes": [ - "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0", - "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.30" - }, - "ptyprocess": { - "hashes": [ - "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", - "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" - ], - "version": "==0.7.0" - }, - "pure-eval": { - "hashes": [ - "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350", - "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3" - ], - "version": "==0.2.2" - }, - "pygments": { - "hashes": [ - "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb", - "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519" - ], - "markers": "python_version >= '3.6'", - "version": "==2.12.0" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "pyperclip": { - "hashes": [ - "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57" - ], - "version": "==1.8.2" - }, - "python-subunit": { - "hashes": [ - "sha256:042039928120fbf392e8c983d60f3d8ae1b88f90a9f8fd7188ddd9c26cad1e48", - "sha256:40f34660c3da3e513cf2e59498a87ef04ebe2b5fe144fa25d476e1f888b19659" - ], - "version": "==1.4.0" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0" - }, - "setuptools": { - "hashes": [ - "sha256:16923d366ced322712c71ccb97164d07472abeecd13f3a6c283f6d5d26722793", - "sha256:db3b8e2f922b2a910a29804776c643ea609badb6a32c4bcc226fd4fd902cce65" - ], - "markers": "python_version >= '3.7'", - "version": "==63.1.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "stack-data": { - "hashes": [ - "sha256:77bec1402dcd0987e9022326473fdbcc767304892a533ed8c29888dacb7dddbc", - "sha256:aa1d52d14d09c7a9a12bb740e6bdfffe0f5e8f4f9218d85e7c73a8c37f7ae38d" - ], - "version": "==0.3.0" - }, - "stestr": { - "hashes": [ - "sha256:c23ee7ab441228d88365904a224fb8442da0c0289f901cf4a9422e929b7be9cd", - "sha256:de06fb51cf281ac720cdda9a73cb4e34d0cf50ffbf57166567ab37ccb2d02253" - ], - "index": "pypi", - "version": "==3.2.1" - }, - "stevedore": { - "hashes": [ - "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c", - "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335" - ], - "markers": "python_version >= '3.6'", - "version": "==3.5.0" - }, - "testtools": { - "hashes": [ - "sha256:57c13433d94f9ffde3be6534177d10fb0c1507cc499319128958ca91a65cb23f", - "sha256:798525999f053e4df4e352c0c198baeb9f5079f34bad5bd57a44e97a54fa0330" - ], - "markers": "python_version >= '3.5'", - "version": "==2.5.0" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '3.7'", - "version": "==0.10.2" - }, - "traitlets": { - "hashes": [ - "sha256:0bb9f1f9f017aa8ec187d8b1b2a7a6626a2a1d877116baba52a129bfa124f8e2", - "sha256:65fa18961659635933100db8ca120ef6220555286949774b9cfc106f941d1c7a" - ], - "markers": "python_version >= '3.7'", - "version": "==5.3.0" - }, - "voluptuous": { - "hashes": [ - "sha256:4b838b185f5951f2d6e8752b68fcf18bd7a9c26ded8f143f92d6d28f3921a3e6", - "sha256:e8d31c20601d6773cb14d4c0f42aee29c6821bbd1018039aac7ac5605b489723" - ], - "version": "==0.13.1" - }, - "wcwidth": { - "hashes": [ - "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", - "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" - ], - "version": "==0.2.5" - } - } -} diff --git a/charms/metallb-speaker/README.md b/charms/metallb-speaker/README.md deleted file mode 100644 index 2f8ce8e..0000000 --- a/charms/metallb-speaker/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# MetalLB-Speaker Charm - -## Overview - -MetalLB offers a software network load balancing implementation that allows for -LoadBalancing services in Kubernetes. Upstream documentation for MetalLB can be -found at - -The metallb-speaker charm is meant to be used together with the metallb-controller charm. -The official documentation for these charms and how to use them with Kubernetes -can be found at . diff --git a/charms/metallb-speaker/charmcraft.yaml b/charms/metallb-speaker/charmcraft.yaml deleted file mode 100644 index 1a5d08a..0000000 --- a/charms/metallb-speaker/charmcraft.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Architectures based on supported arch's in upstream -# https://hub.docker.com/layers/metallb/speaker/v0.12/images/sha256-a1dc3ae8d53b40490d0fb4b25026ec51d88ab2b27bb2b91602780f7eb58b699e -type: charm -bases: - - build-on: - - name: "ubuntu" - channel: "20.04" - architectures: ["amd64"] - run-on: - - name: "ubuntu" - channel: "20.04" - architectures: - - amd64 - - arm - - arm64 - - ppc64le - - s390x - - name: "ubuntu" - channel: "22.04" - architectures: - - amd64 - - arm - - arm64 - - ppc64le - - s390x diff --git a/charms/metallb-speaker/icon.png b/charms/metallb-speaker/icon.png deleted file mode 100644 index f45263b..0000000 Binary files a/charms/metallb-speaker/icon.png and /dev/null differ diff --git a/charms/metallb-speaker/metadata.yaml b/charms/metallb-speaker/metadata.yaml deleted file mode 100644 index a4ad03c..0000000 --- a/charms/metallb-speaker/metadata.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: metallb-speaker -description: | - This charm deploys MetalLB speaker in a Kubernetes model, which provides - a software defined load balancer. -docs: https://discourse.charmhub.io/t/metallb-controller-speaker/6320 -summary: | - MetalLB offers a software network load balancing implementation that allows for - LoadBalancing services in Kubernetes. It is a young open-source project that could - be charmed to integrate it easily with the Canonical suite of projects. Upstream - documentation can be found here : https://metallb.universe.tf/. - The speaker is the deamonset that makes the services reachable. It must be deployed - with its counterpart, metallb-controller, which handles IP address assignments. -series: - - kubernetes -tags: - - kubernetes - - metallb -deployment: - type: daemon -resources: - metallb-speaker-image: - type: oci-image - description: upstream docker image for metallb-controller - upstream-source: 'quay.io/metallb/speaker:v0.12' diff --git a/charms/metallb-speaker/requirements.txt b/charms/metallb-speaker/requirements.txt deleted file mode 100644 index e3acdcd..0000000 --- a/charms/metallb-speaker/requirements.txt +++ /dev/null @@ -1,25 +0,0 @@ --i https://pypi.org/simple -aiohttp==3.6.2; python_version >= '3.6' -async-timeout==3.0.1; python_full_version >= '3.5.3' -attrs==20.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -cachetools==4.1.1; python_version ~= '3.5' -certifi==2020.6.20 -chardet==3.0.4 -oci-image>=1.0.0,<2.0.0 -google-auth==1.22.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -kubernetes==11.0.0 -multidict==4.7.6; python_version >= '3.5' -oauthlib==3.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -ops==0.10.0 -pyasn1-modules==0.2.8 -pyasn1==0.4.8 -python-dateutil==2.8.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -pyyaml==5.3.1 -requests-oauthlib==1.3.0 -requests==2.24.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -rsa==4.6; python_version >= '3.5' -six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -urllib3==1.25.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' -websocket-client==0.57.0 -yarl==1.6.0; python_version >= '3.5' diff --git a/charms/metallb-speaker/src/charm.py b/charms/metallb-speaker/src/charm.py deleted file mode 100755 index b3b179c..0000000 --- a/charms/metallb-speaker/src/charm.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -"""Speaker component for the MetalLB bundle.""" - -import logging -import os -from base64 import b64encode - -from oci_image import OCIImageResource, OCIImageResourceError - -from ops.charm import CharmBase -from ops.framework import StoredState -from ops.main import main -from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus - -import utils - -logger = logging.getLogger(__name__) - - -class MetalLBSpeakerCharm(CharmBase): - """MetalLB Speaker Charm.""" - - _stored = StoredState() - - def __init__(self, *args): - """Charm initialization for events observation.""" - super().__init__(*args) - if not self.unit.is_leader(): - self.unit.status = ActiveStatus() - return - self.image = OCIImageResource(self, "metallb-speaker-image") - self.framework.observe(self.on.install, self._on_start) - self.framework.observe(self.on.start, self._on_start) - self.framework.observe(self.on.leader_elected, self._on_start) - self.framework.observe(self.on.upgrade_charm, self._on_upgrade) - self.framework.observe(self.on.remove, self._on_remove) - # -- initialize states -- - self._stored.set_default(k8s_objects_created=False) - self._stored.set_default(started=False) - self._stored.set_default( - secret=b64encode(utils._random_secret(128).encode("utf-8")).decode("utf-8") - ) - # -- base values -- - self._stored.set_default(namespace=os.environ["JUJU_MODEL_NAME"]) - - def _on_start(self, event): - """Occurs upon install, start, or upgrade of the charm.""" - if self._stored.started: - return - self.unit.status = MaintenanceStatus("Fetching image info") - try: - image_info = self.image.fetch() - except OCIImageResourceError: - logging.exception("An error occured while fetching the image info") - self.unit.status = BlockedStatus("Error fetching image information") - return - - if not self._stored.k8s_objects_created: - self.unit.status = MaintenanceStatus( - "Creating supplementary " "Kubernetes objects" - ) - utils.create_k8s_objects(self._stored.namespace) - self._stored.k8s_objects_created = True - - self.unit.status = MaintenanceStatus("Configuring pod") - self.set_pod_spec(image_info) - - self.unit.status = ActiveStatus() - self._stored.started = True - - def _on_upgrade(self, event): - """Occurs when new charm code or image info is available.""" - self._stored.started = False - self._on_start(event) - - def _on_remove(self, event): - """Remove artifacts created by the K8s API.""" - self.unit.status = MaintenanceStatus( - "Removing supplementary " "Kubernetes objects" - ) - utils.remove_k8s_objects(self._stored.namespace) - self.unit.status = MaintenanceStatus("Removing pod") - self._stored.started = False - self._stored.k8s_objects_created = False - - def set_pod_spec(self, image_info): - """Set pod spec.""" - self.model.pod.set_spec(utils.get_pod_spec(image_info, self._stored.secret)) - - -if __name__ == "__main__": - main(MetalLBSpeakerCharm) diff --git a/charms/metallb-speaker/src/utils.py b/charms/metallb-speaker/src/utils.py deleted file mode 100644 index f16798b..0000000 --- a/charms/metallb-speaker/src/utils.py +++ /dev/null @@ -1,384 +0,0 @@ -"""Kubernetes utils library.""" - -import logging -import os -import random -import string -import sys - -from kubernetes import client, config -from kubernetes.client.rest import ApiException - -logger = logging.getLogger(__name__) - - -def create_k8s_objects(namespace): - """Create all supplementary K8s objects.""" - if supports_policy_v1_beta(): - create_pod_security_policy_with_api(namespace=namespace) - else: - logging.info("Not creating PSP, doesn't support policy_v1_beta") - create_namespaced_role_with_api( - name="config-watcher", - namespace=namespace, - labels={"app": "metallb"}, - resources=["configmaps"], - verbs=["get", "list", "watch"], - ) - create_namespaced_role_with_api( - name="pod-lister", - namespace=namespace, - labels={"app": "metallb"}, - resources=["pods"], - verbs=["list"], - ) - create_namespaced_role_binding_with_api( - name="config-watcher", - namespace=namespace, - labels={"app": "metallb"}, - subject_name="metallb-speaker", - ) - create_namespaced_role_binding_with_api( - name="pod-lister", - namespace=namespace, - labels={"app": "metallb"}, - subject_name="metallb-speaker", - ) - - -def remove_k8s_objects(namespace): - """Remove all supplementary K8s objects.""" - if supports_policy_v1_beta(): - delete_pod_security_policy_with_api(name="speaker") - else: - logging.info("Skipping PSP removal, doesn't support policy_v1_beta") - - delete_namespaced_role_binding_with_api(name="config-watcher", namespace=namespace) - delete_namespaced_role_with_api(name="config-watcher", namespace=namespace) - delete_namespaced_role_binding_with_api(name="pod-lister", namespace=namespace) - delete_namespaced_role_with_api(name="pod-lister", namespace=namespace) - - -def create_pod_security_policy_with_api(namespace): - """Create pod security policy.""" - # Using the API because of LP:1886694 - logging.info("Creating pod security policy with K8s API") - _load_kube_config() - - metadata = client.V1ObjectMeta( - namespace=namespace, name="speaker", labels={"app": "metallb"} - ) - policy_spec = client.PolicyV1beta1PodSecurityPolicySpec( - allow_privilege_escalation=False, - allowed_capabilities=[ - "NET_ADMIN", - "NET_RAW", - "SYS_ADMIN", - ], - default_allow_privilege_escalation=False, - fs_group=client.PolicyV1beta1FSGroupStrategyOptions(rule="RunAsAny"), - host_ipc=False, - host_network=True, - host_pid=False, - host_ports=[ - client.PolicyV1beta1HostPortRange( - max=7472, - min=7472, - ) - ], - privileged=True, - read_only_root_filesystem=True, - required_drop_capabilities=["ALL"], - run_as_user=client.PolicyV1beta1RunAsUserStrategyOptions(rule="RunAsAny"), - se_linux=client.PolicyV1beta1SELinuxStrategyOptions( - rule="RunAsAny", - ), - supplemental_groups=client.PolicyV1beta1SupplementalGroupsStrategyOptions( - rule="RunAsAny" - ), - volumes=["configMap", "secret", "emptyDir"], - ) - - body = client.PolicyV1beta1PodSecurityPolicy(metadata=metadata, spec=policy_spec) - - with client.ApiClient() as api_client: - api_instance = client.PolicyV1beta1Api(api_client) - try: - api_instance.create_pod_security_policy(body, pretty=True) - except ApiException as err: - logging.exception( - "Exception when calling PolicyV1beta1Api" - "->create_pod_security_policy." - ) - if err.status != 409: - # Hook error except for 409 (AlreadyExists) errors - sys.exit(1) - - -def delete_pod_security_policy_with_api(name): - """Delete pod security policy.""" - logging.info('Deleting pod security policy named "speaker" with K8s API') - _load_kube_config() - - body = client.V1DeleteOptions() - with client.ApiClient() as api_client: - api_instance = client.PolicyV1beta1Api(api_client) - try: - api_instance.delete_pod_security_policy(name=name, body=body, pretty=True) - except ApiException: - logging.exception( - "Exception when calling PolicyV1beta1Api" - "->delete_pod_security_policy." - ) - - -def create_namespaced_role_with_api( - name, namespace, labels, resources, verbs, api_groups=[""] -): - """Create namespaced role.""" - # Using API because of bug https://bugs.launchpad.net/juju/+bug/1896076 - logging.info("Creating namespaced role with K8s API") - _load_kube_config() - - body = client.V1Role( - metadata=client.V1ObjectMeta(name=name, namespace=namespace, labels=labels), - rules=[ - client.V1PolicyRule( - api_groups=api_groups, - resources=resources, - verbs=verbs, - ) - ], - ) - with client.ApiClient() as api_client: - api_instance = client.RbacAuthorizationV1Api(api_client) - try: - api_instance.create_namespaced_role(namespace, body, pretty=True) - except ApiException as err: - logging.exception( - "Exception when calling RbacAuthorizationV1Api" - "->create_namespaced_role." - ) - if err.status != 409: - # Hook error except for 409 (AlreadyExists) errors - sys.exit(1) - - -def delete_namespaced_role_with_api(name, namespace): - """Delete a namespaced role.""" - logging.info("Deleting namespaced role with K8s API") - _load_kube_config() - - body = client.V1DeleteOptions() - with client.ApiClient() as api_client: - api_instance = client.RbacAuthorizationV1Api(api_client) - try: - api_instance.delete_namespaced_role( - name=name, namespace=namespace, body=body, pretty=True - ) - except ApiException as err: - logging.exception( - "Exception when calling RbacAuthorizationV1Api" - "->delete_namespaced_role." - ) - if err.status != 409: - # Hook error except for 409 (AlreadyExists) errors - sys.exit(1) - - -def create_namespaced_role_binding_with_api( - name, namespace, labels, subject_name, subject_kind="ServiceAccount" -): - """Bind a namespaced role to a subject.""" - # Using API because of bug https://bugs.launchpad.net/juju/+bug/1896076 - logging.info("Creating role binding with K8s API") - _load_kube_config() - - body = client.V1RoleBinding( - metadata=client.V1ObjectMeta(name=name, namespace=namespace, labels=labels), - role_ref=client.V1RoleRef( - api_group="rbac.authorization.k8s.io", - kind="Role", - name=name, - ), - subjects=[ - client.V1Subject(kind=subject_kind, name=subject_name), - ], - ) - with client.ApiClient() as api_client: - api_instance = client.RbacAuthorizationV1Api(api_client) - try: - api_instance.create_namespaced_role_binding(namespace, body, pretty=True) - except ApiException as err: - logging.exception( - "Exception when calling RbacAuthorizationV1Api" - "->create_namespaced_role_binding." - ) - if err.status != 409: - # Hook error except for 409 (AlreadyExists) errors - sys.exit(1) - - -def delete_namespaced_role_binding_with_api(name, namespace): - """Delete namespaced role binding with K8s API.""" - logging.info("Deleting namespaced role binding with API") - _load_kube_config() - - body = client.V1DeleteOptions() - with client.ApiClient() as api_client: - api_instance = client.RbacAuthorizationV1Api(api_client) - try: - api_instance.delete_namespaced_role_binding( - name=name, namespace=namespace, body=body, pretty=True - ) - except ApiException: - logging.exception( - "Exception when calling RbacAuthorizationV1Api->" - "delete_namespaced_role_binding." - ) - - -def supports_policy_v1_beta(): - """Determine if k8s api supports PolicyV1/beta.""" - logging.info("Determine if k8s api supports PolicyV1/beta") - _load_kube_config() - - with client.ApiClient() as api_client: - api_instance = client.PolicyV1beta1Api(api_client) - try: - api_instance.get_api_resources() - except ApiException as err: - if err.status == 404: - return False - return True - - -def get_pod_spec(image_info, secret_key): - """Get pod spec.""" - policyv1_beta = supports_policy_v1_beta() - rules = [ - { - "apiGroups": [""], - "resources": ["services", "endpoints", "nodes"], - "verbs": ["get", "list", "watch"], - }, - { - "apiGroups": [""], - "resources": ["events"], - "verbs": ["create", "patch"], - }, - ] - if policyv1_beta: - logging.info("Appending PSP-related podspec rules, policyv1_beta supported") - rules.append( - { - "apiGroups": ["policy"], - "resourceNames": ["speaker"], - "resources": ["podsecuritypolicies"], - "verbs": ["use"], - } - ) - else: - logging.info("Skipping PSP-related podspec rules, policyv1_beta not supported") - - spec = { - "version": 3, - "serviceAccount": { - "roles": [{"global": True, "rules": rules}], - }, - "containers": [ - { - "name": "speaker", - "imageDetails": image_info, - "imagePullPolicy": "Always", - "ports": [ - { - "containerPort": 7472, - "protocol": "TCP", - "name": "monitoring", - } - ], - "envConfig": { - "METALLB_NODE_NAME": { - "field": {"path": "spec.nodeName", "api-version": "v1"} - }, - "METALLB_HOST": { - "field": {"path": "status.hostIP", "api-version": "v1"} - }, - "METALLB_ML_BIND_ADDR": { - "field": {"path": "status.podIP", "api-version": "v1"} - }, - "METALLB_ML_LABELS": "app=metallb,component=speaker", - "METALLB_ML_NAMESPACE": { - "field": { - "path": "metadata.namespace", - "api-version": "v1", - } - }, - "METALLB_ML_SECRET_KEY": { - "secret": {"name": "memberlist", "key": "secretkey"} - }, - }, - # TODO: add constraint fields once it exists in pod_spec - # bug : https://bugs.launchpad.net/juju/+bug/1893123 - # 'resources': { - # 'limits': { - # 'cpu': '100m', - # 'memory': '100Mi', - # } - # }, - "kubernetes": { - "securityContext": { - "allowPrivilegeEscalation": False, - "readOnlyRootFilesystem": True, - "capabilities": { - "add": ["NET_ADMIN", "NET_RAW", "SYS_ADMIN"], - "drop": ["ALL"], - }, - }, - # fields do not exist in pod_spec - # 'TerminationGracePeriodSeconds': 2, - }, - } - ], - "kubernetesResources": { - "pod": {"hostNetwork": True}, - "secrets": [ - { - "name": "memberlist", - "type": "Opaque", - "data": { - "secretkey": secret_key, - }, - } - ], - }, - "service": { - "annotations": { - "prometheus.io/port": "7472", - "prometheus.io/scrape": "true", - } - }, - } - return spec - - -def _random_secret(length): - letters = string.ascii_letters - result_str = "".join(random.SystemRandom().choice(letters) for i in range(length)) - return result_str - - -def _load_kube_config(): - # TODO: Remove this workaround when bug LP:1892255 is fixed - from pathlib import Path - - os.environ.update( - dict( - e.split("=") - for e in Path("/proc/1/environ").read_text().split("\x00") - if "KUBERNETES_SERVICE" in e - ) - ) - # end workaround - config.load_incluster_config() diff --git a/charms/metallb-speaker/tests/__init__.py b/charms/metallb-speaker/tests/__init__.py deleted file mode 100644 index 600aceb..0000000 --- a/charms/metallb-speaker/tests/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Init mocking for unit tests.""" - -import sys - -import mock - -sys.path.append("src") - -oci_image = mock.MagicMock() -sys.modules["oci_image"] = oci_image diff --git a/charms/metallb-speaker/tests/test_charm.py b/charms/metallb-speaker/tests/test_charm.py deleted file mode 100644 index 13876d1..0000000 --- a/charms/metallb-speaker/tests/test_charm.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Unit tests.""" - -import unittest -from unittest.mock import Mock, patch - -from charm import MetalLBSpeakerCharm - -from ops.testing import Harness - -from utils import get_pod_spec - - -class TestCharm(unittest.TestCase): - """MetalLB Controller Charm Unit Tests.""" - - @patch.dict("charm.os.environ", {"JUJU_MODEL_NAME": "unit-test-metallb"}) - def setUp(self): - """Test setup.""" - self.harness = Harness(MetalLBSpeakerCharm) - self.harness.set_leader(is_leader=True) - self.harness.begin() - - @patch.dict("charm.os.environ", {"JUJU_MODEL_NAME": "unit-test-metallb"}) - @patch("utils.create_namespaced_role_binding_with_api") - @patch("utils.create_namespaced_role_with_api") - @patch("utils.create_pod_security_policy_with_api") - @patch("utils.supports_policy_v1_beta") - def test_on_start_lt_1_25( - self, - supports_policy_v1_beta, - create_psp, - create_ns_role, - create_ns_role_binding, - ): - """Test installation < 1.25.0.""" - supports_policy_v1_beta.return_value = True - mock_pod_spec = self.harness.charm.set_pod_spec = Mock() - self.assertFalse(self.harness.charm._stored.started) - self.harness.charm.on.start.emit() - mock_pod_spec.assert_called_once() - create_psp.assert_called_once() - self.assertEqual(create_ns_role.call_count, 2) - self.assertEqual(create_ns_role_binding.call_count, 2) - self.assertTrue(self.harness.charm._stored.started) - - @patch.dict("charm.os.environ", {"JUJU_MODEL_NAME": "unit-test-metallb"}) - @patch("utils.create_namespaced_role_binding_with_api") - @patch("utils.create_namespaced_role_with_api") - @patch("utils.create_pod_security_policy_with_api") - @patch("utils.supports_policy_v1_beta") - def test_on_start_gte_1_25( - self, - supports_policy_v1_beta, - create_psp, - create_ns_role, - create_ns_role_binding, - ): - """Test installation >= 1.2.50.""" - supports_policy_v1_beta.return_value = False - mock_pod_spec = self.harness.charm.set_pod_spec = Mock() - self.assertFalse(self.harness.charm._stored.started) - self.harness.charm.on.start.emit() - mock_pod_spec.assert_called_once() - create_psp.assert_not_called() - self.assertEqual(create_ns_role.call_count, 2) - self.assertEqual(create_ns_role_binding.call_count, 2) - self.assertTrue(self.harness.charm._stored.started) - - @patch("utils.delete_namespaced_role_with_api") - @patch("utils.delete_namespaced_role_binding_with_api") - @patch("utils.delete_pod_security_policy_with_api") - @patch("utils.supports_policy_v1_beta") - def test_on_remove_lt_1_25( - self, - supports_policy_v1_beta, - delete_psp, - delete_ns_role_binding, - delete_ns_role, - ): - """Test remove hook < 1.25.0.""" - supports_policy_v1_beta.return_value = True - self.harness.charm.on.remove.emit() - delete_psp.assert_called_once() - self.assertEqual(delete_ns_role.call_count, 2) - self.assertEqual(delete_ns_role_binding.call_count, 2) - self.assertFalse(self.harness.charm._stored.started) - - @patch("utils.delete_namespaced_role_with_api") - @patch("utils.delete_namespaced_role_binding_with_api") - @patch("utils.delete_pod_security_policy_with_api") - @patch("utils.supports_policy_v1_beta") - def test_on_remove_gte_1_25( - self, - supports_policy_v1_beta, - delete_psp, - delete_ns_role_binding, - delete_ns_role, - ): - """Test remove hook >= 1.25.0.""" - supports_policy_v1_beta.return_value = False - self.harness.charm.on.remove.emit() - delete_psp.assert_not_called() - self.assertEqual(delete_ns_role.call_count, 2) - self.assertEqual(delete_ns_role_binding.call_count, 2) - self.assertFalse(self.harness.charm._stored.started) - - @patch("utils.supports_policy_v1_beta") - def test_get_pod_spec(self, supports_policy_v1_beta): - """Test pod spec.""" - psp_rule = { - "apiGroups": ["policy"], - "resourceNames": ["speaker"], - "resources": ["podsecuritypolicies"], - "verbs": ["use"], - } - - supports_policy_v1_beta.return_value = True - spec = get_pod_spec("info", "secret") - rules = spec["serviceAccount"]["roles"][0]["rules"] - assert psp_rule in rules - - supports_policy_v1_beta.return_value = False - spec = get_pod_spec("info", "secret") - rules = spec["serviceAccount"]["roles"][0]["rules"] - assert psp_rule not in rules - - -if __name__ == "__main__": - unittest.main() diff --git a/charms/metallb-speaker/tox.ini b/charms/metallb-speaker/tox.ini deleted file mode 100644 index 8f8933c..0000000 --- a/charms/metallb-speaker/tox.ini +++ /dev/null @@ -1,62 +0,0 @@ -[tox] -skipsdist = True -envlist = unit, lint -sitepackages = False -skip_missing_interpreters = False - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}/src - CHARM_NAME = metallb-speaker - -[testenv:unit] -commands = - pipenv install --dev --ignore-pipfile - coverage erase - stestr run --slowest --test-path=./tests --top-dir=./ - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report -deps = pipenv -setenv = - {[testenv]setenv} - PYTHON=coverage run - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - -[testenv:lint] -commands = - flake8 - black --check {toxinidir} -deps = - flake8 - flake8-docstrings - flake8-import-order - pep8-naming - flake8-colors - black - -[flake8] -ignore = - # line break after binary operator - W504, -exclude = - .git, - __pycache__, - .tox, - mod, - .history, - build, - .build, -max-line-length = 88 -max-complexity = 10 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..7763150 --- /dev/null +++ b/config.yaml @@ -0,0 +1,36 @@ +# 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: + namespace: + type: string + description: | + Namespace that the metallb resources will be installed in. This namespace will be created by the charm, + and should not currently exist + default: "metallb-system" + + image-registry: + type: string + description: | + Image registry for metallb container images. + The value set here will replace the host portion of each image URL in + the release manifests. + default: "rocks.canonical.com:443/cdk" + + metallb-release: + type: string + description: | + Specify the version of metallb to deploy. The version must be available in the upstream/metallb-native/manifests + directory of the charm source code in order to be deployed + default: "v0.13.10" + + iprange: + type: string + description: | + Comma-separated list of CIDRs and/or IPV4 and IPV6 ranges that define the IP addresses + MetalLB will assign to services + Example: + 192.168.10.0/24,192.168.9.1-192.168.9.5,fc00:f853:0ccd:e799::/124 + default: 192.168.1.240-192.168.1.247 \ No newline at end of file diff --git a/docs/local-overlay.yaml b/docs/local-overlay.yaml deleted file mode 100644 index 45fefa1..0000000 --- a/docs/local-overlay.yaml +++ /dev/null @@ -1,17 +0,0 @@ -description: | - A local overlay to deploy the charms from this repo. This is meant for developers. - The appropriate steps to use this would be to: - 1) pull the repo locally - 2) cd metallb-operator - 2) build the charms with `make charms` - 3) deploy with the overlay: - `juju deploy ./bundle --overlay ./docs/local-overlay.yaml` -applications: - metallb-controller: - charm: ../metallb-controller.charm - resources: - metallb-controller-image: 'metallb/controller:v0.12' - metallb-speaker: - charm: ../metallb-speaker.charm - resources: - metallb-speaker-image: 'metallb/speaker:v0.12' diff --git a/docs/rbac-permissions-operators.yaml b/docs/rbac-permissions-operators.yaml deleted file mode 100644 index 378af39..0000000 --- a/docs/rbac-permissions-operators.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# If RBAC is enabled in the cluster, then the operator pod for both -# metallb-controller and metallb-speaker need to be granted permission to # use the K8s API for some specific actions. -# This step is not automated because once bug LP:1896076 and LP:1886694 are fixed, it will no -# longer be necessary to use the K8s API. ---- -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: use-k8s-api -rules: -- apiGroups: ["policy", "rbac.authorization.k8s.io"] - resources: ["podsecuritypolicies", "roles", "rolebindings"] - verbs: ["create", "delete"] -- apiGroups: [""] - resources: ["configmaps"] - verbs: ["get", "list", "watch"] ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: use-k8s-api -subjects: -- kind: ServiceAccount - name: metallb-controller-operator - # change namespace name according to your environment - namespace: metallb-system -- kind: ServiceAccount - name: metallb-speaker-operator - # change namespace name according to your environment - namespace: metallb-system -- kind: ServiceAccount - name: metallb-controller - # change namespace name according to your environment - namespace: metallb-system -- kind: ServiceAccount - name: metallb-speaker - # change namespace name according to your environment - namespace: metallb-system -roleRef: - kind: ClusterRole - name: use-k8s-api - apiGroup: rbac.authorization.k8s.io diff --git a/metadata.yaml b/metadata.yaml new file mode 100644 index 0000000..48e08c8 --- /dev/null +++ b/metadata.yaml @@ -0,0 +1,21 @@ +name: metallb +summary: | + This charm deploys MetalLB in a Kubernetes model, which provides a software + defined load balancer. +docs: https://discourse.charmhub.io/t/metallb/6320 +issues: https://bugs.launchpad.net/operator-metallb +source: https://github.com/charmed-kubernetes/metallb-operator +website: https://metallb.universe.tf/ +description: | + MetalLB offers a software network load balancing implementation that allows for + LoadBalancing services in Kubernetes. It is a young open-source project that could + be charmed to integrate it easily with the Canonical suite of projects. Upstream + documentation can be found here : https://metallb.universe.tf/. + The charm includes both the cluster-wide controller that handles IP address + assignments, along with the speaker DaemonSet that makes the services + reachable. +tags: + - kubernetes + - metallb +assumes: + - k8s-api diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3998e7e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Formatting tools configuration +[tool.black] +line-length = 99 +target-version = ["py38"] + +# Linting tools configuration +[tool.ruff] +line-length = 99 +select = ["E", "W", "F", "C", "N", "D", "I001"] +extend-ignore = [ + "D203", + "D204", + "D213", + "D215", + "D400", + "D404", + "D406", + "D407", + "D408", + "D409", + "D413", +] +ignore = ["E501", "D107"] +extend-exclude = ["__pycache__", "*.egg_info"] +per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} + +[tool.ruff.mccabe] +max-complexity = 10 + +[tool.codespell] +skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" + +[tool.flake8] +ignore = ['D100', 'E501', 'D103', 'D107', 'D101', 'D102', 'W503'] + +[tool.isort] +line_length = 120 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4a34a66 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +ops >= 2.2.0 +lightkube>=0.10.1,<1.0.0 +pyyaml +ops.manifest>=1.1.0,<2.0.0 +tenacity \ No newline at end of file diff --git a/src/charm.py b/src/charm.py new file mode 100755 index 0000000..a451180 --- /dev/null +++ b/src/charm.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. +# +# Learn more at: https://juju.is/docs/sdk + +import ipaddress +import logging + +import ops +from lightkube import Client +from lightkube.core.exceptions import ApiError +from lightkube.generic_resource import create_namespaced_resource +from ops import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus +from ops.main import main +from ops.manifests import Collector, Manifests +from tenacity import before_log, retry, retry_if_exception_type, wait_exponential + +from metallb_manifests import MetallbNativeManifest + +# Log messages can be retrieved using juju debug-log +logger = logging.getLogger(__name__) + + +def _missing_resources(manifest: Manifests): + expected = manifest.resources + installed = manifest.installed_resources() + missing = expected - installed + return missing + + +def _is_ip_address(str_to_test): + try: + ipaddress.ip_address(str_to_test) + return True + except ValueError: + return False + + +def _is_ip_address_range(str_to_test): + addresses = str_to_test.split("-") + if len(addresses) != 2: + return False + + for address in addresses: + if not _is_ip_address(address): + return False + + return True + + +def _is_cidr(str_to_test): + try: + ipaddress.ip_network(str_to_test) + return True + except ValueError: + return False + + +def validate_iprange(iprange): + if not iprange: + return False, "iprange must not be empty" + + items = iprange.split(",") + for item in items: + is_ip_range = _is_ip_address_range(item) + is_cidr = _is_cidr(item) + if not is_ip_range and not is_cidr: + return False, f"{item} is not a valid CIDR or ip range" + + return True, "" + + +class MetallbCharm(ops.CharmBase): + """Charm the service.""" + + def __init__(self, *args): + super().__init__(*args) + if not self.unit.is_leader(): + self.unit.status = BlockedStatus("MetalLB charm cannot be scaled > n1.") + logger.error(f"{self} was initialized without leadership.") + return + + self.native_manifest = MetallbNativeManifest(self, self.config) + self.native_collector = Collector(self.native_manifest) + self.client = Client(namespace=self.model.name, field_manager=self.app.name) + self.pool_name = f"{self.model.name}-{self.app.name}" + # Create generic lightkube resource class for the MetalLB IPAddressPool + # https://metallb.universe.tf/configuration/ + self.IPAddressPool = create_namespaced_resource( + group="metallb.io", + version="v1beta1", + kind="IPAddressPool", + plural="ipaddresspools", + ) + + self.framework.observe(self.on.install, self._install_or_upgrade) + self.framework.observe(self.on.config_changed, self._on_config_changed) + self.framework.observe(self.on.upgrade_charm, self._install_or_upgrade) + self.framework.observe(self.on.update_status, self._update_status) + self.framework.observe(self.on.remove, self._cleanup) + + def _update_status(self, _): + missing = _missing_resources(self.native_manifest) + if len(missing) != 0: + logger.error(f"missing MetalLB resources: {missing}") + self.unit.status = BlockedStatus( + "missing \n".join(sorted(str(rsc) for rsc in missing)) + ) + return + + native_unready = self.native_collector.unready + if native_unready: + logger.warning(f"Unready MetalLB resources: {native_unready}") + self.unit.status = WaitingStatus(", ".join(native_unready)) + return + + try: + self.client.get( + self.IPAddressPool, name=self.pool_name, namespace=self.config["namespace"] + ) + except ApiError as e: + if "not found" in e.status.message: + logger.info("IPAddressPool not found yet") + self.unit.status = WaitingStatus("Waiting for IPAddressPool to be created") + return + else: + # surface any other errors besides not found + logger.exception(e) + self.unit.status = WaitingStatus("Waiting for Kubernetes API") + return + self.unit.status = ActiveStatus("Ready") + self.unit.set_workload_version(self.native_collector.short_version) + self.app.status = ActiveStatus(self.native_collector.long_version) + + def _install_or_upgrade(self, event): + logger.info("Installing MetalLB native manifest resources ...") + self.native_manifest.apply_manifests() + logger.info("MetalLB native manifest has been installed") + + def _cleanup(self, event): + self.unit.status = MaintenanceStatus("Cleaning up MetalLB resources") + self.native_manifest.delete_manifests(ignore_unauthorized=True, ignore_not_found=True) + self.unit.status = MaintenanceStatus("Shutting down") + + def _on_config_changed(self, event): + logger.info("Updating MetalLB IPAddressPool to reflect charm configuration") + # strip all whitespace from string + stripped = "".join(self.config["iprange"].split()) + valid_iprange, msg = validate_iprange(stripped) + if not valid_iprange: + err_msg = f"Invalid iprange: {msg}" + logger.error(err_msg) + self.unit.status = BlockedStatus(err_msg) + return + + addresses = stripped.split(",") + self._update_ip_pool(addresses) + self.unit.status = ActiveStatus() + + # retrying is necessary as the ip address pool webhooks take some time to come up + @retry( + retry=retry_if_exception_type(ApiError), + reraise=True, + before=before_log(logger, logging.INFO), + wait=wait_exponential(multiplier=1, min=2, max=60 * 2), + ) + def _update_ip_pool(self, addresses): + ip_pool = self.IPAddressPool( + metadata={"name": self.pool_name, "namespace": self.config["namespace"]}, + spec={"addresses": addresses}, + ) + + self.client.apply(ip_pool, force=True) + + +if __name__ == "__main__": # pragma: nocover + main(MetallbCharm) diff --git a/src/metallb_manifests.py b/src/metallb_manifests.py new file mode 100644 index 0000000..fde4d49 --- /dev/null +++ b/src/metallb_manifests.py @@ -0,0 +1,89 @@ +import logging +from typing import Dict + +from lightkube.codecs import AnyResource +from ops.manifests import ConfigRegistry, ManifestLabel, Manifests, Patch + +logger = logging.getLogger(__name__) + + +class PatchNamespace(Patch): + def __call__(self, obj: AnyResource): + ns_name = self.manifests.config["namespace"] + # Patch the namespace object itself + if obj.kind == "Namespace": + logger.info(f"Patching namespace name for {obj.kind} {obj.metadata.name} to {ns_name}") + obj.metadata.name = ns_name + return + + # Patch the addresspools CRD + if obj.metadata.name == "addresspools.metallb.io": + logger.info(f"Patching namespace for {obj.kind} {obj.metadata.name} to {ns_name}") + obj.spec.conversion.webhook.clientConfig.service.namespace = ns_name + return + + # Patch the bgppeers CRD + if obj.metadata.name == "bgppeers.metallb.io": + logger.info(f"Patching namespace for {obj.kind} {obj.metadata.name} to {ns_name}") + obj.spec.conversion.webhook.clientConfig.service.namespace = ns_name + return + + # patch ns in webhook configs + if ( + obj.kind == "ValidatingWebhookConfiguration" + or obj.kind == "MutatingWebhookConfiguration" + ): + for webhook in obj.webhooks: + logger.info( + f"Patching clientConfig service namespace for {obj.kind} {obj.metadata.name} to {ns_name}" + ) + webhook.clientConfig.service.namespace = ns_name + + # patch ns in RoleBinding (both ns and subjects ns) + if obj.kind == "RoleBinding": + logger.info(f"Patching namespace for {obj.kind} {obj.metadata.name} to {ns_name}") + obj.metadata.namespace = ns_name + for subject in obj.subjects: + logger.info( + f"Patching subject namespace for {subject.kind} {subject.name} to {ns_name}" + ) + subject.namespace = ns_name + return + + # patch ns in ClusterRoleBinding subjects + if obj.kind == "ClusterRoleBinding": + for subject in obj.subjects: + logger.info( + f"Patching subject namespace for {subject.kind} {subject.name} to {ns_name}" + ) + subject.namespace = ns_name + return + + # Patch any resources with a namespace in their metadata + if obj.metadata.namespace: + logger.info(f"Patching namespace for {obj.kind} {obj.metadata.name} to {ns_name}") + obj.metadata.namespace = ns_name + return + + +class MetallbNativeManifest(Manifests): + def __init__(self, charm, charm_config): + manipulations = [ + ManifestLabel(self), + ConfigRegistry(self), + PatchNamespace(self), + ] + + super().__init__("metallb", charm.model, "upstream/metallb-native", manipulations) + self.charm_config = charm_config + + @property + def config(self) -> Dict: + """Returns config mapped from charm config and joined relations.""" + config = dict(**self.charm_config) + for key, value in dict(**config).items(): + if value == "" or value is None: + del config[key] # blank out keys not currently set to something + + config["release"] = config.pop("metallb-release", None) + return config diff --git a/docs/example-microbot-lb.yaml b/tests/data/microbot.yaml similarity index 76% rename from docs/example-microbot-lb.yaml rename to tests/data/microbot.yaml index dea98a8..f15af61 100644 --- a/docs/example-microbot-lb.yaml +++ b/tests/data/microbot.yaml @@ -1,16 +1,22 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: microbot +--- apiVersion: apps/v1 -kind: Deployment +kind: Deployment metadata: + namespace: microbot creationTimestamp: null - labels: - app: microbot-lb + labels: + app: microbot-lb name: microbot-lb -spec: - replicas: 3 - selector: - matchLabels: +spec: + replicas: 3 + selector: + matchLabels: app: microbot-lb - strategy: {} + strategy: {} template: metadata: creationTimestamp: null @@ -38,6 +44,7 @@ apiVersion: v1 kind: Service metadata: name: microbot-lb + namespace: microbot spec: type: LoadBalancer selector: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..b68c41d --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,48 @@ +import logging +from pathlib import Path + +import pytest +import pytest_asyncio +from lightkube import Client, codecs +from lightkube.generic_resource import create_namespaced_resource +from lightkube.resources.apps_v1 import Deployment +from lightkube.resources.core_v1 import Service + +logger = logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +def client(): + return Client() + + +@pytest.fixture(scope="module") +def ip_address_pool(): + return create_namespaced_resource( + group="metallb.io", + version="v1beta1", + kind="IPAddressPool", + plural="ipaddresspools", + ) + + +@pytest_asyncio.fixture(scope="function") +async def microbot_service_ip(client): + logger.info("Creating microbot resources ...") + path = Path("tests/data/microbot.yaml") + for obj in codecs.load_all_yaml(path.read_text()): + if obj.kind == "Namespace": + namespace = obj.metadata.name + client.create(obj) + + client.wait(Deployment, "microbot-lb", for_conditions=["Available"], namespace=namespace) + logger.info("Microbot deployment is now available") + + svc = client.get(Service, name="microbot-lb", namespace="microbot") + ingress_ip = svc.status.loadBalancer.ingress[0].ip + + yield ingress_ip + + logger.info("Deleting microbot resources ...") + for obj in codecs.load_all_yaml(path.read_text()): + client.delete(type(obj), obj.metadata.name, namespace=obj.metadata.namespace) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py new file mode 100644 index 0000000..f0bb6e6 --- /dev/null +++ b/tests/integration/test_charm.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# Copyright 2023 Stone +# See LICENSE file for licensing details. +import asyncio +import logging +from pathlib import Path + +import aiohttp +import pytest +import yaml +from pytest_operator.plugin import OpsTest + +logger = logging.getLogger(__name__) + +METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) +APP_NAME = METADATA["name"] +NAMESPACE = "metallb-system-test" + + +@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. + + Assert on the unit status before any relations/configurations take place. + """ + # Build and deploy charm from local source folder + charm = await ops_test.build_charm(".") + + # Deploy the charm and wait for active/idle status + await asyncio.gather( + ops_test.model.deploy( + charm, application_name=APP_NAME, trust=True, config={"namespace": NAMESPACE} + ), + ops_test.model.wait_for_idle( + apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1500 + ), + ) + + +async def test_iprange_config_option(ops_test: OpsTest, client, ip_address_pool): + # test that default option is applied correctly before changing + pool_name = f"{ops_test.model_name}-{APP_NAME}" + pool = client.get(ip_address_pool, name=pool_name, namespace=NAMESPACE) + assert pool.spec["addresses"][0] == "192.168.1.240-192.168.1.247" + + app = ops_test.model.applications[APP_NAME] + logger.info("Updating iprange ...") + await app.set_config( + { + "iprange": "10.1.240.240-10.1.240.241", + } + ) + await ops_test.model.wait_for_idle(status="active", timeout=60 * 10) + pool = client.get(ip_address_pool, name=pool_name, namespace=NAMESPACE) + assert pool.spec["addresses"][0] == "10.1.240.240-10.1.240.241" + + +async def test_loadbalancer_service(ops_test: OpsTest, client, microbot_service_ip): + logger.info("Testing microbot load balancer service") + timeout = aiohttp.ClientTimeout(connect=30) + async with aiohttp.request("GET", f"http://{microbot_service_ip}", timeout=timeout) as resp: + logger.info(f"response: {resp}") + assert resp.status == 200 diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..cac3069 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,19 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. +import unittest.mock as mock + +import pytest + + +# Autouse to prevent calling out to the k8s API via lightkube client in manifests +@pytest.fixture(autouse=True) +def lk_manifests_client(): + with mock.patch("ops.manifests.manifest.Client", autospec=True) as mock_lightkube: + yield mock_lightkube.return_value + + +# Autouse to prevent calling out to the k8s API via lightkube client in charm +@pytest.fixture(autouse=True) +def lk_charm_client(): + with mock.patch("charm.Client", autospec=True) as mock_lightkube: + yield mock_lightkube.return_value diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py new file mode 100644 index 0000000..4fa85d0 --- /dev/null +++ b/tests/unit/test_charm.py @@ -0,0 +1,215 @@ +# Copyright 2023 Stone +# See LICENSE file for licensing details. +# +# Learn more about testing at: https://juju.is/docs/sdk/testing + +import unittest.mock as mock + +import ops +import ops.testing +import pytest +import yaml +from lightkube.core.exceptions import ApiError +from ops import ActiveStatus, BlockedStatus, WaitingStatus +from ops.testing import Harness + +from charm import MetallbCharm +from metallb_manifests import MetallbNativeManifest + +ops.testing.SIMULATE_CAN_CONNECT = True + + +@pytest.fixture +def harness(): + harness = Harness(MetallbCharm) + try: + yield harness + finally: + harness.cleanup() + + +def test_not_leader(harness): + harness.begin() + assert harness.charm.model.unit.status == BlockedStatus("MetalLB charm cannot be scaled > n1.") + + +def test_install_applies_manifest_objects(harness, lk_manifests_client): + # Test that the install-handler applies the objects specified in the manifest + lk_manifests_client.reset_mock() + harness.set_leader(True) + harness.begin() + harness.charm.on.install.emit() + + version = harness.charm.config["metallb-release"] + file_name = f"upstream/metallb-native/manifests/{version}/metallb-native.yaml" + expected_objects = list(yaml.safe_load_all(open(file_name))) + + # the apply method is called for every object in the manifest + for call in lk_manifests_client.apply.call_args_list: + # The first (and only) argument to the apply method is the obj + call_obj = call.args[0].to_dict() + # look for this object in the manifest by name + # the manifest objects are manipulated slightly (image registries changed, labels added, etc) + # so it won't be an EXACT match, but each object should still be accounted for + found = False + for obj in expected_objects: + if obj["metadata"]["name"] == call_obj["metadata"]["name"]: + found = True + assert found + + +def test_config_change_updates_ip_pool(harness, lk_charm_client): + lk_charm_client.reset_mock() + + harness.set_leader(True) + harness.begin() + + # test with single ip range + harness.update_config({"iprange": "10.1.240.240-10.1.240.241"}) + harness.charm.on.config_changed.emit() + for call in lk_charm_client.apply.call_args_list: + call_obj = call.args[0].to_dict() + assert len(call_obj["spec"]["addresses"]) == 1 + assert call_obj["spec"]["addresses"][0] == "10.1.240.240-10.1.240.241" + + # test with multiple ranges + lk_charm_client.reset_mock() + harness.update_config( + { + "iprange": "192.168.1.240-192.168.1.247,10.1.240.240-10.1.240.241,192.168.10.0/24,fc00:f853:0ccd:e799::/124" + } + ) + harness.charm.on.config_changed.emit() + for call in lk_charm_client.apply.call_args_list: + call_obj = call.args[0].to_dict() + assert len(call_obj["spec"]["addresses"]) == 4 + assert call_obj["spec"]["addresses"][0] == "192.168.1.240-192.168.1.247" + assert call_obj["spec"]["addresses"][1] == "10.1.240.240-10.1.240.241" + assert call_obj["spec"]["addresses"][2] == "192.168.10.0/24" + assert call_obj["spec"]["addresses"][3] == "fc00:f853:0ccd:e799::/124" + + # test with multiple ranges with spaces thrown in + lk_charm_client.reset_mock() + harness.update_config( + { + "iprange": " 192. 168.1.240-192. 168.1.247, 10.1.240.240 -10.1.240.241, 192.168.10.0/24,fc00:f853:0ccd:e799::/124 " + } + ) + harness.charm.on.config_changed.emit() + for call in lk_charm_client.apply.call_args_list: + call_obj = call.args[0].to_dict() + assert len(call_obj["spec"]["addresses"]) == 4 + assert call_obj["spec"]["addresses"][0] == "192.168.1.240-192.168.1.247" + assert call_obj["spec"]["addresses"][1] == "10.1.240.240-10.1.240.241" + assert call_obj["spec"]["addresses"][2] == "192.168.10.0/24" + assert call_obj["spec"]["addresses"][3] == "fc00:f853:0ccd:e799::/124" + + # test with an empty range + lk_charm_client.reset_mock() + harness.update_config({"iprange": ""}) + harness.charm.on.config_changed.emit() + assert harness.charm.model.unit.status == BlockedStatus( + "Invalid iprange: iprange must not be empty" + ) + + # test with an invalid range + lk_charm_client.reset_mock() + harness.update_config({"iprange": "256.256.256.256-256.256.256.256,10.1.240.240-10.1.240.241"}) + harness.charm.on.config_changed.emit() + assert harness.charm.model.unit.status == BlockedStatus( + "Invalid iprange: 256.256.256.256-256.256.256.256 is not a valid CIDR or ip range" + ) + + # test with an invalid separator in range + lk_charm_client.reset_mock() + harness.update_config({"iprange": "10.1.240.240+10.1.240.241"}) + harness.charm.on.config_changed.emit() + assert harness.charm.model.unit.status == BlockedStatus( + "Invalid iprange: 10.1.240.240+10.1.240.241 is not a valid CIDR or ip range" + ) + + # test with an invalid CIDR + lk_charm_client.reset_mock() + harness.update_config({"iprange": "256.256.256.256/24"}) + harness.charm.on.config_changed.emit() + assert harness.charm.model.unit.status == BlockedStatus( + "Invalid iprange: 256.256.256.256/24 is not a valid CIDR or ip range" + ) + + +def test_remove_deletes_manifest_objects(harness, lk_manifests_client): + # Test that the remove-handler deletes the objects specified in the manifest + lk_manifests_client.reset_mock() + harness.set_leader(True) + harness.begin() + harness.charm.on.remove.emit() + + version = harness.charm.config["metallb-release"] + file_name = f"upstream/metallb-native/manifests/{version}/metallb-native.yaml" + actual_kind_name_list = [] + expected_objects = list(yaml.safe_load_all(open(file_name))) + expected_kind_name_list = [] + for obj in expected_objects: + kind_name = {"kind": obj["kind"], "name": obj["metadata"]["name"]} + expected_kind_name_list.append(kind_name) + + for call in lk_manifests_client.return_value.delete.call_args_list: + # The first argument is the resource class + # The second argument is the object name + kind_name = {"kind": call.args[0].__name__, "name": call.args[1]} + actual_kind_name_list.append(kind_name) + + +def test_update_status(harness, lk_manifests_client, lk_charm_client): + lk_manifests_client.reset_mock() + harness.set_leader(True) + harness.begin() + + # With nothing mocked, all resources will appear as missing + harness.charm.on.update_status.emit() + assert "missing" in harness.charm.model.unit.status.message + assert harness.charm.model.unit.status.name == "blocked" + + # mock to get past missing resources code path + with mock.patch("charm._missing_resources", autospec=True) as mock_missing: + mock_missing.return_value = [] + # Test path where some resources are not ready + with mock.patch.object(harness.charm, "native_collector") as mock_collector: + mock_collector.unready = [ + "some_name: some_obj is not Ready", + "other_name: other_obj is not Ready", + ] + harness.charm.on.update_status.emit() + assert harness.charm.model.unit.status == WaitingStatus( + "some_name: some_obj is not Ready, other_name: other_obj is not Ready" + ) + + # test path where APIError occurs during IP Address Pool lookup + # test path where ip address pool is not found + lk_charm_client.reset_mock() + api_error = ApiError(response=mock.MagicMock()) + api_error.status.message = "not found" + lk_charm_client.get.side_effect = api_error + harness.charm.on.update_status.emit() + assert harness.charm.model.unit.status == WaitingStatus( + "Waiting for IPAddressPool to be created" + ) + + # test path where some other API error occurs + api_error.status.message = "something else happened" + lk_charm_client.get.side_effect = api_error + harness.charm.on.update_status.emit() + assert harness.charm.model.unit.status == WaitingStatus("Waiting for Kubernetes API") + + # test ready path + lk_charm_client.get.side_effect = None + harness.charm.on.update_status.emit() + assert harness.charm.model.unit.status == ActiveStatus("Ready") + + +def test_empty_config_option_not_used_by_manifest(harness): + # Not super important, but can't get 100% coverage without it + harness.update_config({"iprange": ""}) + harness.begin() + manifest = MetallbNativeManifest(harness.charm, harness.charm.config) + assert "iprange" not in manifest.config diff --git a/tox.ini b/tox.ini index 6e7ba4f..8b6a2c3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,36 +1,77 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + [tox] -skipsdist = True +skipsdist=True +skip_missing_interpreters = True envlist = lint, unit -sitepackages = False -skip_missing_interpreters = False + +[vars] +src_path = {toxinidir}/src/ +tst_path = {toxinidir}/tests/ +;lib_path = {toxinidir}/lib/charms/operator_name_with_underscores +all_path = {[vars]src_path} {[vars]tst_path} [testenv] -basepython = python3 +setenv = + PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} + PYTHONBREAKPOINT=pdb.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] -allowlist_externals = tox +description = Check code against coding style standards +deps = + black + flake8-docstrings + flake8-builtins + pyproject-flake8 + pep8-naming + isort + codespell commands = - tox -c {toxinidir}/charms/metallb-controller -e lint - tox -c {toxinidir}/charms/metallb-speaker -e lint + # uncomment the following line if this charm owns a lib + # codespell {[vars]lib_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 \ + --skip "*.yaml" + # pflake8 wrapper supports config from pyproject.toml + pflake8 {[vars]all_path} --classmethod-decorator=classmethod,validator + isort --check-only --diff {[vars]all_path} + black --check --diff {[vars]all_path} [testenv:unit] -allowlist_externals = tox +description = Run unit tests +deps = + pytest + coverage[toml] + -r{toxinidir}/requirements.txt commands = - tox -c {toxinidir}/charms/metallb-controller -e unit - tox -c {toxinidir}/charms/metallb-speaker -e unit - + coverage run --source={[vars]src_path} \ + -m pytest --ignore={[vars]tst_path}integration -vv --tb native -s {posargs} + coverage report [testenv:integration] -setenv = - PYTHONBREAKPOINT=ipdb.set_trace -passenv = - HOME +description = Run integration tests deps = - pyyaml + aiohttp pytest + juju pytest-operator - aiohttp - juju < 3.1 - ipdb -commands = pytest --tb native --show-capture=no --log-cli-level=INFO -s bundle/tests {posargs} - + pytest-asyncio + -r{toxinidir}/requirements.txt +commands = + pytest -v --tb native --asyncio-mode=auto --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} diff --git a/upstream/metallb-native/manifests/v0.13.10/metallb-native.yaml b/upstream/metallb-native/manifests/v0.13.10/metallb-native.yaml new file mode 100644 index 0000000..de6ca21 --- /dev/null +++ b/upstream/metallb-native/manifests/v0.13.10/metallb-native.yaml @@ -0,0 +1,2042 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + pod-security.kubernetes.io/audit: privileged + pod-security.kubernetes.io/enforce: privileged + pod-security.kubernetes.io/warn: privileged + name: metallb-system +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: addresspools.metallb.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== + service: + name: webhook-service + namespace: metallb-system + path: /convert + conversionReviewVersions: + - v1alpha1 + - v1beta1 + group: metallb.io + names: + kind: AddressPool + listKind: AddressPoolList + plural: addresspools + singular: addresspool + scope: Namespaced + versions: + - deprecated: true + deprecationWarning: metallb.io v1alpha1 AddressPool is deprecated + name: v1alpha1 + schema: + openAPIV3Schema: + description: AddressPool is the Schema for the addresspools API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: AddressPoolSpec defines the desired state of AddressPool. + properties: + addresses: + description: A list of IP address ranges over which MetalLB has authority. + You can list multiple ranges in a single pool, they will all share + the same settings. Each range can be either a CIDR prefix, or an + explicit start-end range of IPs. + items: + type: string + type: array + autoAssign: + default: true + description: AutoAssign flag used to prevent MetallB from automatic + allocation for a pool. + type: boolean + bgpAdvertisements: + description: When an IP is allocated from this pool, how should it + be translated into BGP announcements? + items: + properties: + aggregationLength: + default: 32 + description: The aggregation-length advertisement option lets + you “roll up” the /32s into a larger prefix. + format: int32 + minimum: 1 + type: integer + aggregationLengthV6: + default: 128 + description: Optional, defaults to 128 (i.e. no aggregation) + if not specified. + format: int32 + type: integer + communities: + description: BGP communities + items: + type: string + type: array + localPref: + description: BGP LOCAL_PREF attribute which is used by BGP best + path algorithm, Path with higher localpref is preferred over + one with lower localpref. + format: int32 + type: integer + type: object + type: array + protocol: + description: Protocol can be used to select how the announcement is + done. + enum: + - layer2 + - bgp + type: string + required: + - addresses + - protocol + type: object + status: + description: AddressPoolStatus defines the observed state of AddressPool. + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} + - deprecated: true + deprecationWarning: metallb.io v1beta1 AddressPool is deprecated, consider using + IPAddressPool + name: v1beta1 + schema: + openAPIV3Schema: + description: AddressPool represents a pool of IP addresses that can be allocated + to LoadBalancer services. AddressPool is deprecated and being replaced by + IPAddressPool. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: AddressPoolSpec defines the desired state of AddressPool. + properties: + addresses: + description: A list of IP address ranges over which MetalLB has authority. + You can list multiple ranges in a single pool, they will all share + the same settings. Each range can be either a CIDR prefix, or an + explicit start-end range of IPs. + items: + type: string + type: array + autoAssign: + default: true + description: AutoAssign flag used to prevent MetallB from automatic + allocation for a pool. + type: boolean + bgpAdvertisements: + description: Drives how an IP allocated from this pool should translated + into BGP announcements. + items: + properties: + aggregationLength: + default: 32 + description: The aggregation-length advertisement option lets + you “roll up” the /32s into a larger prefix. + format: int32 + minimum: 1 + type: integer + aggregationLengthV6: + default: 128 + description: Optional, defaults to 128 (i.e. no aggregation) + if not specified. + format: int32 + type: integer + communities: + description: BGP communities to be associated with the given + advertisement. + items: + type: string + type: array + localPref: + description: BGP LOCAL_PREF attribute which is used by BGP best + path algorithm, Path with higher localpref is preferred over + one with lower localpref. + format: int32 + type: integer + type: object + type: array + protocol: + description: Protocol can be used to select how the announcement is + done. + enum: + - layer2 + - bgp + type: string + required: + - addresses + - protocol + type: object + status: + description: AddressPoolStatus defines the observed state of AddressPool. + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: bfdprofiles.metallb.io +spec: + group: metallb.io + names: + kind: BFDProfile + listKind: BFDProfileList + plural: bfdprofiles + singular: bfdprofile + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.passiveMode + name: Passive Mode + type: boolean + - jsonPath: .spec.transmitInterval + name: Transmit Interval + type: integer + - jsonPath: .spec.receiveInterval + name: Receive Interval + type: integer + - jsonPath: .spec.detectMultiplier + name: Multiplier + type: integer + name: v1beta1 + schema: + openAPIV3Schema: + description: BFDProfile represents the settings of the bfd session that can + be optionally associated with a BGP session. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BFDProfileSpec defines the desired state of BFDProfile. + properties: + detectMultiplier: + description: Configures the detection multiplier to determine packet + loss. The remote transmission interval will be multiplied by this + value to determine the connection loss detection timer. + format: int32 + maximum: 255 + minimum: 2 + type: integer + echoInterval: + description: Configures the minimal echo receive transmission interval + that this system is capable of handling in milliseconds. Defaults + to 50ms + format: int32 + maximum: 60000 + minimum: 10 + type: integer + echoMode: + description: Enables or disables the echo transmission mode. This + mode is disabled by default, and not supported on multi hops setups. + type: boolean + minimumTtl: + description: 'For multi hop sessions only: configure the minimum expected + TTL for an incoming BFD control packet.' + format: int32 + maximum: 254 + minimum: 1 + type: integer + passiveMode: + description: 'Mark session as passive: a passive session will not + attempt to start the connection and will wait for control packets + from peer before it begins replying.' + type: boolean + receiveInterval: + description: The minimum interval that this system is capable of receiving + control packets in milliseconds. Defaults to 300ms. + format: int32 + maximum: 60000 + minimum: 10 + type: integer + transmitInterval: + description: The minimum transmission interval (less jitter) that + this system wants to use to send BFD control packets in milliseconds. + Defaults to 300ms + format: int32 + maximum: 60000 + minimum: 10 + type: integer + type: object + status: + description: BFDProfileStatus defines the observed state of BFDProfile. + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: bgpadvertisements.metallb.io +spec: + group: metallb.io + names: + kind: BGPAdvertisement + listKind: BGPAdvertisementList + plural: bgpadvertisements + singular: bgpadvertisement + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.ipAddressPools + name: IPAddressPools + type: string + - jsonPath: .spec.ipAddressPoolSelectors + name: IPAddressPool Selectors + type: string + - jsonPath: .spec.peers + name: Peers + type: string + - jsonPath: .spec.nodeSelectors + name: Node Selectors + priority: 10 + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: BGPAdvertisement allows to advertise the IPs coming from the + selected IPAddressPools via BGP, setting the parameters of the BGP Advertisement. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BGPAdvertisementSpec defines the desired state of BGPAdvertisement. + properties: + aggregationLength: + default: 32 + description: The aggregation-length advertisement option lets you + “roll up” the /32s into a larger prefix. Defaults to 32. Works for + IPv4 addresses. + format: int32 + minimum: 1 + type: integer + aggregationLengthV6: + default: 128 + description: The aggregation-length advertisement option lets you + “roll up” the /128s into a larger prefix. Defaults to 128. Works + for IPv6 addresses. + format: int32 + type: integer + communities: + description: The BGP communities to be associated with the announcement. + Each item can be a standard community of the form 1234:1234, a large + community of the form large:1234:1234:1234 or the name of an alias + defined in the Community CRD. + items: + type: string + type: array + ipAddressPoolSelectors: + description: A selector for the IPAddressPools which would get advertised + via this advertisement. If no IPAddressPool is selected by this + or by the list, the advertisement is applied to all the IPAddressPools. + items: + description: A label selector is a label query over a set of resources. + The result of matchLabels and matchExpressions are ANDed. An empty + label selector matches all objects. A null label selector matches + no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the + key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: array + ipAddressPools: + description: The list of IPAddressPools to advertise via this advertisement, + selected by name. + items: + type: string + type: array + localPref: + description: The BGP LOCAL_PREF attribute which is used by BGP best + path algorithm, Path with higher localpref is preferred over one + with lower localpref. + format: int32 + type: integer + nodeSelectors: + description: NodeSelectors allows to limit the nodes to announce as + next hops for the LoadBalancer IP. When empty, all the nodes having are + announced as next hops. + items: + description: A label selector is a label query over a set of resources. + The result of matchLabels and matchExpressions are ANDed. An empty + label selector matches all objects. A null label selector matches + no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the + key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: array + peers: + description: Peers limits the bgppeer to advertise the ips of the + selected pools to. When empty, the loadbalancer IP is announced + to all the BGPPeers configured. + items: + type: string + type: array + type: object + status: + description: BGPAdvertisementStatus defines the observed state of BGPAdvertisement. + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: bgppeers.metallb.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== + service: + name: webhook-service + namespace: metallb-system + path: /convert + conversionReviewVersions: + - v1beta1 + - v1beta2 + group: metallb.io + names: + kind: BGPPeer + listKind: BGPPeerList + plural: bgppeers + singular: bgppeer + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.peerAddress + name: Address + type: string + - jsonPath: .spec.peerASN + name: ASN + type: string + - jsonPath: .spec.bfdProfile + name: BFD Profile + type: string + - jsonPath: .spec.ebgpMultiHop + name: Multi Hops + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: BGPPeer is the Schema for the peers API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BGPPeerSpec defines the desired state of Peer. + properties: + bfdProfile: + type: string + ebgpMultiHop: + description: EBGP peer is multi-hops away + type: boolean + holdTime: + description: Requested BGP hold time, per RFC4271. + type: string + keepaliveTime: + description: Requested BGP keepalive time, per RFC4271. + type: string + myASN: + description: AS number to use for the local end of the session. + format: int32 + maximum: 4294967295 + minimum: 0 + type: integer + nodeSelectors: + description: Only connect to this peer on nodes that match one of + these selectors. + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + minItems: 1 + type: array + required: + - key + - operator + - values + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + type: array + password: + description: Authentication password for routers enforcing TCP MD5 + authenticated sessions + type: string + peerASN: + description: AS number to expect from the remote end of the session. + format: int32 + maximum: 4294967295 + minimum: 0 + type: integer + peerAddress: + description: Address to dial when establishing the session. + type: string + peerPort: + description: Port to dial when establishing the session. + maximum: 16384 + minimum: 0 + type: integer + routerID: + description: BGP router ID to advertise to the peer + type: string + sourceAddress: + description: Source address to use when establishing the session. + type: string + required: + - myASN + - peerASN + - peerAddress + type: object + status: + description: BGPPeerStatus defines the observed state of Peer. + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.peerAddress + name: Address + type: string + - jsonPath: .spec.peerASN + name: ASN + type: string + - jsonPath: .spec.bfdProfile + name: BFD Profile + type: string + - jsonPath: .spec.ebgpMultiHop + name: Multi Hops + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: BGPPeer is the Schema for the peers API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BGPPeerSpec defines the desired state of Peer. + properties: + bfdProfile: + description: The name of the BFD Profile to be used for the BFD session + associated to the BGP session. If not set, the BFD session won't + be set up. + type: string + ebgpMultiHop: + description: To set if the BGPPeer is multi-hops away. Needed for + FRR mode only. + type: boolean + holdTime: + description: Requested BGP hold time, per RFC4271. + type: string + keepaliveTime: + description: Requested BGP keepalive time, per RFC4271. + type: string + myASN: + description: AS number to use for the local end of the session. + format: int32 + maximum: 4294967295 + minimum: 0 + type: integer + nodeSelectors: + description: Only connect to this peer on nodes that match one of + these selectors. + items: + description: A label selector is a label query over a set of resources. + The result of matchLabels and matchExpressions are ANDed. An empty + label selector matches all objects. A null label selector matches + no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the + key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: array + password: + description: Authentication password for routers enforcing TCP MD5 + authenticated sessions + type: string + passwordSecret: + description: passwordSecret is name of the authentication secret for + BGP Peer. the secret must be of type "kubernetes.io/basic-auth", + and created in the same namespace as the MetalLB deployment. The + password is stored in the secret as the key "password". + properties: + name: + description: name is unique within a namespace to reference a + secret resource. + type: string + namespace: + description: namespace defines the space within which the secret + name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + peerASN: + description: AS number to expect from the remote end of the session. + format: int32 + maximum: 4294967295 + minimum: 0 + type: integer + peerAddress: + description: Address to dial when establishing the session. + type: string + peerPort: + default: 179 + description: Port to dial when establishing the session. + maximum: 16384 + minimum: 0 + type: integer + routerID: + description: BGP router ID to advertise to the peer + type: string + sourceAddress: + description: Source address to use when establishing the session. + type: string + vrf: + description: To set if we want to peer with the BGPPeer using an interface + belonging to a host vrf + type: string + required: + - myASN + - peerASN + - peerAddress + type: object + status: + description: BGPPeerStatus defines the observed state of Peer. + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: communities.metallb.io +spec: + group: metallb.io + names: + kind: Community + listKind: CommunityList + plural: communities + singular: community + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: Community is a collection of aliases for communities. Users can + define named aliases to be used in the BGPPeer CRD. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: CommunitySpec defines the desired state of Community. + properties: + communities: + items: + properties: + name: + description: The name of the alias for the community. + type: string + value: + description: The BGP community value corresponding to the given + name. Can be a standard community of the form 1234:1234 or + a large community of the form large:1234:1234:1234. + type: string + type: object + type: array + type: object + status: + description: CommunityStatus defines the observed state of Community. + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: ipaddresspools.metallb.io +spec: + group: metallb.io + names: + kind: IPAddressPool + listKind: IPAddressPoolList + plural: ipaddresspools + singular: ipaddresspool + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.autoAssign + name: Auto Assign + type: boolean + - jsonPath: .spec.avoidBuggyIPs + name: Avoid Buggy IPs + type: boolean + - jsonPath: .spec.addresses + name: Addresses + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: IPAddressPool represents a pool of IP addresses that can be allocated + to LoadBalancer services. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: IPAddressPoolSpec defines the desired state of IPAddressPool. + properties: + addresses: + description: A list of IP address ranges over which MetalLB has authority. + You can list multiple ranges in a single pool, they will all share + the same settings. Each range can be either a CIDR prefix, or an + explicit start-end range of IPs. + items: + type: string + type: array + autoAssign: + default: true + description: AutoAssign flag used to prevent MetallB from automatic + allocation for a pool. + type: boolean + avoidBuggyIPs: + default: false + description: AvoidBuggyIPs prevents addresses ending with .0 and .255 + to be used by a pool. + type: boolean + serviceAllocation: + description: AllocateTo makes ip pool allocation to specific namespace + and/or service. The controller will use the pool with lowest value + of priority in case of multiple matches. A pool with no priority + set will be used only if the pools with priority can't be used. + If multiple matching IPAddressPools are available it will check + for the availability of IPs sorting the matching IPAddressPools + by priority, starting from the highest to the lowest. If multiple + IPAddressPools have the same priority, choice will be random. + properties: + namespaceSelectors: + description: NamespaceSelectors list of label selectors to select + namespace(s) for ip pool, an alternative to using namespace + list. + items: + description: A label selector is a label query over a set of + resources. The result of matchLabels and matchExpressions + are ANDed. An empty label selector matches all objects. A + null label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists or + DoesNotExist, the values array must be empty. This + array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: array + namespaces: + description: Namespaces list of namespace(s) on which ip pool + can be attached. + items: + type: string + type: array + priority: + description: Priority priority given for ip pool while ip allocation + on a service. + type: integer + serviceSelectors: + description: ServiceSelectors list of label selector to select + service(s) for which ip pool can be used for ip allocation. + items: + description: A label selector is a label query over a set of + resources. The result of matchLabels and matchExpressions + are ANDed. An empty label selector matches all objects. A + null label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists or + DoesNotExist, the values array must be empty. This + array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: array + type: object + required: + - addresses + type: object + status: + description: IPAddressPoolStatus defines the observed state of IPAddressPool. + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: l2advertisements.metallb.io +spec: + group: metallb.io + names: + kind: L2Advertisement + listKind: L2AdvertisementList + plural: l2advertisements + singular: l2advertisement + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.ipAddressPools + name: IPAddressPools + type: string + - jsonPath: .spec.ipAddressPoolSelectors + name: IPAddressPool Selectors + type: string + - jsonPath: .spec.interfaces + name: Interfaces + type: string + - jsonPath: .spec.nodeSelectors + name: Node Selectors + priority: 10 + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: L2Advertisement allows to advertise the LoadBalancer IPs provided + by the selected pools via L2. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: L2AdvertisementSpec defines the desired state of L2Advertisement. + properties: + interfaces: + description: A list of interfaces to announce from. The LB IP will + be announced only from these interfaces. If the field is not set, + we advertise from all the interfaces on the host. + items: + type: string + type: array + ipAddressPoolSelectors: + description: A selector for the IPAddressPools which would get advertised + via this advertisement. If no IPAddressPool is selected by this + or by the list, the advertisement is applied to all the IPAddressPools. + items: + description: A label selector is a label query over a set of resources. + The result of matchLabels and matchExpressions are ANDed. An empty + label selector matches all objects. A null label selector matches + no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the + key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: array + ipAddressPools: + description: The list of IPAddressPools to advertise via this advertisement, + selected by name. + items: + type: string + type: array + nodeSelectors: + description: NodeSelectors allows to limit the nodes to announce as + next hops for the LoadBalancer IP. When empty, all the nodes having are + announced as next hops. + items: + description: A label selector is a label query over a set of resources. + The result of matchLabels and matchExpressions are ANDed. An empty + label selector matches all objects. A null label selector matches + no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the + key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: array + type: object + status: + description: L2AdvertisementStatus defines the observed state of L2Advertisement. + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app: metallb + name: controller + namespace: metallb-system +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app: metallb + name: speaker + namespace: metallb-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app: metallb + name: controller + namespace: metallb-system +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resourceNames: + - memberlist + resources: + - secrets + verbs: + - list +- apiGroups: + - apps + resourceNames: + - controller + resources: + - deployments + verbs: + - get +- apiGroups: + - metallb.io + resources: + - bgppeers + verbs: + - get + - list +- apiGroups: + - metallb.io + resources: + - addresspools + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - bfdprofiles + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - ipaddresspools + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - bgpadvertisements + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - l2advertisements + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - communities + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app: metallb + name: pod-lister + namespace: metallb-system +rules: +- apiGroups: + - "" + resources: + - pods + verbs: + - list +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - addresspools + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - bfdprofiles + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - bgppeers + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - l2advertisements + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - bgpadvertisements + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - ipaddresspools + verbs: + - get + - list + - watch +- apiGroups: + - metallb.io + resources: + - communities + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app: metallb + name: metallb-system:controller +rules: +- apiGroups: + - "" + resources: + - services + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - nodes + verbs: + - list +- apiGroups: + - "" + resources: + - services/status + verbs: + - update +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - policy + resourceNames: + - controller + resources: + - podsecuritypolicies + verbs: + - use +- apiGroups: + - admissionregistration.k8s.io + resourceNames: + - metallb-webhook-configuration + resources: + - validatingwebhookconfigurations + - mutatingwebhookconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - admissionregistration.k8s.io + resources: + - validatingwebhookconfigurations + - mutatingwebhookconfigurations + verbs: + - list + - watch +- apiGroups: + - apiextensions.k8s.io + resourceNames: + - addresspools.metallb.io + - bfdprofiles.metallb.io + - bgpadvertisements.metallb.io + - bgppeers.metallb.io + - ipaddresspools.metallb.io + - l2advertisements.metallb.io + - communities.metallb.io + resources: + - customresourcedefinitions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app: metallb + name: metallb-system:speaker +rules: +- apiGroups: + - "" + resources: + - services + - endpoints + - nodes + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - policy + resourceNames: + - speaker + resources: + - podsecuritypolicies + verbs: + - use +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app: metallb + name: controller + namespace: metallb-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: controller +subjects: +- kind: ServiceAccount + name: controller + namespace: metallb-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app: metallb + name: pod-lister + namespace: metallb-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: pod-lister +subjects: +- kind: ServiceAccount + name: speaker + namespace: metallb-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app: metallb + name: metallb-system:controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: metallb-system:controller +subjects: +- kind: ServiceAccount + name: controller + namespace: metallb-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app: metallb + name: metallb-system:speaker +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: metallb-system:speaker +subjects: +- kind: ServiceAccount + name: speaker + namespace: metallb-system +--- +apiVersion: v1 +data: + excludel2.yaml: | + announcedInterfacesToExclude: ["docker.*", "cbr.*", "dummy.*", "virbr.*", "lxcbr.*", "veth.*", "lo", "^cali.*", "^tunl.*", "flannel.*", "kube-ipvs.*", "cni.*", "^nodelocaldns.*"] +kind: ConfigMap +metadata: + name: metallb-excludel2 + namespace: metallb-system +--- +apiVersion: v1 +kind: Secret +metadata: + name: webhook-server-cert + namespace: metallb-system +--- +apiVersion: v1 +kind: Service +metadata: + name: webhook-service + namespace: metallb-system +spec: + ports: + - port: 443 + targetPort: 9443 + selector: + component: controller +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: metallb + component: controller + name: controller + namespace: metallb-system +spec: + revisionHistoryLimit: 3 + selector: + matchLabels: + app: metallb + component: controller + template: + metadata: + annotations: + prometheus.io/port: "7472" + prometheus.io/scrape: "true" + labels: + app: metallb + component: controller + spec: + containers: + - args: + - --port=7472 + - --log-level=info + env: + - name: METALLB_ML_SECRET_NAME + value: memberlist + - name: METALLB_DEPLOYMENT + value: controller + image: quay.io/metallb/controller:v0.13.10 + livenessProbe: + failureThreshold: 3 + httpGet: + path: /metrics + port: monitoring + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: controller + ports: + - containerPort: 7472 + name: monitoring + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /metrics + port: monitoring + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - all + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + nodeSelector: + kubernetes.io/os: linux + securityContext: + fsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccountName: controller + terminationGracePeriodSeconds: 0 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + labels: + app: metallb + component: speaker + name: speaker + namespace: metallb-system +spec: + selector: + matchLabels: + app: metallb + component: speaker + template: + metadata: + annotations: + prometheus.io/port: "7472" + prometheus.io/scrape: "true" + labels: + app: metallb + component: speaker + spec: + containers: + - args: + - --port=7472 + - --log-level=info + env: + - name: METALLB_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: METALLB_HOST + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: METALLB_ML_BIND_ADDR + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: METALLB_ML_LABELS + value: app=metallb,component=speaker + - name: METALLB_ML_SECRET_KEY_PATH + value: /etc/ml_secret_key + image: quay.io/metallb/speaker:v0.13.10 + livenessProbe: + failureThreshold: 3 + httpGet: + path: /metrics + port: monitoring + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: speaker + ports: + - containerPort: 7472 + name: monitoring + - containerPort: 7946 + name: memberlist-tcp + - containerPort: 7946 + name: memberlist-udp + protocol: UDP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /metrics + port: monitoring + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - NET_RAW + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /etc/ml_secret_key + name: memberlist + readOnly: true + - mountPath: /etc/metallb + name: metallb-excludel2 + readOnly: true + hostNetwork: true + nodeSelector: + kubernetes.io/os: linux + serviceAccountName: speaker + terminationGracePeriodSeconds: 2 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + volumes: + - name: memberlist + secret: + defaultMode: 420 + secretName: memberlist + - configMap: + defaultMode: 256 + name: metallb-excludel2 + name: metallb-excludel2 +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: metallb-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: metallb-system + path: /validate-metallb-io-v1beta2-bgppeer + failurePolicy: Fail + name: bgppeersvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + resources: + - bgppeers + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: metallb-system + path: /validate-metallb-io-v1beta1-addresspool + failurePolicy: Fail + name: addresspoolvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - addresspools + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: metallb-system + path: /validate-metallb-io-v1beta1-bfdprofile + failurePolicy: Fail + name: bfdprofilevalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - DELETE + resources: + - bfdprofiles + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: metallb-system + path: /validate-metallb-io-v1beta1-bgpadvertisement + failurePolicy: Fail + name: bgpadvertisementvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - bgpadvertisements + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: metallb-system + path: /validate-metallb-io-v1beta1-community + failurePolicy: Fail + name: communityvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - communities + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: metallb-system + path: /validate-metallb-io-v1beta1-ipaddresspool + failurePolicy: Fail + name: ipaddresspoolvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - ipaddresspools + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: metallb-system + path: /validate-metallb-io-v1beta1-l2advertisement + failurePolicy: Fail + name: l2advertisementvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - l2advertisements + sideEffects: None diff --git a/upstream/metallb-native/version b/upstream/metallb-native/version new file mode 100644 index 0000000..11e6abc --- /dev/null +++ b/upstream/metallb-native/version @@ -0,0 +1 @@ +v0.13.10 \ No newline at end of file