aboutsummaryrefslogtreecommitdiff
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------

from os import PathLike
from pathlib import Path
from typing import Dict, Optional, Union

from ..._utils.utils import load_yaml
from ...constants._common import FILE_PREFIX, DefaultOpenEncoding
from ...entities._validation import MutableValidationResult, ValidationResultBuilder


class InternalEnvironment:
    # conda section
    CONDA_DEPENDENCIES = "conda_dependencies"
    CONDA_DEPENDENCIES_FILE = "conda_dependencies_file"
    PIP_REQUIREMENTS_FILE = "pip_requirements_file"
    DEFAULT_PYTHON_VERSION = "3.8.5"
    # docker section
    BUILD = "build"
    DOCKERFILE = "dockerfile"

    def __init__(
        self,
        docker: Optional[Dict] = None,
        conda: Optional[Dict] = None,
        os: Optional[str] = None,
        name: Optional[str] = None,
        version: Optional[str] = None,
        python: Optional[Dict] = None,
    ):
        self.docker = docker
        self.conda = conda
        self.os = os if os else "Linux"
        self.name = name
        self.version = version
        self.python = python
        self._docker_file_resolved = False

    @staticmethod
    def _parse_file_path(value: str) -> str:
        return value[len(FILE_PREFIX) :] if value.startswith(FILE_PREFIX) else value

    def _validate_conda_section(
        self, base_path: Union[str, PathLike], skip_path_validation: bool
    ) -> MutableValidationResult:
        validation_result = ValidationResultBuilder.success()
        if not self.conda:
            return validation_result
        dependencies_field_names = {self.CONDA_DEPENDENCIES, self.CONDA_DEPENDENCIES_FILE, self.PIP_REQUIREMENTS_FILE}
        if len(set(self.conda) & dependencies_field_names) > 1:
            validation_result.append_warning(
                yaml_path="conda",
                message="Duplicated declaration of dependencies, will honor in the order "
                "conda_dependencies, conda_dependencies_file and pip_requirements_file.",
            )
        if self.conda.get(self.CONDA_DEPENDENCIES_FILE):
            conda_dependencies_file = self.conda[self.CONDA_DEPENDENCIES_FILE]
            if not skip_path_validation and not (Path(base_path) / conda_dependencies_file).is_file():
                validation_result.append_error(
                    yaml_path=f"conda.{self.CONDA_DEPENDENCIES_FILE}",
                    message=f"Cannot find conda dependencies file: {conda_dependencies_file!r}",
                )
        if self.conda.get(self.PIP_REQUIREMENTS_FILE):
            pip_requirements_file = self.conda[self.PIP_REQUIREMENTS_FILE]
            if not skip_path_validation and not (Path(base_path) / pip_requirements_file).is_file():
                validation_result.append_error(
                    yaml_path=f"conda.{self.PIP_REQUIREMENTS_FILE}",
                    message=f"Cannot find pip requirements file: {pip_requirements_file!r}",
                )
        return validation_result

    def _validate_docker_section(
        self, base_path: Union[str, PathLike], skip_path_validation: bool
    ) -> MutableValidationResult:
        validation_result = ValidationResultBuilder.success()
        if not self.docker:
            return validation_result
        if not self.docker.get(self.BUILD) or not self.docker[self.BUILD].get(self.DOCKERFILE):
            return validation_result
        dockerfile_file = self.docker[self.BUILD][self.DOCKERFILE]
        dockerfile_file = self._parse_file_path(dockerfile_file)
        if (
            not self._docker_file_resolved
            and not skip_path_validation
            and not (Path(base_path) / dockerfile_file).is_file()
        ):
            validation_result.append_error(
                yaml_path=f"docker.{self.BUILD}.{self.DOCKERFILE}",
                message=f"Dockerfile not exists: {dockerfile_file}",
            )
        return validation_result

    def validate(self, base_path: Union[str, PathLike], skip_path_validation: bool = False) -> MutableValidationResult:
        """Validate the environment section.

        This is a public method but won't be exposed to user given InternalEnvironment is an internal class.

        :param base_path: The base path
        :type base_path: Union[str, PathLike]
        :param skip_path_validation: Whether to skip path validation. Defaults to False
        :type skip_path_validation: bool
        :return: The validation result
        :rtype: MutableValidationResult
        """
        validation_result = ValidationResultBuilder.success()
        if self.os is not None and self.os not in {"Linux", "Windows", "linux", "windows"}:
            validation_result.append_error(
                yaml_path="os",
                message=f"Only support 'Linux' and 'Windows', but got {self.os!r}",
            )
        validation_result.merge_with(self._validate_conda_section(base_path, skip_path_validation))
        validation_result.merge_with(self._validate_docker_section(base_path, skip_path_validation))
        return validation_result

    def _resolve_conda_section(self, base_path: Union[str, PathLike]) -> None:
        if not self.conda:
            return
        if self.conda.get(self.CONDA_DEPENDENCIES_FILE):
            conda_dependencies_file = self.conda.pop(self.CONDA_DEPENDENCIES_FILE)
            self.conda[self.CONDA_DEPENDENCIES] = load_yaml(Path(base_path) / conda_dependencies_file)
            return
        if self.conda.get(self.PIP_REQUIREMENTS_FILE):
            pip_requirements_file = self.conda.pop(self.PIP_REQUIREMENTS_FILE)
            with open(Path(base_path) / pip_requirements_file, encoding=DefaultOpenEncoding.READ) as f:
                pip_requirements = f.read().splitlines()
                self.conda = {
                    self.CONDA_DEPENDENCIES: {
                        "name": "project_environment",
                        "dependencies": [
                            f"python={self.DEFAULT_PYTHON_VERSION}",
                            {
                                "pip": pip_requirements,
                            },
                        ],
                    }
                }
            return

    def _resolve_docker_section(self, base_path: Union[str, PathLike]) -> None:
        if not self.docker:
            return
        if not self.docker.get(self.BUILD) or not self.docker[self.BUILD].get(self.DOCKERFILE):
            return
        dockerfile_file = self.docker[self.BUILD][self.DOCKERFILE]
        if not dockerfile_file.startswith(FILE_PREFIX):
            return
        dockerfile_file = self._parse_file_path(dockerfile_file)
        with open(Path(base_path) / dockerfile_file, "r", encoding=DefaultOpenEncoding.READ) as f:
            self.docker[self.BUILD][self.DOCKERFILE] = f.read()
            self._docker_file_resolved = True
        return

    def resolve(self, base_path: Union[str, PathLike]) -> None:
        self._resolve_conda_section(base_path)
        self._resolve_docker_section(base_path)