示例#1
0
def _internal_batch_operation(
    terraform_domain: str,
    workspaces: List[Workspace],
    *,
    json: Dict[str, Any],
    on_success: SuccessHandler[Workspace],
    on_failure: ErrorHandler[Workspace],
    no_tls: bool = False,
    token: Optional[str] = None,
    write_output: bool = False
) -> bool:
    headers = get_api_headers(terraform_domain, token=token, write_error_messages=write_output)
    all_successful = True
    base_url = f"{get_protocol(no_tls)}://{terraform_domain}/api/v2"
    for workspace in workspaces:
        url = f"{base_url}/workspaces/{workspace.workspace_id}"
        response = safe_http_request(
            lambda: throttle(lambda: requests.patch(url, json=json, headers=headers))
        )
        if response.status_code == 200:
            on_success(workspace)
        else:
            all_successful = False
            on_failure(workspace, response)
    return all_successful
def _get_existing_variables(
        base_url: str,
        headers: Dict[str, str],
        workspace: Workspace,
        *,
        write_output: bool = False) -> Optional[Dict[str, Variable]]:
    """
    Fetches the variables for a given workspaces. This method will eagerly exit and return None if
    anything unexpected is encountered. This is done prophylactically as the ensuing update/create
    operations hinge on the successful completion of this method.

    :param base_url: A URL fragment onto which a path will be appended to construct the Terraform
                     API endpoint.
    :param headers: The headers to provide in the API HTTP request to fetch the variables.
    :param workspace: The workspace for which variables will be fetched.
    :param write_output: Whether to print a tabulated result of the patch operations to STDOUT.
    :return: A dictionary mapping variable IDs to variables, or None if an error occurred.
    """
    def write_parse_error(json_object: Any) -> None:
        if write_output:
            print(
                f"Warning: a variable was not successfully parsed from {json.dumps(json_object)}",
                file=sys.stderr)

    # yapf: disable
    response = safe_http_request(
        lambda: throttle(lambda: requests.get(
            f"{base_url}/workspaces/{workspace.workspace_id}/vars",
            headers=headers
        ))
    )
    # yapf: enable

    variables = {}
    if response.status_code == 200:
        body = response.json()
        if "data" in body and isinstance(body["data"], list):
            for obj in body["data"]:
                if isinstance(obj,
                              dict) and "id" in obj and "attributes" in obj:
                    variable_id = obj["id"]
                    variable = Variable.from_json(obj["attributes"])
                    if variable is not None:
                        variables[variable_id] = variable
                    else:
                        write_parse_error(obj)
                        return None
                else:
                    write_parse_error(obj)
                    return None
        else:
            write_parse_error(response.json())
            return None
    else:
        if write_output:
            print(
                f'Error: failed to get the existing variables for workspace "{workspace.name}".',
                file=sys.stderr)
        return None
    return variables
def _create_variables(
    base_url: str, headers: Dict[str, str], *, workspace: Workspace,
    creations: List[Variable], on_success: Callable[[Variable], None],
    on_failure: Callable[[Variable, Union[Response, ErrorResponse]], None]
) -> bool:
    """
    Fetches the variables for a given workspaces.

    :param base_url: A URL fragment onto which a path will be appended to construct the Terraform
                     API endpoint.
    :param headers: The headers to provide in the API HTTP request to fetch the variables.
    :param workspace: The workspace for which variables will be fetched.
    :param creations: A list of variables to be created.
    :param on_success: A function which will be passed a Variable object when that variable has
                       been successfully patched.
    :param on_failure: A function which will be passed a Variable object when that variable has
                       NOT been successfully patched.
    :return: Whether all HTTP operations were successful. If even a single one failed, returns
             False.
    """

    all_successful = True
    for variable in creations:
        data = {"data": {"type": "vars", "attributes": variable.to_json()}}
        response = safe_http_request(lambda: throttle(lambda: requests.post(
            f"{base_url}/workspaces/{workspace.workspace_id}/vars",
            headers=headers,
            json=data)))
        if response.status_code == 201:
            on_success(variable)
        else:
            on_failure(variable, response)
            all_successful = False
    return all_successful
示例#4
0
def exhaust_pages(endpoint: str,
                  *,
                  json_mapper: Callable[[Any], A],
                  token: Optional[str] = None,
                  write_error_messages: bool = False) -> List[A]:
    """
    Iterates through every page that will be returned by a given Terraform API endpoint.

    :param endpoint: The full URL of a GET-able Terraform API endpoint (either Terraform Cloud or
                     Enterprise).
    :param json_mapper: A mapping function that takes the value of the "data" field as input and
                        returns a new value (which will be aggregated for all pages).
    :param token: A token suitable for authenticating against the Terraform API. If not specified, a
                  token will be searched for in the documented locations.
    :param write_error_messages: Whether to write error messages to STDERR.
    :return: A list of outputs from the json_mapper function.
    """

    current_page = 1
    aggregate = []
    headers = get_api_headers(parse_domain(endpoint),
                              token=token,
                              write_error_messages=write_error_messages)
    while current_page is not None:
        parameters = {
            # See: https://www.terraform.io/docs/cloud/api/index.html#pagination
            "page[number]": current_page,
            "page[size]": 100
        }
        response = safe_http_request(lambda: throttle(lambda: requests.get(
            endpoint, headers=headers, params=parameters)))
        if response.status_code == 200:
            json = response.json()
            if "data" in json:
                aggregate.append(json_mapper(json["data"]))
            current_page = _get_next_page(json)
        else:
            if write_error_messages:
                # yapf: disable
                print((
                    f"Error: the Terraform API returned an error response from {endpoint} with "
                    f"parameters {parameters} - response from the API was {response.json()}"
                ), file=sys.stderr)
                # yapf: enable
            current_page = None
    return aggregate
示例#5
0
def _get_active_runs_for_workspace(
    terraform_domain: str,
    workspace: Workspace,
    *,
    no_tls: bool = False,
    token: Optional[str] = None
) -> List[Run]:
    required_attributes = ["created-at", "status", "status-timestamps", "has-changes"]
    active_runs = []

    headers = get_api_headers(terraform_domain, token=token, write_error_messages=False)
    base_url = f"{get_protocol(no_tls)}://{terraform_domain}/api/v2"
    endpoint = f"{base_url}/workspaces/{workspace.workspace_id}/runs"
    parameters = {
        # See: https://www.terraform.io/docs/cloud/api/index.html#pagination
        # Note that this method only checks the most recent 100 runs for the workspace (this will be
        # sufficient in practice)
        "page[number]": 1,
        "page[size]": 100
    }
    response = safe_http_request(
        lambda: throttle(lambda: requests.get(endpoint, headers=headers, params=parameters))
    )

    if response.status_code == 200:
        json = response.json()
        if "data" in json and len(json["data"]) > 0:
            for run_json in json["data"]:
                if "id" in run_json and "attributes" in run_json:
                    attributes = run_json["attributes"]
                    if all([x in attributes for x in required_attributes]):
                        run = Run(
                            run_id=run_json["id"],
                            workspace=workspace,
                            created_at=attributes["created-at"],
                            status=attributes["status"],
                            all_status_timestamps=attributes["status-timestamps"],
                            has_changes=attributes["has-changes"]
                        )
                        if run.is_active and run.has_changes:
                            active_runs.append(run)
    return active_runs
def lock_or_unlock_workspaces(
    terraform_domain: str,
    organization: str,
    workspaces: List[Workspace],
    *,
    set_lock: bool,
    no_tls: bool = False,
    token: Optional[str] = None,
    write_output: bool = False
) -> bool:
    """
    Locks or unlocks each of the given workspaces.

    :param terraform_domain: The domain corresponding to the targeted Terraform installation (either
                             Terraform Cloud or Enterprise).
    :param organization: The organization containing the workspaces to lock/unlock.
    :param workspaces: The workspaces to lock or unlock.
    :param set_lock: The desired state of the workspaces' locks. If True, workspaces will be locked.
                     If False, workspaces will be unlocked.
    :param no_tls: Whether to use SSL/TLS encryption when communicating with the Terraform API.
    :param token: A token suitable for authenticating against the Terraform API. If not specified, a
                  token will be searched for in the documented locations.
    :param write_output: Whether to print a tabulated result of the patch operations to STDOUT.
    :return: Whether all lock/unlock operations were successful. If even a single one failed,
             returns False.
    """

    headers = get_api_headers(terraform_domain, token=token, write_error_messages=write_output)
    operation = "lock" if set_lock else "unlock"
    base_url = f"{get_protocol(no_tls)}://{terraform_domain}/api/v2"
    report = []
    all_successful = True
    for workspace in workspaces:
        url = f"{base_url}/workspaces/{workspace.workspace_id}/actions/{operation}"
        response = safe_http_request(lambda: throttle(lambda: requests.post(url, headers=headers)))
        if response.status_code == 200:
            report.append([workspace.name, workspace.is_locked, set_lock, "success", "none"])
        elif response.status_code == 409:
            report.append([
                workspace.name,
                workspace.is_locked,
                set_lock,
                "success",
                f"workspace was already {operation}ed"
            ])
        else:
            all_successful = False
            report.append([
                workspace.name,
                workspace.is_locked,
                workspace.is_locked,
                "error",
                wrap_text(str(response.json()), MESSAGE_COLUMN_CHARACTER_COUNT)
            ])

    if write_output:
        print((
            f'Terraform workspace {operation} results for organization "{organization}" at '
            f'"{terraform_domain}":'
        ))
        print()
        print(
            tabulate(
                sorted(report, key=lambda x: (x[3], x[0])),
                headers=["Workspace", "Lock State Before", "Lock State After", "Status", "Message"]
            )
        )
        print()

    return all_successful
def delete_variables(terraform_domain: str,
                     organization: str,
                     workspaces: List[Workspace],
                     *,
                     variables: List[str],
                     no_tls: bool = False,
                     token: Optional[str] = None,
                     write_output: bool = False) -> bool:
    """
    Deletes one or more variables for the workspaces. If a variable does not exist in a particular
    workspace, no operation is performed relative to that variable (this is a safe operation). This
    behavior allows this method to be idempotent.

    :param terraform_domain: The domain corresponding to the targeted Terraform installation (either
                             Terraform Cloud or Enterprise).
    :param organization: The organization containing the workspaces to patch.
    :param workspaces: The workspaces to patch.
    :param variables: The keys of the variables to delete.
    :param no_tls: Whether to use SSL/TLS encryption when communicating with the Terraform API.
    :param token: A token suitable for authenticating against the Terraform API. If not specified, a
                  token will be searched for in the documented locations.
    :param write_output: Whether to print a tabulated result of the patch operations to STDOUT.
    :return: Whether all HTTP operations were successful. If even a single one failed, returns
             False.
    """

    if len(variables) == 0:
        if write_output:
            print("No variables to delete - returning successful immediately.")
        return True

    report = []
    headers = get_api_headers(terraform_domain,
                              token=token,
                              write_error_messages=write_output)
    base_url = f"{get_protocol(no_tls)}://{terraform_domain}/api/v2"
    all_successful = True
    for workspace in workspaces:
        existing_variables = _get_existing_variables(base_url,
                                                     headers,
                                                     workspace,
                                                     write_output=write_output)
        if existing_variables is None:  # Reminder: it will be none if something went wrong
            all_successful = False
            continue
        for variable_id, variable in existing_variables.items():
            if variable.key in variables:
                response = safe_http_request(
                    lambda: throttle(lambda: requests.delete(
                        f"{base_url}/workspaces/{workspace.workspace_id}/vars/{variable_id}",
                        headers=headers)))
                if response.status_code == 204:
                    report.append([
                        workspace.name, variable.key, "delete", "success",
                        "none"
                    ])
                else:
                    all_successful = False
                    report.append([
                        workspace.name, variable.key, "delete", "error",
                        wrap_text(str(response.json()),
                                  MESSAGE_COLUMN_CHARACTER_COUNT)
                    ])

    if write_output:
        print((
            f'Terraform workspace variable deletion results for organization "{organization}" at '
            f'"{terraform_domain}":'))
        print()
        print(
            tabulate(sorted(report, key=lambda x: (x[3], x[2], x[0], x[1])),
                     headers=[
                         "Workspace", "Variable", "Operation", "Status",
                         "Message"
                     ]))
        print()

    return all_successful