Example #1
0
    def build(self):
        """Building Docker image from dockerfile"""
        log.info(logger.LINE_DOUBLE)
        log.info('Preparing to build Docker image...')
        tmp_folder, self.args.old_package_url = '', ''

        if self.args.source == 'local' and self.args.package_url.startswith(('http://', 'https://', 'ftp://')):
            log.info('Downloading needed files...')
            self.args.old_package_url = self.args.package_url
            archive_name = self.args.old_package_url.split('/')[-1]
            tmp_folder = self.location / 'tmp'

            download_file(self.args.package_url, tmp_folder / archive_name, parents_=True)

            self.args.package_url = (tmp_folder / archive_name).relative_to(self.location)
            self.args.package_url = str(pathlib.PurePosixPath(self.args.package_url))
            log.info('Downloading finished')
        self.kwargs['package_url'] = self.args.package_url

        log.info('Building Docker image...')
        self.args.file = pathlib.Path(self.args.file)
        if self.args.file.is_absolute():
            self.args.file = pathlib.Path(self.args.file).relative_to(self.location)
        self.builder = DockerImageBuilder()
        curr_time = timeit.default_timer()
        if not self.args.openshift:
            self.kwargs.pop('openshift')
        log.info(f"Build log location: {self.logdir / 'image_build.log'}")
        self.image = self.builder.build_docker_image(dockerfile=self.args.file,
                                                     directory=str(self.location),
                                                     tag=self.image_name,
                                                     build_args=self.kwargs,
                                                     logfile=self.logdir / 'image_build.log')
        log.info(f'Build time: {format_timedelta(timeit.default_timer() - curr_time)}')

        if not self.image:
            raise FailedBuild(f'Error building Docker image {self.args.tags}')
        log.info(f'Save image data in {self.args.image_json_path} file')
        try:
            if not self.args.image_json_path.parent.exists():
                self.args.image_json_path.parent.mkdir()
            with self.args.image_json_path.open(mode='w', encoding='utf-8') as f:
                json.dump({'image_name': self.image_name,
                           'product_version': self.args.product_version,
                           'distribution': self.args.distribution,
                           'os': self.args.os}, f, ensure_ascii=False, indent=4)
        except Exception:
            log.exception(f'Failed to save image data in {self.args.image_json_path} file')

        log.info(f'Docker image {self.args.tags} built successfully')

        if self.args.old_package_url:
            self.args.package_url, self.args.old_package_url = self.args.old_package_url, self.args.package_url
            self.kwargs['package_url'] = self.args.package_url
        if tmp_folder and tmp_folder.exists():
            shutil.rmtree(tmp_folder, ignore_errors=True)
        log.info('Build dependencies deleted')
Example #2
0
class Launcher:
    """Main class implementing high-end framework logic"""
    def __init__(self, product_name: str, arguments: argparse.Namespace,
                 log_dir: pathlib.Path):
        self.render: typing.Optional[DockerFileRender] = None
        self.builder: typing.Optional[DockerImageBuilder] = None
        self.product_name = product_name
        self.args = arguments
        self.image: typing.Optional[Image] = None
        self.image_name = arguments.tags[0]
        self.kwargs: typing.Dict[str, str] = {}
        self.location = pathlib.Path(os.path.realpath(__file__)).parent
        self.mount_root: pathlib.Path = self.location / 'tests' / 'tmp' / 'mount'
        self.docker_api: typing.Optional[DockerAPI] = None
        self.logdir = log_dir

    def save(self):
        """Save Docker image as a local binary file"""
        log.info(logger.LINE_DOUBLE)
        log.info('Saving built Docker image...')
        curr_time = timeit.default_timer()
        share_root = pathlib.Path(self.args.nightly_save_path)
        archive_name = ''
        for tag in self.args.tags:
            if not tag.endswith('latest'):
                tag = tag.split('/')[-1]  # remove registry from tag
                archive_name = f'{tag.replace(":", "_")}.bin'
                break
        try:
            if not self.image:
                self.image = self.docker_api.client.images.get(
                    self.args.tags[0])
            with open(str(pathlib.PurePosixPath(share_root / archive_name)),
                      'wb') as file:
                for chunk in self.image.save(
                        chunk_size=DEFAULT_DATA_CHUNK_SIZE):
                    if chunk:
                        file.write(chunk)
            log.info(
                f'Save time: {format_timedelta(timeit.default_timer() - curr_time)}'
            )
        except (PermissionError, FileExistsError, FileNotFoundError,
                ReadTimeoutError, ReadTimeout) as file_err:
            log.exception(
                f'Saving the image was failed due to file-related error: {file_err}'
            )
            return ExitCode.failed_save
        except APIError as err:
            log.exception(f'Saving the image was failed: {err}')
            return ExitCode.failed_save
        return ExitCode.success

    def set_docker_api(self):
        """Setting up Docker Python API client"""
        self.docker_api = DockerAPI()

    def setup_build_args(self):
        """Setting up arguments passed to a template engine and `docker build` command"""
        self.kwargs.update({
            'product_name': self.product_name,
            'product_version': self.args.product_version,
            'package_url': self.args.package_url,
            'build_id': self.args.build_id,
            'year': self.args.year,
            'distribution': self.args.distribution,
            'rhel_platform': self.args.rhel_platform,
            'os': self.args.os,
            'OPENVINO_WHEELS_URL': self.args.wheels_url,
            'OPENVINO_WHEELS_VERSION': self.args.wheels_version,
        })
        self.kwargs.update(get_system_proxy())

        if self.args.build_arg:
            for arg in self.args.build_arg:
                self.kwargs.update({arg.split('=')[0]: arg.split('=')[-1]})

    def generate_docker_file(self, save_in_logs=False):
        """Generating dockerfile based on templates and CLI options"""
        log.info('Preparing to generate the dockerfile...')
        self.render = DockerFileRender(self.args.os)
        if save_in_logs:
            save_to_dir = self.logdir
        else:
            save_to_dir = pathlib.Path(
                self.location) / 'dockerfiles' / self.args.os
        self.args.file = self.render.generate_dockerfile(
            self.args, save_to_dir, self.kwargs)
        if 'hadolint' in self.args.linter_check:
            log.info(logger.LINE_DOUBLE)
            log.info('Running linter checks on the generated dockerfile...')
            handolint_report = self.logdir / 'dockerfile_linter_hadolint.html'
            curr_time = timeit.default_timer()
            result = pytest.main(args=[
                f'{self.location / "tests" / "linters"}', '-k', 'hadolint',
                '--dockerfile',
                str(self.args.file),
                f'--junitxml={self.logdir / "hadolint.xml"}',
                f'--html={handolint_report}', '--self-contained-html',
                '--tb=short', '--color=yes'
            ])
            log.info(
                f'Linter Dockerfile check time: {format_timedelta(timeit.default_timer() - curr_time)}'
            )
            if result == pytest.ExitCode.OK:
                log.info('Linter checks: PASSED')
            else:
                log.warning('Linter checks: FAILED')
            log.info(f'Hadolint report location: {handolint_report}')

    def build(self):
        """Building Docker image from dockerfile"""
        log.info(logger.LINE_DOUBLE)
        log.info('Preparing to build Docker image...')
        tmp_folder, self.args.old_package_url = '', ''

        if self.args.ocl_release:
            log.warning(
                'The --ocl_release argument is deprecated since 2022.1.0 and no longer used.'
            )

        if self.args.source == 'local' and self.args.package_url.startswith(
            ('http://', 'https://', 'ftp://')):
            log.info('Downloading needed files...')
            self.args.old_package_url = self.args.package_url
            archive_name = self.args.old_package_url.split('/')[-1]
            tmp_folder = self.location / 'tmp'

            download_file(self.args.package_url,
                          tmp_folder / archive_name,
                          parents_=True)

            self.args.package_url = (tmp_folder / archive_name).relative_to(
                self.location)
            self.args.package_url = str(
                pathlib.PurePosixPath(self.args.package_url))
            log.info(f'Downloading {self.args.package_url} finished')
        self.kwargs['package_url'] = self.args.package_url

        log.info('Building Docker image...')
        self.args.file = pathlib.Path(self.args.file)
        if self.args.file.is_absolute():
            self.args.file = pathlib.Path(self.args.file).relative_to(
                self.location)
        self.builder = DockerImageBuilder()
        curr_time = timeit.default_timer()
        log.info(f"Build log location: {self.logdir / 'image_build.log'}")
        self.image = self.builder.build_docker_image(
            dockerfile=self.args.file,
            directory=str(self.location),
            tag=self.image_name,
            build_args=self.kwargs,
            logfile=self.logdir / 'image_build.log')
        log.info(
            f'Build time: {format_timedelta(timeit.default_timer() - curr_time)}'
        )

        if not self.image:
            raise FailedBuildError(
                f'Error building Docker image {self.args.tags}')
        log.info(f'Save image data in {self.args.image_json_path} file')
        try:
            if not self.args.image_json_path.parent.exists():
                self.args.image_json_path.parent.mkdir()
            with self.args.image_json_path.open(mode='w',
                                                encoding='utf-8') as f:
                json.dump(
                    {
                        'image_name': self.image_name,
                        'product_version': self.args.product_version,
                        'wheels_version': self.args.wheels_version,
                        'distribution': self.args.distribution,
                        'os': self.args.os
                    },
                    f,
                    ensure_ascii=False,
                    indent=4)
        except Exception:
            log.exception(
                f'Failed to save image data in {self.args.image_json_path} file'
            )

        log.info(f'Docker image {self.args.tags} built successfully')

        if self.args.old_package_url:
            self.args.package_url, self.args.old_package_url = self.args.old_package_url, self.args.package_url
            self.kwargs['package_url'] = self.args.package_url
        if tmp_folder and tmp_folder.exists():
            shutil.rmtree(tmp_folder, ignore_errors=True)
        log.info('Build dependencies deleted')

    def dive_linter_check(self) -> typing.Union[int, ExitCode]:
        """Checking the Docker image size optimality using the Dive tool (https://github.com/wagoodman/dive)"""
        log.info(logger.LINE_DOUBLE)
        log.info('Running dive checks on the docker image...')
        log.info('This may take some time for big image...')
        dive_report = self.logdir / 'image_linter_dive.html'
        curr_time = timeit.default_timer()
        result = pytest.main(args=[
            f'{self.location / "tests" / "linters"}', '-k', 'dive', '--image',
            self.image_name, f'--junitxml={self.logdir / "dive.xml"}',
            f'--html={dive_report}', '--self-contained-html', '--tb=short',
            '--color=yes'
        ])
        log.info(
            f'Linter check time: {format_timedelta(timeit.default_timer() - curr_time)}'
        )
        if result == pytest.ExitCode.OK:
            log.info('Dive checks: PASSED')
        else:
            log.warning('Dive image checks: FAILED')
        log.info(f'Dive report location: {dive_report}')
        return result

    def sdl_check(self) -> typing.Union[int, ExitCode]:
        """Checking the Docker image security

        Learn more:
        * docker-bench-security (https://github.com/docker/docker-bench-security)
        * Snyk (https://snyk.io/product/container-vulnerability-management/)
        """
        log.info(logger.LINE_DOUBLE)
        log.info('Running SDL checks on host and the image...')
        sdl_report = str(self.logdir / 'sdl.html')
        curr_time = timeit.default_timer()
        sdl_check = ' or '.join(self.args.sdl_check)
        dockerfile_args = []
        if self.args.file:
            dockerfile_args.extend(
                ['--dockerfile',
                 str(self.args.file.absolute())])

        result = pytest.main(args=[
            f'{self.location / "tests" / "security"}', '-k', sdl_check,
            '--image', self.image_name, *dockerfile_args,
            f'--junitxml={self.logdir / "sdl.xml"}', f'--html={sdl_report}',
            '--self-contained-html', '--tb=short', '--color=yes'
        ])
        log.info(
            f'Security checks time: {format_timedelta(timeit.default_timer() - curr_time)}'
        )
        if result == pytest.ExitCode.OK:
            log.info('SDL checks: PASSED')
        else:
            log.warning('SDL checks: FAILED')
        log.info(f'SDL report location: {sdl_report}')
        return result

    def test(self):
        """Run pytest-based tests on the built Docker image"""
        log.info(logger.LINE_DOUBLE)
        log.info(
            f'Preparing to run tests on the Docker image {self.image_name}...')
        result = pytest.ExitCode.OK
        if self.args.sdl_check:
            result_sdl = self.sdl_check()
            if result_sdl != pytest.ExitCode.OK:
                result = result_sdl
        if 'dive' in self.args.linter_check:
            result_dive = self.dive_linter_check()
            if result_dive != pytest.ExitCode.OK:
                result = result_dive
        test_report = self.logdir / 'tests.html'
        curr_time = timeit.default_timer()
        result_tests = pytest.main([  # noqa
            f'{self.location / "tests" / "functional"}',
            '-k',
            self.args.test_expression,
            '-m',
            self.args.test_mark_expression,
            '--image',
            self.image_name,
            '--distribution',
            self.args.distribution,
            '--image_os',
            self.args.os,
            '--product_version',
            self.args.product_version,
            '--wheels_version',
            self.args.wheels_version,
            '--mount_root',
            str(self.mount_root),
            '--package_url',
            self.args.package_url,
            '--wheels_url',
            getattr(self.args, 'wheels_url', ''),
            '--registry',
            getattr(self.args, 'registry', ''),
            f"--junitxml={self.logdir / 'tests.xml'}",
            f'--html={test_report}',
            '--self-contained-html',
            '--tb=short',
            '--color=yes',
        ])
        log.info(
            f'Testing time: {format_timedelta(timeit.default_timer() - curr_time)}'
        )
        log.info(f'Testing report location: {test_report}')
        log.info(f'Testing detailed logs location: {test_report.parent}')
        if result_tests != pytest.ExitCode.OK and result_tests != pytest.ExitCode.NO_TESTS_COLLECTED:
            result = result_tests
        if result == pytest.ExitCode.OK:
            log.info('Tests: PASSED')
        else:
            raise FailedTestError('Tests: FAILED')

    def tag(self):
        """Tag built Docker image"""
        log.info(logger.LINE_DOUBLE)
        log.info('Tagging built Docker image...')
        try:
            for tag in self.args.tags:
                if self.args.registry not in self.image_name:
                    is_passed = self.docker_api.client.images.get(
                        self.image_name).tag(self.args.registry + '/' + tag)
                    if not is_passed:
                        raise FailedDeployError(
                            f"Can't tag {self.image_name} image to {self.args.registry}/{tag}"
                        )
                    log.info(
                        f'Image {self.image_name} successfully tagged as {self.args.registry}/{tag}'
                    )
        except ImageNotFound:
            raise FailedDeployError(f'Image not found: {self.image_name}')
        except APIError as err:
            raise FailedDeployError(f'Tagging failed: {err}')

    def deploy(self):
        """Push built Docker image to repo specified in CLI"""
        log.info(logger.LINE_DOUBLE)
        log.info('Publishing built Docker image...')

        for tag in self.args.tags:
            log_name = f'deploy_{tag.replace("/", "_").replace(":", "_")}.log'
            log_path_file = self.logdir / log_name
            log.info(f'Image {tag} push log location: {log_path_file}')
            logger.switch_to_custom(log_path_file)
            curr_time = timeit.default_timer()

            attempt_number = 1
            while attempt_number <= MAX_DEPLOY_RETRIES:
                log.info(f'Try deploy the image, attempt #{attempt_number}')
                attempt_number += 1
                try:
                    if self.args.registry in tag:
                        log_generator = self.docker_api.client.images.push(
                            tag, stream=True, decode=True)
                    else:
                        log_generator = self.docker_api.client.images.push(
                            self.args.registry + '/' + tag,
                            stream=True,
                            decode=True)
                    for line in log_generator:
                        for key, value in line.items():
                            if 'error' in key or 'errorDetail' in key:
                                raise FailedDeployError(f'{value}')
                            log.info(f'{value}')
                    break
                except (APIError, ReadTimeoutError) as err:
                    log.warning(
                        f'Something went wrong during pushing the image, trying again after sleeping '
                        f'{SLEEP_BETWEEN_RETRIES}s: \n\t {err}')
                    time.sleep(SLEEP_BETWEEN_RETRIES)
                    continue
            else:
                raise FailedDeployError(
                    f'Push had failed after {MAX_DEPLOY_RETRIES} attempts')
            logger.switch_to_summary()
            log.info(
                f'Push time: {format_timedelta(timeit.default_timer() - curr_time)}'
            )
            log.info('Image successfully published')

    def rmi(self):
        """Remove Docker image from the host machine"""
        image = self.docker_api.client.images.get(self.image_name)
        self.docker_api.client.images.remove(image.short_id, force=True)