Exemplo n.º 1
0
def test_get_api_headers(mocker: MockerFixture) -> None:
    token = "test"
    mocker.patch("terraform_manager.terraform.find_token", return_value=token)
    headers = get_api_headers(TEST_TERRAFORM_DOMAIN, token=None)
    assert headers == {
        "Authorization": f"Bearer {token}",
        "Content-Type": HTTP_CONTENT_TYPE
    }

    token = "test123"
    headers = get_api_headers(TEST_TERRAFORM_DOMAIN, token=token)
    assert headers == {
        "Authorization": f"Bearer {token}",
        "Content-Type": HTTP_CONTENT_TYPE
    }
Exemplo n.º 2
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
Exemplo n.º 3
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
Exemplo n.º 4
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
Exemplo n.º 5
0
def test_get_api_headers_missing_token(mocker: MockerFixture) -> None:
    mocker.patch("terraform_manager.terraform.find_token", return_value=None)
    headers = get_api_headers(TEST_TERRAFORM_DOMAIN, token=None)
    assert headers == {"Content-Type": HTTP_CONTENT_TYPE}
Exemplo n.º 6
0
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
Exemplo n.º 7
0
def configure_variables(terraform_domain: str,
                        organization: str,
                        workspaces: List[Workspace],
                        *,
                        variables: List[Variable],
                        no_tls: bool = False,
                        token: Optional[str] = None,
                        write_output: bool = False) -> bool:
    """
    Creates or updates (in-place) one or more variables for the workspaces. If variables already
    exist with same keys, they will instead be updated so that all their fields equal the ones given
    in the variables passed to this method. This behavior allows this method to be idempotent. If
    any of the specified variables are invalid, no operations will be performed by this method.

    :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 variables to either create or update (or a mix thereof).
    :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 OR any of the
             variables are invalid, returns False.
    """

    if len(variables) == 0:
        if write_output:
            print(
                "No variables to configure - returning successful immediately."
            )
        return True
    elif not all([variable.is_valid for variable in variables]):
        if write_output:
            print(
                "At least one variable is invalid, so no variables will be configured.",
                file=sys.stderr)
        return False

    report = []

    def on_success(w: Workspace, create: bool) -> SuccessHandler[Variable]:
        operation = "create" if create else "update"

        def callback(v: Variable) -> None:
            report.append([w.name, v.key, operation, "success", "none"])

        return callback

    def on_failure(w: Workspace, create: bool) -> ErrorHandler[Variable]:
        operation = "create" if create else "update"

        def callback(v: Variable, response: Union[Response,
                                                  ErrorResponse]) -> None:
            report.append([
                w.name, v.key, operation, "error",
                wrap_text(str(response.json()), MESSAGE_COLUMN_CHARACTER_COUNT)
            ])

        return callback

    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:
        updates_needed = {}
        creations_needed = []
        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 new_variable in variables:
            needs_update = False
            for variable_id, old_variable in existing_variables.items():
                if old_variable.key == new_variable.key:
                    needs_update = True
                    updates_needed[variable_id] = new_variable
                    break
            if not needs_update:
                creations_needed.append(new_variable)

        create_result = _create_variables(
            base_url,
            headers,
            workspace=workspace,
            creations=creations_needed,
            on_success=on_success(workspace, True),
            on_failure=on_failure(workspace, True))
        if all_successful:
            all_successful = create_result

        update_result = _update_variables(
            base_url,
            headers,
            workspace=workspace,
            updates=updates_needed,
            on_success=on_success(workspace, False),
            on_failure=on_failure(workspace, False))
        if all_successful:
            all_successful = update_result

    if write_output:
        print((
            f'Terraform workspace variable configuration results for organization "{organization}" '
            f'at "{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
Exemplo n.º 8
0
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