Skip to content

Commit

Permalink
fix(pebble): place pebble in a separate location (#528)
Browse files Browse the repository at this point in the history
The context here is that starting from Ubuntu 24.04, the 'base-files' package
(and related chisel slices) provides "bin" as a symlink to "usr/bin". This
breaks the previous "phantom" pebble part because it created "bin" as a regular
directory which then conflicts with the symlink.

Moving forward, the pebble binary is now placed in ".rock/bin/". This way we
won't get further collisions and this reflects the fact that the location of
the binary is, and should be seen, as an implementation detail.
  • Loading branch information
tigarmo authored Apr 9, 2024
1 parent 1101844 commit 3dddfa1
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 38 deletions.
20 changes: 20 additions & 0 deletions rockcraft/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Project-wide constants."""

# Rock control data location
ROCK_CONTROL_DIR = ".rock"
5 changes: 4 additions & 1 deletion rockcraft/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,4 +551,7 @@ def _add_pebble_data(yaml_data: dict[str, Any]) -> None:
# Project already has a pebble part: this is not supported.
raise CraftValidationError('Cannot override the default "pebble" part')

parts["pebble"] = Pebble.PEBBLE_PART_SPEC
model = BuildPlanner.unmarshal(yaml_data)
build_base = model.build_base if model.build_base else model.base

parts["pebble"] = Pebble.get_part_spec(build_base)
7 changes: 3 additions & 4 deletions rockcraft/oci.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

from rockcraft import errors, layers
from rockcraft.architectures import SUPPORTED_ARCHS
from rockcraft.constants import ROCK_CONTROL_DIR
from rockcraft.pebble import Pebble
from rockcraft.utils import get_snap_command_path

Expand Down Expand Up @@ -357,9 +358,7 @@ def set_entrypoint(self, entrypoint_service: str | None, build_base: str) -> Non
"""Set the OCI image entrypoint. It is always Pebble."""
emit.progress("Configuring entrypoint...")
image_path = self.path / self.image_name
entrypoint = [f"/{Pebble.PEBBLE_BINARY_PATH}", "enter"]
if build_base in ["[email protected]", "[email protected]"]:
entrypoint.append("--verbose")
entrypoint = Pebble.get_entrypoint(build_base)
if entrypoint_service:
entrypoint.extend(["--args", entrypoint_service])
params = ["--clear=config.entrypoint"]
Expand Down Expand Up @@ -463,7 +462,7 @@ def set_control_data(self, metadata: dict[str, Any]) -> None:
local_control_data_path = Path(tempfile.mkdtemp())

# the rock control data structure starts with the folder ".rock"
control_data_rock_folder = local_control_data_path / ".rock"
control_data_rock_folder = local_control_data_path / ROCK_CONTROL_DIR
control_data_rock_folder.mkdir()

rock_metadata_file = control_data_rock_folder / "metadata.yaml"
Expand Down
49 changes: 46 additions & 3 deletions rockcraft/pebble.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from craft_application.errors import CraftValidationError
from craft_cli import emit

from rockcraft.constants import ROCK_CONTROL_DIR


def _alias_generator(name: str) -> str:
"""Convert underscores to dashes in aliases."""
Expand Down Expand Up @@ -166,18 +168,28 @@ class Pebble:

PEBBLE_PATH = "var/lib/pebble/default"
PEBBLE_LAYERS_PATH = f"{PEBBLE_PATH}/layers"
PEBBLE_BINARY_PATH = "bin/pebble"
PEBBLE_PART_SPEC = {
PEBBLE_BINARY_DIR = f"{ROCK_CONTROL_DIR}/bin"
PEBBLE_BINARY_PATH = f"{PEBBLE_BINARY_DIR}/pebble"
PEBBLE_BINARY_PATH_PREVIOUS = "bin/pebble"
_BASE_PART_SPEC = {
"plugin": "nil",
"stage-snaps": ["pebble/latest/stable"],
"stage": [PEBBLE_BINARY_PATH],
# We need this because "services" is Optional, but the directory must exist
"override-prime": str(
"craftctl default\n"
f"mkdir -p {PEBBLE_LAYERS_PATH}\n"
f"chmod 777 {PEBBLE_PATH}"
),
}
PEBBLE_PART_SPEC = {
**_BASE_PART_SPEC,
"organize": {"bin": PEBBLE_BINARY_DIR},
"stage": [PEBBLE_BINARY_PATH],
}
PEBBLE_PART_SPEC_PREVIOUS = {
**_BASE_PART_SPEC,
"stage": [PEBBLE_BINARY_PATH_PREVIOUS],
}

def define_pebble_layer(
self,
Expand Down Expand Up @@ -229,3 +241,34 @@ def define_pebble_layer(
)

tmp_new_layer.chmod(0o777)

@staticmethod
def get_part_spec(build_base: str) -> dict[str, Any]:
"""Get the part providing the pebble binary for a given build base."""
part_spec: dict[str, Any] = Pebble.PEBBLE_PART_SPEC

if Pebble._is_focal_or_jammy(build_base):
part_spec = Pebble.PEBBLE_PART_SPEC_PREVIOUS

return part_spec

@staticmethod
def get_entrypoint(build_base: str) -> list[str]:
"""Get the rock's entry point for a given build base."""
is_legacy = Pebble._is_focal_or_jammy(build_base)

pebble_path = Pebble.PEBBLE_BINARY_PATH
if is_legacy:
# Previously pebble existed in /bin/pebble
pebble_path = Pebble.PEBBLE_BINARY_PATH_PREVIOUS

entrypoint = [f"/{pebble_path}", "enter"]

if is_legacy:
entrypoint += ["--verbose"]

return entrypoint

@staticmethod
def _is_focal_or_jammy(build_base: str) -> bool:
return build_base in ("[email protected]", "[email protected]")
2 changes: 1 addition & 1 deletion tests/spread/rockcraft/base-devel/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ execute: |
rm base-devel_0.1_amd64.rock
docker images base-devel:0.1
id=$(docker run --rm -d base-devel:0.1)
test "$(docker inspect "$id" -f '{{json .Config.Entrypoint}}')" = '["/bin/pebble","enter"]'
test "$(docker inspect "$id" -f '{{json .Config.Entrypoint}}')" = '["/.rock/bin/pebble","enter"]'
docker rm -f "$id"
restore: |
Expand Down
27 changes: 14 additions & 13 deletions tests/spread/rockcraft/chisel/rockcraft.yaml
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
name: chiseled-dotnet
summary: A "bare" rock containing the .NET runtime
description: A "bare" rock containing the .NET runtime
name: chiseled-base-files
summary: An Ubuntu 24.04 rock with chiseled base-files
description: |
Check that a simple rock containing base-files is able to build successfully
with the part that provides the pebble binary.
license: Apache-2.0

version: "0.0.1"

base: bare
build_base: [email protected]
run-user: _daemon_
services:
dotnet:
override: replace
command: /usr/lib/dotnet/dotnet [ --info ]
startup: enabled
build-base: devel

platforms:
amd64:

parts:

chisel-part:
plugin: nil
stage-packages:
- dotnet-runtime-6.0_libs
# This slice has "bin" as a symlink to "usr/bin"
- base-files_base
# This slice has "test"
- coreutils_bins
# This is needed to generate /etc/passwd
- base-passwd_data
13 changes: 7 additions & 6 deletions tests/spread/rockcraft/chisel/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ execute: |
run_rockcraft pack
ROCK=$(ls ./*.rock)
IMG_NAME=chiselled-image
IMG_NAME=chiseled-base-files
test -f "$ROCK"
# copy image to docker
docker images
sudo /snap/rockcraft/current/bin/skopeo --insecure-policy copy "oci-archive:$ROCK" "docker-daemon:$IMG_NAME:latest"
rm "$ROCK"
docker images $IMG_NAME:latest
docker inspect $IMG_NAME:latest --format '{{.Config.User}}' | MATCH "_daemon_"
id=$(docker run --rm -d $IMG_NAME)
docker logs "$id"
docker rm -f "$id"
# Check that the file "md5sum" exists in "/bin". This file is provided
# by the "coreutils_bins" slice as "/usr/bin/lmd5sum", so this check will
# only succeed if:
# * the "/bin -> usr/bin" symlink is correct, and;
# * pebble itself is found and working correctly (for the exec call).
docker run --rm $IMG_NAME -v exec test -f /bin/md5sum
4 changes: 2 additions & 2 deletions tests/unit/commands/test_expand_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@
plugin: nil
stage-snaps:
- pebble/latest/stable
stage:
- bin/pebble
override-prime: |-
craftctl default
mkdir -p var/lib/pebble/default/layers
chmod 777 var/lib/pebble/default
stage:
- bin/pebble
services:
my-service:
override: merge
Expand Down
27 changes: 24 additions & 3 deletions tests/unit/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pathlib import Path

import pytest
import yaml

from rockcraft.pebble import Pebble

ENVIRONMENT_YAML = """\
Expand Down Expand Up @@ -57,10 +60,28 @@ def test_application_expand_environment(new_dir, default_application):
}


def test_application_pebble_part(new_dir, default_application):
@pytest.mark.parametrize(
("base", "build_base", "expected_spec"),
[
# 24.04 and beyond: pebble exists in .rock/bin/
("bare", "devel", Pebble.PEBBLE_PART_SPEC),
("[email protected]", "devel", Pebble.PEBBLE_PART_SPEC),
# 20.04 and 22.04: pebble exists in bin/
("[email protected]", None, Pebble.PEBBLE_PART_SPEC_PREVIOUS),
("[email protected]", None, Pebble.PEBBLE_PART_SPEC_PREVIOUS),
("[email protected]", "[email protected]", Pebble.PEBBLE_PART_SPEC_PREVIOUS),
("[email protected]", "[email protected]", Pebble.PEBBLE_PART_SPEC_PREVIOUS),
],
)
def test_application_pebble_part(
new_dir, default_application, base, build_base, expected_spec
):
"""Test that loading the project through the application adds the Pebble part."""
project_file = Path(new_dir) / "rockcraft.yaml"
project_file.write_text(ENVIRONMENT_YAML)
new_yaml = yaml.safe_load(ENVIRONMENT_YAML)
new_yaml["base"] = base
new_yaml["build-base"] = build_base
project_file.write_text(yaml.safe_dump(new_yaml))

project = default_application.project
assert project.parts["pebble"] == Pebble.PEBBLE_PART_SPEC
assert project.parts["pebble"] == expected_spec
11 changes: 8 additions & 3 deletions tests/unit/test_oci.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import tests
from rockcraft import errors, oci
from rockcraft.architectures import SUPPORTED_ARCHS
from rockcraft.pebble import Pebble

MOCK_NEW_USER = {
"user": "foo",
Expand Down Expand Up @@ -562,23 +563,26 @@ def test_set_default_user(self, mock_run):
]

@pytest.mark.parametrize(
("service", "build_base", "verbose", "service_config"),
("service", "build_base", "pebble_binary", "verbose", "service_config"),
[
(
None,
"[email protected]",
Pebble.PEBBLE_BINARY_PATH_PREVIOUS,
["--config.entrypoint", "--verbose"],
[],
),
(
None,
"[email protected]",
Pebble.PEBBLE_BINARY_PATH,
[],
[],
),
(
"test-service",
"[email protected]",
Pebble.PEBBLE_BINARY_PATH_PREVIOUS,
["--config.entrypoint", "--verbose"],
[
"--config.entrypoint",
Expand All @@ -590,6 +594,7 @@ def test_set_default_user(self, mock_run):
(
"test-service",
"[email protected]",
Pebble.PEBBLE_BINARY_PATH,
[],
[
"--config.entrypoint",
Expand All @@ -601,7 +606,7 @@ def test_set_default_user(self, mock_run):
],
)
def test_set_entrypoint_default(
self, mock_run, service, build_base, verbose, service_config
self, mock_run, service, build_base, pebble_binary, verbose, service_config
):
image = oci.Image("a:b", Path("/tmp"))

Expand All @@ -614,7 +619,7 @@ def test_set_entrypoint_default(
"/tmp/a:b",
"--clear=config.entrypoint",
"--config.entrypoint",
"/bin/pebble",
f"/{pebble_binary}",
"--config.entrypoint",
"enter",
]
Expand Down
11 changes: 9 additions & 2 deletions tests/unit/test_pebble.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,17 @@ class TestPebble:
def test_attributes(self):
assert Pebble.PEBBLE_PATH == "var/lib/pebble/default"
assert Pebble.PEBBLE_LAYERS_PATH == "var/lib/pebble/default/layers"
assert Pebble.PEBBLE_BINARY_PATH == "bin/pebble"
assert Pebble.PEBBLE_BINARY_PATH == ".rock/bin/pebble"
assert Pebble.PEBBLE_BINARY_PATH_PREVIOUS == "bin/pebble"
assert all(
field in Pebble.PEBBLE_PART_SPEC
for field in ["plugin", "stage-snaps", "stage", "override-prime"]
for field in [
"plugin",
"stage-snaps",
"organize",
"stage",
"override-prime",
]
)

@pytest.mark.parametrize(
Expand Down

0 comments on commit 3dddfa1

Please sign in to comment.