def test_execute_command(self): """Test executing a command successfully""" self.mock_process.poll.return_value = 0 exit_code, stdout = execute_command(['echo', '1 ']) self.assertEqual(self.mock_popen.call_count, 1) self.assertEqual(exit_code, 0) self.assertEqual(stdout, [])
def test_execute_command_retry(self, mock_open, mock_network_error): """Test retrying execution because of network errors""" self.mock_process.poll.side_effect = [1, 1, 1, 0] mock_network_error.side_effect = [["Throttling"], []] mock_stdout_read = mock_open.return_value mock_stdout_read.readline.return_value = b'' exit_code, stdout = execute_command(['echo', '1'], retry=True) self.assertEqual(self.mock_popen.call_count, 2) self.assertEqual(exit_code, 0) self.assertEqual(stdout, [])
def test_execute_command_max_retry(self, mock_open, mock_network_error): """Test retrying execution because of network errors up to 5 times""" self.mock_process.poll.return_value = 255 mock_network_error.side_effect = [["Throttling"], ["unexpected EOF"], ["Throttling"], ["unexpected EOF"], ["Throttling"], ["unexpected EOF"]] mock_stdout_read = mock_open.return_value mock_stdout_read.readline.return_value = b'' exit_code, stdout = execute_command(['echo', '1'], retry=True) self.assertEqual(self.mock_popen.call_count, MAX_RETRIES) self.assertEqual(exit_code, 255) self.assertEqual(stdout, [])
def execute(self, operation: str, debug: bool = False) -> Tuple[int, List[str], bool]: """ Function for executing this pipeline entry. :param operation: The Terraform operation to execute. IE: apply, plan :param debug: True if Terraform debug info should be printed. :return: A tuple of the exit code, output of the command, and whether changes were detected. """ command_env = os.environ.copy() command_env.update(self.envvars) if debug: command_env["TF_LOG"] = "DEBUG" # pylint: disable=unused-variable plan_file, plan_file_name = tempfile.mkstemp(suffix=".tfplan") # We're using --no-resolve-envvars here because we've already resolved the environment variables in # the constructor. We are then passing in those environment variables explicitly in the # execute_command call below. base_args = ["tf", "--no-resolve-envvars", self.path] init_args = base_args + ["init"] + self.variables plan_args = base_args + [ "plan", "-detailed-exitcode", f"-out={plan_file_name}" ] + self.variables operation_args = base_args + [operation] + self.variables if operation in ["apply", "destroy"]: operation_args += ["-auto-approve"] init_exit_code, output = execute_command( init_args, print_output=False, capture_stderr=True, env=command_env, ) if init_exit_code != 0: return init_exit_code, output, True # We need to plan first before we apply so we can actually see a plan... Thanks Hashicorp if operation in ["apply"]: plan_exit_code, plan_stdout = execute_command(plan_args, print_output=False, capture_stderr=True, env=command_env) output += ["\n"] + plan_stdout if plan_exit_code != 2: return ( plan_exit_code, output, False, ) operation_args += [plan_file_name] if operation in ["plan"]: operation_args += ["--detailed-exitcode"] operation_exit_code, operation_stdout = execute_command( operation_args, print_output=False, capture_stderr=True, env=command_env) output += ["\n"] + operation_stdout changes_detected = True if operation in ["plan"]: if operation_exit_code == 2: # Set exit code to 0 so the command doesn't fail out operation_exit_code = 0 elif operation_exit_code != 2: changes_detected = False return ( operation_exit_code, output, changes_detected, )
def execute(self, operation: str, debug: bool = False) -> Tuple[int, List[str], bool]: """ Function for executing this Graph Entry. :param operation: The Terraform operation to execute. IE: apply, plan :param debug: True if Terraform debug info should be printed. :return: A tuple of the exit code, output of the command, and whether changes were detected. """ print(f"Executing {self.abs_path} {operation} ...") self.state = "Executing" command_env = os.environ.copy() command_env.update(self.envvars) if debug: command_env["TF_LOG"] = "DEBUG" # We're using --no-resolve-envvars here because we've already resolved the environment variables in # the constructor. We are then passing in those environment variables explicitly in the # execute_command call below. base_args = ["tf", "--no-resolve-envvars", self.abs_path] init_args = base_args + ["init"] + self.variables operation_args = base_args + [operation] + self.variables init_exit_code, init_stdout = execute_command( init_args, print_output=False, capture_stderr=True, env=command_env, audit_api_url=self.wrapper_config.audit_api_url, cwd=self.abs_path) if init_exit_code != 0: self.state = "Failed" return init_exit_code, init_stdout, True shell = False if operation in ["apply", "destroy"]: operation_args = ["yes", "yes", "|"] + operation_args shell = True operation_exit_code, operation_stdout = execute_command( " ".join(operation_args) if shell else operation_args, print_output=False, capture_stderr=True, env=command_env, shell=shell, audit_api_url=self.wrapper_config.audit_api_url, cwd=self.path) if operation_exit_code == 0: self.state = "Success" else: self.state = "Failed" changes_detected = True if any("Resources: 0 added, 0 changed, 0 destroyed" in line for line in operation_stdout): changes_detected = False print(f"\nFinished executing {self.abs_path} {operation} ...") return ( operation_exit_code, init_stdout + ["\n"] + operation_stdout, changes_detected, )
def execute(self, operation: str, debug: bool = False) -> Tuple[int, List[str], bool]: """ Function for executing this Graph Entry. :param operation: The Terraform operation to execute. IE: apply, plan :param debug: True if Terraform debug info should be printed. :return: A tuple of the exit code, output of the command, and whether changes were detected. """ self.state = "Executing" command_env = os.environ.copy() command_env.update(self.envvars) if debug: command_env["TF_LOG"] = "DEBUG" # pylint: disable=unused-variable plan_file, plan_file_name = tempfile.mkstemp(suffix=".tfplan") # We're using --no-resolve-envvars here because we've already resolved the environment variables in # the constructor. We are then passing in those environment variables explicitly in the # execute_command call below. base_args = ["tf", "--no-resolve-envvars", self.path] init_args = base_args + ["init"] + self.variables plan_args = base_args + [ "plan", "-detailed-exitcode", "-out=%s" % plan_file_name ] + self.variables operation_args = base_args + [operation] + self.variables if operation in ["apply", "destroy"]: operation_args += ["-auto-approve"] init_exit_code, init_stdout = execute_command( init_args, print_output=False, capture_stderr=True, env=command_env, ) if init_exit_code != 0: self.state = "Failed" return init_exit_code, init_stdout, True if operation in ["apply"]: plan_exit_code, plan_stdout = execute_command(plan_args, print_output=False, capture_stderr=True, env=command_env) operation_args += [plan_file_name] else: plan_exit_code = 0 plan_stdout = [] changes_detected = plan_exit_code != 0 if plan_exit_code in (0, 2): self.state = "Success" else: self.state = "Failed" if plan_exit_code != 2: return ( plan_exit_code, init_stdout + ["\n"] + plan_stdout, changes_detected, ) operation_exit_code, operation_stdout = execute_command( operation_args, print_output=False, capture_stderr=True, env=command_env) if operation_exit_code == 0: self.state = "Success" else: self.state = "Failed" return ( operation_exit_code, init_stdout + ["\n"] + plan_stdout + ["\n"] + operation_stdout, changes_detected, )