Skip to content

Commit

Permalink
Support builds-on/runs-on arm (#94)
Browse files Browse the repository at this point in the history
* Support builds-on/runs-on arm

* Build ARM64 and AMD64 charms

* Make use of self-hosted runners

* Use runner arch in build cache

* attempt to use better-build workflows

* Use arch specific better builder

* Switch back to using main branch of operator-workflows

* re-enable arm64 builds

* Set arch constraint on every application in a bundle

* Update the charm contribution guide

* Appropriately add arch to constraints

* Adapt to latest workflow merge

* add arch constraint to each application

* Merge from main
  • Loading branch information
addyess authored Nov 13, 2024
1 parent 574183d commit 7fb4e90
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 90 deletions.
66 changes: 0 additions & 66 deletions .github/workflows/build-charm.yaml

This file was deleted.

10 changes: 7 additions & 3 deletions .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ on:
pull_request:

jobs:

extra-args:
runs-on: ubuntu-latest
outputs:
Expand Down Expand Up @@ -41,12 +40,17 @@ jobs:
matrix:
arch:
- {id: amd64, builder-label: ubuntu-22.04, tester-arch: x64}
suite: ["k8s", "etcd", "ceph"]
- {id: arm64, builder-label: ARM64, tester-arch: ARM64}
suite: [k8s, etcd, ceph]
exclude:
- {arch: {id: arm64}, suite: ceph}
with:
identifier: ${{ matrix.arch.id }}-${{ matrix.suite }}
builder-runner-label: ${{ matrix.arch.builder-label }}
charmcraft-channel: ${{ needs.charmcraft-channel.outputs.channel }}
extra-arguments: ${{needs.extra-args.outputs.args}} -k test_${{ matrix.suite }}
extra-arguments: >-
${{needs.extra-args.outputs.args}} -k test_${{ matrix.suite }}
${{ matrix.arch.id == 'arm64' && ' --lxd-containers' || '' }}
juju-channel: 3/stable
load-test-enabled: false
provider: lxd
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/promote-charms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
charm-directory: ${{ fromJson(needs.select-charms.outputs.charms) }}
arch:
- amd64
- arm64
uses: canonical/operator-workflows/.github/workflows/promote_charm.yaml@main
with:
base-architecture: ${{ matrix.arch }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/publish-charms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ jobs:
- { path: ./charms/worker/, tagPrefix: k8s-worker }
arch:
- amd64
- arm64
secrets: inherit
with:
channel: ${{needs.configure-channel.outputs.track}}/${{needs.configure-channel.outputs.risk}}
working-directory: ${{ matrix.charm.path }}
charmcraft-channel: ${{ needs.charmcraft-channel.outputs.channel }}
tag-prefix: ${{ matrix.charm.tagPrefix }}
identifier: ${{ matrix.arch}}-k8s
identifier: ${{matrix.arch}}-k8s
10 changes: 4 additions & 6 deletions charms/CONTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ The `k8s` and `k8s-worker` charms are noticeably tucked into one-another.
```
└── worker
├── charmcraft.yaml
├── requirements.txt
└── k8s
├── charmcraft.yaml
├── lib
Expand All @@ -25,15 +24,14 @@ The unique parts of the charm are what are in each charm's top-level directory:

```
charmcraft.yaml
config.yaml
actions.yaml
metadata.yaml
requirements.yaml
.jujuignore
icon.svg
README.md
```

In order to exclude the `k8s` exclusive components from the `k8s-worker` charm, charmcraft will read the `worker/.jujuignore` file to determine what to leave out of the final charm.

### What's not
### What's shared

The shared portions of each charm are within `worker/k8s` (except for the above mentioned exclusions). This includes shared libraries from `worker/k8s/lib`, shared source from `worker/k8s/src`, shared python dependencies from `worker/k8s/requirements.txt`

Expand Down
15 changes: 15 additions & 0 deletions charms/worker/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ bases:
- name: ubuntu
channel: "24.04"
architectures: [amd64]
- build-on:
- name: ubuntu
channel: "20.04"
architectures: [arm64]
run-on:
- name: ubuntu
channel: "20.04"
architectures: [arm64]
- name: ubuntu
channel: "22.04"
architectures: [arm64]
- name: ubuntu
channel: "24.04"
architectures: [arm64]
config:
options:
labels:
Expand All @@ -59,6 +73,7 @@ parts:
plugin: charm
build-packages: [git]
charm-entrypoint: k8s/src/charm.py
charm-requirements: [k8s/requirements.txt]
promote:
# move paths out of ./k8s to ./ since
# charmcraft assumes ./lib to be there
Expand Down
14 changes: 14 additions & 0 deletions charms/worker/k8s/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ bases:
- name: ubuntu
channel: "24.04"
architectures: [amd64]
- build-on:
- name: ubuntu
channel: "20.04"
architectures: [arm64]
run-on:
- name: ubuntu
channel: "20.04"
architectures: [arm64]
- name: ubuntu
channel: "22.04"
architectures: [arm64]
- name: ubuntu
channel: "24.04"
architectures: [arm64]
config:
options:
annotations:
Expand Down
2 changes: 2 additions & 0 deletions charms/worker/k8s/templates/snap_installation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ amd64:
- name: k8s
install-type: store
channel: edge
classic: true
arm64:
- name: k8s
install-type: store
channel: edge
classic: true
2 changes: 0 additions & 2 deletions charms/worker/requirements.txt

This file was deleted.

81 changes: 70 additions & 11 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import contextlib
import json
import logging
import re
import shlex
from dataclasses import dataclass, field
from itertools import chain
Expand Down Expand Up @@ -82,14 +83,33 @@ class Charm:
Attrs:
ops_test: Instance of the pytest-operator plugin
arch: Cloud Architecture
path: Path to the charm file
metadata: Charm's metadata
app_name: Preferred name of the juju application
"""

ops_test: OpsTest
arch: str
path: Path
_charmfile: Optional[Path] = None
_URL_RE = re.compile(r"ch:(?P<arch>\w+)/(?P<series>\w+)/(?P<charm>.+)")

@staticmethod
def craft_url(charm: str, series: str, arch: str) -> str:
"""Craft a charm URL.
Args:
charm: Charm name
series: Cloud series
arch: Cloud architecture
Returns:
string: URL to the charm
"""
if m := Charm._URL_RE.match(charm):
charm = m.group("charm")
return f"ch:{arch}/{series}/{charm}"

@property
def metadata(self) -> dict:
Expand Down Expand Up @@ -122,7 +142,8 @@ async def resolve(self, charm_files: List[str]) -> Path:
Path().glob(charm_name), # Look in top-level path
self.path.glob(charm_name), # Look in charm-level path
)
self._charmfile, *_ = filter(lambda s: s.name.startswith(header), potentials)
arch_choices = filter(lambda s: self.arch in str(s), potentials)
self._charmfile, *_ = filter(lambda s: s.name.startswith(header), arch_choices)
log.info("For %s found charmfile %s", self.app_name, self._charmfile)
except ValueError:
log.warning("No pre-built charm is available, let's build it")
Expand All @@ -142,19 +163,25 @@ class Bundle:
ops_test: Instance of the pytest-operator plugin
path: Path to the bundle file
content: Loaded content from the path
arch: Cloud Architecture
render: Path to a rendered bundle
applications: Mapping of applications in the bundle.
"""

ops_test: OpsTest
path: Path
arch: str
_content: Mapping = field(default_factory=dict)

@property
def content(self) -> Mapping:
"""Yaml content of the bundle loaded into a dict"""
if not self._content:
self._content = yaml.safe_load(self.path.read_bytes())
loaded = yaml.safe_load(self.path.read_bytes())
series = loaded.get("series", "focal")
for app in loaded["applications"].values():
app["charm"] = Charm.craft_url(app["charm"], series=series, arch=self.arch)
self._content = loaded
return self._content

@property
Expand All @@ -165,6 +192,7 @@ def applications(self) -> Mapping[str, dict]:
@property
def render(self) -> Path:
"""Path to written bundle config to be deployed."""
self.add_constraints({"arch": self.arch})
target = self.ops_test.tmp_path / "bundles" / self.path.name
target.parent.mkdir(exist_ok=True, parents=True)
yaml.safe_dump(self.content, target.open("w"))
Expand Down Expand Up @@ -202,6 +230,25 @@ def add_constraints(self, constraints: Dict[str, str]):
app["constraints"] = " ".join(f"{k}={v}" for k, v in existing.items())


async def cloud_arch(ops_test: OpsTest) -> str:
"""Return current architecture of the selected controller
Args:
ops_test (OpsTest): ops_test plugin
Returns:
string describing current architecture of the underlying cloud
"""
assert ops_test.model, "Model must be present"
controller = await ops_test.model.get_controller()
controller_model = await controller.get_model("controller")
arch = set(
machine.safe_data["hardware-characteristics"]["arch"]
for machine in controller_model.machines.values()
)
return arch.pop()


async def cloud_type(ops_test: OpsTest) -> Tuple[str, bool]:
"""Return current cloud type of the selected controller
Expand Down Expand Up @@ -304,14 +351,26 @@ async def deploy_model(
log.fatal("Failed to determine model: model_name=%s", model_name)


def bundle_file(request) -> Path:
"""Fixture to get bundle file.
Args:
request: pytest request object
Returns:
path to test's bundle file
"""
_file = "test-bundle.yaml"
bundle_marker = request.node.get_closest_marker("bundle_file")
if bundle_marker:
_file = bundle_marker.args[0]
return Path(__file__).parent / "data" / _file


@pytest_asyncio.fixture(scope="module")
async def kubernetes_cluster(request: pytest.FixtureRequest, ops_test: OpsTest):
"""Deploy local kubernetes charms."""
bundle_file = "test-bundle.yaml"
bundle_marker = request.node.get_closest_marker("bundle_file")
if bundle_marker:
bundle_file = bundle_marker.args[0]
bundle_path = Path(__file__).parent / "data" / bundle_file
bundle_path = bundle_file(request)
model = "main"

with ops_test.model_context(model) as the_model:
Expand All @@ -321,13 +380,14 @@ async def kubernetes_cluster(request: pytest.FixtureRequest, ops_test: OpsTest):
return

log.info("Deploying cluster using %s bundle.", bundle_file)
arch = await cloud_arch(ops_test)

charm_path = ("worker/k8s", "worker")
charms = [Charm(ops_test, Path("charms") / p) for p in charm_path]
charms = [Charm(ops_test, arch, Path("charms") / p) for p in charm_path]
charm_files = await asyncio.gather(
*[charm.resolve(request.config.option.charm_files) for charm in charms]
)
bundle = Bundle(ops_test, bundle_path)
bundle = Bundle(ops_test, bundle_path, arch)
_type, _vms = await cloud_type(ops_test)
if _type == "lxd" and not _vms:
log.info("Drop lxd machine constraints")
Expand All @@ -337,7 +397,6 @@ async def kubernetes_cluster(request: pytest.FixtureRequest, ops_test: OpsTest):
bundle.add_constraints({"virt-type": "virtual-machine"})
if request.config.option.apply_proxy:
await cloud_proxied(ops_test)

for path, charm in zip(charm_files, charms):
bundle.switch(charm.app_name, path)
async with deploy_model(request, ops_test, model, bundle) as the_model:
Expand All @@ -353,7 +412,7 @@ async def grafana_agent(kubernetes_cluster: Model):
machine_series = juju.utils.get_version_series(data["base"].split("@")[1])

await kubernetes_cluster.deploy(
f"ch:{machine_arch}/{machine_series}/grafana-agent",
Charm.craft_url("grafana-agent", machine_series, machine_arch),
channel="stable",
series=machine_series,
)
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ deps =
types-PyYAML
types-requests
-r{toxinidir}/test_requirements.txt
-r{toxinidir}/charms/worker/requirements.txt
-r{toxinidir}/charms/worker/k8s/requirements.txt
commands =
pydocstyle {[vars]src_path}
codespell {toxinidir} --skip {toxinidir}/.git --skip {toxinidir}/.tox \
Expand Down

0 comments on commit 7fb4e90

Please sign in to comment.