Пример #1
0
def test_ended_commanderror_argparsing(capsys, create_message_handler):
    """Reports just the message to stdout."""
    mh = create_message_handler()
    mh.init(mh.NORMAL)
    mh.ended_cmderror(CommandError("test controlled error", argsparsing=True))
    captured = capsys.readouterr()
    assert captured.out == "test controlled error\n"
Пример #2
0
    def __init__(
        self,
        all_parts: Dict[str, Any],
        *,
        work_dir: pathlib.Path,
        project_dir: pathlib.Path,
        ignore_local_sources: List[str],
    ):
        self._all_parts = all_parts.copy()
        self._project_dir = project_dir

        # set the cache dir for parts package management
        cache_dir = BaseDirectory.save_cache_path("charmcraft")

        try:
            self._lcm = LifecycleManager(
                {"parts": all_parts},
                application_name="charmcraft",
                work_dir=work_dir,
                cache_dir=cache_dir,
                ignore_local_sources=ignore_local_sources,
            )
        except PartsError as err:
            raise CommandError(
                f"Error bootstrapping lifecycle manager: {err}") from err
Пример #3
0
    def run(self, parsed_args):
        """Run the command."""
        if parsed_args.name:
            charm_name = parsed_args.name
        else:
            charm_name = get_name_from_metadata()
            if charm_name is None:
                raise CommandError(
                    "Can't access name in 'metadata.yaml' file. The 'list-lib' command must "
                    "either be executed from a valid project directory, or specify a charm "
                    "name using the --charm-name option.")

        # get tips from the Store
        store = Store(self.config.charmhub)
        to_query = [{'charm_name': charm_name}]
        libs_tips = store.get_libraries_tips(to_query)

        if not libs_tips:
            logger.info("No libraries found for charm %s.", charm_name)
            return

        headers = ['Library name', 'API', 'Patch']
        data = sorted((item.lib_name, item.api, item.patch)
                      for item in libs_tips.values())

        table = tabulate(data,
                         headers=headers,
                         tablefmt='plain',
                         numalign='left')
        for line in table.splitlines():
            logger.info(line)
Пример #4
0
def get_paths_to_include(dirpath):
    """Get all file/dir paths to include."""
    allpaths = set()

    # all mandatory files, which must exist (currently only bundles.yaml is mandatory, and
    # it's verified before)
    for fname in MANDATORY_FILES:
        allpaths.add(dirpath / fname)

    # the extra files, which must be relative
    config = load_yaml(dirpath / 'charmcraft.yaml') or {}
    prime_specs = config.get('parts', {}).get('bundle', {}).get('prime', [])

    for spec in prime_specs:
        # check if it's an absolute path using POSIX's '/' (not os.path.sep, as the charm's
        # config is independent of where charmcraft is running)
        if spec[0] == '/':
            raise CommandError(
                "Extra files in prime config can not be absolute: {!r}".format(
                    spec))

        fpaths = sorted(fpath for fpath in dirpath.glob(spec)
                        if fpath.is_file())
        logger.debug("Including per prime config %r: %s.", spec, fpaths)
        allpaths.update(fpaths)

    return sorted(allpaths)
Пример #5
0
    def validate_entrypoint(self, filepath):
        """Validate that the entrypoint exists and is executable."""
        if filepath is None:
            filepath = self.basedir / 'src' / 'charm.py'
        else:
            filepath = filepath.expanduser().absolute()

        if not filepath.exists():
            raise CommandError("Charm entry point was not found: {!r}".format(str(filepath)))
        if self.basedir not in filepath.parents:
            raise CommandError(
                "Charm entry point must be inside the project: {!r}".format(str(filepath)))
        if not os.access(str(filepath), os.X_OK):  # access does not support pathlib in 3.5
            raise CommandError(
                "Charm entry point must be executable: {!r}".format(str(filepath)))
        return filepath
Пример #6
0
    def _pack_bundle(self):
        """Pack a bundle."""
        project = self.config.project
        config_parts = self.config.parts.copy()
        bundle_part = config_parts.setdefault("bundle", {})
        prime = bundle_part.setdefault("prime", [])

        # get the config files
        bundle_filepath = project.dirpath / "bundle.yaml"
        bundle_config = load_yaml(bundle_filepath)
        if bundle_config is None:
            raise CommandError(
                "Missing or invalid main bundle file: {!r}.".format(str(bundle_filepath))
            )
        bundle_name = bundle_config.get("name")
        if not bundle_name:
            raise CommandError(
                "Invalid bundle config; missing a 'name' field indicating the bundle's name in "
                "file {!r}.".format(str(bundle_filepath))
            )

        # set prime filters
        for fname in MANDATORY_FILES:
            fpath = project.dirpath / fname
            if not fpath.exists():
                raise CommandError("Missing mandatory file: {!r}.".format(str(fpath)))
        prime.extend(MANDATORY_FILES)

        # set source for buiding
        bundle_part["source"] = str(project.dirpath)

        # run the parts lifecycle
        logger.debug("Parts definition: %s", config_parts)
        lifecycle = parts.PartsLifecycle(
            config_parts,
            work_dir=project.dirpath / build.BUILD_DIRNAME,
            ignore_local_sources=[bundle_name + ".zip"],
        )
        lifecycle.run(Step.PRIME)

        # pack everything
        create_manifest(lifecycle.prime_dir, project.started_at, None, [])
        zipname = project.dirpath / (bundle_name + ".zip")
        build_zip(zipname, lifecycle.prime_dir)

        logger.info("Created %r.", str(zipname))
Пример #7
0
 def request_urlpath_text(self, method: str, urlpath: str, *args,
                          **kwargs) -> str:
     """Return a request.Response to a urlpath."""
     try:
         return super().request(method, self.api_base_url + urlpath, *args,
                                **kwargs).text
     except craft_store.errors.CraftStoreError as err:
         raise CommandError(str(err)) from err
Пример #8
0
 def run(self, parsed_args):
     """Run the command."""
     # decide if this will work on a charm or a bundle
     if self.config.type == "charm" or not self.config.project.config_provided:
         self._pack_charm(parsed_args)
     elif self.config.type == "bundle":
         if parsed_args.entrypoint is not None:
             raise CommandError(
                 "The -e/--entry option is valid only when packing a charm")
         if parsed_args.requirement is not None:
             raise CommandError(
                 "The -r/--requirement option is valid only when packing a charm"
             )
         self._pack_bundle(parsed_args)
     else:
         raise CommandError("Unknown type {!r} in charmcraft.yaml".format(
             self.config.type))
Пример #9
0
    def validate_from(self, dirpath):
        """Validate that the charm dir is there and yes, a directory."""
        if dirpath is None:
            dirpath = pathlib.Path.cwd()
        else:
            dirpath = dirpath.expanduser().absolute()

        if not dirpath.exists():
            raise CommandError(
                "the charm directory was not found: {!r}".format(str(dirpath)))
        if not dirpath.is_dir():
            raise CommandError(
                "the charm directory is not really a directory: {!r}".format(
                    str(dirpath)))

        self.basedir = dirpath
        return dirpath
Пример #10
0
    def run(self, parsed_args):
        """Run the command."""
        lib_name = parsed_args.name
        valid_all_chars = set(string.ascii_lowercase + string.digits + '_')
        valid_first_char = string.ascii_lowercase
        if set(lib_name) - valid_all_chars or not lib_name or lib_name[
                0] not in valid_first_char:
            raise CommandError(
                "Invalid library name (can be only lowercase alphanumeric "
                "characters and underscore, starting with alpha).")

        charm_name = get_name_from_metadata()
        if charm_name is None:
            raise CommandError(
                "Cannot access name in 'metadata.yaml' file. The 'create-lib' command needs to "
                "be executed in a valid project's directory.")

        # all libraries born with API version in 0
        full_name = 'charms.{}.v0.{}'.format(charm_name, lib_name)
        lib_data = _get_lib_info(full_name=full_name)
        lib_path = lib_data.path
        if lib_path.exists():
            raise CommandError(
                'This library already exists: {}'.format(lib_path))

        store = Store()
        lib_id = store.create_library_id(charm_name, lib_name)

        # create the new library file from the template
        env = get_templates_environment('charmlibs')
        template = env.get_template('new_library.py.j2')
        context = dict(lib_id=lib_id)
        try:
            lib_path.parent.mkdir(parents=True, exist_ok=True)
            lib_path.write_text(template.render(context))
        except OSError as exc:
            raise CommandError(
                "Got an error when trying to write the library in {}: {!r}".
                format(lib_path, exc))

        logger.info("Library %s created with id %s.", full_name, lib_id)
        logger.info(
            "Make sure to add the library file to your project; for example 'git add %s'.",
            lib_path)
Пример #11
0
    def validate_bases_indices(self, bases_indices):
        """Validate that bases index is valid."""
        if not bases_indices:
            return

        for bases_index in bases_indices:
            if bases_index < 0:
                raise CommandError(
                    f"Bases index '{bases_index}' is invalid (must be >= 0).")

            if not self.config.bases:
                raise CommandError(
                    "No bases configuration found, required when using --bases-index.",
                )

            if bases_index >= len(self.config.bases):
                raise CommandError(
                    f"No bases configuration found for specified index '{bases_index}'."
                )
Пример #12
0
    def error_decorator(self, *args, **kwargs):
        """Handle craft-store error situations and login scenarios."""
        try:
            return method(self, *args, **kwargs)
        except craft_store.errors.NotLoggedIn:
            emit.progress("Credentials not found. Trying to log in...")
        except craft_store.errors.StoreServerError as error:
            if error.response.status_code == 401:
                emit.progress("Existing credentials no longer valid. Trying to log in...")
            else:
                raise CommandError(str(error)) from error
        except craft_store.errors.CraftStoreError as error:
            raise CommandError(
                f"Server error while communicating to the Store: {error!s}"
            ) from error

        self.login()

        return method(self, *args, **kwargs)
Пример #13
0
    def clean_project_environments(
        self,
        *,
        charm_name: str,
        project_path: pathlib.Path,
    ) -> List[str]:
        """Clean up any environments created for project.

        :param charm_name: Name of project.
        :param project_path: Directory of charm project.

        :returns: List of containers deleted.
        """
        deleted: List[str] = []

        # Nothing to do if provider is not installed.
        if not self.is_provider_available():
            return deleted

        inode = project_path.stat().st_ino

        try:
            names = self.multipass.list()
        except multipass.MultipassError as error:
            raise CommandError(str(error)) from error

        for name in names:
            match_regex = f"^charmcraft-{charm_name}-{inode}-.+-.+-.+$"
            if re.match(match_regex, name):
                emit.trace(f"Deleting Multipass VM {name!r}.")
                try:
                    self.multipass.delete(
                        instance_name=name,
                        purge=True,
                    )
                except multipass.MultipassError as error:
                    raise CommandError(str(error)) from error

                deleted.append(name)
            else:
                emit.trace(f"Not deleting Multipass VM {name!r}.")

        return deleted
Пример #14
0
    def validate_entrypoint(self, filepath):
        """Validate that the entrypoint exists and is executable."""
        if filepath is None:
            return None

        filepath = filepath.expanduser().absolute()

        if not filepath.exists():
            raise CommandError("Charm entry point was not found: {!r}".format(
                str(filepath)))
        if self.basedir not in filepath.parents:
            raise CommandError(
                "Charm entry point must be inside the project: {!r}".format(
                    str(filepath)))
        if not os.access(filepath, os.X_OK):
            raise CommandError(
                "Charm entry point must be executable: {!r}".format(
                    str(filepath)))
        return filepath
Пример #15
0
def test_main_controlled_error():
    """Work raised CommandError: message handler notified properly, use indicated return code."""
    simulated_exception = CommandError("boom", retcode=33)
    with patch("charmcraft.main.message_handler") as mh_mock:
        with patch("charmcraft.main.Dispatcher.run") as d_mock:
            d_mock.side_effect = simulated_exception
            retcode = main(["charmcraft", "version"])

    assert retcode == 33
    mh_mock.ended_cmderror.assert_called_once_with(simulated_exception)
Пример #16
0
def test_main_controlled_error():
    """Work raised CommandError: message handler notified properly, use indicated return code."""
    simulated_exception = CommandError('boom', retcode=33)
    with patch.object(logsetup, 'message_handler') as mh_mock:
        with patch('charmcraft.main.Dispatcher.run') as d_mock:
            d_mock.side_effect = simulated_exception
            retcode = main(['charmcraft', 'version'])

    assert retcode == 33
    assert mh_mock.ended_cmderror.called_once_with(simulated_exception)
Пример #17
0
    def run(self, parsed_args):
        """Run the command."""
        lib_name = parsed_args.name
        valid_all_chars = set(string.ascii_lowercase + string.digits + '_')
        valid_first_char = string.ascii_lowercase
        if set(lib_name) - valid_all_chars or not lib_name or lib_name[0] not in valid_first_char:
            raise CommandError(
                "Invalid library name. Must only use lowercase alphanumeric "
                "characters and underscore, starting with alpha.")

        charm_name = get_name_from_metadata()
        if charm_name is None:
            raise CommandError(
                "Cannot find a valid charm name in metadata.yaml. Check you are in a charm "
                "directory with metadata.yaml.")

        # '-' is valid in charm names, but not in a python import
        # mutate the name so the path is a valid import
        importable_charm_name = create_importable_name(charm_name)

        # all libraries born with API version 0
        full_name = 'charms.{}.v0.{}'.format(importable_charm_name, lib_name)
        lib_data = _get_lib_info(full_name=full_name)
        lib_path = lib_data.path
        if lib_path.exists():
            raise CommandError('This library already exists: {}'.format(lib_path))

        store = Store(self.config.charmhub)
        lib_id = store.create_library_id(charm_name, lib_name)

        # create the new library file from the template
        env = get_templates_environment('charmlibs')
        template = env.get_template('new_library.py.j2')
        context = dict(lib_id=lib_id)
        try:
            lib_path.parent.mkdir(parents=True, exist_ok=True)
            lib_path.write_text(template.render(context))
        except OSError as exc:
            raise CommandError(
                "Error writing the library in {}: {!r}.".format(lib_path, exc))

        logger.info("Library %s created with id %s.", full_name, lib_id)
        logger.info("Consider 'git add %s'.", lib_path)
Пример #18
0
def test_main_no_args():
    """The setup.py entry_point function needs to work with no arguments."""
    with patch('sys.argv', ['charmcraft']):
        with patch.object(logsetup, 'message_handler') as mh_mock:
            with patch('charmcraft.main.Dispatcher.run') as d_mock:
                d_mock.side_effect = CommandError('boom', retcode=42)
                retcode = main()

    assert retcode == 42
    assert mh_mock.ended_ok.called_once()
Пример #19
0
    def _discover_charm(self, charm_filepath):
        """Discover the charm name and file path.

        If received path is None, a metadata.yaml will be searched in the current directory. If
        path is given the name is taken from the filename.

        """
        if charm_filepath is None:
            # discover the info using project's metadata, asume the file has the project's name
            # with a .charm extension
            charm_name = get_name_from_metadata()
            if charm_name is None:
                raise CommandError(
                    "Can't access name in 'metadata.yaml' file. The 'upload' command needs to be "
                    "executed in a valid project's directory, or point to a charm file with "
                    "the --charm-file option.")

            charm_filepath = pathlib.Path(charm_name + '.charm').absolute()
            if not os.access(str(charm_filepath),
                             os.R_OK):  # access doesnt support pathlib in 3.5
                raise CommandError(
                    "Can't access charm file {!r}. You can indicate a charm file with "
                    "the --charm-file option.".format(str(charm_filepath)))

        else:
            # the path is given, asume the charm name is part of the file name
            # XXX Facundo 2020-06-30: Actually, we need to open the ZIP file, extract the
            # included metadata.yaml file, and read the name from there. Issue: #77.
            charm_filepath = charm_filepath.expanduser()
            if not os.access(str(charm_filepath),
                             os.R_OK):  # access doesnt support pathlib in 3.5
                raise CommandError(
                    "Can't access the indicated charm file: {!r}".format(
                        str(charm_filepath)))
            if not charm_filepath.is_file():
                raise CommandError(
                    "The indicated charm is not a file: {!r}".format(
                        str(charm_filepath)))

            charm_name = charm_filepath.stem

        return charm_name, charm_filepath
Пример #20
0
def ensure_charmcraft_environment_is_supported():
    """Assert that environment is supported.

    :raises CommandError: if unsupported environment.
    """
    if (not is_charmcraft_running_in_supported_environment()
            and not is_charmcraft_running_in_developer_mode()):
        raise CommandError(
            "For a supported user experience, please use the Charmcraft snap. "
            "For more information, please see https://juju.is/docs/sdk/setting-up-charmcraft"
        )
Пример #21
0
    def unmarshal(cls, obj: Dict[str, Any]):
        """Unmarshal object with necessary translations and error handling.

        :returns: valid CharmMetadata.

        :raises CommandError: On failure to unmarshal object.
        """
        try:
            return cls.parse_obj(obj)
        except pydantic.error_wrappers.ValidationError as error:
            raise CommandError(format_pydantic_errors(error.errors(), file_name=CHARM_METADATA))
Пример #22
0
def test_bundle_debug_with_error(tmp_path, bundle_yaml, bundle_config,
                                 mock_parts, mock_launch_shell):
    mock_parts.PartsLifecycle.return_value.run.side_effect = CommandError(
        "fail")
    bundle_yaml(name="testbundle")
    bundle_config.set(type="bundle")
    (tmp_path / "README.md").write_text("test readme")

    with pytest.raises(CommandError):
        PackCommand(bundle_config).run(get_namespace(debug=True))

    assert mock_launch_shell.mock_calls == [mock.call()]
Пример #23
0
 def run(self):
     """Really run the command."""
     if isinstance(self.command, HelpCommand):
         self.command.run(self.parsed_args, self.commands)
     else:
         if self.command.needs_config and not self.command.config.project.config_provided:
             raise CommandError(
                 "The specified command needs a valid 'charmcraft.yaml' configuration file (in "
                 "the current directory or where specified with --project-dir option); see "
                 "the reference: https://discourse.charmhub.io/t/charmcraft-configuration/4138"
             )
         self.command.run(self.parsed_args)
Пример #24
0
    def run(self, target_step: Step) -> None:
        """Run the parts lifecycle.

        :param target_step: The final step to execute.

        :raises CommandError: On error during lifecycle ops.
        :raises RuntimeError: On unexpected error.
        """
        previous_dir = os.getcwd()
        try:
            os.chdir(self._project_dir)

            # invalidate build if packing a charm and entrypoint changed
            if "charm" in self._all_parts:
                charm_part = self._all_parts["charm"]
                entrypoint = os.path.normpath(charm_part["charm-entrypoint"])
                dis_entrypoint = os.path.normpath(
                    _get_dispatch_entrypoint(self.prime_dir))
                if entrypoint != dis_entrypoint:
                    self._lcm.clean(Step.BUILD, part_names=["charm"])
                    self._lcm.reload_state()

            emit.trace(
                f"Executing parts lifecycle in {str(self._project_dir)!r}")
            actions = self._lcm.plan(target_step)
            emit.trace(f"Parts actions: {actions}")
            with self._lcm.action_executor() as aex:
                aex.execute(actions)
        except RuntimeError as err:
            raise RuntimeError(
                f"Parts processing internal error: {err}") from err
        except OSError as err:
            msg = err.strerror
            if err.filename:
                msg = f"{err.filename}: {msg}"
            raise CommandError(f"Parts processing error: {msg}") from err
        except Exception as err:
            raise CommandError(f"Parts processing error: {err}") from err
        finally:
            os.chdir(previous_dir)
Пример #25
0
    def _discover_charm(self, charm_filepath):
        """Discover the charm name and file path.

        If received path is None, a metadata.yaml will be searched in the current directory. If
        path is given the name is taken from the filename.

        """
        if charm_filepath is None:
            # discover the info using project's metadata, asume the file has the project's name
            # with a .charm extension
            charm_name = get_name_from_metadata()
            if charm_name is None:
                raise CommandError(
                    "Cannot find a valid charm name in metadata.yaml to upload. Check you are in "
                    "a charm directory with metadata.yaml, or use --charm-file=foo.charm."
                )

            charm_filepath = pathlib.Path(charm_name + '.charm').absolute()
            if not os.access(str(charm_filepath),
                             os.R_OK):  # access doesnt support pathlib in 3.5
                raise CommandError(
                    "Cannot access charm at {!r}. Try --charm-file=foo.charm".
                    format(str(charm_filepath)))

        else:
            # the path is given, asume the charm name is part of the file name
            # XXX Facundo 2020-06-30: Actually, we need to open the ZIP file, extract the
            # included metadata.yaml file, and read the name from there. Issue: #77.
            charm_filepath = charm_filepath.expanduser()
            if not os.access(str(charm_filepath),
                             os.R_OK):  # access doesnt support pathlib in 3.5
                raise CommandError("Cannot access {!r}.".format(
                    str(charm_filepath)))
            if not charm_filepath.is_file():
                raise CommandError("{!r} is not a file.".format(
                    str(charm_filepath)))

            charm_name = charm_filepath.stem

        return charm_name, charm_filepath
Пример #26
0
    def get_destination_url(self, reference):
        """Get the fully qualified URL in the destination registry for a tag/digest reference."""
        if not self.dst_registry.is_manifest_already_uploaded(reference):
            raise CommandError(
                "The {!r} image does not exist in the destination registry".format(
                    reference
                )
            )

        # need to actually get the manifest, because this is what we'll end up getting the v2 one
        _, digest, _ = self.dst_registry.get_manifest(reference)
        final_fqu = self.dst_registry.get_fully_qualified_url(digest)
        return final_fqu
Пример #27
0
def _process_run(cmd: List[str]) -> None:
    """Run an external command logging its output.

    :raises CommandError: if execution crashes or ends with return code not zero.
    """
    emit.progress(f"Running external command {cmd}")
    try:
        proc = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            universal_newlines=True,
        )
    except Exception as err:
        raise CommandError(f"Subprocess execution crashed for command {cmd}") from err

    for line in proc.stdout:
        emit.trace(f"   :: {line.rstrip()}")
    retcode = proc.wait()

    if retcode:
        raise CommandError(f"Subprocess command {cmd} execution failed with retcode {retcode}")
Пример #28
0
def assert_response_ok(response, expected_status=200):
    """Assert the response is ok."""
    if response.status_code != expected_status:
        if response.headers.get("Content-Type") in JSON_RELATED_MIMETYPES:
            errors = response.json().get("errors")
        else:
            errors = None
        raise CommandError(
            "Wrong status code from server (expected={}, got={}) errors={} headers={}".format(
                expected_status, response.status_code, errors, response.headers
            )
        )

    if response.headers.get("Content-Type") not in JSON_RELATED_MIMETYPES:
        return

    result = response.json()
    if "errors" in result:
        raise CommandError(
            "Response with errors from server: {}".format(result["errors"])
        )
    return result
Пример #29
0
    def _hit(self, method, urlpath, body=None):
        """Issue a request to the Store."""
        url = self.api_base_url + urlpath
        logger.debug("Hitting the store: %s %s %s", method, url, body)
        resp = self._auth_client.request(method, url, body)
        if not resp.ok:
            raise CommandError(self._parse_store_error(resp))

        logger.debug("Store ok: %s", resp.status_code)
        # XXX Facundo 2020-06-30: we need to wrap this .json() call, and raise UnknownError (after
        # logging in debug the received raw response). This would catch weird "html" responses,
        # for example, without making charmcraft to badly crash. Related: issue #73.
        data = resp.json()
        return data
Пример #30
0
    def handle_dependencies(self):
        """Handle from-directory and virtualenv dependencies."""
        logger.debug("Installing dependencies")

        # virtualenv with other dependencies (if any)
        if self.requirement_paths:
            retcode = polite_exec(['pip3', 'list'])
            if retcode:
                raise CommandError("problems using pip")

            venvpath = self.buildpath / VENV_DIRNAME
            cmd = [
                'pip3', 'install',  # base command
                '--target={}'.format(venvpath),  # put all the resulting files in that specific dir
            ]
            if _pip_needs_system():
                logger.debug("adding --system to work around pip3 defaulting to --user")
                cmd.append("--system")
            for reqspath in self.requirement_paths:
                cmd.append('--requirement={}'.format(reqspath))  # the dependencies file(s)
            retcode = polite_exec(cmd)
            if retcode:
                raise CommandError("problems installing dependencies")