Skip to content

Commit 81eeb0d

Browse files
committed
Features providing Policy Evaluation, Policy Set Version and Policy Set Outcome API Specs
1 parent 416139a commit 81eeb0d

10 files changed

Lines changed: 596 additions & 0 deletions

examples/policy_evaluation.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import os
5+
6+
from pytfe import TFEClient, TFEConfig
7+
from pytfe.models import PolicyEvaluationListOptions
8+
9+
10+
def _print_header(title: str):
11+
print("\n" + "=" * 80)
12+
print(title)
13+
print("=" * 80)
14+
15+
16+
def main():
17+
parser = argparse.ArgumentParser(
18+
description="Policy Evaluations demo for python-tfe SDK"
19+
)
20+
parser.add_argument(
21+
"--address", default=os.getenv("TFE_ADDRESS", "https://app.terraform.io")
22+
)
23+
parser.add_argument("--token", default=os.getenv("TFE_TOKEN", ""))
24+
parser.add_argument(
25+
"--task-stage-id",
26+
required=True,
27+
help="Task stage ID to list policy evaluations for",
28+
)
29+
parser.add_argument("--page", type=int, default=1)
30+
parser.add_argument("--page-size", type=int, default=20)
31+
args = parser.parse_args()
32+
33+
if not args.token:
34+
print("Error: TFE_TOKEN environment variable or --token argument is required")
35+
return
36+
37+
cfg = TFEConfig(address=args.address, token=args.token)
38+
client = TFEClient(cfg)
39+
40+
# List all policy evaluations for the given task stage
41+
_print_header(f"Listing policy evaluations for task stage: {args.task_stage_id}")
42+
43+
options = PolicyEvaluationListOptions(
44+
page_number=args.page,
45+
page_size=args.page_size,
46+
)
47+
48+
try:
49+
pe_list = client.policy_evaluations.list(args.task_stage_id, options)
50+
51+
print(f"Total policy evaluations: {pe_list.total_count}")
52+
print(f"Page {pe_list.current_page} of {pe_list.total_pages}")
53+
print()
54+
55+
if not pe_list.items:
56+
print("No policy evaluations found for this task stage.")
57+
else:
58+
for pe in pe_list.items:
59+
print(f"- ID: {pe.id}")
60+
print(f" Status: {pe.status}")
61+
print(f" Policy Kind: {pe.policy_kind}")
62+
63+
if pe.result_count:
64+
print(" Result Count:")
65+
if pe.result_count.passed is not None:
66+
print(f" - Passed: {pe.result_count.passed}")
67+
if pe.result_count.advisory_failed is not None:
68+
print(
69+
f" - Advisory Failed: {pe.result_count.advisory_failed}"
70+
)
71+
if pe.result_count.mandatory_failed is not None:
72+
print(
73+
f" - Mandatory Failed: {pe.result_count.mandatory_failed}"
74+
)
75+
if pe.result_count.errored is not None:
76+
print(f" - Errored: {pe.result_count.errored}")
77+
78+
if pe.status_timestamp:
79+
print(" Status Timestamps:")
80+
if pe.status_timestamp.passed_at:
81+
print(f" - Passed At: {pe.status_timestamp.passed_at}")
82+
if pe.status_timestamp.failed_at:
83+
print(f" - Failed At: {pe.status_timestamp.failed_at}")
84+
if pe.status_timestamp.running_at:
85+
print(f" - Running At: {pe.status_timestamp.running_at}")
86+
if pe.status_timestamp.canceled_at:
87+
print(f" - Canceled At: {pe.status_timestamp.canceled_at}")
88+
if pe.status_timestamp.errored_at:
89+
print(f" - Errored At: {pe.status_timestamp.errored_at}")
90+
91+
if pe.task_stage:
92+
print(f" Task Stage: {pe.task_stage.id} ({pe.task_stage.type})")
93+
94+
if pe.created_at:
95+
print(f" Created At: {pe.created_at}")
96+
if pe.updated_at:
97+
print(f" Updated At: {pe.updated_at}")
98+
99+
print()
100+
101+
except Exception as e:
102+
print(f"Error listing policy evaluations: {e}")
103+
return
104+
105+
106+
if __name__ == "__main__":
107+
main()

src/pytfe/client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
from .resources.plan import Plans
1313
from .resources.policy import Policies
1414
from .resources.policy_check import PolicyChecks
15+
from .resources.policy_evaluation import PolicyEvaluations
1516
from .resources.policy_set import PolicySets
17+
from .resources.policy_set_outcome import PolicySets as PolicySetOutcomes
18+
from .resources.policy_set_version import PolicySetVersions
1619
from .resources.projects import Projects
1720
from .resources.query_run import QueryRuns
1821
from .resources.registry_module import RegistryModules
@@ -76,8 +79,11 @@ def __init__(self, config: TFEConfig | None = None):
7679
self.query_runs = QueryRuns(self._transport)
7780
self.run_events = RunEvents(self._transport)
7881
self.policies = Policies(self._transport)
82+
self.policy_evaluations = PolicyEvaluations(self._transport)
7983
self.policy_checks = PolicyChecks(self._transport)
8084
self.policy_sets = PolicySets(self._transport)
85+
self.policy_set_outcomes = PolicySetOutcomes(self._transport)
86+
self.policy_set_versions = PolicySetVersions(self._transport)
8187

8288
# SSH Keys
8389
self.ssh_keys = SSHKeys(self._transport)

src/pytfe/errors.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,18 @@ class InvalidPoliciesError(InvalidValues):
445445

446446
def __init__(self, message: str = "must provide at least one policy"):
447447
super().__init__(message)
448+
449+
450+
# Policy Evaluation errors
451+
class InvalidTaskStageIDError(InvalidValues):
452+
"""Raised when an invalid task stage ID is provided."""
453+
454+
def __init__(self, message: str = "invalid value for task stage ID"):
455+
super().__init__(message)
456+
457+
458+
class InvalidPolicyEvaluationIDError(InvalidValues):
459+
"""Raised when an invalid policy evaluation ID is provided."""
460+
461+
def __init__(self, message: str = "invalid value for policy evaluation ID"):
462+
super().__init__(message)

src/pytfe/models/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@
115115
PolicyStatus,
116116
PolicyStatusTimestamps,
117117
)
118+
from .policy_evaluation import (
119+
PolicyAttachable,
120+
PolicyEvaluation,
121+
PolicyEvaluationList,
122+
PolicyEvaluationListOptions,
123+
PolicyEvaluationStatus,
124+
PolicyEvaluationStatusTimestamps,
125+
PolicyResultCount,
126+
)
118127
from .policy_set import (
119128
PolicySet,
120129
PolicySetAddPoliciesOptions,
@@ -526,6 +535,14 @@
526535
"PolicyStatusTimestamps",
527536
"PolicyCheckListOptions",
528537
"PolicyCheckList",
538+
# Policy Evaluation
539+
"PolicyAttachable",
540+
"PolicyEvaluation",
541+
"PolicyEvaluationList",
542+
"PolicyEvaluationListOptions",
543+
"PolicyEvaluationStatus",
544+
"PolicyEvaluationStatusTimestamps",
545+
"PolicyResultCount",
529546
# Policy
530547
"Policy",
531548
"PolicyCreateOptions",
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime
4+
from enum import Enum
5+
6+
from pydantic import BaseModel, ConfigDict, Field
7+
8+
from .policy_types import PolicyKind
9+
10+
11+
class PolicyEvaluationStatus(str, Enum):
12+
"""PolicyEvaluationStatus is an enum that represents all possible statuses for a policy evaluation"""
13+
14+
POLICYEVALUATIONPASSED = "passed"
15+
POLICYEVALUATIONFAILED = "failed"
16+
POLICYEVALUATIONPENDING = "pending"
17+
POLICYEVALUATIONRUNNING = "running"
18+
POLICYEVALUATIONCANCELED = "canceled"
19+
POLICYEVALUATIONERRORED = "errored"
20+
POLICYEVALUATIONUNREACHABLE = "unreachable"
21+
POLICYEVALUATIONOVERRIDDEN = "overridden"
22+
23+
24+
class PolicyEvaluation(BaseModel):
25+
"""PolicyEvaluation represents the policy evaluations that are part of the task stage."""
26+
27+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
28+
29+
id: str
30+
status: PolicyEvaluationStatus | None = Field(None, alias="status")
31+
policy_kind: PolicyKind | None = Field(None, alias="policy-kind")
32+
status_timestamp: PolicyEvaluationStatusTimestamps | None = Field(
33+
None, alias="status-timestamp"
34+
)
35+
result_count: PolicyResultCount | None = Field(None, alias="result-count")
36+
created_at: datetime | None = Field(None, alias="created-at")
37+
updated_at: datetime | None = Field(None, alias="updated-at")
38+
39+
# The task stage the policy evaluation belongs to
40+
task_stage: PolicyAttachable | None = Field(None, alias="policy-attachable")
41+
42+
43+
class PolicyEvaluationStatusTimestamps(BaseModel):
44+
"""PolicyEvaluationStatusTimestamps represents the set of timestamps recorded for a policy evaluation"""
45+
46+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
47+
48+
passed_at: datetime | None = Field(None, alias="passed-at")
49+
failed_at: datetime | None = Field(None, alias="failed-at")
50+
running_at: datetime | None = Field(None, alias="running-at")
51+
canceled_at: datetime | None = Field(None, alias="canceled-at")
52+
errored_at: datetime | None = Field(None, alias="errored-at")
53+
54+
55+
class PolicyAttachable(BaseModel):
56+
"""The task stage the policy evaluation belongs to"""
57+
58+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
59+
60+
id: str
61+
type: str | None = Field(None, alias="type")
62+
63+
64+
class PolicyResultCount(BaseModel):
65+
"""PolicyResultCount represents the count of the policy results"""
66+
67+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
68+
69+
advisory_failed: int | None = Field(None, alias="advisory-failed")
70+
mandatory_failed: int | None = Field(None, alias="mandatory-failed")
71+
passed: int | None = Field(None, alias="passed")
72+
errored: int | None = Field(None, alias="errored")
73+
74+
75+
class PolicyEvaluationList(BaseModel):
76+
"""PolicyEvaluationList represents a list of policy evaluations"""
77+
78+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
79+
80+
items: list[PolicyEvaluation] | None = Field(default_factory=list)
81+
current_page: int | None = None
82+
next_page: str | None = None
83+
prev_page: str | None = None
84+
total_count: int | None = None
85+
total_pages: int | None = None
86+
87+
88+
class PolicyEvaluationListOptions(BaseModel):
89+
"""PolicyEvaluationListOptions represents the options for listing policy evaluations"""
90+
91+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
92+
93+
page_number: int | None = Field(None, alias="page[number]")
94+
page_size: int | None = Field(None, alias="page[size]")
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import annotations
2+
3+
from pydantic import BaseModel, ConfigDict, Field
4+
5+
from .policy_evaluation import PolicyEvaluation, PolicyResultCount
6+
7+
8+
class PolicySetOutcome(BaseModel):
9+
"""PolicySetOutcome represents outcome of the policy set that are part of the policy evaluation"""
10+
11+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
12+
13+
id: str
14+
outcomes: list[Outcome] = Field(default_factory=list, alias="outcomes")
15+
error: str | None = Field(None, alias="error")
16+
overridable: bool | None = Field(None, alias="overridable")
17+
policy_set_name: str | None = Field(None, alias="policy-set-name")
18+
policy_set_description: str | None = Field(None, alias="policy-set-description")
19+
result_count: PolicyResultCount | None = Field(None, alias="result-count")
20+
21+
# The policy evaluation that this outcome belongs to
22+
policy_evaluation: PolicyEvaluation | None = Field(None, alias="policy-evaluation")
23+
24+
25+
class Outcome(BaseModel):
26+
"""Outcome represents the outcome of the individual policy"""
27+
28+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
29+
30+
enforcement_level: str | None = Field(None, alias="enforcement-level")
31+
query: str | None = Field(None, alias="query")
32+
status: str | None = Field(None, alias="status")
33+
policy_name: str | None = Field(None, alias="policy-name")
34+
description: str | None = Field(None, alias="description")
35+
36+
37+
class PolicySetOutcomeList(BaseModel):
38+
"""PolicySetOutcomeList represents a list of policy set outcomes"""
39+
40+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
41+
42+
items: list[PolicySetOutcome] | None = Field(default_factory=list)
43+
current_page: int | None = None
44+
next_page: str | None = None
45+
prev_page: str | None = None
46+
total_count: int | None = None
47+
total_pages: int | None = None
48+
49+
50+
class PolicySetOutcomeListFilter(BaseModel):
51+
"""PolicySetOutcomeListFilter represents the filters that are supported while listing a policy set outcome"""
52+
53+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
54+
55+
status: str | None = Field(None, alias="status")
56+
enforcement_level: str | None = Field(None, alias="enforcement-level")
57+
58+
59+
class PolicySetOutcomeListOptions(BaseModel):
60+
"""PolicySetOutcomeListOptions represents the options for listing policy set outcomes."""
61+
62+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
63+
64+
filter: dict[str, PolicySetOutcomeListFilter] | None = None
65+
page_number: int | None = Field(None, alias="page[number]")
66+
page_size: int | None = Field(None, alias="page[size]")

0 commit comments

Comments
 (0)