Skip to content

Commit 9920ffa

Browse files
authored
Merge branch 'next-1.0.0' into feature/org-tokens-api-specs
2 parents 6d3aa76 + 9d6c3ae commit 9920ffa

152 files changed

Lines changed: 3946 additions & 2570 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Copyright IBM Corp. 2025, 2026
2+
# SPDX-License-Identifier: MPL-2.0
3+
14
blank_issues_enabled: false
25
contact_links:
36
- name: HCP Terraform and Terraform Enterprise Troubleshooting and Feature Requests

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,4 @@ protos.bin
137137

138138
# Local RSA keys
139139
.local/*
140+

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
# Unreleased
22

3+
# v0.1.3
4+
5+
## Enhancements
6+
7+
### Iterator Pattern Migration
8+
* Migrated Run resource list operations to iterator pattern by @NimishaShrivastava-dev [#91](https://github.com/hashicorp/python-tfe/pull/91)
9+
* Migrated Policy resource list operations to iterator pattern by @TanyaSingh369-svg [#92](https://github.com/hashicorp/python-tfe/pull/92)
10+
* Migrated Policy Set resource list operations to iterator pattern by @TanyaSingh369-svg [#95](https://github.com/hashicorp/python-tfe/pull/95)
11+
* Migrated Run Event resource list operations to iterator pattern by @NimishaShrivastava-dev [#97](https://github.com/hashicorp/python-tfe/pull/97)
12+
* Migrated SSH Keys resource list operations to iterator pattern by @NimishaShrivastava-dev [#101](https://github.com/hashicorp/python-tfe/pull/101)
13+
* Migrated Notification Configuration resource list operations to iterator pattern by @TanyaSingh369-svg [#109](https://github.com/hashicorp/python-tfe/pull/109)
14+
* Migrated Variable Set list operations to iterator pattern by @isivaselvan [#113](https://github.com/hashicorp/python-tfe/pull/113)
15+
* Migrated Variable Set Variables list operations to iterator pattern by @isivaselvan [#113](https://github.com/hashicorp/python-tfe/pull/113)
16+
* Migrated State Version list operations to iterator pattern by @isivaselvan [#113](https://github.com/hashicorp/python-tfe/pull/113)
17+
* Migrated State Version Output list operations to iterator pattern by @isivaselvan [#113](https://github.com/hashicorp/python-tfe/pull/113)
18+
* Migrated Policy Check list operations to iterator pattern by @isivaselvan [#113](https://github.com/hashicorp/python-tfe/pull/113)
19+
* Refreshed examples and unit tests to align with iterator pattern updates by @NimishaShrivastava-dev, @TanyaSingh369-svg, @isivaselvan [#91](https://github.com/hashicorp/python-tfe/pull/91) [#92](https://github.com/hashicorp/python-tfe/pull/92) [#95](https://github.com/hashicorp/python-tfe/pull/95) [#97](https://github.com/hashicorp/python-tfe/pull/97) [#101](https://github.com/hashicorp/python-tfe/pull/101) [#109](https://github.com/hashicorp/python-tfe/pull/109) [#113](https://github.com/hashicorp/python-tfe/pull/113)
20+
21+
### Project and Workspace Management
22+
* Updated Project create and update models, including Project model refinements by @isivaselvan [#120](https://github.com/hashicorp/python-tfe/pull/120)
23+
* Updated Project endpoints for list-effective-tag-bindings and delete-tag-bindings by @isivaselvan [#120](https://github.com/hashicorp/python-tfe/pull/120)
24+
* Refactored Workspace models to improve validation with Pydantic by @isivaselvan [#106](https://github.com/hashicorp/python-tfe/pull/106)
25+
26+
## Breaking Change
27+
28+
### List Method Behavior
29+
* Standardized list methods across multiple resources to iterator-based behavior, replacing legacy list response patterns by @NimishaShrivastava-dev, @TanyaSingh369-svg, @isivaselvan [#91](https://github.com/hashicorp/python-tfe/pull/91) [#92](https://github.com/hashicorp/python-tfe/pull/92) [#95](https://github.com/hashicorp/python-tfe/pull/95) [#97](https://github.com/hashicorp/python-tfe/pull/97) [#101](https://github.com/hashicorp/python-tfe/pull/101) [#109](https://github.com/hashicorp/python-tfe/pull/109) [#113](https://github.com/hashicorp/python-tfe/pull/113)
30+
31+
## Bug Fixes
32+
* Fixed pagination parameter handling across iterator-based page traversal by @isivaselvan [#111](https://github.com/hashicorp/python-tfe/pull/111)
33+
* Fixed state version and state version output model import/export registration by @isivaselvan [#105](https://github.com/hashicorp/python-tfe/pull/105)
34+
* Fixed the tag based filtering of workspace in list operation by @isivaselvan [#106](https://github.com/hashicorp/python-tfe/pull/106)
35+
* Fixed the project response of workspace relationship by @isivaselvan [#106](https://github.com/hashicorp/python-tfe/pull/106)
36+
* Fixed configuration version examples and added terraform+cloud support for ConfigurationSource usage by @isivaselvan [#107](https://github.com/hashicorp/python-tfe/pull/107)
37+
* Fixed configuration upload packaging flow (tarfile-based handling) by @isivaselvan [#107](https://github.com/hashicorp/python-tfe/pull/107)
38+
* Updated agent pool workspace assign/remove operations to consistently return AgentPool objects by @KshitijaChoudhari [#110](https://github.com/hashicorp/python-tfe/pull/110)
39+
* Updated Run relationships handling for improved model consistency by @ibm-richard [#119](https://github.com/hashicorp/python-tfe/pull/119)
40+
* Updated additional Run Source attributes by @isivaselvan [#123](https://github.com/hashicorp/python-tfe/pull/123)
41+
342
# v0.1.2
443

544
## Features

bin/publish-pypi.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#!/usr/bin/env bash
2+
# Copyright IBM Corp. 2025, 2026
3+
# SPDX-License-Identifier: MPL-2.0
4+
25
set -euo pipefail
36

47

examples/agent.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Copyright IBM Corp. 2025, 2026
2+
# SPDX-License-Identifier: MPL-2.0
3+
14
"""Simple Individual Agent operations example with the TFE Python SDK.
25
36
This example demonstrates:

examples/agent_pool.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1+
# Copyright IBM Corp. 2025, 2026
2+
# SPDX-License-Identifier: MPL-2.0
3+
14
"""Simple Agent Pool operations example with the TFE Python SDK.
25
36
This example demonstrates:
47
1. Agent Pool CRUD operations (Create, Read, Update, Delete)
58
2. Agent token creation and management
6-
3. Using the organization SDK client
9+
3. Workspace assignment using assign_to_workspaces and remove_from_workspaces
710
4. Proper error handling
811
912
Make sure to set the following environment variables:
1013
- TFE_TOKEN: Your Terraform Cloud/Enterprise API token
1114
- TFE_ADDRESS: Your Terraform Cloud/Enterprise URL (optional, defaults to https://app.terraform.io)
1215
- TFE_ORG: Your organization name
16+
- TFE_WORKSPACE_ID: A workspace ID for testing workspace assignment (optional)
1317
1418
Usage:
1519
export TFE_TOKEN="your-token-here"
@@ -24,8 +28,10 @@
2428
from pytfe.errors import NotFound
2529
from pytfe.models import (
2630
AgentPoolAllowedWorkspacePolicy,
31+
AgentPoolAssignToWorkspacesOptions,
2732
AgentPoolCreateOptions,
2833
AgentPoolListOptions,
34+
AgentPoolRemoveFromWorkspacesOptions,
2935
AgentPoolUpdateOptions,
3036
AgentTokenCreateOptions,
3137
)
@@ -37,6 +43,9 @@ def main():
3743
token = os.environ.get("TFE_TOKEN")
3844
org = os.environ.get("TFE_ORG")
3945
address = os.environ.get("TFE_ADDRESS", "https://app.terraform.io")
46+
workspace_id = os.environ.get(
47+
"TFE_WORKSPACE_ID"
48+
) # optional, for workspace assignment
4049

4150
if not token:
4251
print("TFE_TOKEN environment variable is required")
@@ -96,7 +105,27 @@ def main():
96105
updated_pool = client.agent_pools.update(new_pool.id, update_options)
97106
print(f"Updated agent pool name to: {updated_pool.name}")
98107

99-
# Example 5: Create an agent token
108+
# Example 5: Workspace assignment
109+
# assign_to_workspaces sends PATCH /agent-pools/:id with relationships.allowed-workspaces
110+
# remove_from_workspaces sends PATCH /agent-pools/:id with relationships.excluded-workspaces
111+
if workspace_id:
112+
print("\n Assigning workspace to agent pool...")
113+
updated_pool = client.agent_pools.assign_to_workspaces(
114+
new_pool.id,
115+
AgentPoolAssignToWorkspacesOptions(workspace_ids=[workspace_id]),
116+
)
117+
print(f" Assigned workspace {workspace_id} to pool {updated_pool.name}")
118+
119+
print("\n Removing workspace from agent pool...")
120+
updated_pool = client.agent_pools.remove_from_workspaces(
121+
new_pool.id,
122+
AgentPoolRemoveFromWorkspacesOptions(workspace_ids=[workspace_id]),
123+
)
124+
print(f" Removed workspace {workspace_id} from pool {updated_pool.name}")
125+
else:
126+
print("\n Skipping workspace assignment (set TFE_WORKSPACE_ID to test)")
127+
128+
# Example 6: Create an agent token
100129
print("\n Creating agent token...")
101130
token_options = AgentTokenCreateOptions(
102131
description="SDK example token" # Optional description
@@ -107,7 +136,7 @@ def main():
107136
if agent_token.token:
108137
print(f" Token (first 10 chars): {agent_token.token[:10]}...")
109138

110-
# Example 6: List agent tokens
139+
# Example 7: List agent tokens
111140
print("\n Listing agent tokens...")
112141
tokens = client.agent_tokens.list(new_pool.id)
113142

@@ -117,7 +146,7 @@ def main():
117146
for token in token_list:
118147
print(f" - {token.description or 'No description'} (ID: {token.id})")
119148

120-
# Example 7: Clean up - delete the token and pool
149+
# Example 8: Clean up - delete the token and pool
121150
print("\n Cleaning up...")
122151
client.agent_tokens.delete(agent_token.id)
123152
print("Deleted agent token")

examples/apply.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Copyright IBM Corp. 2025, 2026
2+
# SPDX-License-Identifier: MPL-2.0
3+
14
from __future__ import annotations
25

36
import argparse

examples/configuration_version.py

Lines changed: 17 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#!/usr/bin/env python3
2+
# Copyright IBM Corp. 2025, 2026
3+
# SPDX-License-Identifier: MPL-2.0
4+
25
"""
36
Complete Configuration Version Testing Suite
47
@@ -277,29 +280,8 @@ def main():
277280
print(f" - {filename} ({size} bytes)")
278281

279282
try:
280-
# Create tar.gz archive manually since go-slug isn't available
281-
print("Creating tar.gz archive manually...")
282-
283-
import tarfile
284-
285-
# Create tar.gz archive in memory
286-
archive_buffer = io.BytesIO()
287-
with tarfile.open(fileobj=archive_buffer, mode="w:gz") as tar:
288-
# Add all files from the temp directory
289-
for filename in files:
290-
filepath = os.path.join(temp_dir, filename)
291-
tar.add(filepath, arcname=filename)
292-
293-
archive_buffer.seek(0)
294-
archive_bytes = archive_buffer.getvalue()
295-
print(f"Created archive: {len(archive_bytes)} bytes")
296-
297-
# Use the SDK's upload_tar_gzip method instead of direct HTTP calls
298-
print("Uploading archive using SDK method...")
299-
archive_buffer.seek(0) # Reset buffer position
300-
client.configuration_versions.upload_tar_gzip(
301-
new_cv.upload_url, archive_buffer
302-
)
283+
print("Uploading Terraform configuration...")
284+
client.configuration_versions.upload(new_cv.upload_url, temp_dir)
303285
print("Terraform configuration uploaded successfully!")
304286

305287
# Wait and check status
@@ -408,8 +390,6 @@ def main():
408390
# =====================================================
409391
# TEST 4: UPLOAD CONFIGURATION VERSION
410392
# =====================================================
411-
# Test 4: Upload function (requires go-slug)
412-
# =====================================================
413393
print("\n4. Testing upload() function:")
414394
try:
415395
# Create a fresh configuration version specifically for upload testing
@@ -441,33 +421,19 @@ def main():
441421
print(f"\n Uploading configuration to CV: {fresh_cv.id}")
442422
print(f"Upload URL: {upload_url[:60]}...")
443423

444-
try:
445-
client.configuration_versions.upload(upload_url, temp_dir)
446-
print("Configuration uploaded successfully!")
424+
client.configuration_versions.upload(upload_url, temp_dir)
425+
print("Configuration uploaded successfully!")
447426

448-
# Check status after upload
449-
print("\n Checking status after upload:")
450-
time.sleep(3) # Give TFE time to process
451-
updated_cv = client.configuration_versions.read(fresh_cv.id)
452-
print(f"Status after upload: {updated_cv.status}")
427+
# Check status after upload
428+
print("\n Checking status after upload:")
429+
time.sleep(3) # Give TFE time to process
430+
updated_cv = client.configuration_versions.read(fresh_cv.id)
431+
print(f"Status after upload: {updated_cv.status}")
453432

454-
if updated_cv.status.value != "pending":
455-
print("Status changed (upload processed)")
456-
else:
457-
print("Status still pending (may need more time)")
458-
459-
except ImportError as e:
460-
if "go-slug" in str(e):
461-
print("go-slug package not available")
462-
print("Install with: pip install go-slug")
463-
print(
464-
"Upload function exists but requires go-slug for packaging"
465-
)
466-
print(
467-
"Function correctly raises ImportError when go-slug unavailable"
468-
)
469-
else:
470-
raise
433+
if updated_cv.status.value != "pending":
434+
print("Status changed (upload processed)")
435+
else:
436+
print("Status still pending (may need more time)")
471437

472438
except Exception as e:
473439
print(f"Error: {e}")
@@ -868,7 +834,7 @@ def main():
868834
"TEST 2: create() - Create new configuration versions with different options"
869835
)
870836
print("TEST 3: read() - Read configuration version details and validate fields")
871-
print("TEST 4: upload() - Upload Terraform configurations (requires go-slug)")
837+
print("TEST 4: upload() - Upload Terraform configurations (stdlib tarfile)")
872838
print("TEST 5: download() - Download configuration version archives")
873839
print("TEST 6: archive() - Archive configuration versions")
874840
print("TEST 7: read_with_options() - Read with include options")

examples/notification_configuration.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#!/usr/bin/env python3
2+
# Copyright IBM Corp. 2025, 2026
3+
# SPDX-License-Identifier: MPL-2.0
4+
25
"""
36
Example usage of Notification Configuration API
47
@@ -32,9 +35,15 @@ def main():
3235

3336
print("=== Python TFE Notification Configuration Example ===\n")
3437

35-
# Resolve workspace and team from environment (fallback to demo placeholders)
36-
workspace_id = os.getenv("TFE_WORKSPACE_ID", "ws-example123456789")
37-
workspace_name = os.getenv("TFE_ORG", "your-workspace-name")
38+
# Resolve organization and workspace from environment variables
39+
org_name = os.environ["TFE_ORG"]
40+
workspace_name = os.getenv("TFE_WORKSPACE_NAME", "test-api")
41+
workspace_id = os.getenv("TFE_WORKSPACE_ID", "")
42+
if not workspace_id:
43+
print(f"Looking up workspace '{workspace_name}' in org '{org_name}'...")
44+
ws = client.workspaces.read(workspace_name, organization=org_name)
45+
workspace_id = ws.id
46+
print(f"Resolved workspace ID: {workspace_id}")
3847
print(f"Using workspace: {workspace_name} (ID: {workspace_id})")
3948

4049
team_id = os.getenv("TFE_TEAM_ID", "team-example123456789")
@@ -47,13 +56,13 @@ def main():
4756
# ===== List notification configurations for workspace =====
4857
print("1. Listing notification configurations for workspace...")
4958
try:
50-
workspace_notifications = client.notification_configurations.list(
51-
subscribable_id=workspace_id
59+
workspace_notifications_list = list(
60+
client.notification_configurations.list(subscribable_id=workspace_id)
5261
)
5362
print(
54-
f"Found {len(workspace_notifications.items)} notification configurations"
63+
f"Found {len(workspace_notifications_list)} notification configurations"
5564
)
56-
for nc in workspace_notifications.items:
65+
for nc in workspace_notifications_list:
5766
print(f"- {nc.name} (ID: {nc.id}, Enabled: {nc.enabled})")
5867
except Exception as e:
5968
print(f"Error listing workspace notifications: {e}")
@@ -69,13 +78,15 @@ def main():
6978
options = NotificationConfigurationListOptions(
7079
subscribable_choice=team_choice
7180
)
72-
team_notifications = client.notification_configurations.list(
73-
subscribable_id=team_id, options=options
81+
team_notifications_list = list(
82+
client.notification_configurations.list(
83+
subscribable_id=team_id, options=options
84+
)
7485
)
7586
print(
76-
f"Found {len(team_notifications.items)} team notification configurations"
87+
f"Found {len(team_notifications_list)} team notification configurations"
7788
)
78-
for nc in team_notifications.items:
89+
for nc in team_notifications_list:
7990
print(f"- {nc.name} (ID: {nc.id}, Enabled: {nc.enabled})")
8091
except Exception as e:
8192
error_msg = str(e).lower()
@@ -93,16 +104,20 @@ def main():
93104
workspace_choice = NotificationConfigurationSubscribableChoice(
94105
workspace={"id": workspace_id}
95106
)
96-
slack_url = os.getenv(
107+
# Use GENERIC destination type with a URL that returns HTTP 200.
108+
# SLACK/MICROSOFT_TEAMS destinations are auto-verified by HCP Terraform
109+
# at creation time; a fake Slack URL returns 302 and causes the create
110+
# call to fail immediately. GENERIC webhooks + httpbin always succeed.
111+
webhook_url = os.getenv(
97112
"WEBHOOK_URL",
98-
"https://hooks.slack.com/services/YOUR_SLACK_WORKSPACE/YOUR_CHANNEL/YOUR_WEBHOOK_TOKEN",
113+
"https://httpbin.org/status/200",
99114
)
100115
create_options = NotificationConfigurationCreateOptions(
101-
destination_type=NotificationDestinationType.SLACK,
116+
destination_type=NotificationDestinationType.GENERIC,
102117
enabled=True,
103-
name="Python TFE Example Slack Notification",
118+
name="Python TFE Example Generic Notification",
104119
subscribable_choice=workspace_choice,
105-
url=slack_url,
120+
url=webhook_url,
106121
triggers=[
107122
NotificationTriggerType.COMPLETED,
108123
NotificationTriggerType.ERRORED,
@@ -175,12 +190,17 @@ def main():
175190

176191
except Exception as e:
177192
error_msg = str(e).lower()
178-
if "verification failed" in error_msg and "404" in error_msg:
179-
print(" Webhook verification failed (expected with fake URL)")
180-
print("The fake Slack URL returns 404 - this is normal for testing")
181-
print("To test real verification, use a webhook from:")
182-
print("webhook.site (instant test URL)")
183-
print("Slack, Teams, or Discord webhook")
193+
if "verification failed" in error_msg and (
194+
"404" in error_msg or "302" in error_msg
195+
):
196+
print("Webhook verification failed (expected with fake URL)")
197+
print(
198+
"The URL returned a non-200 response - this is normal for testing"
199+
)
200+
print("To test real verification, use a webhook from webhook.site,")
201+
print(
202+
"Slack, Teams, or Discord, or set WEBHOOK_URL=https://httpbin.org/status/200"
203+
)
184204
else:
185205
print(f" Error in workspace notification operations: {e}")
186206

examples/oauth_client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#!/usr/bin/env python3
2+
# Copyright IBM Corp. 2025, 2026
3+
# SPDX-License-Identifier: MPL-2.0
4+
25
"""
36
Complete OAuth Client Testing Suite
47

0 commit comments

Comments
 (0)