Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion examples/agent_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
1. Agent Pool CRUD operations (Create, Read, Update, Delete)
2. Agent token creation and management
3. Workspace assignment using assign_to_workspaces and remove_from_workspaces
4. Proper error handling
4. Project assignment using update_allowed_projects
5. Dedicated relationship update methods: update_allowed_workspaces,
update_allowed_projects, update_excluded_workspaces
6. Proper error handling

Make sure to set the following environment variables:
- TFE_TOKEN: Your Terraform Cloud/Enterprise API token
- TFE_ADDRESS: Your Terraform Cloud/Enterprise URL (optional, defaults to https://app.terraform.io)
- TFE_ORG: Your organization name
- TFE_WORKSPACE_ID: A workspace ID for testing workspace assignment (optional)
- TFE_PROJECT_ID: A project ID for testing project assignment (optional)

Usage:
export TFE_TOKEN="your-token-here"
Expand All @@ -27,9 +31,12 @@
from pytfe import TFEClient, TFEConfig
from pytfe.errors import NotFound
from pytfe.models import (
AgentPoolAllowedProjectsUpdateOptions,
AgentPoolAllowedWorkspacePolicy,
AgentPoolAllowedWorkspacesUpdateOptions,
AgentPoolAssignToWorkspacesOptions,
AgentPoolCreateOptions,
AgentPoolExcludedWorkspacesUpdateOptions,
AgentPoolListOptions,
AgentPoolRemoveFromWorkspacesOptions,
AgentPoolUpdateOptions,
Expand All @@ -46,6 +53,7 @@ def main():
workspace_id = os.environ.get(
"TFE_WORKSPACE_ID"
) # optional, for workspace assignment
project_id = os.environ.get("TFE_PROJECT_ID") # optional, for project assignment

if not token:
print("TFE_TOKEN environment variable is required")
Expand Down Expand Up @@ -122,9 +130,62 @@ def main():
AgentPoolRemoveFromWorkspacesOptions(workspace_ids=[workspace_id]),
)
print(f" Removed workspace {workspace_id} from pool {updated_pool.name}")

# Example 5b: Dedicated update methods
# update_allowed_workspaces / update_excluded_workspaces send the
# relationship array unconditionally — even an empty list — so they can
# also CLEAR the relationship.
print(
"\n Using update_allowed_workspaces (dedicated, supports clearing)..."
)
updated_pool = client.agent_pools.update_allowed_workspaces(
new_pool.id,
AgentPoolAllowedWorkspacesUpdateOptions(workspace_ids=[workspace_id]),
)
print(
f" Set allowed-workspaces to [{workspace_id}] on pool {updated_pool.name}"
)

print("\n Clearing allowed-workspaces via update_allowed_workspaces...")
updated_pool = client.agent_pools.update_allowed_workspaces(
new_pool.id,
AgentPoolAllowedWorkspacesUpdateOptions(workspace_ids=[]),
)
print(f" Cleared allowed-workspaces on pool {updated_pool.name}")

print(
"\n Using update_excluded_workspaces (dedicated, supports clearing)..."
)
updated_pool = client.agent_pools.update_excluded_workspaces(
new_pool.id,
AgentPoolExcludedWorkspacesUpdateOptions(workspace_ids=[workspace_id]),
)
print(
f" Set excluded-workspaces to [{workspace_id}] on pool {updated_pool.name}"
)
else:
print("\n Skipping workspace assignment (set TFE_WORKSPACE_ID to test)")

# Example 5c: Project assignment (parity — AllowedProjects relationship)
if project_id:
print("\n Assigning project to agent pool (update_allowed_projects)...")
updated_pool = client.agent_pools.update_allowed_projects(
new_pool.id,
AgentPoolAllowedProjectsUpdateOptions(project_ids=[project_id]),
)
print(
f" Set allowed-projects to [{project_id}] on pool {updated_pool.name}"
)

print("\n Clearing allowed-projects via update_allowed_projects...")
updated_pool = client.agent_pools.update_allowed_projects(
new_pool.id,
AgentPoolAllowedProjectsUpdateOptions(project_ids=[]),
)
print(f" Cleared allowed-projects on pool {updated_pool.name}")
else:
print("\n Skipping project assignment (set TFE_PROJECT_ID to test)")

# Example 6: Create an agent token
print("\n Creating agent token...")
token_options = AgentTokenCreateOptions(
Expand Down
6 changes: 6 additions & 0 deletions src/pytfe/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
Agent,
AgentListOptions,
AgentPool,
AgentPoolAllowedProjectsUpdateOptions,
AgentPoolAllowedWorkspacePolicy,
AgentPoolAllowedWorkspacesUpdateOptions,
AgentPoolAssignToWorkspacesOptions,
AgentPoolCreateOptions,
AgentPoolExcludedWorkspacesUpdateOptions,
AgentPoolListOptions,
AgentPoolReadOptions,
AgentPoolRemoveFromWorkspacesOptions,
Expand Down Expand Up @@ -463,9 +466,12 @@
# Agent & pools
"Agent",
"AgentPool",
"AgentPoolAllowedProjectsUpdateOptions",
"AgentPoolAllowedWorkspacePolicy",
"AgentPoolAllowedWorkspacesUpdateOptions",
"AgentPoolAssignToWorkspacesOptions",
"AgentPoolCreateOptions",
"AgentPoolExcludedWorkspacesUpdateOptions",
"AgentPoolListOptions",
"AgentPoolReadOptions",
"AgentPoolRemoveFromWorkspacesOptions",
Expand Down
34 changes: 34 additions & 0 deletions src/pytfe/models/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ class AgentPoolListOptions(BaseModel):
include: list[str] | None = None
# Optional: Filter by allowed workspace policy
allowed_workspace_policy: AgentPoolAllowedWorkspacePolicy | None = None
# Optional: String (workspace name) used to filter the results
allowed_workspaces_name: str | None = None
# Optional: String (project name) used to filter the results
allowed_projects_name: str | None = None


class AgentPoolCreateOptions(BaseModel):
Expand All @@ -87,6 +91,8 @@ class AgentPoolCreateOptions(BaseModel):
allowed_workspace_policy: AgentPoolAllowedWorkspacePolicy | None = None
# Optional: IDs of workspaces allowed to use this pool (sent as relationships.allowed-workspaces)
allowed_workspace_ids: list[str] = Field(default_factory=list)
# Optional: IDs of projects allowed to use this pool (sent as relationships.allowed-projects)
allowed_project_ids: list[str] = Field(default_factory=list)
# Optional: IDs of workspaces excluded from this pool (sent as relationships.excluded-workspaces)
excluded_workspace_ids: list[str] = Field(default_factory=list)

Expand All @@ -102,6 +108,8 @@ class AgentPoolUpdateOptions(BaseModel):
allowed_workspace_policy: AgentPoolAllowedWorkspacePolicy | None = None
# Optional: Full replacement list of workspace IDs allowed to use this pool
allowed_workspace_ids: list[str] = Field(default_factory=list)
# Optional: Full replacement list of project IDs allowed to use this pool
allowed_project_ids: list[str] = Field(default_factory=list)
# Optional: Full replacement list of workspace IDs excluded from this pool
excluded_workspace_ids: list[str] = Field(default_factory=list)

Expand All @@ -128,6 +136,32 @@ class AgentPoolRemoveFromWorkspacesOptions(BaseModel):
workspace_ids: list[str] = Field(default_factory=list)


# Dedicated relationship-update options.
# Unlike the main create/update options, these always send their relationship array in the
# payload — even when empty — so that the caller can clear existing relationships.


class AgentPoolAllowedWorkspacesUpdateOptions(BaseModel):
"""Options for updating the allowed-workspaces relationship on an agent pool."""

workspace_ids: list[str] = Field(default_factory=list)


class AgentPoolAllowedProjectsUpdateOptions(BaseModel):
"""Options for updating the allowed-projects relationship on an agent pool."""

project_ids: list[str] = Field(default_factory=list)


class AgentPoolExcludedWorkspacesUpdateOptions(BaseModel):
"""Options for updating the excluded-workspaces relationship on an agent pool.

Supports full replacement including clearing (pass an empty list).
"""

workspace_ids: list[str] = Field(default_factory=list)


# Agent Options


Expand Down
120 changes: 119 additions & 1 deletion src/pytfe/resources/agent_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@

from __future__ import annotations

from collections.abc import Iterator
from collections.abc import Iterator, Sequence
from typing import Any, cast

from ..models.agent import (
AgentPool,
AgentPoolAllowedProjectsUpdateOptions,
AgentPoolAllowedWorkspacePolicy,
AgentPoolAllowedWorkspacesUpdateOptions,
AgentPoolAssignToWorkspacesOptions,
AgentPoolCreateOptions,
AgentPoolExcludedWorkspacesUpdateOptions,
AgentPoolListOptions,
AgentPoolReadOptions,
AgentPoolRemoveFromWorkspacesOptions,
Expand Down Expand Up @@ -143,6 +146,12 @@ def list(
params["filter[allowed_workspace_policy]"] = (
options.allowed_workspace_policy.value
)
if options.allowed_workspaces_name:
params["filter[allowed_workspaces][name]"] = (
options.allowed_workspaces_name
)
if options.allowed_projects_name:
params["filter[allowed_projects][name]"] = options.allowed_projects_name

items_iter = self._list(path, params=params)

Expand Down Expand Up @@ -214,6 +223,13 @@ def create(self, organization: str, options: AgentPoolCreateOptions) -> AgentPoo
for ws_id in options.allowed_workspace_ids
]
}
if options.allowed_project_ids:
relationships["allowed-projects"] = {
"data": [
{"type": "projects", "id": proj_id}
for proj_id in options.allowed_project_ids
]
}
if options.excluded_workspace_ids:
relationships["excluded-workspaces"] = {
"data": [
Expand Down Expand Up @@ -351,6 +367,13 @@ def update(self, agent_pool_id: str, options: AgentPoolUpdateOptions) -> AgentPo
for ws_id in options.allowed_workspace_ids
]
}
if options.allowed_project_ids:
relationships["allowed-projects"] = {
"data": [
{"type": "projects", "id": proj_id}
for proj_id in options.allowed_project_ids
]
}
if options.excluded_workspace_ids:
relationships["excluded-workspaces"] = {
"data": [
Expand Down Expand Up @@ -552,3 +575,98 @@ def remove_from_workspaces(
),
agent_count=_safe_int(agent_pool_data["agent_count"]),
)

def _patch_relationship(
self,
agent_pool_id: str,
relationship_key: str,
items_data: Sequence[dict[str, Any]],
) -> AgentPool:
"""Internal helper: PATCH agent-pools/:id with a single relationship key.

Always sends the relationship array in the payload even when empty, which
allows callers to clear an existing relationship by passing an empty list.
"""
if not valid_string_id(agent_pool_id):
raise ValueError("Agent pool ID is required and must be valid")

path = f"/api/v2/agent-pools/{agent_pool_id}"
payload: dict[str, Any] = {
"data": {
"type": "agent-pools",
"id": agent_pool_id,
"attributes": {},
"relationships": {relationship_key: {"data": items_data}},
}
}
response = self.t.request("PATCH", path, json_body=payload)
data = response.json()["data"]

attr = data.get("attributes", {}) or {}
return AgentPool(
id=_safe_str(data.get("id")) or "",
name=_safe_str(attr.get("name")),
created_at=cast(Any, attr.get("created-at")),
organization_scoped=_safe_bool(attr.get("organization-scoped")),
allowed_workspace_policy=_safe_workspace_policy(
attr.get("allowed-workspace-policy")
),
agent_count=_safe_int(attr.get("agent-count", 0)),
)

def update_allowed_workspaces(
self, agent_pool_id: str, options: AgentPoolAllowedWorkspacesUpdateOptions
) -> AgentPool:
"""Update the complete allowed-workspaces list for an agent pool.

Args:
agent_pool_id: Agent pool ID
options: Options containing the new full list of workspace IDs

Returns:
Updated AgentPool object

Raises:
ValueError: If agent_pool_id is invalid
TFEError: If API request fails
"""
items = [{"type": "workspaces", "id": ws_id} for ws_id in options.workspace_ids]
return self._patch_relationship(agent_pool_id, "allowed-workspaces", items)

def update_allowed_projects(
self, agent_pool_id: str, options: AgentPoolAllowedProjectsUpdateOptions
) -> AgentPool:
"""Update the complete allowed-projects list for an agent pool.

Args:
agent_pool_id: Agent pool ID
options: Options containing the new full list of project IDs

Returns:
Updated AgentPool object

Raises:
ValueError: If agent_pool_id is invalid
TFEError: If API request fails
"""
items = [{"type": "projects", "id": proj_id} for proj_id in options.project_ids]
return self._patch_relationship(agent_pool_id, "allowed-projects", items)

def update_excluded_workspaces(
self, agent_pool_id: str, options: AgentPoolExcludedWorkspacesUpdateOptions
) -> AgentPool:
"""Update the complete excluded-workspaces list for an agent pool.

Args:
agent_pool_id: Agent pool ID
options: Options containing the new full list of workspace IDs to exclude

Returns:
Updated AgentPool object

Raises:
ValueError: If agent_pool_id is invalid
TFEError: If API request fails
"""
items = [{"type": "workspaces", "id": ws_id} for ws_id in options.workspace_ids]
return self._patch_relationship(agent_pool_id, "excluded-workspaces", items)
Loading
Loading