Skip to content

Commit

Permalink
Merge branch 'main' into work/1913/CRAFT-3425/python-poetry
Browse files Browse the repository at this point in the history
  • Loading branch information
lengau authored Oct 11, 2024
2 parents df6780b + 534c028 commit 4d41c95
Show file tree
Hide file tree
Showing 27 changed files with 639 additions and 186 deletions.
1 change: 1 addition & 0 deletions .github/workflows/spread.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ jobs:
name: Run spread
env:
CHARMCRAFT_AUTH: ${{ secrets.CHARMCRAFT_AUTH }}
CHARMCRAFT_SINGLE_CHARM_AUTH: ${{ secrets.CHARMCRAFT_SINGLE_CHARM_AUTH }}
CHARM_DEFAULT_NAME: gh-ci-charmcraft-charm
BUNDLE_DEFAULT_NAME: gh-ci-charmcraft-bundle
run: |
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ jobs:
if [[ $(lsb_release --codename --short) == 'jammy' ]]; then
python3 -m pip install -U pip
fi
- name: Setup LXD
uses: canonical/[email protected]
if: ${{ runner.os == 'Linux' }}
- name: Install skopeo (mac)
# This is only necessary for Linux until skopeo >= 1.11 is in repos.
# Once we're running on Noble, we can get skopeo from apt.
Expand Down
20 changes: 10 additions & 10 deletions charmcraft/application/commands/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class WhoamiCommand(CharmcraftCommand):
)
format_option = True

def run(self, parsed_args):
def run(self, parsed_args: argparse.Namespace) -> None:
"""Run the command."""
try:
macaroon_info = self._services.store.client.whoami()
Expand All @@ -259,7 +259,7 @@ def run(self, parsed_args):
return

human_msgs = []
prog_info = {"logged": True}
prog_info: dict[str, Any] = {"logged": True}

human_msgs.append(f"name: {macaroon_info['account']['display-name']}")
prog_info["name"] = macaroon_info["account"]["display-name"]
Expand All @@ -275,20 +275,20 @@ def run(self, parsed_args):
prog_info["permissions"] = permissions

if packages := macaroon_info.get("packages"):
grouped = {}
grouped: dict[str, list[dict[str, str]]] = {}
for package in packages:
grouped.setdefault(package.type, []).append(package)
grouped.setdefault(package["type"], []).append(package)
for package_type, title in [("charm", "charms"), ("bundle", "bundles")]:
if package_type in grouped:
human_msgs.append(f"{title}:")
pkg_info = []
for item in grouped[package_type]:
if item.name is not None:
human_msgs.append(f"- name: {item.name}")
pkg_info.append({"name": item.name})
elif item.id is not None:
human_msgs.append(f"- id: {item.id}")
pkg_info.append({"id": item.id})
if (name := item.get("name")) is not None:
human_msgs.append(f"- name: {name}")
pkg_info.append({"name": name})
elif (pkg_id := item.get("id")) is not None:
human_msgs.append(f"- id: {pkg_id}")
pkg_info.append({"id": pkg_id})
prog_info[title] = pkg_info

if channels := macaroon_info.get("channels"):
Expand Down
7 changes: 6 additions & 1 deletion charmcraft/charm_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,12 @@ def _install_dependencies(self, staging_venv_dir: pathlib.Path):
# known working version of pip.
if get_pip_version(pip_cmd) < MINIMUM_PIP_VERSION:
_process_run(
[pip_cmd, "install", "--force-reinstall", f"pip@{KNOWN_GOOD_PIP_URL}"]
[
pip_cmd,
"install",
"--force-reinstall",
f"pip@{KNOWN_GOOD_PIP_URL}",
]
)

with instrum.Timer("Installing all dependencies"):
Expand Down
2 changes: 1 addition & 1 deletion charmcraft/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"centos@7",
"almalinux@9",
]
Expand All @@ -73,7 +74,6 @@
BaseName("ubuntu", "18.04"),
BaseName("ubuntu", "20.04"),
BaseName("ubuntu", "22.04"),
BaseName("ubuntu", "23.10"),
BaseName("ubuntu", "24.04"),
BaseName("ubuntu", "devel"),
BaseName("centos", "7"),
Expand Down
91 changes: 88 additions & 3 deletions charmcraft/services/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,55 @@
"""Service class for creating providers."""
from __future__ import annotations

import contextlib
import io
from collections.abc import Generator

from craft_application.models import BuildInfo

try:
import fcntl
except ModuleNotFoundError: # Not available on Windows.
fcntl = None # type: ignore[assignment]
import os
import pathlib
from typing import cast

import craft_application
import craft_providers
from craft_application import services
from craft_cli import emit
from craft_providers import bases

from charmcraft import env
from charmcraft import env, models


class ProviderService(services.ProviderService):
"""Business logic for getting providers."""

def __init__(
self,
app: craft_application.AppMetadata,
services: craft_application.ServiceFactory,
*,
project: models.CharmcraftProject,
work_dir: pathlib.Path,
build_plan: list[BuildInfo],
provider_name: str | None = None,
install_snap: bool = True,
) -> None:
super().__init__(
app,
services,
project=project,
work_dir=work_dir,
build_plan=build_plan,
provider_name=provider_name,
install_snap=install_snap,
)
self._cache_path: pathlib.Path | None = None
self._lock: io.TextIOBase | None = None

def setup(self) -> None:
"""Set up the provider service for Charmcraft."""
super().setup()
Expand All @@ -56,12 +92,61 @@ def get_base(
If no cache_path is included, adds one.
"""
self._cache_path = cast(
pathlib.Path, kwargs.get("cache_path", env.get_host_shared_cache_path())
)
self._lock = _maybe_lock_cache(self._cache_path)

# Forward the shared cache path.
if "cache_path" not in kwargs:
kwargs["cache_path"] = env.get_host_shared_cache_path()
kwargs["cache_path"] = self._cache_path if self._lock else None
return super().get_base(
base_name,
instance_name=instance_name,
# craft-application annotation is incorrect
**kwargs, # type: ignore[arg-type]
)

@contextlib.contextmanager
def instance(
self,
build_info: BuildInfo,
*,
work_dir: pathlib.Path,
allow_unstable: bool = True,
**kwargs: bool | str | None,
) -> Generator[craft_providers.Executor, None, None]:
"""Instance override for Charmcraft."""
with super().instance(
build_info, work_dir=work_dir, allow_unstable=allow_unstable, **kwargs
) as instance:
try:
yield instance
finally:
if fcntl is not None and self._lock:
fcntl.flock(self._lock, fcntl.LOCK_UN)
self._lock.close()


def _maybe_lock_cache(path: pathlib.Path) -> io.TextIOBase | None:
"""Lock the cache so we only have one copy of Charmcraft using it at a time."""
if fcntl is None: # Don't lock on Windows - just don't cache.
return None
cache_lock_path = path / "charmcraft.lock"

emit.trace("Attempting to lock the cache path")
lock_file = cache_lock_path.open("w+")
try:
# Exclusive lock, but non-blocking.
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
emit.progress(
"Shared cache locked by another process; running without cache.", permanent=True
)
return None
else:
pid = str(os.getpid())
lock_file.write(pid)
lock_file.flush()
os.fsync(lock_file.fileno())
emit.trace(f"Cache path locked by this process ({pid})")
return lock_file
25 changes: 11 additions & 14 deletions charmcraft/templates/init-kubernetes/tests/unit/test_charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,19 @@
#
# Learn more about testing at: https://juju.is/docs/sdk/testing

import ops
import ops.testing
import pytest
from ops import testing

from charm import {{ class_name }}


@pytest.fixture
def harness():
harness = ops.testing.Harness({{ class_name }})
harness.begin()
yield harness
harness.cleanup()
def test_pebble_ready():
# Arrange:
ctx = testing.Context({{ class_name }})
container = testing.Container("some-container", can_connect=True)
state_in = testing.State(containers={container})

# Act:
state_out = ctx.run(ctx.on.pebble_ready(container), state_in)

def test_pebble_ready(harness: ops.testing.Harness[{{ class_name }}]):
# Simulate the container coming up and emission of pebble-ready event
harness.container_pebble_ready("some-container")
# Ensure we set an ActiveStatus with no message
assert harness.model.unit.status == ops.ActiveStatus()
# Assert:
assert state_out.unit_status == testing.ActiveStatus()
2 changes: 2 additions & 0 deletions charmcraft/templates/init-kubernetes/tox.ini.j2
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ description = Run unit tests
deps =
pytest
coverage[toml]
ops[testing]
-r {tox_root}/requirements.txt
commands =
coverage run --source={[vars]src_path} \
Expand All @@ -64,6 +65,7 @@ commands =
description = Run static type checks
deps =
pyright
ops[testing]
-r {tox_root}/requirements.txt
commands =
pyright {posargs}
Expand Down
22 changes: 9 additions & 13 deletions charmcraft/templates/init-machine/tests/unit/test_charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,15 @@

import unittest

import ops
import ops.testing
from charm import {{ class_name }}

from ops import testing

class TestCharm(unittest.TestCase):
def setUp(self):
self.harness = ops.testing.Harness({{ class_name }})
self.addCleanup(self.harness.cleanup)
from charm import {{ class_name }}

def test_start(self):
# Simulate the charm starting
self.harness.begin_with_initial_hooks()

# Ensure we set an ActiveStatus with no message
self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus())
def test_start():
# Arrange:
ctx = testing.Context({{ class_name }})
# Act:
state_out = ctx.run(ctx.on.start(), testing.State())
# Assert:
assert state_out.unit_status == testing.ActiveStatus()
2 changes: 2 additions & 0 deletions charmcraft/templates/init-machine/tox.ini.j2
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ description = Run unit tests
deps =
pytest
coverage[toml]
ops[testing]
-r {tox_root}/requirements.txt
commands =
coverage run --source={[vars]src_path} \
Expand All @@ -64,6 +65,7 @@ commands =
description = Run static type checks
deps =
pyright
ops[testing]
-r {tox_root}/requirements.txt
commands =
pyright {posargs}
Expand Down
18 changes: 9 additions & 9 deletions charmcraft/templates/init-simple/src/charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,21 @@ class {{ class_name }}(ops.CharmBase):
if log_level in VALID_LOG_LEVELS:
# The config is good, so update the configuration of the workload
container = self.unit.get_container("httpbin")
# Verify that we can connect to the Pebble API in the workload container
if container.can_connect():
# Push an updated layer with the new config
# Push an updated layer with the new config
try:
container.add_layer("httpbin", self._pebble_layer, combine=True)
container.replan()

logger.debug("Log level for gunicorn changed to '%s'", log_level)
self.unit.status = ops.ActiveStatus()
else:
except ops.pebble.ConnectionError:
# We were unable to connect to the Pebble API, so we defer this event
self.unit.status = ops.MaintenanceStatus("waiting for Pebble API")
event.defer()
self.unit.status = ops.WaitingStatus("waiting for Pebble API")
return

logger.debug("Log level for gunicorn changed to '%s'", log_level)
self.unit.status = ops.ActiveStatus()
else:
# In this case, the config option is bad, so block the charm and notify the operator.
self.unit.status = ops.BlockedStatus("invalid log level: '{log_level}'")
self.unit.status = ops.BlockedStatus(f"invalid log level: '{log_level}'")

@property
def _pebble_layer(self) -> ops.pebble.LayerDict:
Expand Down
Loading

0 comments on commit 4d41c95

Please sign in to comment.