Example #1
0
def test_bases_minimal_short_form(create_config):
    tmp_path = create_config("""
        type: charm
        bases:
          - name: test-name
            channel: test-channel
    """)

    config = load(tmp_path)
    assert config.bases == [
        BasesConfiguration(
            **{
                "build-on": [
                    Base(
                        name="test-name",
                        channel="test-channel",
                        architectures=[get_host_architecture()],
                    ),
                ],
                "run-on": [
                    Base(
                        name="test-name",
                        channel="test-channel",
                        architectures=[get_host_architecture()],
                    ),
                ],
            })
    ]
Example #2
0
def test_multiple_long_form_bases(create_config):
    tmp_path = create_config("""
        type: charm
        bases:
          - build-on:
              - name: test-build-name-1
                channel: test-build-channel-1
            run-on:
              - name: test-run-name-1
                channel: test-run-channel-1
                architectures: [amd64, arm64]
          - build-on:
              - name: test-build-name-2
                channel: test-build-channel-2
            run-on:
              - name: test-run-name-2
                channel: test-run-channel-2
                architectures: [amd64, arm64]
    """)

    config = load(tmp_path)
    assert config.bases == [
        BasesConfiguration(
            **{
                "build-on": [
                    Base(
                        name="test-build-name-1",
                        channel="test-build-channel-1",
                        architectures=[get_host_architecture()],
                    ),
                ],
                "run-on": [
                    Base(
                        name="test-run-name-1",
                        channel="test-run-channel-1",
                        architectures=["amd64", "arm64"],
                    ),
                ],
            }),
        BasesConfiguration(
            **{
                "build-on": [
                    Base(
                        name="test-build-name-2",
                        channel="test-build-channel-2",
                        architectures=[get_host_architecture()],
                    ),
                ],
                "run-on": [
                    Base(
                        name="test-run-name-2",
                        channel="test-run-channel-2",
                        architectures=["amd64", "arm64"],
                    ),
                ],
            }),
    ]
Example #3
0
    def is_base_available(cls, base: Base) -> Tuple[bool, Union[str, None]]:
        """Check if provider can provide an environment matching given base.

        :param base: Base to check.

        :returns: Tuple of bool indicating whether it is a match, with optional
                reason if not a match.
        """
        arch = get_host_architecture()
        if arch not in base.architectures:
            return (
                False,
                f"host architecture {arch!r} not in base architectures {base.architectures!r}",
            )

        if base.name != "ubuntu":
            return (
                False,
                f"name {base.name!r} is not yet supported (must be 'ubuntu')",
            )

        if base.channel not in BASE_CHANNEL_TO_BUILDD_IMAGE_ALIAS.keys():
            return (
                False,
                f"channel {base.channel!r} is not yet supported (must be '18.04' or '20.04')",
            )

        return True, None
Example #4
0
def launched_environment(
    *,
    charm_name: str,
    project_path: pathlib.Path,
    base: Base,
    bases_index: int,
    build_on_index: int,
    lxd_project: str = "charmcraft",
    lxd_remote: str = "local",
):
    """Launch environment for specified base.

    :param charm_name: Name of project.
    :param project_path: Path to project.
    :param base: Base to create.
    :param bases_index: Index of `bases:` entry.
    :param build_on_index: Index of `build-on` within bases entry.
    """
    alias = BASE_CHANNEL_TO_BUILDD_IMAGE_ALIAS[base.channel]
    target_arch = get_host_architecture()

    instance_name = get_instance_name(
        bases_index=bases_index,
        build_on_index=build_on_index,
        project_name=charm_name,
        project_path=project_path,
        target_arch=target_arch,
    )

    environment = get_command_environment()
    image_remote = configure_buildd_image_remote()
    base_configuration = CharmcraftBuilddBaseConfiguration(
        alias=alias, environment=environment, hostname=instance_name
    )
    instance = lxd.launch(
        name=instance_name,
        base_configuration=base_configuration,
        image_name=base.channel,
        image_remote=image_remote,
        auto_clean=True,
        auto_create_project=True,
        map_user_uid=True,
        use_snapshots=True,
        project=lxd_project,
        remote=lxd_remote,
    )

    # Mount project.
    instance.mount(host_source=project_path, target=get_managed_environment_project_path())

    try:
        yield instance
    finally:
        # Ensure to unmount everything and stop instance upon completion.
        instance.unmount_all()
        instance.stop()
Example #5
0
def get_host_as_base() -> Base:
    """Get host OS represented as Base.

    The host OS name is translated to lower-case for consistency.

    :returns: Base configuration matching host.
    """
    os_platform = get_os_platform()
    host_arch = get_host_architecture()
    name = os_platform.system.lower()
    channel = os_platform.release

    return Base(name=name, channel=channel, architectures=[host_arch])
Example #6
0
    def unmarshal(cls, obj: Dict[str, Any], project: Project):
        """Unmarshal object with necessary translations and error handling.

        (1) Perform any necessary translations.

        (2) Standardize error reporting.

        :returns: valid CharmcraftConfig.

        :raises CommandError: On failure to unmarshal object.
        """
        try:
            # Ensure optional type is specified if loading the yaml.
            # This can be removed once charmcraft.yaml is mandatory.
            if "type" not in obj:
                obj["type"] = None

            # Ensure short-form bases are expanded into long-form
            # base configurations.  Doing it here rather than a Union
            # type will simplify user facing errors.
            bases = obj.get("bases")
            if bases is None:
                if obj["type"] in (None, "charm"):
                    notify_deprecation("dn03")
                # Set default bases to Ubuntu 20.04 to match strict snap's
                # effective behavior.
                bases = [
                    {
                        "name": "ubuntu",
                        "channel": "20.04",
                        "architectures": [get_host_architecture()],
                    }
                ]

            # Expand short-form bases if only the bases is a valid list. If it
            # is not a valid list, parse_obj() will properly handle the error.
            if isinstance(bases, list):
                cls.expand_short_form_bases(bases)

            return cls.parse_obj({"project": project, **obj})
        except pydantic.error_wrappers.ValidationError as error:
            raise CommandError(format_pydantic_errors(error.errors()))
Example #7
0
    def unmarshal(cls, obj: Dict[str, Any], project: Project):
        """Unmarshal object with necessary translations and error handling.

        (1) Perform any necessary translations.

        (2) Standardize error reporting.

        :returns: valid CharmcraftConfig.

        :raises CraftError: On failure to unmarshal object.
        """
        try:
            # Ensure short-form bases are expanded into long-form
            # base configurations.  Doing it here rather than a Union
            # type will simplify user facing errors.
            bases = obj.get("bases")
            if bases is None:
                # "type" is accessed with get because this code happens before
                # pydantic actually validating that type is present
                if obj.get("type") == "charm":
                    notify_deprecation("dn03")
                # Set default bases to Ubuntu 20.04 to match strict snap's
                # effective behavior.
                bases = [{
                    "name": "ubuntu",
                    "channel": "20.04",
                    "architectures": [get_host_architecture()],
                }]

            # Expand short-form bases if only the bases is a valid list. If it
            # is not a valid list, parse_obj() will properly handle the error.
            if isinstance(bases, list):
                cls.expand_short_form_bases(bases)

            return cls.parse_obj({"project": project, **obj})
        except pydantic.error_wrappers.ValidationError as error:
            raise CraftError(format_pydantic_errors(error.errors()))
Example #8
0
def test_get_host_architecture(platform_arch, deb_arch):
    """Test all platform mappings in addition to unknown."""
    with patch("platform.machine", return_value=platform_arch):
        assert get_host_architecture() == deb_arch
Example #9
0
class Base(ModelConfigDefaults):
    """Represents a base."""

    name: pydantic.StrictStr
    channel: pydantic.StrictStr
    architectures: List[pydantic.StrictStr] = [get_host_architecture()]
Example #10
0
    def launched_environment(
        self,
        *,
        charm_name: str,
        project_path: pathlib.Path,
        base: Base,
        bases_index: int,
        build_on_index: int,
    ):
        """Launch environment for specified base.

        :param charm_name: Name of project.
        :param project_path: Path to project.
        :param base: Base to create.
        :param bases_index: Index of `bases:` entry.
        :param build_on_index: Index of `build-on` within bases entry.
        """
        alias = BASE_CHANNEL_TO_BUILDD_IMAGE_ALIAS[base.channel]
        target_arch = get_host_architecture()

        instance_name = self.get_instance_name(
            bases_index=bases_index,
            build_on_index=build_on_index,
            project_name=charm_name,
            project_path=project_path,
            target_arch=target_arch,
        )

        environment = self.get_command_environment()
        base_configuration = CharmcraftBuilddBaseConfiguration(
            alias=alias, environment=environment, hostname=instance_name
        )

        try:
            instance = multipass.launch(
                name=instance_name,
                base_configuration=base_configuration,
                image_name=base.channel,
                cpus=2,
                disk_gb=64,
                mem_gb=2,
                auto_clean=True,
            )
        except (bases.BaseConfigurationError, MultipassError) as error:
            raise CommandError(str(error)) from error

        try:
            # Mount project.
            instance.mount(host_source=project_path, target=get_managed_environment_project_path())
        except MultipassError as error:
            raise CommandError(str(error)) from error

        try:
            yield instance
        finally:
            # Ensure to unmount everything and stop instance upon completion.
            try:
                instance.unmount_all()
                instance.stop()
            except MultipassError as error:
                raise CommandError(str(error)) from error