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