Example #1
0
def _load_file(cls: Type[T], path: Path) -> T:
    if not path.exists():
        return cls()

    with path.open() as f:
        try:
            config = toml.load(f)
        except ValueError as e:
            raise ConfigurationError(f"TOML file {path} is unparasble: {e}") from e

    # In previous versions of remote, `include_vcs_ignore_patterns` key was named with a typo
    # Now we need to check if config is using the old name to maintain backward compatibility
    _backward_compatible_sanitize(config, {"include_vsc_ignore_patterns": "include_vcs_ignore_patterns"})

    try:
        return cls.parse_obj(config)
    except ValidationError as e:
        messages = []
        for err in e.errors():
            location = ".".join((str(x) for x in err["loc"]))
            reason = err["msg"]
            messages.append(f"  - {location}: {reason}")

        msg = "\n".join(messages)
        raise ConfigurationError(f"Invalid value in configuration file {path}:\n{msg}") from e
Example #2
0
def _extract_shell_info(line: str, env_vars: List[str]) -> Tuple[str, str]:
    if not env_vars:
        return DEFAULT_SHELL, DEFAULT_SHELL_OPTIONS

    vars_string = env_vars[0]

    env = {}
    items = vars_string.split()
    index = 0
    while index < len(items):
        key, value = items[index].split("=")
        if value.startswith("'") or value.startswith('"'):
            control_character = value[0]
            while index < len(items) - 1:
                if value[-1] == control_character:
                    break
                index += 1
                value += " " + items[index]
            if not value[-1] == control_character:
                raise ConfigurationError(
                    f"Config line {line} is corrupted. Cannot parse end {key}={value}"
                )

        env[key] = value.strip("\"'")

        index += 1
    print(env)
    # TODO: these shell types are not used in new implementation, need to remove them
    shell = env.pop("RSHELL", DEFAULT_SHELL)
    shell_options = env.pop("RSHELL_OPTS", DEFAULT_SHELL_OPTIONS)
    if env:
        raise ConfigurationError(
            f"Config line {line} contains unexpected env variables: {env}. Only RSHELL and RSHELL_OPTS can be used"
        )
    return shell, shell_options
Example #3
0
def load_ignores(workspace_root: Path) -> SyncRules:
    ignores: Dict[str, List[str]] = defaultdict(list)
    ignores["both"].extend(BASE_IGNORES)

    ignore_file = workspace_root / IGNORE_FILE_NAME
    if not ignore_file.exists():
        return _postprocess(ignores)

    active_section = "both"
    is_new_format = None
    for line in ignore_file.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue

        matcher = IGNORE_SECTION_REGEX.match(line)
        if matcher is None:
            if is_new_format is None:
                is_new_format = False
            ignores[active_section].append(line)
        else:
            if is_new_format is None:
                is_new_format = True
            elif not is_new_format:
                raise ConfigurationError(
                    f"Few ignore patters were listed in {IGNORE_FILE_NAME} before the first section {matcher.group(1)} appeared. "
                    "Please list all ignored files after a section declaration if you use new ignore format"
                )
            active_section = matcher.group(1)

    return _postprocess(ignores)
Example #4
0
def _postprocess(ignores):
    pull = ignores.pop("pull", [])
    push = ignores.pop("push", [])
    both = ignores.pop("both", [])
    if ignores:
        raise ConfigurationError(
            f"{IGNORE_FILE_NAME} file has unexpected sections: {', '.join(ignores.keys())}. Please remove them"
        )
    return SyncRules(pull=pull, push=push, both=both)
Example #5
0
def resolve_workspace_root(
        working_dir: Path) -> Tuple[ConfigurationMedium, Path]:
    """Find and return the directory in this tree that has remote-ing set up"""
    possible_directory = working_dir
    root = Path("/")
    while possible_directory != root:
        for meduim in CONFIG_MEDIUMS:
            if meduim.is_workspace_root(possible_directory):
                return meduim, possible_directory
        possible_directory = possible_directory.parent

    raise ConfigurationError(
        f"Cannot resolve the remote workspace in {working_dir}")
Example #6
0
def load_default_configuration_num(workspace_root: Path) -> int:
    # If REMOTE_HOST_INDEX is set, that overrides settings in .remoteindex
    env_index = os.environ.get("REMOTE_HOST_INDEX")
    if env_index:
        try:
            return int(env_index)
        except ValueError:
            raise ConfigurationError(
                f"REMOTE_HOST_INDEX env variable contains symbols other than numbers: '{env_index}'. "
                "Please set the coorect index value to continue")

    index_file = workspace_root / INDEX_FILE_NAME
    if not index_file.exists():
        return 0

    # Configuration uses 1-base index and we need to have 0-based
    text = index_file.read_text().strip()
    try:
        return int(text) - 1
    except ValueError:
        raise ConfigurationError(
            f"File {index_file} contains symbols other than numbers: '{text}'. "
            "Please remove it or replace the value to continue")
Example #7
0
def _load_file(cls, path: Path):
    if not path.exists():
        return cls()

    with path.open() as f:
        try:
            config = toml.load(f)
        except ValueError as e:
            raise ConfigurationError(
                f"TOML file {path} is unparasble: {e}") from e

    try:
        return cls.parse_obj(config)
    except ValidationError as e:
        messages = []
        for err in e.errors():
            location = ".".join((str(x) for x in err["loc"]))
            reason = err["msg"]
            messages.append(f"  - {location}: {reason}")

        msg = "\n".join(messages)
        raise ConfigurationError(
            f"Invalid value in configuration file {path}:\n{msg}") from e
Example #8
0
    def load_config(self, workspace_root: Path) -> WorkspaceConfig:
        local_config = load_local_config(workspace_root)
        local_ignores_config = load_local_ignores_config(workspace_root)

        # We might accidentally modify config value, so we need to create a copy of it
        global_config = self.global_config.copy()
        config_dict = {
            field: _merge_field(field, global_config, local_config, local_ignores_config)
            for field in WorkCycleConfig.__fields__
        }
        merged_config = WorkCycleConfig.parse_obj(config_dict)

        if merged_config.hosts is None:
            raise ConfigurationError("You need to provide at least one remote host to connect")

        configurations = []
        configuration_index = 0
        for num, connection in enumerate(merged_config.hosts):
            if connection.default:
                configuration_index = num

            configurations.append(
                RemoteConfig(
                    host=connection.host,
                    directory=connection.directory or self._generate_remote_directory_from_path(workspace_root),
                    supports_gssapi=connection.supports_gssapi_auth,
                    label=connection.label,
                    port=connection.port,
                )
            )
        ignores = SyncRules(
            pull=_get_exclude(merged_config.pull, workspace_root),
            push=_get_exclude(merged_config.push, workspace_root),
            both=_get_exclude(merged_config.both, workspace_root) + [WORKSPACE_CONFIG],
        )

        includes = SyncRules(
            pull=merged_config.pull.include if merged_config.pull else [],
            push=merged_config.push.include if merged_config.push else [],
            both=merged_config.both.include if merged_config.both else [],
        )

        return WorkspaceConfig(
            root=workspace_root,
            configurations=configurations,
            default_configuration=configuration_index,
            ignores=ignores,
            includes=includes,
        )
Example #9
0
    def load_config(self, workspace_root: Path) -> WorkspaceConfig:
        configurations = load_configurations(workspace_root)
        configuration_index = load_default_configuration_num(workspace_root)
        if configuration_index > len(configurations) - 1:
            raise ConfigurationError(
                f"Configuration #{configuration_index + 1} requested but there are only {len(configurations)} declared"
            )
        ignores = load_ignores(workspace_root)

        return WorkspaceConfig(
            root=workspace_root,
            configurations=configurations,
            default_configuration=configuration_index,
            ignores=ignores,
        )
Example #10
0
def parse_config_line(line: str) -> RemoteConfig:
    # The line should look like this:
    # sdas-ld2:.remotes/814f27f15f4e7a0842cada353dfc765a RSHELL=zsh
    entry, *env_items = line.split(maxsplit=1)
    shell, shell_options = _extract_shell_info(line, env_items)

    parts = entry.split(":")
    if len(parts) != 2:
        raise ConfigurationError(
            f"The configuration string is malformed: {parts}. Please use host-name:remote_dir format"
        )
    host, directory = parts
    return RemoteConfig(host=host,
                        directory=Path(directory),
                        shell=shell,
                        shell_options=shell_options)
Example #11
0
def load_local_config(workspace_root: Path) -> LocalConfig:
    config_file = workspace_root / WORKSPACE_CONFIG
    config = _load_file(LocalConfig, config_file)

    if config.extends is not None:
        duplicate_fields = []
        for field in WorkCycleConfig.__fields__:
            if getattr(config, field) and getattr(config.extends, field):
                duplicate_fields.append(field)

        if duplicate_fields:
            fields_str = ",".join(duplicate_fields)
            raise ConfigurationError(
                f"Following fields are specified in for overwrite and extend in {config_file} file: {fields_str}."
            )

    return config