Skip to content

Commit

Permalink
feat: backport part of skopeo uploads to 2.x
Browse files Browse the repository at this point in the history
  • Loading branch information
lengau committed Jun 13, 2024
1 parent 539230c commit 74967ea
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 24 deletions.
44 changes: 36 additions & 8 deletions charmcraft/commands/store/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from tabulate import tabulate

from charmcraft.cmdbase import BaseCommand
from charmcraft import parts, utils
from charmcraft import errors, parts, utils

from charmcraft.commands.store.registry import ImageHandler, OCIRegistry, LocalDockerdInterface
from charmcraft.commands.store.store import Store, Entity
Expand Down Expand Up @@ -1849,6 +1849,7 @@ def run(self, parsed_args):
dockerd = LocalDockerdInterface()

server_image_digest = None
# "Classic" method - use Docker
if ":" in parsed_args.image:
# the user provided a digest; check if the specific image is
# already in Canonical's registry
Expand All @@ -1868,14 +1869,41 @@ def run(self, parsed_args):

if server_image_digest is None:
if image_info is None:
raise CraftError("Image not found locally.")
emit.progress(
"Image not found locally. Passing path directly to skopeo.",
permanent=True,
)
skopeo = utils.Skopeo()
registry_url_without_https = self.config.charmhub.registry_url[8:]
with emit.open_stream("Running Skopeo") as stream:
skopeo.copy(
parsed_args.image,
f"docker://{registry_url_without_https}/{image_name}",
dest_username=credentials.username,
dest_password=credentials.password,
all_images=True,
stdout=stream,
stderr=stream,
)
try:
image_info = skopeo.inspect(parsed_args.image)
except errors.SubprocessError as exc:
raise errors.CraftError(
"Could not inspect OCI image.", details=f"{exc.message}\n{exc.details}"
)
try:
server_image_digest = image_info["Digest"]
except KeyError:
raise errors.CraftError("Could not get digest for image.")

# upload it from local registry
emit.progress("Uploading from local registry.", permanent=True)
server_image_digest = ih.upload_from_local(image_info)
emit.progress(
f"Image uploaded, new remote digest: {server_image_digest}.", permanent=True
)
else:
# upload it from local registry
emit.progress("Uploading from local registry.", permanent=True)
server_image_digest = ih.upload_from_local(image_info)
emit.progress(
f"Image uploaded, new remote digest: {server_image_digest}.",
permanent=True,
)

# all is green, get the blob to upload to Charmhub
content = store.get_oci_image_blob(
Expand Down
22 changes: 22 additions & 0 deletions charmcraft/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"""Charmcraft error classes."""
import io
import pathlib
import shlex
import subprocess
import textwrap
from typing import Iterable, Mapping

from craft_cli import CraftError
Expand Down Expand Up @@ -121,3 +124,22 @@ def __init__(self, extra_dependencies: Iterable[str]):

class ExtensionError(CraftError):
"""Error related to extension handling."""


class SubprocessError(CraftError):
"""A craft-cli friendly subprocess error."""

@classmethod
def from_subprocess(cls, error: subprocess.CalledProcessError):
"""Convert a CalledProcessError to a craft-cli error."""
error_details = f"Full command: {shlex.join(error.cmd)}\nError text:\n"
if isinstance(error.stderr, str):
error_details += textwrap.indent(error.stderr, " ")
else:
stderr = error.stderr
stderr.seek(io.SEEK_SET)
error_details += textwrap.indent(stderr.read(), " ")
return cls(
f"Error while running {error.cmd[0]} (return code {error.returncode})",
details=error_details,
)
36 changes: 20 additions & 16 deletions charmcraft/utils/skopeo.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import shutil
import subprocess
from collections.abc import Sequence
from typing import Any, cast, overload
from typing import Any, Dict, List, Optional, Union, cast, overload

from charmcraft import errors

Expand All @@ -34,9 +34,9 @@ def __init__(
*,
skopeo_path: str = "",
insecure_policy: bool = False,
arch: str | None = None,
os: str | None = None,
tmpdir: pathlib.Path | None = None,
arch: Union[str, None] = None,
os: Union[str, None] = None,
tmpdir: Union[pathlib.Path, None] = None,
debug: bool = False,
) -> None:
if skopeo_path:
Expand All @@ -55,7 +55,7 @@ def __init__(

self._run_skopeo([self._skopeo, "--version"], capture_output=True, text=True)

def get_global_command(self) -> list[str]:
def get_global_command(self) -> List[str]:
"""Prepare the global skopeo options."""
command = [self._skopeo]
if self._insecure_policy:
Expand Down Expand Up @@ -84,12 +84,12 @@ def copy(
*,
all_images: bool = False,
preserve_digests: bool = False,
source_username: str | None = None,
source_password: str | None = None,
dest_username: str | None = None,
dest_password: str | None = None,
stdout: io.FileIO | int | None = None,
stderr: io.FileIO | int | None = None,
source_username: Optional[str] = None,
source_password: Optional[str] = None,
dest_username: Optional[str] = None,
dest_password: Optional[str] = None,
stdout: Union[io.FileIO, int, None] = None,
stderr: Union[io.FileIO, int, None] = None,
) -> subprocess.CompletedProcess:
"""Copy an OCI image using Skopeo."""
command = [
Expand Down Expand Up @@ -122,19 +122,23 @@ def copy(
@overload
def inspect(
self, image: str, *, format_template: None = None, raw: bool = False, tags: bool = True
) -> dict[str, Any]: ...
) -> Dict[str, Any]:
...

@overload
def inspect(
self, image: str, *, format_template: str, raw: bool = False, tags: bool = True
) -> str: ...
) -> str:
...

def inspect(
self,
image: str,
image,
*,
format_template: str | None = None,
format_template=None,
raw: bool = False,
tags: bool = True,
) -> dict[str, Any] | str:
):
"""Inspect an image."""
command = [*self.get_global_command(), "inspect"]
if format_template is not None:
Expand Down
26 changes: 26 additions & 0 deletions snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,32 @@ parts:
organize:
bin/craftctl: libexec/charmcraft/craftctl

skopeo: # Copied from Rockcraft
plugin: nil
source: https://github.com/containers/skopeo.git
source-tag: v1.15.1
override-build: |
CGO=1 go build -ldflags -linkmode=external ./cmd/skopeo
mkdir "$CRAFT_PART_INSTALL"/bin
install -m755 skopeo "$CRAFT_PART_INSTALL"/bin/skopeo
stage-packages:
- libgpgme11
- libassuan0
- libbtrfs0
- libdevmapper1.02.1
build-attributes:
- enable-patchelf
build-snaps:
- go/1.21/stable
build-packages:
- libgpgme-dev
- libassuan-dev
- libbtrfs-dev
- libdevmapper-dev
- pkg-config
organize:
bin/skopeo: libexec/charmcraft/skopeo

hooks:
configure:
environment:
Expand Down
3 changes: 3 additions & 0 deletions tests/spread/store/resources/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ execute: |
last_revision_created=$(echo $last_revision | jq -r .created_at)
[[ $start_datetime < $last_revision_created ]]
# Check that skopeo upload-resource works.
charmcraft upload-resource $CHARM_DEFAULT_NAME example-image --image=docker://hello-world@sha256:18a657d0cc1c7d0678a3fbea8b7eb4918bba25968d3e1b0adebfa71caddbc346
# release and check full status
charmcraft release $CHARM_DEFAULT_NAME -r $last_charm_revno -c edge --resource=example-file:$last_file_revno --resource=example-image:$last_image_revno
edge_release=$(charmcraft status $CHARM_DEFAULT_NAME --format=json | jq -r '.[] | select(.track=="latest") | .mappings[0].releases | .[] | select(.channel=="latest/edge")')
Expand Down

0 comments on commit 74967ea

Please sign in to comment.