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}
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')
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
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." )
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
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)
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
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
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
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
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')
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
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." )
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
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
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) ])
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
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
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
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
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)
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()
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