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()], ), ], }) ]
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"], ), ], }), ]
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
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()
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])
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()))
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()))
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
class Base(ModelConfigDefaults): """Represents a base.""" name: pydantic.StrictStr channel: pydantic.StrictStr architectures: List[pydantic.StrictStr] = [get_host_architecture()]
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