def add_artifact(self, name, value, description=''):
        """Insert/Update an artifact with the given pattern:

        "name": {
            "description": "file description",
            "value": "step-result.txt"
        }

        Parameters
        ----------
        name : str
            Required name of the artifact
        value : str
            Required content
        description : str, optional
            Optional description (defaults to empty)

        """
        if not name:
            raise StepRunnerException('Name is required to add artifact')

        # False can be the value
        if value == '' or value is None:
            raise StepRunnerException('Value is required to add artifact')

        self.__artifacts[name] = {'description': description, 'value': value}
예제 #2
0
    def add_step_result(self, step_result):
        """Add a single step_result to the workflow list.

        If the new result step is not already in the list
           - simply append, done
        Else
           - find the old step result
           - merge the old artifacts into the new artifacts
           - delete the old step result
           - append the new step result
           - note: the delete/append is needed because it is a list

        Parameters
        ----------
        step_result : StepResult
           An StepResult object to add to the list

        Raises
        ------
        Raises a StepRunnerException if an instance other than
        StepResult is passed as a parameter
        """

        if isinstance(step_result, StepResult):
            if self.__step_result_exists(step_result):
                raise StepRunnerException(
                    f'Can not add duplicate StepResult for step ({step_result.step_name}),'
                    f' sub step ({step_result.sub_step_name}),'
                    f' and environment ({step_result.environment}).')

            self.workflow_list.append(step_result)

        else:
            raise StepRunnerException('expect StepResult instance type')
예제 #3
0
    def __git_commit_file(git_commit_message, file_path, repo_dir):
        try:
            sh.git.add(  # pylint: disable=no-member
                file_path,
                _cwd=repo_dir,
                _out=sys.stdout,
                _err=sys.stderr)
        except sh.ErrorReturnCode as error:
            # NOTE: this should never happen
            raise StepRunnerException(
                f"Unexpected error adding file ({file_path}) to commit"
                f" in git repository ({repo_dir}): {error}") from error

        try:
            sh.git.commit(  # pylint: disable=no-member
                '--allow-empty',
                '--all',
                '--message',
                git_commit_message,
                _cwd=repo_dir,
                _out=sys.stdout,
                _err=sys.stderr)
        except sh.ErrorReturnCode as error:
            # NOTE: this should never happen
            raise StepRunnerException(
                f"Unexpected error commiting file ({file_path})"
                f" in git repository ({repo_dir}): {error}") from error
예제 #4
0
    def __argocd_app_sync(argocd_app_name, argocd_sync_timeout_seconds):
        try:
            sh.argocd.app.sync(  # pylint: disable=no-member
                '--prune',
                '--timeout',
                argocd_sync_timeout_seconds,
                argocd_app_name,
                _out=sys.stdout,
                _err=sys.stderr)
        except sh.ErrorReturnCode as error:
            raise StepRunnerException(
                f"Error synchronization ArgoCD Application ({argocd_app_name}): {error}"
            ) from error

        try:
            sh.argocd.app.wait(  # pylint: disable=no-member
                '--timeout',
                argocd_sync_timeout_seconds,
                '--health',
                argocd_app_name,
                _out=sys.stdout,
                _err=sys.stderr)
        except sh.ErrorReturnCode as error:
            raise StepRunnerException(
                f"Error waiting for ArgoCD Application ({argocd_app_name}) synchronization: {error}"
            ) from error
    def _validate_required_config_or_previous_step_result_artifact_keys(self):
        """Validates that the required configuration keys or previous step result artifacts
        are set and have valid values.

        Validates that:
        * required configuration is given
        * either both git-username and git-password are set or neither.

        Raises
        ------
        StepRunnerException
            If step configuration or previous step result artifacts have invalid required values
        """
        super(
        )._validate_required_config_or_previous_step_result_artifact_keys()

        # ensure container image registry is given if to be used in container image deploy address
        if not self.get_value('use-container-image-short-addres'):
            container_image_registry = self.get_value([
                'container-image-pull-registry',
                'container-image-registry',
                'container-image-push-registry',
            ])
            if container_image_registry is None:
                raise StepRunnerException(
                    "If using container image address with container image registry"
                    " (use-container-image-short-addres is False)"
                    " then container image registry ('container-image-pull-registry',"
                    " 'container-image-push-registry', 'container-image-registry') must be given."
                )

        # ensure container image tag or digest provided as needed
        if self.get_value('use-container-image-digest'):
            container_image_digest = self.get_value([
                'container-image-pull-digest',
                'container-image-push-digest',
                'container-image-digest',
            ])
            if container_image_digest is None:
                raise StepRunnerException(
                    "If deploying container image with container image digest"
                    " (use-container-image-digest is True)"
                    " in the container image address" \
                    " then container image digest ('container-image-pull-digest'," \
                    " 'container-image-push-digest', 'container-image-digest') must be given."
                )
        else:
            container_image_tag = self.get_value([
                'container-image-pull-tag',
                'container-image-push-tag',
                'container-image-tag',
            ])
            if container_image_tag is None:
                raise StepRunnerException(
                    "If deploying container image with container image tag"
                    " (use-container-image-digest is False)"
                    " in the container image address" \
                    " then container image digest ('container-image-pull-tag'," \
                    " 'container-image-push-tag', 'container-image-tag') must be given."
                )
예제 #6
0
    def _validate_required_config_or_previous_step_result_artifact_keys(self):
        """Validates that the required configuration keys or previous step result artifacts
        are set and have valid values.
        Validates that:
        * required configuration is given
        * either username and password are set but not token, token is set but not username
        and password, or none are set.
        Raises
        ------
        StepRunnerException
            If step configuration or previous step result artifacts have invalid required values
        """
        super()._validate_required_config_or_previous_step_result_artifact_keys()

        # if token ensure no username and password
        if self.get_value('token'):
            if (self.get_value('username')
                    or self.get_value('password')):
                raise StepRunnerException(
                    "Either 'username' or 'password 'is set. Neither can be set with a token."
                )
        # if no token present, ensure either both git-username and git-password are set or neither
        else:
            if (self.get_value('username') and self.get_value('password') is None
                        or self.get_value('username') is None and self.get_value('password')):
                raise StepRunnerException(
                    "Either 'username' or 'password 'is not set. Neither or both must be set."
                )
    def __sign_image(pgp_private_key_fingerprint, image_signatures_directory,
                     container_image_tag):
        # sign image
        print(f"Sign image ({container_image_tag}) "
              f"with PGP private key ({pgp_private_key_fingerprint})")
        try:
            # NOTE: for some reason the output from podman sign goes to stderr so....
            #       merge the two streams
            sh.podman.image(  # pylint: disable=no-member
                "sign",
                f"--sign-by={pgp_private_key_fingerprint}",
                f"--directory={image_signatures_directory}",
                f"docker://{container_image_tag}",
                _out=sys.stdout,
                _err_to_out=True,
                _tee='out')
        except sh.ErrorReturnCode as error:
            raise StepRunnerException(
                f"Error signing image ({container_image_tag}): {error}"
            ) from error

        # get image signature file path
        signature_file_paths = glob.glob(
            f"{image_signatures_directory}/**/signature-*", recursive=True)

        if len(signature_file_paths) != 1:
            raise StepRunnerException(
                f"Unexpected number of signature files, expected 1: {signature_file_paths}"
            )

        signature_file_path = signature_file_paths[0]
        print(f"Signed image ({container_image_tag}) with PGP private key "
              f"({pgp_private_key_fingerprint}): '{signature_file_path}'")

        return signature_file_path
예제 #8
0
    def merge(self, other):
        """Merge the artifacts and evidence from another StepResult into
        this StepResult. The other StepResult must have the same step name,
        sub step name and environment.

        Parameters
        ----------
        step_result : StepResult
            The second StepResult instance to merge into this one

        Raises
        ------
        StepRunnerException if the StepResult to merge does not have a
        matching step name, sub-step name, or environment.
        """

        if not isinstance(other, StepResult):
            raise StepRunnerException('expect StepResult instance type')

        if other.step_name != self.step_name or \
            other.sub_step_name != self.sub_step_name or \
            other.environment != self.environment:
            raise StepRunnerException(
                    'Other StepResult does not have matching ' \
                    'step name, sub step name, or environment.'
                  )

        for artifact in other.artifacts.values():
            self.add_artifact(artifact.name, artifact.value,
                              artifact.description)

        for evidence in other.evidence.values():
            self.add_evidence(evidence.name, evidence.value,
                              evidence.description)
예제 #9
0
def write_effective_pom(
    pom_file_path,
    output_path,
    profiles=None
):
    """Generates the effective pom for a given pom and writes it to a given directory

    Parameters
    ----------
    pom_file_path : str
        Path to pom file to render the effective pom for.
    output_path : str
        Path to write the effective pom to.
    profiles : list
        Maven profiles to use when generating the effective pom.

    See
    ---
    * https://maven.apache.org/plugins/maven-help-plugin/effective-pom-mojo.html

    Returns
    -------
    str
        Absolute path to the written effective pom generated from the given pom file path.

    Raises
    ------
    StepRunnerException
        If issue generating effective pom.
    """

    if not os.path.isabs(output_path):
        raise StepRunnerException(
            f"Given output path ({output_path}) is not absolute which will mean your output"
            f" file will actually end up being relative to the pom file ({pom_file_path}) rather"
            " than your expected root. Rather then handling this, just give this function an"
            " absolute path."
            " If you are a user seeing this, a programmer messed up somewhere, report an issue."
        )

    profiles_arguments = ""
    if profiles:
        if isinstance(profiles, str):
            profiles = [profiles]
        profiles_arguments = ['-P', f"{','.join(profiles)}"]

    try:
        sh.mvn( # pylint: disable=no-member
            'help:effective-pom',
            f'-f={pom_file_path}',
            f'-Doutput={output_path}',
            *profiles_arguments
        )
    except sh.ErrorReturnCode as error:
        raise StepRunnerException(
            f"Error generating effective pom for '{pom_file_path}' to '{output_path}': {error}"
        ) from error

    return output_path
예제 #10
0
    def __git_tag_and_push(repo_dir, tag, url=None, force_push_tags=False):
        """
        Raises
        ------
        StepRunnerException
        * if error pushing commits
        * if error tagging repository
        * if error pushing tags
        """

        git_push = sh.git.push.bake(url) if url else sh.git.push

        # push commits
        try:
            git_push(_cwd=repo_dir, _out=sys.stdout)
        except sh.ErrorReturnCode as error:
            raise StepRunnerException(
                f"Error pushing commits from repository directory ({repo_dir}) to"
                f" repository ({url}): {error}") from error

        # tag
        try:
            # NOTE:
            # this force is only needed locally in case of a re-run of the same pipeline
            # without a fresh check out. You will notice there is no force on the push
            # making this an acceptable work around to the issue since on the off chance
            # actually overwriting a tag with a different comment, the push will fail
            # because the tag will be attached to a different git hash.
            sh.git.tag(  # pylint: disable=no-member
                tag,
                '-f',
                _cwd=repo_dir,
                _out=sys.stdout,
                _err=sys.stderr)
        except sh.ErrorReturnCode as error:
            raise StepRunnerException(
                f"Error tagging repository ({repo_dir}) with tag ({tag}): {error}"
            ) from error

        git_push_additional_arguments = []
        if force_push_tags:
            git_push_additional_arguments += ["--force"]

        # push tag
        try:
            git_push('--tag',
                     *git_push_additional_arguments,
                     _cwd=repo_dir,
                     _out=sys.stdout)
        except sh.ErrorReturnCode as error:
            raise StepRunnerException(
                f"Error pushing tags from repository directory ({repo_dir}) to"
                f" repository ({url}): {error}") from error
예제 #11
0
    def __get_step_implementer_class(step_name, step_implementer_name):
        """Given a step name and a step implementer name dynamically loads the Class.

        Parameters
        ----------
        step_name : str
            Name of the step to load the given step implementer for.
            This is only used if the given step_implementer_name does not include
            a module path.
        step_implementer_name : str
            Either the short name of a StepImplementer class which will be dynamically
            loaded from the 'ploigos_step_runner.step_implementers.{step_name}' module or
            A class name that includes a dot seperated module name to load the Class from.

        Returns
        -------
        StepImplementer
            Dynamically loaded subclass of StepImplementer for given step name with
            given step implementer name.

        Raises
        ------
        StepRunnerException
            If could not find class to load
            If loaded class is not a subclass of StepImplementer
        """
        parts = step_implementer_name.split('.')
        class_name = parts.pop()
        module_name = '.'.join(parts)

        if not module_name:
            step_module_part = step_name.replace('-', '_')
            module_name = f"{StepRunner.__DEFAULT_MODULE}.{step_module_part}"

        clazz = import_and_get_class(module_name, class_name)
        if not clazz:
            raise StepRunnerException(
                f"Could not dynamically load step ({step_name}) step implementer"
                + f" ({step_implementer_name}) from module ({module_name})" +
                f" with class name ({class_name})")
        if not issubclass(clazz, StepImplementer):
            raise StepRunnerException(
                f"Step ({step_name}) is configured to use step implementer" +
                f" ({step_implementer_name}) from module ({module_name}) with"
                +
                f" class name ({class_name}), and dynamically loads as class ({clazz})"
                +
                f" which is not a subclass of required parent class ({StepImplementer})."
            )

        return clazz
예제 #12
0
    def _argocd_app_sync(
            argocd_app_name,
            argocd_sync_timeout_seconds,
            argocd_sync_retry_limit,
            argocd_sync_prune=True
    ):  # pylint: disable=line-too-long
        # add any additional flags
        argocd_sync_additional_flags = []
        if argocd_sync_prune:
            argocd_sync_additional_flags.append('--prune')

        try:
            sh.argocd.app.sync(  # pylint: disable=no-member
                *argocd_sync_additional_flags,
                '--timeout',
                argocd_sync_timeout_seconds,
                '--retry-limit',
                argocd_sync_retry_limit,
                argocd_app_name,
                _out=sys.stdout,
                _err=sys.stderr)
        except sh.ErrorReturnCode as error:
            if not argocd_sync_prune:
                prune_warning = ". Sync 'prune' option is disabled." \
                    " If sync error (see logs) was due to resource(s) that need to be pruned," \
                    " and the pruneable resources are intentionally there then see the ArgoCD" \
                    " documentation for instructions for argo to ignore the resource(s)." \
                    " See: https://argoproj.github.io/argo-cd/user-guide/sync-options/#no-prune-resources" \
                    " and https://argoproj.github.io/argo-cd/user-guide/compare-options/#ignoring-resources-that-are-extraneous"
            else:
                prune_warning = ""

            raise StepRunnerException(
                f"Error synchronization ArgoCD Application ({argocd_app_name})"
                f"{prune_warning}: {error}") from error

        try:
            sh.argocd.app.wait(  # pylint: disable=no-member
                '--timeout',
                argocd_sync_timeout_seconds,
                '--health',
                argocd_app_name,
                _out=sys.stdout,
                _err=sys.stderr)
        except sh.ErrorReturnCode as error:
            raise StepRunnerException(
                f"Error waiting for ArgoCD Application ({argocd_app_name}) synchronization: {error}"
            ) from error
예제 #13
0
    def _argocd_app_wait_for_operation(argocd_app_name,
                                       argocd_timeout_seconds):
        """Waits for an existing operation on an ArgoCD Application to finish.

        Parameters
        ----------
        argocd_app_name : str
            Name of ArgoCD Application to wait for existing operations on.
        argocd_timeout_seconds : int
            Number of sections to wait before timing out waiting for existing operations to finish.

        Raises
        ------
        StepRunnerException
            If error (including timeout) waiting for existing ArgoCD Application operation to finish
        """
        try:
            print(
                f"Wait for existing ArgoCD operations on Application ({argocd_app_name})"
            )
            sh.argocd.app.wait(  # pylint: disable=no-member
                argocd_app_name,
                '--operation',
                '--timeout',
                argocd_timeout_seconds,
                _out=sys.stdout,
                _err=sys.stderr)
        except sh.ErrorReturnCode as error:
            raise StepRunnerException(
                f"Error waiting for existing ArgoCD operations on Application ({argocd_app_name})"
                f": {error}") from error
    def test_fail(self, mock_write_working_file, mock_run_maven_step):
        with TempDirectory() as test_dir:
            parent_work_dir_path = os.path.join(test_dir.path, 'working')

            step_config = {}
            step_implementer = self.create_step_implementer(
                step_config=step_config,
                parent_work_dir_path=parent_work_dir_path,
            )

            # run step with mock failure
            mock_run_maven_step.side_effect = StepRunnerException(
                'Mock error running maven')
            actual_step_result = step_implementer._run_step()

            # create expected step result
            expected_step_result = StepResult(
                step_name='foo',
                sub_step_name='MavenGeneric',
                sub_step_implementer_name='MavenGeneric')
            expected_step_result.add_artifact(
                description="Standard out and standard error from maven.",
                name='maven-output',
                value='/mock/mvn_output.txt')
            expected_step_result.message = "Error running maven. " \
                "More details maybe found in 'maven-output' report artifact: "\
                "Mock error running maven"
            expected_step_result.success = False

            # verify step result
            self.assertEqual(actual_step_result, expected_step_result)

            mock_write_working_file.assert_called_once()
            mock_run_maven_step.assert_called_with(
                mvn_output_file_path='/mock/mvn_output.txt')
예제 #15
0
def run_tox(tox_output_file_path, tox_args):
    """
    Run a tox command

    Paramters
    ---------
    tox_output_file_path:
        String
    tox_args:
        Commandline arguments to tox
    """

    try:
        with open(tox_output_file_path, 'w', encoding='utf-8') as tox_output_file:
            out_callback = create_sh_redirect_to_multiple_streams_fn_callback([
                sys.stdout,
                tox_output_file
            ])
            err_callback = create_sh_redirect_to_multiple_streams_fn_callback([
                sys.stderr,
                tox_output_file
            ])

            sh.tox( # pylint: disable=no-member
                tox_args,
                _out=out_callback,
                _err=err_callback
            )
    except sh.ErrorReturnCode as error:
        raise StepRunnerException(
            f"Error running tox. {error}"
        ) from error
예제 #16
0
    def _validate_required_config_or_previous_step_result_artifact_keys(self):
        """Validates that the required configuration keys or previous step result artifacts
        are set and have valid values.

        Validates that:
        * required configuration is given
        * either both git-username and git-password are set or neither.

        Raises
        ------
        StepRunnerException
            If step configuration or previous step result artifacts have invalid required values
        """
        super(
        )._validate_required_config_or_previous_step_result_artifact_keys()

        # ensure either both git-username and git-password are set or neither
        runtime_auth_config = {}
        for auth_config_key in AUTHENTICATION_CONFIG:
            runtime_auth_config_value = self.get_value(auth_config_key)

            if runtime_auth_config_value is not None:
                runtime_auth_config[
                    auth_config_key] = runtime_auth_config_value

        if (any(element in runtime_auth_config for element in AUTHENTICATION_CONFIG)) and \
                (not all(element in runtime_auth_config for element in AUTHENTICATION_CONFIG)):
            raise StepRunnerException(
                "Either 'git-username' or 'git-password 'is not set. Neither or both must be set."
            )
예제 #17
0
    def __git_url(self):
        git_url = None
        if self.get_value('url'):
            git_url = self.get_value('url')
        else:
            try:
                out = StringIO()
                sh.git.config('--get',
                              'remote.origin.url',
                              _encoding='UTF-8',
                              _decode_errors='ignore',
                              _out=out,
                              _err=sys.stderr,
                              _tee='err')
                git_url = out.getvalue().rstrip()

                # remove ANYTHING@ from beginning of git_url since step will pass in its own
                # username and password
                #
                # Regex:
                #   ^[^@]+@ - match from beginning of line any character up until
                #             an @ and then the @
                #   (.*) - match any character and capture to capture group 1
                #   \1 - capture group 1 which is the http or https if there was one
                #   \2 - capture group 2 which is anything after the first @ if there was one
                git_url = re.sub(r"^(http://|https://)[^@]+@(.*)", r"\1\2",
                                 git_url)

            except sh.ErrorReturnCode as error:  # pylint: disable=undefined-variable
                raise StepRunnerException(
                    f"Error invoking git config --get remote.origin.url: {error}"
                ) from error
        return git_url
예제 #18
0
    def __auto_increment_version(self, auto_increment_version_segment, step_result):
        """Automatically increments a given version segment.

        Parameters
        ---------
        auto_increment_version_segment : str
            The version segment to auto increment.
            One of: major, minor, or patch
        step_result : StepResult
            Step result to add step results to.
        """
        mvn_auto_increment_version_output_file_path = self.write_working_file(
            'mvn_versions_set_output.txt'
        )
        try:
            # SEE: https://www.mojohaus.org/build-helper-maven-plugin/parse-version-mojo.html
            new_version = None
            if auto_increment_version_segment == 'major':
                new_version = r'${parsedVersion.nextMajorVersion}.0.0'
            elif auto_increment_version_segment == 'minor':
                new_version = r'${parsedVersion.majorVersion}.${parsedVersion.nextMinorVersion}.0'
            elif auto_increment_version_segment == 'patch':
                new_version = r'${parsedVersion.majorVersion}' \
                    r'.${parsedVersion.minorVersion}' \
                    r'.${parsedVersion.nextIncrementalVersion}'

            additional_arguments = [
                f'-DnewVersion={new_version}'
            ]

            # determine if should auto increment all modules
            auto_increment_all_module_versions = self.get_value(
                'auto-increment-all-module-versions'
            )
            if auto_increment_all_module_versions:
                additional_arguments.append('-DprocessAllModules')

            run_maven(
                mvn_output_file_path=mvn_auto_increment_version_output_file_path,
                settings_file=self.maven_settings_file,
                pom_file=self.get_value('pom-file'),
                phases_and_goals=[
                    'build-helper:parse-version',
                    'versions:set',
                    'versions:commit'
                ],
                additional_arguments=additional_arguments
            )
        except StepRunnerException as error:
            raise StepRunnerException(f"Error running maven to auto increment version segment"
                f" ({auto_increment_version_segment})."
                f" More details maybe found in 'maven-auto-increment-version-output'"
                f" report artifact: {error}") from error
        finally:
            step_result.add_artifact(
                description="Standard out and standard error from running maven" \
                    " to auto increment version.",
                name='maven-auto-increment-version-output',
                value=mvn_auto_increment_version_output_file_path
            )
    def setup_previous_result(self, work_dir_path, artifact_config={}):
        step_result = StepResult(
            step_name='test-step',
            sub_step_name='test-sub-step-name',
            sub_step_implementer_name='test-step-implementer-name')
        for key1, val1 in artifact_config.items():
            description = ''
            value = ''
            for key2, val2 in val1.items():
                if key2 == 'description':
                    description = val2
                elif key2 == 'value':
                    value = val2
                else:
                    raise StepRunnerException(
                        f'Given field is not apart of an artifact: {key2}')
            step_result.add_artifact(
                name=key1,
                value=value,
                description=description,
            )
        workflow_result = WorkflowResult()
        workflow_result.add_step_result(step_result=step_result)
        pickle_filename = os.path.join(work_dir_path,
                                       'step-runner-results.pkl')
        workflow_result.write_to_pickle_file(pickle_filename=pickle_filename)

        return workflow_result
예제 #20
0
    def test_fail_getting_git_repo(
        self,
        mock_repo,
        mock_git_url,
        mock_git_commit_utc_timestamp
    ):
        with TempDirectory() as temp_dir:
            # setup
            step_config = {
                'repo-root': temp_dir.path
            }
            step_implementer = self.create_step_implementer(
                step_config=step_config,
                step_name='generate-metadata',
                implementer='Git'
            )

            # setup mocks
            mock_repo.side_effect = StepRunnerException('mock error')

            # run test
            actual_step_result = step_implementer._run_step()

            # verify results
            expected_step_result = StepResult(
                step_name='generate-metadata',
                sub_step_name='Git',
                sub_step_implementer_name='Git'
            )
            expected_step_result.success = False
            expected_step_result.message = f'mock error'

            self.assertEqual(actual_step_result, expected_step_result)
    def test_dynamically_determined_test_reports_dir_errors(
            self, mock_attempt_get_test_report_directory):
        with TempDirectory() as test_dir:
            # setup test
            parent_work_dir_path = os.path.join(test_dir.path, 'working')
            step_config = {}
            step_implementer = self.create_step_implementer(
                step_config=step_config,
                parent_work_dir_path=parent_work_dir_path,
            )

            # setup mocks
            mock_attempt_get_test_report_directory.side_effect = StepRunnerException(
                'mock error')

            # run test
            actual_test_report_dir = step_implementer._MavenIntegrationTest__get_test_report_dirs(
            )

            # verify results
            self.assertEqual(actual_test_report_dir, None)
            mock_attempt_get_test_report_directory.assert_has_calls([
                call(plugin_name='maven-failsafe-plugin',
                     configuration_key='reportsDirectory',
                     default='target/failsafe-reports'),
                call(plugin_name='maven-surefire-plugin',
                     configuration_key='reportsDirectory',
                     default='target/surefire-reports',
                     require_phase_execution_config=True)
            ])
예제 #22
0
    def __argocd_get_app_manifest(self, argocd_app_name, source='live'):
        """Get ArgoCD Application manifest.

        Parameters
        ----------
        argocd_app_name : str
            Name of the ArgoCD Application to get the manifest for.
        source : str (live,git)
            Get the manifest from the 'live' version of the 'git' version.

        Returns
        -------
        str
            Path to the retrieved ArgoCD manifest file.

        Raises
        ------
        StepRunnerException
            If error getting ArogCD manifest.
        """
        arogcd_app_manifest_file = self.write_working_file(
            'deploy_argocd_manifests.yml')
        try:
            sh.argocd.app.manifests(  # pylint: disable=no-member
                f'--source={source}',
                argocd_app_name,
                _out=arogcd_app_manifest_file,
                _err=sys.stderr)
        except sh.ErrorReturnCode as error:
            raise StepRunnerException(
                f"Error reading ArgoCD Application ({argocd_app_name}) manifest: {error}"
            ) from error

        return arogcd_app_manifest_file
예제 #23
0
def run_npm(npm_output_file_path, npm_args):
    """
    Run an npm command

    Parameters
    ----------
    npm_output_file_path:
        String
    npm_args:
        Commandline arguments to npm
    """
    try:
        with open(npm_output_file_path, 'w',
                  encoding='utf-8') as npm_output_file:
            out_callback = create_sh_redirect_to_multiple_streams_fn_callback(
                [sys.stdout, npm_output_file])
            err_callback = create_sh_redirect_to_multiple_streams_fn_callback(
                [sys.stderr, npm_output_file])

            sh.npm(  # pylint: disable=no-member
                npm_args,
                _out=out_callback,
                _err=err_callback)
    except sh.ErrorReturnCode as error:
        raise StepRunnerException(f"Error running npm. {error}") from error
예제 #24
0
    def git_tag(self, git_tag_value):
        """Create a git tag.

        Parameters
        ----------
        git_tag_value : str
            Value to tag Git repository with.

        Raises
        ------
        StepRunnerException
            If given git repo root is not a Git repository.
            If error creating Git tag.
        """
        try:
            # NOTE:
            #   this force is only needed locally in case of a re-run of the same pipeline
            #   without a fresh check out. You will notice there is no force on the push
            #   making this an acceptable work around to the issue since on the off chance
            #   actually overwriting a tag with a different comment, the push will fail
            #   because the tag will be attached to a different git hash.
            self.git_repo.create_tag(git_tag_value, force=True)
        except (GitCommandError, Exception) as error:
            raise StepRunnerException(
                f"Error creating git tag ({git_tag_value}): {error}"
            ) from error
    def __argocd_sign_in(
        argocd_api,
        username,
        password,
        insecure=False
    ):
        """Signs into ArgoCD CLI.

        Raises
        ------
        StepRunnerException
            If error signing into ArgoCD CLI.
        """
        try:
            insecure_flag = None
            if insecure:
                insecure_flag = '--insecure'

            sh.argocd.login(  # pylint: disable=no-member
                argocd_api,
                f'--username={username}',
                f'--password={password}',
                insecure_flag,
                _out=sys.stdout,
                _err=sys.stderr
            )
        except sh.ErrorReturnCode as error:
            raise StepRunnerException(f"Error logging in to ArgoCD: {error}") from error
예제 #26
0
def write_effective_pom(pom_file_path, output_path):
    """Generates the effective pom for a given pom and writes it to a given directory

    Parameters
    ----------
    pom_file_path : str
        Path to pom file to render the effective pom for.
    output_path : str
        Path to write the effective pom to.

    See
    ---
    * https://maven.apache.org/plugins/maven-help-plugin/effective-pom-mojo.html

    Returns
    -------
    str
        Absolute path to the written effective pom generated from the given pom file path.

    Raises
    ------
    StepRunnerException
        If issue generating effective pom.
    """

    try:
        sh.mvn(  # pylint: disable=no-member
            'help:effective-pom', f'-f={pom_file_path}',
            f'-Doutput={output_path}')
    except sh.ErrorReturnCode as error:
        raise StepRunnerException(
            f"Error generating effective pom for '{pom_file_path}' to '{output_path}': {error}"
        ) from error

    return output_path
예제 #27
0
    def test_run_step_error_git_push(self, git_push_mock, git_tag_mock,
                                     git_url_mock, get_tag_mock):
        with TempDirectory() as temp_dir:
            tag = '1.0+69442c8'
            url = '[email protected]:ploigos/ploigos-step-runner.git'
            parent_work_dir_path = os.path.join(temp_dir.path, 'working')

            artifact_config = {
                'version': {
                    'description': '',
                    'value': tag
                },
                'container-image-version': {
                    'description': '',
                    'value': tag
                }
            }
            workflow_result = self.setup_previous_result(
                parent_work_dir_path, artifact_config)

            step_config = {
                'url': url,
                'git-username': '******',
                'git-password': '******'
            }
            step_implementer = self.create_step_implementer(
                step_config=step_config,
                workflow_result=workflow_result,
                parent_work_dir_path=parent_work_dir_path)

            def get_tag_side_effect():
                return tag

            get_tag_mock.side_effect = get_tag_side_effect

            def git_url_side_effect():
                return url

            git_url_mock.side_effect = git_url_side_effect

            # this is the test here
            git_push_mock.side_effect = StepRunnerException('mock error')
            result = step_implementer._run_step()

            # verify the test results
            expected_step_result = StepResult(step_name='tag-source',
                                              sub_step_name='Git',
                                              sub_step_implementer_name='Git')
            expected_step_result.add_artifact(name='tag', value=tag)
            expected_step_result.success = False
            expected_step_result.message = "mock error"

            # verifying all mocks were called
            get_tag_mock.assert_called_once_with()
            git_tag_mock.assert_called_once_with(tag)
            git_url_mock.assert_called_once_with()
            git_push_mock.assert_called_once_with(None)

            self.assertEqual(result, expected_step_result)
예제 #28
0
    def _argocd_app_wait_for_health(argocd_app_name, argocd_timeout_seconds):
        """Waits for ArgoCD Application to reach Healthy state.

        Parameters
        ----------
        argocd_app_name : str
            Name of ArgoCD Application to wait for Healthy state of.
        argocd_timeout_seconds : int
            Number of sections to wait before timing out waiting for Healthy state.

        Raises
        ------
        StepRunnerException
            If error (including timeout) waiting for existing ArgoCD Application Healthy state.
            If ArgoCD Application transitions from Healthy to Degraded while waiting for Healthy
            state.
        """
        for wait_for_health_retry in range(
                ArgoCDGeneric.MAX_ATTEMPT_TO_WAIT_FOR_ARGOCD_OP_RETRIES):
            argocd_output_buff = StringIO()
            try:
                print(
                    f"Wait for Healthy ArgoCD Application ({argocd_app_name}")
                out_callback = create_sh_redirect_to_multiple_streams_fn_callback(
                    [sys.stdout, argocd_output_buff])
                err_callback = create_sh_redirect_to_multiple_streams_fn_callback(
                    [sys.stderr, argocd_output_buff])
                sh.argocd.app.wait(  # pylint: disable=no-member
                    argocd_app_name,
                    '--health',
                    '--timeout',
                    argocd_timeout_seconds,
                    _out=out_callback,
                    _err=err_callback)
                break
            except sh.ErrorReturnCode as error:
                # if error waiting for Healthy state because entered Degraded state
                # while waiting for Healthy state
                # try again to wait for Healthy state assuming that on next attempt the
                # new degradation of Health will resolve itself.
                #
                # NOTE: this can happen based on bad timing if for instance an
                #       HorizontalPodAutoscaller doesn't enter Degraded state until after we are
                #       already waiting for the ArgoCD Application to enter Healthy state,
                #       but then the HorizontalPodAutoscaller will, after a time, become Healthy.
                if re.match(
                        ArgoCDGeneric.
                        ARGOCD_HEALTH_STATE_TRANSITIONED_FROM_HEALTHY_TO_DEGRADED,
                        argocd_output_buff.getvalue()):
                    print(
                        f"ArgoCD Application ({argocd_app_name}) entered Degraded state"
                        " while waiting for it to enter Healthy state."
                        f" Try ({wait_for_health_retry} out of"
                        f" {ArgoCDGeneric.MAX_ATTEMPT_TO_WAIT_FOR_ARGOCD_OP_RETRIES}) again to"
                        " wait for Healthy state.")
                else:
                    raise StepRunnerException(
                        f"Error waiting for Healthy ArgoCD Application ({argocd_app_name}): {error}"
                    ) from error
    def test_fail_set_version(self, mock_settings_file,
                              mock_write_working_file, mock_run_maven_step,
                              mock_run_maven):
        with TempDirectory() as test_dir:
            parent_work_dir_path = os.path.join(test_dir.path, 'working')

            pom_file = os.path.join(test_dir.path, 'mock-pom.xml')
            maven_push_artifact_repo_id = 'mock-repo-id'
            maven_push_artifact_repo_url = 'https://mock-repo.ploigos.com'
            version = '0.42.0-mock'
            step_config = {
                'pom-file': pom_file,
                'maven-push-artifact-repo-id': maven_push_artifact_repo_id,
                'maven-push-artifact-repo-url': maven_push_artifact_repo_url,
                'version': version
            }
            step_implementer = self.create_step_implementer(
                step_config=step_config,
                parent_work_dir_path=parent_work_dir_path,
            )

            # run step with mvn version:set failure
            mock_run_maven.side_effect = StepRunnerException(
                'mock error setting new pom version')
            actual_step_result = step_implementer._run_step()

            # create expected step result
            expected_step_result = StepResult(
                step_name='deploy',
                sub_step_name='MavenDeploy',
                sub_step_implementer_name='MavenDeploy')
            expected_step_result.success = False
            expected_step_result.message = "Error running 'maven deploy' to push artifacts. " \
                "More details maybe found in 'maven-output' report artifact: " \
                "mock error setting new pom version"
            expected_step_result.add_artifact(
                description=
                "Standard out and standard error from running maven to update version.",
                name='maven-update-version-output',
                value='/mock/mvn_versions_set_output.txt')
            expected_step_result.add_artifact(
                description="Standard out and standard error from running maven to " \
                    "push artifacts to repository.",
                name='maven-push-artifacts-output',
                value='/mock/mvn_deploy_output.txt'
            )

            # verify step result
            self.assertEqual(actual_step_result, expected_step_result)

            mock_write_working_file.assert_called()
            mock_run_maven.assert_called_with(
                mvn_output_file_path='/mock/mvn_versions_set_output.txt',
                settings_file='/fake/settings.xml',
                pom_file=pom_file,
                phases_and_goals=['versions:set'],
                additional_arguments=[f'-DnewVersion={version}'])
            mock_run_maven_step.assert_not_called()
예제 #30
0
def git_commit_file(git_commit_message, file_path, repo_dir):
    """Adds and commits a file.
    NOTE: In the future, this should be split between two methods, to allow adding multiple files
          before committing.

    Parameters
    ----------
    git_commit_message : str
        The message to apply on commit.
    file_path : str
        The file to commit.
    repo_dir : str
        Path to an existing git repository.

    Raises
    ------
    StepRunnerException
    * if error adding or committing the file
    """
    try:
        sh.git.add(  # pylint: disable=no-member
            file_path,
            _cwd=repo_dir,
            _out=sys.stdout,
            _err=sys.stderr)
    except sh.ErrorReturnCode as error:
        # NOTE: this should never happen
        raise StepRunnerException(
            f"Unexpected error adding file ({file_path}) to commit"
            f" in git repository ({repo_dir}): {error}") from error

    try:
        sh.git.commit(  # pylint: disable=no-member
            '--allow-empty',
            '--all',
            '--message',
            git_commit_message,
            _cwd=repo_dir,
            _out=sys.stdout,
            _err=sys.stderr)
    except sh.ErrorReturnCode as error:
        # NOTE: this should never happen
        raise StepRunnerException(
            f"Unexpected error commiting file ({file_path})"
            f" in git repository ({repo_dir}): {error}") from error