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

compatible: Add arm64 support to build_charm.yaml and release_charm.yaml #141

Merged
merged 4 commits into from
Feb 27, 2024
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
12 changes: 6 additions & 6 deletions .github/workflows/build_charm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ jobs:
build:
strategy:
matrix:
bases_index: ${{ fromJSON(needs.collect-bases.outputs.bases) }}
name: 'Build charm | base #${{ matrix.bases_index }}'
base: ${{ fromJSON(needs.collect-bases.outputs.bases) }}
name: 'Build charm | base #${{ matrix.base.index }}'
needs:
- get-workflow-version
- collect-bases
runs-on: ubuntu-latest
runs-on: ${{ matrix.base.runner }}
timeout-minutes: 120
steps:
- name: Install CLI
Expand Down Expand Up @@ -130,7 +130,7 @@ jobs:
- run: snap list
- name: Get pack command
id: pack-command
run: get-pack-command --cache='${{ inputs.cache }}' --charm-directory='${{ inputs.path-to-charm-directory }}' --bases-index='${{ matrix.bases_index }}'
run: get-pack-command --cache='${{ inputs.cache }}' --charm-directory='${{ inputs.path-to-charm-directory }}' --bases-index='${{ matrix.base.index }}'
- name: Pack charm
id: pack
working-directory: ${{ inputs.path-to-charm-directory }}
Expand All @@ -139,14 +139,14 @@ jobs:
if: ${{ failure() && steps.pack.outcome == 'failure' }}
uses: actions/upload-artifact@v4
with:
name: logs-charmcraft-build-${{ needs.collect-bases.outputs.artifact-prefix-with-inputs }}-base-${{ matrix.bases_index }}
name: logs-charmcraft-build-${{ needs.collect-bases.outputs.artifact-prefix-with-inputs }}-base-${{ matrix.base.index }}
path: ~/.local/state/charmcraft/log/
if-no-files-found: error
- run: touch .empty
- name: Upload charm package
uses: actions/upload-artifact@v4
with:
name: ${{ needs.collect-bases.outputs.artifact-prefix-with-inputs }}-base-${{ matrix.bases_index }}
name: ${{ needs.collect-bases.outputs.artifact-prefix-with-inputs }}-base-${{ matrix.base.index }}
# .empty file required to preserve directory structure
# See https://github.com/actions/upload-artifact/issues/344#issuecomment-1379232156
path: |
Expand Down
6 changes: 6 additions & 0 deletions python/cli/data_platform_workflows_cli/charmcraft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import enum


class Architecture(str, enum.Enum):
X64 = "amd64"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to name it AMD64 to keep it consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

X64 matches github naming

ARM64 = "arm64"
25 changes: 24 additions & 1 deletion python/cli/data_platform_workflows_cli/collect_charm_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@

import yaml

from . import charmcraft

RUNNERS = {
charmcraft.Architecture.X64: "ubuntu-latest",
charmcraft.Architecture.ARM64: [
"self-hosted",
"data-platform",
"ubuntu",
"ARM64",
"4cpu16ram",
],
}


def main():
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
Expand All @@ -21,7 +34,17 @@ def main():
yaml_data = yaml.safe_load(
pathlib.Path(args.charm_directory, "charmcraft.yaml").read_text()
)
bases = [index for index, _ in enumerate(yaml_data["bases"])]
# GitHub runner for each base
runners = []
for base in yaml_data["bases"]:
# Bases format: https://discourse.charmhub.io/t/charmcraft-bases-provider-support/4713
architectures = (base.get("build-on") or base).get("architectures", ["amd64"])
assert (
len(architectures) == 1
), f"Multiple architectures ({architectures}) in one (charmcraft.yaml) base not supported. Use one base per architecture"
carlcsaposs-canonical marked this conversation as resolved.
Show resolved Hide resolved
architecture = charmcraft.Architecture(architectures[0])
runners.append(RUNNERS[architecture])
bases = [{"index": index, "runner": runner} for index, runner in enumerate(runners)]
logging.info(f"Collected {bases=}")
default_prefix = (
f'packed-charm-cache-{args.cache}-{args.charm_directory.replace("/", "-")}'
Expand Down
128 changes: 81 additions & 47 deletions python/cli/data_platform_workflows_cli/release_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import yaml

from . import charmcraft


@dataclasses.dataclass
class OCIResource:
Expand Down Expand Up @@ -47,75 +49,107 @@ def main():
charm_directory = pathlib.Path(args.charm_directory)

# Upload charm file(s) & store revision
charm_revisions: list[int] = []
charm_revisions: dict[charmcraft.Architecture, list[int]] = {}
for charm_file in charm_directory.glob("*.charm"):
logging.info(f"Uploading {charm_file=}")
# Examples of `charm_file.name`:
# - "mysql-router-k8s_ubuntu-22.04-amd64.charm"
# - "mysql-router-k8s_ubuntu-22.04-amd64-arm64.charm"
architectures = (
charm_file.name.split("_")[-1].removesuffix(".charm").split("-")[2:]
)
assert (
len(architectures) == 1
), f"Multiple architectures ({architectures}) in one (charmcraft.yaml) base not supported. Use one base per architecture"
architecture = charmcraft.Architecture(architectures[0])
logging.info(f"Uploading {charm_file=} {architecture=}")
output = run(["charmcraft", "upload", "--format", "json", charm_file])
revision: int = json.loads(output)["revision"]
logging.info(f"Uploaded charm {revision=}")
charm_revisions.append(revision)
charm_revisions.setdefault(architecture, []).append(revision)
assert len(charm_revisions) > 0, "No charm packages found"

metadata_file = yaml.safe_load((charm_directory / "metadata.yaml").read_text())
charm_name = metadata_file["name"]

# (Only for Kubernetes charms) upload OCI image(s) & store revision
oci_resources: list[OCIResource] = []
oci_resources: dict[charmcraft.Architecture, list[OCIResource]] = {}
resources = metadata_file.get("resources", {})
for resource_name, resource in resources.items():
if resource["type"] != "oci-image":
continue
image_name = resource["upstream-source"]
logging.info(f"Downloading OCI image: {image_name}")
run(["docker", "pull", image_name])
image_id = run(
["docker", "image", "inspect", image_name, "--format", "'{{.Id}}'"]
)
image_id = image_id.rstrip("\n").strip("'").removeprefix("sha256:")
assert "\n" not in image_id, f"Multiple local images found for {image_name}"
logging.info(f"Uploading charm resource: {resource_name}")
output = run(
[
for architecture in charm_revisions.keys():
# Format: ST111 - Multi-architecture `upstream-source` in charm OCI resources
# https://docs.google.com/document/d/19pzpza7zj7qswDRSHBlpqdBrA7Ndcnyh6_75cCxMKSo/edit
upstream_source = resource.get("upstream-source")
if upstream_source is not None and "upstream-sources" in resource:
raise ValueError(
"`upstream-sources` and `upstream-source` cannot be used simultaneously. Use only `upstream-sources`"
)
elif upstream_source:
# Default to X64
upstream_sources = {charmcraft.Architecture.X64.value: upstream_source}
else:
upstream_sources = resource["upstream-sources"]
image_name = upstream_sources[architecture.value]
logging.info(f"Downloading OCI image ({architecture=}): {image_name}")
run(["docker", "pull", image_name])
image_id = run(
["docker", "image", "inspect", image_name, "--format", "'{{.Id}}'"]
)
image_id = image_id.rstrip("\n").strip("'").removeprefix("sha256:")
assert "\n" not in image_id, f"Multiple local images found for {image_name}"
logging.info(f"Uploading charm resource: {resource_name}")
output = run(
[
"charmcraft",
"upload-resource",
"--format",
"json",
charm_name,
resource_name,
"--image",
image_id,
]
)
revision: int = json.loads(output)["revision"]
logging.info(f"Uploaded charm resource {revision=}")
oci_resources.setdefault(architecture, []).append(
OCIResource(resource_name, revision)
)

# Release charm file(s)
for architecture, revisions in charm_revisions.items():
for charm_revision in revisions:
logging.info(f"Releasing {charm_revision=} {architecture=}")
command = [
"charmcraft",
"upload-resource",
"--format",
"json",
"release",
charm_name,
resource_name,
"--image",
image_id,
"--revision",
str(charm_revision),
"--channel",
args.channel,
]
)
revision: int = json.loads(output)["revision"]
logging.info(f"Uploaded charm resource {revision=}")
oci_resources.append(OCIResource(resource_name, revision))

# Release charm file(s)
for charm_revision in charm_revisions:
logging.info(f"Releasing {charm_revision=}")
command = [
"charmcraft",
"release",
charm_name,
"--revision",
str(charm_revision),
"--channel",
args.channel,
]
for oci in oci_resources:
command += ["--resource", f"{oci.resource_name}:{oci.revision}"]
run(command)
for oci in oci_resources[architecture]:
command += ["--resource", f"{oci.resource_name}:{oci.revision}"]
run(command)

# Output GitHub release info
release_tag = f"rev{max(charm_revisions)}"
if len(charm_revisions) == 1:
revisions = []
for revs in charm_revisions.values():
revisions.extend(revs)
release_tag = f"rev{max(revisions)}"
if len(revisions) == 1:
release_title = "Revision "
else:
release_title = "Revisions "
release_title += ", ".join(str(revision) for revision in charm_revisions)
release_notes = f"Released to {args.channel}\nOCI images:\n" + "\n".join(
f"- {dataclasses.asdict(oci)}" for oci in oci_resources
)
release_title += ", ".join(str(revision) for revision in revisions)
oci_info = "OCI images:"
for architecture, resources in oci_resources.items():
oci_info += f"\n- {architecture}:"
for oci in resources:
oci_info += f"\n - {dataclasses.asdict(oci)}"
release_notes = f"Released to {args.channel}\n{oci_info}"
with open("release_notes.txt", "w") as file:
file.write(release_notes)
output = f"release_tag={release_tag}\nrelease_title={release_title}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ async def build_charm(
self, charm_path: typing.Union[str, os.PathLike], bases_index: int = None
) -> pathlib.Path:
charm_path = pathlib.Path(charm_path)
# TODO: add support for multiple architectures
if bases_index is not None:
charmcraft_yaml = yaml.safe_load((charm_path / "charmcraft.yaml").read_text())
assert charmcraft_yaml["type"] == "charm"
Expand All @@ -31,7 +32,7 @@ async def build_charm(
version = base.get("build-on", [base])[0]["channel"]
packed_charms = list(charm_path.glob(f"*{version}-amd64.charm"))
else:
packed_charms = list(charm_path.glob("*.charm"))
packed_charms = list(charm_path.glob("*-amd64.charm"))
if len(packed_charms) == 1:
# python-libjuju's model.deploy(), juju deploy, and juju bundle files expect local charms
# to begin with `./` or `/` to distinguish them from Charmhub charms.
Expand Down