Skip to content
Merged
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
13 changes: 10 additions & 3 deletions examples/agent_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from pytfe import TFEClient, TFEConfig
from pytfe.errors import NotFound
from pytfe.models import (
AgentPoolAllowedWorkspacePolicy,
AgentPoolAssignToProjectsOptions,
AgentPoolAssignToWorkspacesOptions,
AgentPoolCreateOptions,
AgentPoolListOptions,
Expand All @@ -46,6 +46,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 @@ -81,7 +82,6 @@ def main():
create_options = AgentPoolCreateOptions(
name=unique_name,
organization_scoped=True, # Optional parameter
allowed_workspace_policy=AgentPoolAllowedWorkspacePolicy.ALL_WORKSPACES, # Optional
)

new_pool = client.agent_pools.create(org, create_options)
Expand All @@ -92,7 +92,6 @@ def main():
pool_details = client.agent_pools.read(new_pool.id)
print(f"Name: {pool_details.name}")
print(f"Organization Scoped: {pool_details.organization_scoped}")
print(f"Policy: {pool_details.allowed_workspace_policy}")
print(f"Agent Count: {pool_details.agent_count}")

# Example 4: Update the agent pool
Expand Down Expand Up @@ -125,6 +124,14 @@ def main():
else:
print("\n Skipping workspace assignment (set TFE_WORKSPACE_ID to test)")

if project_id:
print("\n Assigning project to agent pool...")
updated_pool = client.agent_pools.assign_to_projects(
new_pool.id,
AgentPoolAssignToProjectsOptions(project_ids=[project_id]),
)
print(f" Assigned project {project_id} to pool {updated_pool.name}")

# Example 6: Create an agent token
print("\n Creating agent token...")
token_options = AgentTokenCreateOptions(
Expand Down
15 changes: 15 additions & 0 deletions src/pytfe/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,3 +702,18 @@ class InvalidTokenIDError(InvalidValues):

def __init__(self, message: str = "invalid value for token ID"):
super().__init__(message)


# Agent Pool errors
class InvalidAgentPoolIDError(InvalidValues):
"""Raised when an invalid agent pool ID is provided."""

def __init__(self, message: str = "invalid value for agent pool ID"):
super().__init__(message)


class RequiredProjectError(RequiredFieldMissing):
"""Raised when a required project field is missing."""

def __init__(self, message: str = "project is required"):
super().__init__(message)
20 changes: 18 additions & 2 deletions src/pytfe/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Agent,
AgentListOptions,
AgentPool,
AgentPoolAllowedWorkspacePolicy,
AgentPoolAssignToProjectsOptions,
AgentPoolAssignToWorkspacesOptions,
AgentPoolCreateOptions,
AgentPoolListOptions,
Expand Down Expand Up @@ -509,8 +509,8 @@
# Agent & pools
"Agent",
"AgentPool",
"AgentPoolAllowedWorkspacePolicy",
"AgentPoolAssignToWorkspacesOptions",
"AgentPoolAssignToProjectsOptions",
"AgentPoolCreateOptions",
"AgentPoolListOptions",
"AgentPoolReadOptions",
Expand Down Expand Up @@ -867,3 +867,19 @@
"TaskStage": TaskStage,
},
)
AgentPool.model_rebuild(
raise_errors=False,
_types_namespace={"Project": Project},
)
Project.model_rebuild(
raise_errors=False,
_types_namespace={"AgentPool": AgentPool},
)
RunTask.model_rebuild(
raise_errors=False,
_types_namespace={"WorkspaceRunTask": WorkspaceRunTask, "AgentPool": AgentPool},
)
Workspace.model_rebuild(
raise_errors=False,
_types_namespace={"AgentPool": AgentPool, "Run": Run, "TaskStage": TaskStage},
)
139 changes: 93 additions & 46 deletions src/pytfe/models/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,20 @@

from datetime import datetime
from enum import Enum
from typing import Any
from typing import TYPE_CHECKING

from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field, model_validator

from ..errors import (
InvalidNameError,
RequiredNameError,
)
from ..utils import valid_string, valid_string_id
from .organization import Organization
from .workspace import Workspace

if TYPE_CHECKING:
from .project import Project


class AgentStatus(str, Enum):
Expand All @@ -24,13 +35,6 @@ class AgentStatus(str, Enum):
UNKNOWN = "unknown"


class AgentPoolAllowedWorkspacePolicy(str, Enum):
"""Agent pool allowed workspace policy enumeration."""

ALL_WORKSPACES = "all-workspaces"
SPECIFIC_WORKSPACES = "specific-workspaces"


class Agent(BaseModel):
"""Agent represents a Terraform Enterprise agent."""

Expand All @@ -48,72 +52,112 @@ class Agent(BaseModel):
class AgentPool(BaseModel):
"""Agent Pool represents a Terraform Enterprise agent pool."""

model_config = ConfigDict(populate_by_name=True, validate_by_name=True)

id: str
name: str | None = None
created_at: datetime | None = None
organization_scoped: bool | None = None
allowed_workspace_policy: AgentPoolAllowedWorkspacePolicy | None = None
agent_count: int = 0
name: str | None = Field(default=None, alias="name")
created_at: datetime | None = Field(default=None, alias="created-at")
organization_scoped: bool | None = Field(default=None, alias="organization-scoped")
agent_count: int | None = Field(default=None, alias="agent-count")

# Relations
organization: Any | None = None # Organization type from main types
workspaces: list[Any] = Field(default_factory=list) # Workspace types
organization: Organization | None = Field(default=None, alias="organization")
workspaces: list[Workspace] = Field(default_factory=list, alias="workspaces")
agents: list[Agent] = Field(default_factory=list)
allowed_workspaces: list[Workspace] = Field(
default_factory=list, alias="allowed-workspaces"
)
excluded_workspaces: list[Workspace] = Field(
default_factory=list, alias="excluded-workspaces"
)
allowed_projects: list[Project] = Field(
default_factory=list, alias="allowed-projects"
)


# Agent Pool Options
class AgentPoolIncludeOpt(str, Enum):
AGENT_POOL_WORKSPACES = "workspaces"


class AgentPoolListOptions(BaseModel):
"""Options for listing agent pools."""

# Pagination options
page_number: int | None = None
page_size: int | None = None
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)

page_size: int | None = Field(default=None, alias="page[size]")
# Optional: Include related resources
include: list[str] | None = None
# Optional: Filter by allowed workspace policy
allowed_workspace_policy: AgentPoolAllowedWorkspacePolicy | None = None
include: list[AgentPoolIncludeOpt] | None = Field(default=None, alias="include")
query: str | None = Field(default=None, alias="q")
allowed_workspace_name: str | None = Field(
default=None, alias="filter[allowed_workspaces][name]"
)
allowed_project_name: str | None = Field(
default=None, alias="filter[allowed_projects][name]"
)
sort: str | None = Field(default=None, alias="sort")


class AgentPoolCreateOptions(BaseModel):
"""Options for creating an agent pool."""

model_config = ConfigDict(populate_by_name=True, validate_by_name=True)

# Required: A name to identify the agent pool
name: str
# Optional: Whether the agent pool is organization scoped
organization_scoped: bool | None = None
# Optional: Allowed workspace policy
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 workspaces excluded from this pool (sent as relationships.excluded-workspaces)
excluded_workspace_ids: list[str] = Field(default_factory=list)
name: str = Field(alias="name")
organization_scoped: bool | None = Field(default=None, alias="organization-scoped")
allowed_workspace_ids: list[str] | None = Field(
default=None, alias="allowed-workspaces"
)
excluded_workspace_ids: list[str] | None = Field(
default=None, alias="excluded-workspaces"
)
allowed_project_ids: list[str] | None = Field(
default=None, alias="allowed-projects"
)

@model_validator(mode="after")
def valid(self) -> AgentPoolCreateOptions:
"""Validate the options for creating an agent pool."""
if not valid_string(self.name):
raise RequiredNameError()
if not valid_string_id(self.name):
raise InvalidNameError()

return self


class AgentPoolUpdateOptions(BaseModel):
"""Options for updating an agent pool."""

model_config = ConfigDict(populate_by_name=True, validate_by_name=True)

# Optional: A name to identify the agent pool
name: str | None = None
# Optional: Whether the agent pool is organization scoped
organization_scoped: bool | None = None
# Optional: Allowed workspace policy
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 workspace IDs excluded from this pool
excluded_workspace_ids: list[str] = Field(default_factory=list)
name: str | None = Field(default=None, alias="name")
organization_scoped: bool | None = Field(default=None, alias="organization-scoped")
allowed_workspace_ids: list[str] | None = Field(
default=None, alias="allowed-workspaces"
)
excluded_workspace_ids: list[str] | None = Field(
default=None, alias="excluded-workspaces"
)
allowed_project_ids: list[str] | None = Field(
default=None, alias="allowed-projects"
)

@model_validator(mode="after")
def valid(self) -> AgentPoolUpdateOptions:
"""Validate the options for updating an agent pool."""
if self.name is not None and not valid_string_id(self.name):
raise InvalidNameError()

return self


class AgentPoolReadOptions(BaseModel):
"""Options for reading an agent pool."""

# Optional: Include related resources
include: list[str] | None = None


# Agent Pool Workspace Assignment Options
include: list[AgentPoolIncludeOpt] | None = Field(default=None, alias="include")


class AgentPoolAssignToWorkspacesOptions(BaseModel):
Expand All @@ -128,7 +172,10 @@ class AgentPoolRemoveFromWorkspacesOptions(BaseModel):
workspace_ids: list[str] = Field(default_factory=list)


# Agent Options
class AgentPoolAssignToProjectsOptions(BaseModel):
"""Options for assigning an agent pool to projects."""

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


class AgentListOptions(BaseModel):
Expand Down
6 changes: 5 additions & 1 deletion src/pytfe/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@

from __future__ import annotations

from typing import TYPE_CHECKING

from pydantic import BaseModel, ConfigDict, Field

from .agent import AgentPool
from .common import TagBinding
from .organization import Organization

if TYPE_CHECKING:
from .agent import AgentPool


class Project(BaseModel):
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
Expand Down
3 changes: 2 additions & 1 deletion src/pytfe/models/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
UnsupportedOperationsError,
)
from ..utils import has_tags_regex_defined, is_valid_workspace_name, valid_string
from .agent import AgentPool
from .common import EffectiveTagBinding, Tag, TagBinding
from .configuration_version import ConfigurationVersion
from .data_retention_policy import DataRetentionPolicyChoice
Expand All @@ -32,6 +31,7 @@
from .variable import Variable

if TYPE_CHECKING:
from .agent import AgentPool
from .run import Run


Expand Down Expand Up @@ -525,6 +525,7 @@ class VCSRepoOptions(BaseModel):
def _rebuild_workspace_model() -> None:
"""Rebuild Workspace model to resolve forward references."""
try:
from .agent import AgentPool # noqa: F401
from .run import Run # noqa: F401
from .task_stage import TaskStage # noqa: F401

Expand Down
2 changes: 1 addition & 1 deletion src/pytfe/models/workspace_run_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@ class WorkspaceRunTaskUpdateOptions(BaseModel):

# WorkspaceRunTask is now fully defined; rebuild RunTask so Pydantic can
# resolve the forward reference in RunTask.workspace_run_tasks.
RunTask.model_rebuild()
RunTask.model_rebuild(raise_errors=False)
Loading
Loading