Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support kube-control v2 schema #23

Merged
merged 1 commit into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .charmcraft-channel
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.x/stable
9 changes: 6 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ jobs:
name: Lint Unit
uses: charmed-kubernetes/workflows/.github/workflows/lint-unit.yaml@main
with:
python: "['3.8', '3.10']"
python: "['3.8', '3.10', '3.12']"
needs:
- call-inclusive-naming-check

charmcraft-build:
name: Build Charm
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Read charmcraft version file
id: charmcraft
run: echo "channel=$(cat .charmcraft-channel)" >> $GITHUB_OUTPUT
- name: Install and prepare LXD snap environment
run: |
sudo apt-get remove -qy lxd lxd-client | true
Expand All @@ -46,7 +49,7 @@ jobs:
sudo iptables -P FORWARD ACCEPT
- name: Install Charmcraft
run: |
sudo snap install charmcraft --classic --channel=latest/stable
sudo snap install charmcraft --classic --channel=${{ steps.charmcraft.outputs.channel }}
- name: Build Charm
run: |
sg lxd -c 'charmcraft pack -v'
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ ops
pyyaml
pydantic==1.*
ops.manifest>=1.1.0,<2.0.0
ops.interface-kube-control @ git+https://github.com/charmed-kubernetes/interface-kube-control.git@main#subdirectory=ops
ops.interface-tls-certificates @ git+https://github.com/charmed-kubernetes/interface-tls-certificates.git@main#subdirectory=ops
ops.interface-kube-control @ git+https://github.com/charmed-kubernetes/interface-kube-control.git@edc07bce7ea4c25d472fa4d95834602a7ebce5cd#subdirectory=ops
ops.interface-tls-certificates @ git+https://github.com/charmed-kubernetes/interface-tls-certificates.git@4a1081da098154b96337a09c8e9c40acff2d330e#subdirectory=ops
ops.interface-azure @ git+https://github.com/charmed-kubernetes/interface-azure-integration.git@main#subdirectory=ops
65 changes: 33 additions & 32 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@
import logging
from pathlib import Path

from ops.charm import CharmBase
from ops.framework import StoredState
import ops
from ops.interface_azure.requires import AzureIntegrationRequires
from ops.interface_kube_control import KubeControlRequirer
from ops.interface_tls_certificates import CertificatesRequires
from ops.main import main
from ops.manifests import Collector, ManifestClientError
from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus

from config import CharmConfig
from disk_manifests import AzureDiskManifests
Expand All @@ -22,19 +19,19 @@
log = logging.getLogger(__name__)


class AzureCloudProviderCharm(CharmBase):
class AzureCloudProviderCharm(ops.CharmBase):
"""Dispatch logic for the AzureCloudProvider charm."""

CA_CERT_PATH = Path("/srv/kubernetes/ca.crt")

stored = StoredState()
stored = ops.StoredState()

def __init__(self, *args):
super().__init__(*args)

# Relation Validator and datastore
self.integrator = AzureIntegrationRequires(self, "azure-integration")
self.kube_control = KubeControlRequirer(self)
self.kube_control = KubeControlRequirer(self, schemas="0,1")
self.certificates = CertificatesRequires(self)
# Config Validator and datastore
self.charm_config = CharmConfig(self)
Expand Down Expand Up @@ -117,14 +114,14 @@ def _update_status(self, _):

unready = self.collector.unready
if unready:
self.unit.status = WaitingStatus(", ".join(unready))
self.unit.status = ops.WaitingStatus(", ".join(unready))
else:
self.unit.status = ActiveStatus("Ready")
self.unit.status = ops.ActiveStatus("Ready")
self.unit.set_workload_version(self.collector.short_version)
self.app.status = ActiveStatus(self.collector.long_version)
self.app.status = ops.ActiveStatus(self.collector.long_version)

def _kube_control(self, event):
self.kube_control.set_auth_request(self.unit.name)
self.kube_control.set_auth_request(self.unit.name, "system:masters")
return self._merge_config(event)

def _cluster_tag(self, event):
Expand All @@ -136,16 +133,16 @@ def _cluster_tag(self, event):
return self._merge_config(event)

def _check_kube_control(self, event):
self.unit.status = MaintenanceStatus("Evaluating kubernetes authentication.")
self.unit.status = ops.MaintenanceStatus("Evaluating kubernetes authentication.")
evaluation = self.kube_control.evaluate_relation(event)
if evaluation:
if "Waiting" in evaluation:
self.unit.status = WaitingStatus(evaluation)
self.unit.status = ops.WaitingStatus(evaluation)
else:
self.unit.status = BlockedStatus(evaluation)
self.unit.status = ops.BlockedStatus(evaluation)
return False
if not self.kube_control.get_auth_credentials(self.unit.name):
self.unit.status = WaitingStatus("Waiting for kube-control: unit credentials")
self.unit.status = ops.WaitingStatus("Waiting for kube-control: unit credentials")
return False
self.kube_control.create_kubeconfig(
self.CA_CERT_PATH, "/root/.kube/config", "root", self.unit.name
Expand All @@ -156,13 +153,17 @@ def _check_kube_control(self, event):
return True

def _check_certificates(self, event):
self.unit.status = MaintenanceStatus("Evaluating certificates.")
if self.kube_control.get_ca_certificate():
log.info("CA Certificate is available from kube-control.")
return True

self.unit.status = ops.MaintenanceStatus("Evaluating certificates.")
evaluation = self.certificates.evaluate_relation(event)
if evaluation:
if "Waiting" in evaluation:
self.unit.status = WaitingStatus(evaluation)
self.unit.status = ops.WaitingStatus(evaluation)
else:
self.unit.status = BlockedStatus(evaluation)
self.unit.status = ops.BlockedStatus(evaluation)
return False
self.CA_CERT_PATH.write_text(self.certificates.ca)
return True
Expand All @@ -173,21 +174,21 @@ def _request_azure_features(self, event):
self._merge_config(event=event)

def _check_azure_relation(self, event):
self.unit.status = MaintenanceStatus("Evaluating azure.")
self.unit.status = ops.MaintenanceStatus("Evaluating azure.")
evaluation = self.integrator.evaluate_relation(event)
if evaluation:
if "Waiting" in evaluation:
self.unit.status = WaitingStatus(evaluation)
self.unit.status = ops.WaitingStatus(evaluation)
else:
self.unit.status = BlockedStatus(evaluation)
self.unit.status = ops.BlockedStatus(evaluation)
return False
return True

def _check_config(self):
self.unit.status = MaintenanceStatus("Evaluating charm config.")
self.unit.status = ops.MaintenanceStatus("Evaluating charm config.")
evaluation = self.charm_config.evaluate()
if evaluation:
self.unit.status = BlockedStatus(evaluation)
self.unit.status = ops.BlockedStatus(evaluation)
return False
return True

Expand All @@ -204,12 +205,12 @@ def _merge_config(self, event):
if not self._check_config():
return

self.unit.status = MaintenanceStatus("Evaluating Manifests")
self.unit.status = ops.MaintenanceStatus("Evaluating Manifests")
new_hash = 0
for controller in self.collector.manifests.values():
evaluation = controller.evaluate()
if evaluation:
self.unit.status = BlockedStatus(evaluation)
self.unit.status = ops.BlockedStatus(evaluation)
return
new_hash += controller.hash()

Expand All @@ -223,30 +224,30 @@ def _install_or_upgrade(self, event, config_hash=None):
log.info("Skipping until the config is evaluated.")
return True

self.unit.status = MaintenanceStatus("Deploying Azure Cloud Provider")
self.unit.status = ops.MaintenanceStatus("Deploying Azure Cloud Provider")
self.unit.set_workload_version("")
for controller in self.collector.manifests.values():
try:
controller.apply_manifests()
except ManifestClientError as e:
self.unit.status = WaitingStatus("Waiting for kube-apiserver")
log.warn(f"Encountered retryable installation error: {e}")
self.unit.status = ops.WaitingStatus("Waiting for kube-apiserver")
log.warning("Encountered retryable installation error: %s", e)
event.defer()
return False
return True

def _cleanup(self, event):
if self.stored.config_hash:
self.unit.status = MaintenanceStatus("Cleaning up Azure Cloud Provider")
self.unit.status = ops.MaintenanceStatus("Cleaning up Azure Cloud Provider")
for controller in self.collector.manifests.values():
try:
controller.delete_manifests(ignore_unauthorized=True)
except ManifestClientError:
self.unit.status = WaitingStatus("Waiting for kube-apiserver")
self.unit.status = ops.WaitingStatus("Waiting for kube-apiserver")
event.defer()
return
self.unit.status = MaintenanceStatus("Shutting down")
self.unit.status = ops.MaintenanceStatus("Shutting down")


if __name__ == "__main__":
main(AzureCloudProviderCharm)
ops.main(AzureCloudProviderCharm)
44 changes: 32 additions & 12 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.
import asyncio
import logging
import random
import string
Expand All @@ -9,6 +10,7 @@
from lightkube import AsyncClient, KubeConfig
from lightkube.models.meta_v1 import ObjectMeta
from lightkube.resources.core_v1 import Namespace
from pytest_operator import OpsTest

log = logging.getLogger(__name__)

Expand All @@ -18,20 +20,38 @@ def module_name(request):
return request.module.__name__.replace("_", "-")


async def get_leader(app):
"""Find leader unit of an application.

Args:
app: Juju application
Returns:
int: index to leader unit
"""
is_leader = await asyncio.gather(*(u.is_leader_from_status() for u in app.units))
for idx, flag in enumerate(is_leader):
if flag:
return idx


@pytest.fixture()
async def kubeconfig(ops_test):
async def kubeconfig(ops_test: OpsTest):
for choice in ["kubernetes-control-plane", "k8s"]:
if app := ops_test.model.applications.get(choice):
break
else:
pytest.fail("No kubernetes-control-plane or k8s application found")
leader_idx = await get_leader(app)
leader = app.units[leader_idx]

kubeconfig_path = ops_test.tmp_path / "kubeconfig"
retcode, stdout, stderr = await ops_test.run(
"juju",
"scp",
"kubernetes-control-plane/leader:/home/ubuntu/config",
kubeconfig_path,
)
action = await leader.run_action("get-kubeconfig")
data = await action.wait()
retcode, kubeconfig = (data.results.get(key, {}) for key in ["return-code", "kubeconfig"])
if retcode != 0:
log.error(f"retcode: {retcode}")
log.error(f"stdout:\n{stdout.strip()}")
log.error(f"stderr:\n{stderr.strip()}")
pytest.fail("Failed to copy kubeconfig from kubernetes-control-plane")
log.error("Failed to copy kubeconfig from %s (%s)", app.name, data.results)
pytest.fail(f"Failed to copy kubeconfig from {app.name}")
kubeconfig_path.write_text(kubeconfig)
assert Path(kubeconfig_path).stat().st_size, "kubeconfig file is 0 bytes"
yield kubeconfig_path

Expand All @@ -42,7 +62,7 @@ async def kubernetes(kubeconfig, module_name):
namespace = f"{module_name}-{rand_str}"
config = KubeConfig.from_file(kubeconfig)
client = AsyncClient(
config=config.get(context_name="juju-context"),
config=config.get(context_name=config.current_context),
namespace=namespace,
trust_env=False,
)
Expand Down
1 change: 0 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ description = Run unit tests
deps =
pytest
pytest-cov
pytest-asyncio
ipdb
-r{toxinidir}/requirements.txt
commands =
Expand Down