Skip to content

Commit bcfd550

Browse files
committed
fix few doc
1 parent 9fc67c1 commit bcfd550

8 files changed

Lines changed: 576 additions & 14 deletions

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ For full details — environment variables, redaction guarantees, and how to add
175175
- [Manage workspace variables](./docs/scenarios/manage-workspace-variables.md)
176176
- [Team access onboarding](./docs/scenarios/team-access-onboarding.md)
177177
- [State management](./docs/scenarios/state-management.md)
178+
- [Errored state recovery](./docs/scenarios/errored-state-recovery.md)
179+
- [Agent pool setup](./docs/scenarios/agent-pool-setup.md)
180+
- [Notification configurations](./docs/scenarios/notification-configurations.md)
178181
- [Policy enforcement](./docs/scenarios/policy-enforcement.md)
179182
- [Run task integration](./docs/scenarios/run-task-integration.md)
180183
- Operations guides:

docs/TESTS.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ def test_create_workspace(self, client):
136136
client._transport.request = MagicMock(return_value=mock_response)
137137

138138
# Execute the operation
139-
options = WorkspaceCreateOptions(name="new-workspace", organization="test-org")
140-
workspace = client.workspaces.create(options)
139+
options = WorkspaceCreateOptions(name="new-workspace")
140+
workspace = client.workspaces.create("test-org", options)
141141

142142
# Assertions
143143
assert workspace.id == "ws-new"
@@ -156,15 +156,20 @@ Always test validation and error handling:
156156

157157
```python
158158
def test_create_workspace_invalid_org(self, client):
159-
"""Test creating workspace with invalid organization."""
159+
"""Test creating workspace with an empty organization name."""
160+
options = WorkspaceCreateOptions(name="test")
160161
with pytest.raises(InvalidOrgError):
161-
options = WorkspaceCreateOptions(name="test", organization="")
162-
client.workspaces.create(options)
162+
client.workspaces.create("", options)
163163

164164
def test_read_workspace_invalid_id(self, client):
165-
"""Test reading workspace with invalid ID."""
165+
"""Test read_by_id with an empty workspace ID."""
166166
with pytest.raises(InvalidWorkspaceIDError):
167-
client.workspaces.read(workspace_id="")
167+
client.workspaces.read_by_id("")
168+
169+
def test_read_workspace_invalid_name(self, client):
170+
"""Test read with an empty workspace name."""
171+
with pytest.raises(InvalidWorkspaceValueError):
172+
client.workspaces.read("", organization="valid-org")
168173
```
169174

170175
### 4. Test Pagination

docs/api/workspaces.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ Example: [workspace.py](../../examples/workspace.py)
1313
| Method | Purpose |
1414
|---|---|
1515
| `client.workspaces.list(organization, options=None)` | Iterate workspaces in an organization. |
16-
| `client.workspaces.read(organization, name)` | Read by organization/name. |
16+
| `client.workspaces.read(name, *, organization)` | Read by name. `organization` is keyword-only. |
1717
| `client.workspaces.read_by_id(workspace_id)` | Read by workspace ID. |
1818
| `client.workspaces.create(organization, options)` | Create a workspace. |
19-
| `client.workspaces.update(organization, name, options)` | Update by organization/name. |
19+
| `client.workspaces.update(name, options, *, organization)` | Update by name. `organization` is keyword-only. |
2020
| `client.workspaces.update_by_id(workspace_id, options)` | Update by workspace ID. |
21-
| `client.workspaces.delete(...)` / `delete_by_id(...)` | Delete a workspace. |
22-
| `client.workspaces.safe_delete(...)` / `safe_delete_by_id(...)` | Delete with the API safe-delete path. |
21+
| `client.workspaces.delete(name, *, organization)` / `delete_by_id(workspace_id)` | Delete a workspace. |
22+
| `client.workspaces.safe_delete(name, *, organization)` / `safe_delete_by_id(workspace_id)` | Delete with the API safe-delete path. |
2323
| `client.workspaces.lock(...)`, `unlock(...)`, `force_unlock(...)` | Manage workspace locks. |
2424
| `client.workspaces.assign_ssh_key(...)`, `unassign_ssh_key(...)` | Manage workspace SSH key assignment. |
25+
| `client.workspaces.current_assessment_result(workspace_id)` | Read the latest health assessment, or `None` if assessments are disabled. |
26+
| `client.workspaces.list_applicable_varsets(workspace_id)` | Iterate variable sets that apply to a workspace (direct, inherited, and global). |
2527
| `client.workspaces.list_remote_state_consumers(...)` and related methods | Manage remote state consumers. |
2628
| `client.workspaces.list_tags(...)`, `add_tags(...)`, `remove_tags(...)` | Manage workspace tags. |
2729
| `client.workspaces.list_tag_bindings(...)` and related methods | Manage tag bindings. |
@@ -62,10 +64,26 @@ print(workspace.id)
6264
## Read by name or ID
6365

6466
```python
65-
workspace = client.workspaces.read("my-organization", "example-workspace")
67+
workspace = client.workspaces.read("example-workspace", organization="my-organization")
6668
same_workspace = client.workspaces.read_by_id(workspace.id)
6769
```
6870

71+
`organization` is a keyword-only argument on `read`, `update`, `delete`, and
72+
`safe_delete`. The workspace name is positional; the organization name must be
73+
passed by keyword. The same applies when updating or deleting by name:
74+
75+
```python
76+
from pytfe.models import WorkspaceUpdateOptions
77+
78+
client.workspaces.update(
79+
"example-workspace",
80+
WorkspaceUpdateOptions(description="Updated by pyTFE"),
81+
organization="my-organization",
82+
)
83+
84+
client.workspaces.delete("example-workspace", organization="my-organization")
85+
```
86+
6987
Prefer ID-based methods in automation when you already have the workspace ID.
7088
They avoid ambiguity when names change.
7189

docs/scenarios/agent-pool-setup.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Scenario: Agent pool setup
2+
3+
HCP Terraform agents let Terraform runs reach private networks that the hosted
4+
runners cannot. A working agent setup needs four pieces:
5+
6+
1. An agent pool in the organization.
7+
2. An agent authentication token for each running agent process.
8+
3. Workspaces (or projects) configured to use the pool.
9+
4. One or more agent processes started with the token, pointing at the API
10+
address.
11+
12+
This scenario covers the SDK side: creating the pool, generating a token, and
13+
attaching workspaces. Starting the agent process itself is done outside Python
14+
with the `tfc-agent` binary.
15+
16+
Upstream docs:
17+
18+
- Agents: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/agents
19+
- Agent tokens: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/agent-tokens
20+
- Agent pool concepts: https://developer.hashicorp.com/terraform/cloud-docs/agents
21+
22+
## Prerequisites
23+
24+
```bash
25+
export TFE_TOKEN="your-api-token"
26+
export TFE_ADDRESS="https://app.terraform.io"
27+
```
28+
29+
The token needs `manage-agent-pools` permission on the organization, and
30+
workspace write access on any workspaces you intend to attach.
31+
32+
## Step 1: Create the agent pool
33+
34+
```python
35+
from pytfe import TFEClient
36+
from pytfe.models import AgentPoolCreateOptions
37+
38+
39+
client = TFEClient()
40+
organization = "my-organization"
41+
42+
pool = client.agent_pools.create(
43+
organization,
44+
AgentPoolCreateOptions(
45+
name="private-network-pool",
46+
organization_scoped=False,
47+
allowed_workspace_ids=["ws-abc123"],
48+
),
49+
)
50+
51+
print(pool.id, pool.name)
52+
```
53+
54+
Set `organization_scoped=True` to allow every workspace in the organization to
55+
use the pool. Set it to `False` and pass `allowed_workspace_ids` (and/or
56+
`allowed_project_ids`) to scope the pool explicitly. Scoped pools are safer for
57+
shared organizations because they prevent unrelated workspaces from picking up
58+
the agent.
59+
60+
## Step 2: Create an agent token
61+
62+
Each running agent process needs its own token:
63+
64+
```python
65+
from pytfe.models import AgentTokenCreateOptions
66+
67+
token = client.agent_tokens.create(
68+
pool.id,
69+
AgentTokenCreateOptions(description="agent-host-1"),
70+
)
71+
72+
print(token.id)
73+
print(token.token)
74+
```
75+
76+
The token value is returned only at creation time. Store it in a secret manager
77+
immediately and reference it from the agent host's environment.
78+
79+
For multiple agents, create one token per host so revocation is granular:
80+
81+
```python
82+
for host in ["agent-host-1", "agent-host-2", "agent-host-3"]:
83+
t = client.agent_tokens.create(
84+
pool.id,
85+
AgentTokenCreateOptions(description=host),
86+
)
87+
save_to_secret_manager(host, t.token)
88+
```
89+
90+
## Step 3: Attach workspaces to the pool
91+
92+
A workspace uses an agent pool when its `execution_mode` is `agent` and its
93+
`agent_pool_id` references the pool:
94+
95+
```python
96+
from pytfe.models import ExecutionMode, WorkspaceUpdateOptions
97+
98+
client.workspaces.update_by_id(
99+
"ws-abc123",
100+
WorkspaceUpdateOptions(
101+
execution_mode=ExecutionMode.AGENT,
102+
agent_pool_id=pool.id,
103+
),
104+
)
105+
```
106+
107+
For a scoped pool, you can also widen or narrow the allowed list later:
108+
109+
```python
110+
from pytfe.models import AgentPoolAssignToWorkspacesOptions
111+
112+
client.agent_pools.assign_to_workspaces(
113+
pool.id,
114+
AgentPoolAssignToWorkspacesOptions(
115+
workspace_ids=["ws-abc123", "ws-def456"],
116+
),
117+
)
118+
```
119+
120+
`assign_to_workspaces` replaces the allowed-workspaces list in full; it does
121+
not append. Always pass the complete intended list.
122+
123+
## Step 4: Start the agent process
124+
125+
Outside Python, on the host that has the network path to your private
126+
infrastructure:
127+
128+
```bash
129+
export TFC_AGENT_TOKEN="<token value from Step 2>"
130+
export TFC_AGENT_NAME="agent-host-1"
131+
export TFC_ADDRESS="https://app.terraform.io"
132+
133+
tfc-agent
134+
```
135+
136+
Confirm the agent registered:
137+
138+
```python
139+
for agent in client.agents.list(pool.id):
140+
print(agent.id, agent.name, agent.status)
141+
```
142+
143+
A healthy agent reports `status="idle"` or `status="busy"`.
144+
145+
## Cleanup
146+
147+
Revoke tokens when a host is decommissioned. Delete the pool only after no
148+
workspaces or projects reference it.
149+
150+
```python
151+
client.agent_tokens.delete(token.id)
152+
153+
# Detach the workspace before deleting the pool.
154+
client.workspaces.update_by_id(
155+
"ws-abc123",
156+
WorkspaceUpdateOptions(execution_mode=ExecutionMode.REMOTE),
157+
)
158+
client.agent_pools.delete(pool.id)
159+
```
160+
161+
## Operational notes
162+
163+
- Treat agent tokens like SSH keys: one per host, rotated, stored in a secret
164+
manager.
165+
- Prefer scoped pools over organization-scoped pools when only some workspaces
166+
need private-network access.
167+
- Agent processes hold long-lived connections. Restart them after rotating
168+
tokens or upgrading the agent binary.
169+
- An agent pool with no running agents will leave runs queued indefinitely.
170+
Monitor `client.agents.list(pool.id)` from your observability stack.

docs/scenarios/api-driven-run.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ terraform_dir = Path("./terraform")
5858

5959
def read_or_create_workspace() -> Workspace:
6060
try:
61-
return client.workspaces.read(organization, workspace_name)
61+
return client.workspaces.read(workspace_name, organization=organization)
6262
except TFEError:
6363
return client.workspaces.create(
6464
organization,

0 commit comments

Comments
 (0)