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
116 changes: 116 additions & 0 deletions examples/stack_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Copyright IBM Corp. 2025, 2026
# SPDX-License-Identifier: MPL-2.0

from __future__ import annotations

import argparse
import os

from pytfe import TFEClient, TFEConfig
from pytfe.models import (
StackConfigurationCreateOptions,
StackConfigurationListOptions,
)


def _print_header(title: str):
print("\n" + "=" * 80)
print(title)
print("=" * 80)


def main():
parser = argparse.ArgumentParser(
description="Stack Configurations demo for python-tfe SDK"
)
parser.add_argument(
"--address", default=os.getenv("TFE_ADDRESS", "https://app.terraform.io")
)
parser.add_argument("--token", default=os.getenv("TFE_TOKEN", ""))
parser.add_argument("--stack-id", required=True, help="Stack ID (e.g. st-xxxxx)")
parser.add_argument(
"--page-size",
type=int,
default=100,
help="Page size for listing configurations",
)
parser.add_argument(
"--create", action="store_true", help="Create a new stack configuration"
)
parser.add_argument(
"--speculative",
action="store_true",
help="Mark created configuration as speculative",
)
parser.add_argument(
"--read", action="store_true", help="Read a specific stack configuration"
)
parser.add_argument(
"--upload-url",
action="store_true",
help="Fetch the upload URL for a stack configuration",
)
parser.add_argument(
"--fetch-from-vcs",
action="store_true",
help="Trigger fetch of latest config from VCS",
)
parser.add_argument("--id", help="Stack configuration ID (e.g. stc-xxxxx)")
args = parser.parse_args()

cfg = TFEConfig(address=args.address, token=args.token)
client = TFEClient(cfg)

# 1) Always list existing stack configurations
_print_header(f"Listing stack configurations for stack: {args.stack_id}")
options = StackConfigurationListOptions(page_size=args.page_size)
config_count = 0
for config in client.stack_configurations.list(
stack_id=args.stack_id, options=options
):
config_count += 1
print(f"- ID: {config.id}")
print(f" Status: {config.status.value if config.status else None}")
print(f" Sequence: {config.sequence_number}")
print(f" Speculative: {config.speculative}")
print(f" Created: {config.created_at}")
print(f" Updated: {config.updated_at}")
print()

if config_count == 0:
print("No stack configurations found.")
else:
print(f"Total: {config_count} stack configurations")

# 2) Create a new stack configuration
if args.create:
_print_header("Creating a new stack configuration")
create_opts = StackConfigurationCreateOptions(
speculative_enabled=args.speculative
)
config = client.stack_configurations.create(
stack_id=args.stack_id, options=create_opts
)
print(f"Created stack configuration: {config.id}")
print(f" Status: {config.status.value if config.status else None}")
print(f" Speculative: {config.speculative}")
print(f" Sequence: {config.sequence_number}")
print(f" Created: {config.created_at}")

# 3) Read a specific stack configuration
if args.read:
if not args.id:
print("--id is required for --read")
else:
_print_header(f"Reading stack configuration: {args.id}")
config = client.stack_configurations.read(stack_configuration_id=args.id)
print(f"ID: {config.id}")
print(f"Status: {config.status.value if config.status else None}")
print(f"Sequence: {config.sequence_number}")
print(f"Speculative: {config.speculative}")
print(f"Created: {config.created_at}")
print(f"Updated: {config.updated_at}")


if __name__ == "__main__":
main()
12 changes: 12 additions & 0 deletions src/pytfe/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@
from .resources.run_task import RunTasks
from .resources.run_trigger import RunTriggers
from .resources.ssh_keys import SSHKeys
from .resources.stack import Stacks
from .resources.stack_configuration import StackConfigurations
from .resources.state_version_outputs import StateVersionOutputs
from .resources.state_versions import StateVersions
from .resources.team import Teams
from .resources.team_project_access import TeamProjectAccesses
from .resources.user import Users
from .resources.variable import Variables
from .resources.variable_sets import VariableSets, VariableSetVariables
Expand Down Expand Up @@ -88,6 +92,10 @@ def __init__(self, config: TFEConfig | None = None):
self.registry_provider_versions = RegistryProviderVersions(self._transport)
self.registry_provider_platforms = RegistryProviderPlatforms(self._transport)

# Stack resources
self.stacks = Stacks(self._transport)
self.stack_configurations = StackConfigurations(self._transport)

# State and execution resources
self.state_versions = StateVersions(self._transport)
self.state_version_outputs = StateVersionOutputs(self._transport)
Expand All @@ -107,6 +115,10 @@ def __init__(self, config: TFEConfig | None = None):
# SSH Keys
self.ssh_keys = SSHKeys(self._transport)

# Team project access
self.teams = Teams(self._transport)
self.team_project_accesses = TeamProjectAccesses(self._transport)

# Reserved Tag Key
self.reserved_tag_key = ReservedTagKeys(self._transport)

Expand Down
15 changes: 15 additions & 0 deletions src/pytfe/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,3 +627,18 @@ def __init__(
message: str = "invalid value for registry-name. It must be either private or public",
):
super().__init__(message)


# Stack Configuration errors
class InvalidStackIDError(InvalidValues):
"""Raised when an invalid stack ID is provided."""

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


class InvalidStackConfigurationIDError(InvalidValues):
"""Raised when an invalid stack configuration ID is provided."""

def __init__(self, message: str = "invalid value for stack configuration ID"):
super().__init__(message)
19 changes: 19 additions & 0 deletions src/pytfe/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,16 @@
SSHKeyListOptions,
SSHKeyUpdateOptions,
)
from .stack_configuration import (
StackComponent,
StackConfiguration,
StackConfigurationCreateOptions,
StackConfigurationIncludeOps,
StackConfigurationListOptions,
StackConfigurationReadOptions,
StackConfigurationSource,
StackConfigurationStatus,
)
from .state_version import (
StateVersion,
StateVersionCreateOptions,
Expand Down Expand Up @@ -513,6 +523,15 @@
"RegistryProviderPlatformID",
"RegistryProviderPlatformListOptions",
"RegistryProviderPlatformPermissions",
# Stack Configuration
"StackComponent",
"StackConfiguration",
"StackConfigurationCreateOptions",
"StackConfigurationIncludeOps",
"StackConfigurationListOptions",
"StackConfigurationReadOptions",
"StackConfigurationSource",
"StackConfigurationStatus",
# Query runs
"QueryRun",
"QueryRunActions",
Expand Down
100 changes: 100 additions & 0 deletions src/pytfe/models/stack_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright IBM Corp. 2025, 2026
# SPDX-License-Identifier: MPL-2.0

from __future__ import annotations

from datetime import datetime
from enum import Enum

from pydantic import BaseModel, ConfigDict, Field

from .configuration_version import IngressAttributes
from .stack import Stack


class StackConfigurationStatus(str, Enum):
"""StackConfigurationStatus represents the status of a stack configuration."""

PENDING = "pending"
QUEUED = "queued"
PREPARING = "preparing"
COMPLETED = "completed"
FAILED = "failed"


class StackComponent(BaseModel):
"""StackComponent represents a stack component, specified by configuration"""

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

name: str = Field(alias="name", default="")
correlator: str = Field(alias="correlator", default="")
expanded: bool | None = Field(alias="expanded", default=None)
removed: bool | None = Field(alias="removed", default=None)


class StackConfigurationSource(str, Enum):
"""StackConfigurationSource controls how configuration content is sourced."""

MANUAL = "manual"
FETCH = "fetch"
REUSE = "reuse"


class StackConfigurationIncludeOps(str, Enum):
"""StackConfigurationIncludeOps represents include options for stack configuration endpoints."""

INGRESS_ATTRIBUTES = "ingress_attributes"
STACK_DIAGNOSTICS = "stack_diagnostics"


class StackConfiguration(BaseModel):
"""StackConfiguration represents a snapshot of a stack's configuration."""

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

id: str
status: StackConfigurationStatus | None = Field(default=None, alias="status")
sequence_number: int | None = Field(default=None, alias="sequence-number")
components: list[StackComponent] = Field(default_factory=list, alias="components")
preparing_event_stream_url: str = Field(
default="", alias="preparing-event-stream-url"
)
created_at: datetime | None = Field(default=None, alias="created-at")
updated_at: datetime | None = Field(default=None, alias="updated-at")
speculative: bool | None = Field(default=None, alias="speculative")

# Relations
stack: Stack | None = Field(default=None, alias="stack")
ingress_attributes: IngressAttributes | None = Field(
default=None, alias="ingress-attributes"
)


class StackConfigurationCreateOptions(BaseModel):
"""Options for creating a stack configuration."""

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

speculative_enabled: bool = Field(default=False, alias="speculative")
destroy_all: bool = Field(default=False, alias="destroy-all")
selected_deployments: list[str] | None = Field(
default=None, alias="selected-deployments"
)


class StackConfigurationListOptions(BaseModel):
"""Options for listing stack configurations."""

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

page_size: int | None = Field(default=None, alias="page[size]")
include: list[StackConfigurationIncludeOps] | None = None


class StackConfigurationReadOptions(BaseModel):
"""Options for reading a stack configuration."""

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

include: list[StackConfigurationIncludeOps] | None = None
7 changes: 7 additions & 0 deletions src/pytfe/resources/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ def force_delete(self, stack_id: str) -> None:
)
return None

def fetch_latest_from_vcs(self, stack_id: str) -> Stack:
"""FetchLatestFromVcs updates the configuration of a stack, triggering stack preparation."""
path = f"/api/v2/stacks/{stack_id}/fetch-latest-from-vcs"
r = self.t.request("POST", path=path)
data = r.json().get("data", {})
return self._stack_from(data)

def _stack_from(self, data: dict) -> Stack:
attrs = data.get("attributes", {})
attrs["id"] = data.get("id")
Expand Down
Loading
Loading