Skip to content

Commit

Permalink
[DPE-5588] Check against invalid arch (#344)
Browse files Browse the repository at this point in the history
  • Loading branch information
sinclert-canonical authored Dec 16, 2024
1 parent 5308a78 commit c2d653f
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 1 deletion.
61 changes: 61 additions & 0 deletions src/architecture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

"""Architecture utilities module"""

import logging
import os
import pathlib
import platform

import yaml
from ops.charm import CharmBase
from ops.model import BlockedStatus

logger = logging.getLogger(__name__)


class WrongArchitectureWarningCharm(CharmBase):
"""A fake charm class that only signals a wrong architecture deploy."""

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

hw_arch = platform.machine()
self.unit.status = BlockedStatus(
f"Charm incompatible with {hw_arch} architecture. "
f"If this app is being refreshed, rollback"
)
raise RuntimeError(
f"Incompatible architecture: this charm revision does not support {hw_arch}. "
f"If this app is being refreshed, rollback with instructions from Charmhub docs. "
f"If this app is being deployed for the first time, remove it and deploy it again "
f"using a compatible revision."
)


def is_wrong_architecture() -> bool:
"""Checks if charm was deployed on wrong architecture."""
charm_path = os.environ.get("CHARM_DIR", "")
manifest_path = pathlib.Path(charm_path, "manifest.yaml")

if not manifest_path.exists():
logger.error("Cannot check architecture: manifest file not found in %s", manifest_path)
return False

manifest = yaml.safe_load(manifest_path.read_text())

manifest_archs = []
for base in manifest["bases"]:
base_archs = base.get("architectures", [])
manifest_archs.extend(base_archs)

hardware_arch = platform.machine()
if ("amd64" in manifest_archs and hardware_arch == "x86_64") or (
"arm64" in manifest_archs and hardware_arch == "aarch64"
):
logger.debug("Charm architecture matches")
return False

logger.error("Charm architecture does not match")
return True
7 changes: 7 additions & 0 deletions src/kubernetes_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

"""MySQL Router Kubernetes charm"""

import ops

from architecture import WrongArchitectureWarningCharm, is_wrong_architecture

if is_wrong_architecture() and __name__ == "__main__":
ops.main.main(WrongArchitectureWarningCharm)

import enum
import functools
import json
Expand Down
17 changes: 16 additions & 1 deletion tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import pathlib
import subprocess
import tempfile
from typing import Dict, List, Optional
from pathlib import Path
from typing import Dict, List, Optional, Union

import mysql.connector
import tenacity
Expand Down Expand Up @@ -655,3 +656,17 @@ def get_juju_status(model_name: str) -> str:
model_name: The model for which to retrieve juju status for
"""
return subprocess.check_output(["juju", "status", "--model", model_name]).decode("utf-8")


async def get_charm(charm_path: Union[str, Path], architecture: str, bases_index: int) -> Path:
"""Fetches packed charm from CI runner without checking for architecture."""
charm_path = Path(charm_path)
charmcraft_yaml = yaml.safe_load((charm_path / "charmcraft.yaml").read_text())
assert charmcraft_yaml["type"] == "charm"

base = charmcraft_yaml["bases"][bases_index]
build_on = base.get("build-on", [base])[0]
version = build_on["channel"]
packed_charms = list(charm_path.glob(f"*{version}-{architecture}.charm"))

return packed_charms[0].resolve(strict=True)
65 changes: 65 additions & 0 deletions tests/integration/test_architecture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

from pathlib import Path

import pytest
import yaml
from pytest_operator.plugin import OpsTest

from . import markers
from .helpers import get_charm

METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
MYSQL_ROUTER_APP_NAME = METADATA["name"]


@pytest.mark.group(1)
@markers.amd64_only
async def test_arm_charm_on_amd_host(ops_test: OpsTest) -> None:
"""Tries deploying an arm64 charm on amd64 host."""
charm = await get_charm(".", "arm64", 1)

resources = {
"mysql-router-image": METADATA["resources"]["mysql-router-image"]["upstream-source"]
}

await ops_test.model.deploy(
charm,
application_name=MYSQL_ROUTER_APP_NAME,
num_units=1,
resources=resources,
base="[email protected]",
)

await ops_test.model.wait_for_idle(
apps=[MYSQL_ROUTER_APP_NAME],
status="error",
raise_on_error=False,
)


@pytest.mark.group(1)
@markers.arm64_only
async def test_amd_charm_on_arm_host(ops_test: OpsTest) -> None:
"""Tries deploying an amd64 charm on arm64 host."""
charm = await get_charm(".", "amd64", 0)

resources = {
"mysql-router-image": METADATA["resources"]["mysql-router-image"]["upstream-source"]
}

await ops_test.model.deploy(
charm,
application_name=MYSQL_ROUTER_APP_NAME,
num_units=1,
resources=resources,
base="[email protected]",
)

await ops_test.model.wait_for_idle(
apps=[MYSQL_ROUTER_APP_NAME],
status="error",
raise_on_error=False,
)
48 changes: 48 additions & 0 deletions tests/unit/test_architecture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

from architecture import is_wrong_architecture

TEST_MANIFEST = """
bases:
- architectures:
- {arch}
channel: '22.04'
name: ubuntu
"""


def test_wrong_architecture_file_not_found(monkeypatch):
"""Tests if the function returns False when the charm file doesn't exist."""
monkeypatch.setattr("os.environ", {"CHARM_DIR": "/tmp"})
monkeypatch.setattr("pathlib.Path.exists", lambda *args, **kwargs: False)
assert not is_wrong_architecture()


def test_wrong_architecture_amd64(monkeypatch):
"""Tests if the function correctly identifies arch when charm is AMD."""
manifest = TEST_MANIFEST.format(arch="amd64")
monkeypatch.setattr("os.environ", {"CHARM_DIR": "/tmp"})
monkeypatch.setattr("pathlib.Path.exists", lambda *args, **kwargs: True)
monkeypatch.setattr("pathlib.Path.read_text", lambda *args, **kwargs: manifest)

monkeypatch.setattr("platform.machine", lambda *args, **kwargs: "x86_64")
assert not is_wrong_architecture()

monkeypatch.setattr("platform.machine", lambda *args, **kwargs: "aarch64")
assert is_wrong_architecture()


def test_wrong_architecture_arm64(monkeypatch):
"""Tests if the function correctly identifies arch when charm is ARM."""
manifest = TEST_MANIFEST.format(arch="arm64")
monkeypatch.setattr("os.environ", {"CHARM_DIR": "/tmp"})
monkeypatch.setattr("pathlib.Path.exists", lambda *args, **kwargs: True)
monkeypatch.setattr("pathlib.Path.read_text", lambda *args, **kwargs: manifest)

monkeypatch.setattr("platform.machine", lambda *args, **kwargs: "x86_64")
assert is_wrong_architecture()

monkeypatch.setattr("platform.machine", lambda *args, **kwargs: "aarch64")
assert not is_wrong_architecture()

0 comments on commit c2d653f

Please sign in to comment.