Skip to content

Commit 364a065

Browse files
authored
Merge pull request #51 from Portkey-AI/feat/workspace-default-guardrails
feat(workspace): add portkey_workspace_defaults resource
2 parents a13a3d6 + c252744 commit 364a065

13 files changed

Lines changed: 927 additions & 8 deletions

File tree

docs/RESOURCE_MATRIX.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@
66

77
| Category | Resources | Data Sources | Test Status |
88
|----------|:---------:|:------------:|-------------|
9-
| Organization | 4 | 4 | ⚠️ Workspace delete blocked |
9+
| Organization | 5 | 4 | ⚠️ Workspace delete blocked |
1010
| AI Gateway | 6 | 12 | ✅ All passing |
1111
| Governance | 3 | 6 | ✅ All passing |
1212
| Access Control | 1 | 2 | ✅ All passing |
1313
| MCP Gateway | 3 | 2 | ✅ All passing (11 tests) |
1414
| Secret Management | 1 | 2 | ✅ Plan-time validation covered |
1515
| Identity / SCIM | 1 | 1 | ✅ Unit + acceptance |
16-
| **Total** | **18** | **29** | **All passing** |
16+
| **Total** | **19** | **29** | **All passing** |
1717

1818
## Provider Resources
1919

2020
| Resource | Create | Read | Update | Delete | Import | API Status | Test Status |
2121
|----------|:------:|:----:|:------:|:------:|:------:|------------|-------------|
2222
| `portkey_workspace` |||| ⚠️ || Delete requires name in body | ⚠️ 10 tests, delete blocked by backend |
23+
| `portkey_workspace_defaults` |||| ⚠️ || Default input/output guardrails live on the workspace; written via PUT `/admin/workspaces/{id}` `defaults`. Create omits unset fields (preserves existing guardrails); Update treats a removed field as a clear. No reset endpoint — Delete clears both lists | ✅ Unit + acc tests |
2324
| `portkey_workspace_member` || ⚠️ |||| getMember API has issues | Skipped |
2425
| `portkey_workspace_security_settings` |||| ⚠️ || API requires full 35-field object on PUT (sparse rejected as 400 AB01); Delete removes state only, no API reset endpoint exists | ✅ 2 acc tests (basic + partial-preserves-others) |
2526
| `portkey_user_invite` |||||| Update API doesn't exist | ✅ Passing |
@@ -101,11 +102,16 @@ None - all primary resources are now implemented!
101102
```
102103
POST /admin/workspaces → Create
103104
GET /admin/workspaces → List
104-
GET /admin/workspaces/{id} → Read (also surfaces `security_settings` bag)
105+
GET /admin/workspaces/{id} → Read (also surfaces `security_settings` bag
106+
and `defaults.input_guardrails` /
107+
`defaults.output_guardrails`)
105108
PUT /admin/workspaces/{id} → Update (also accepts `security_settings`;
106109
partial PUT of that field returns
107110
400 AB01, full 35-field object
108-
required)
111+
required. Also accepts
112+
`defaults.input_guardrails` /
113+
`defaults.output_guardrails` — the
114+
write path for `portkey_workspace_defaults`)
109115
DELETE /admin/workspaces/{id} → Delete (requires {"name": "..."} in body)
110116
```
111117

docs/resources/workspace.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ resource "portkey_workspace" "with_limits" {
6161

6262
To clear limits, simply remove the `usage_limits` or `rate_limits` blocks from your config and re-apply.
6363

64+
To attach default input/output guardrails to a workspace, use the [`portkey_workspace_defaults`](workspace_defaults.md) resource. Guardrails are exposed as a separate resource because the Portkey Admin API requires guardrails to live in the target workspace, which creates an unresolvable Terraform DAG cycle if they were an attribute on `portkey_workspace`.
65+
6466
<!-- schema generated by tfplugindocs -->
6567
## Schema
6668

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
page_title: "portkey_workspace_defaults Resource - portkey"
3+
subcategory: ""
4+
description: |-
5+
Manages default input/output guardrails for a Portkey workspace.
6+
---
7+
8+
# portkey_workspace_defaults (Resource)
9+
10+
Manages default input/output guardrails for a Portkey workspace. Every request routed through the workspace's API keys will pass through the configured guardrails in order.
11+
12+
This resource is separate from `portkey_workspace` because the Portkey Admin API requires guardrails to live in the target workspace they are attached to. Modeling guardrails as an attribute on `portkey_workspace` would create an unresolvable Terraform DAG cycle when the workspace, guardrails, and attachment are all managed in a single config. Exactly one `portkey_workspace_defaults` may exist per workspace.
13+
14+
## Example Usage
15+
16+
```terraform
17+
resource "portkey_workspace" "prod" {
18+
name = "Production"
19+
description = "Prod workspace"
20+
}
21+
22+
resource "portkey_guardrail" "pii" {
23+
name = "pii-check"
24+
workspace_id = portkey_workspace.prod.id
25+
checks = jsonencode([{
26+
id = "default.wordCount"
27+
parameters = {
28+
minWords = 1
29+
maxWords = 4000
30+
}
31+
}])
32+
actions = jsonencode({
33+
onFail = "log"
34+
message = "guardrail triggered"
35+
})
36+
}
37+
38+
resource "portkey_workspace_defaults" "prod" {
39+
workspace_id = portkey_workspace.prod.id
40+
input_guardrails = [portkey_guardrail.pii.slug]
41+
output_guardrails = [portkey_guardrail.pii.slug]
42+
}
43+
```
44+
45+
~> **Note:** Reference guardrails by `slug` for stable plans. The Admin API accepts either guardrail IDs or slugs on write but returns slugs on read (under admin-API-key auth), so state will always contain slugs. Storing `portkey_guardrail.foo.slug` keeps state and HCL in sync; using `portkey_guardrail.foo.id` (a UUID) produces a permanent plan diff.
46+
47+
~> **Note:** Setting either list to `[]` **always** clears all attached guardrails of that kind. Omitting the attribute, however, behaves differently on create versus update (see below).
48+
49+
- **On create** (including the first `apply` when adopting this resource against a workspace that already has guardrails — for example, ones attached through the Portkey UI), an omitted attribute is **not** sent to the API, so any existing guardrails of that kind are **preserved**, not cleared. To clear on create, set the attribute explicitly to `[]`.
50+
- **On update**, removing an attribute you previously managed **clears** that kind (it is treated as an explicit removal). If the attribute was never set, omitting it stays a no-op.
51+
52+
Destroying the resource clears both lists via the workspace update endpoint; there is no separate delete endpoint for workspace defaults.
53+
54+
## Schema
55+
56+
### Required
57+
58+
- `workspace_id` (String) ID or slug of the workspace to configure defaults for.
59+
60+
### Optional
61+
62+
- `input_guardrails` (List of String) Guardrails applied to inbound requests, as a list of guardrail slugs (or IDs — the API accepts both). The API returns slugs on read under admin-API-key auth, so prefer `portkey_guardrail.foo.slug` in HCL to avoid a permanent plan diff. Setting to `[]` clears all input guardrails.
63+
- `output_guardrails` (List of String) Guardrails applied to model responses, as a list of guardrail slugs (or IDs — the API accepts both). The API returns slugs on read under admin-API-key auth, so prefer `portkey_guardrail.foo.slug` in HCL to avoid a permanent plan diff. Setting to `[]` clears all output guardrails.
64+
65+
### Read-Only
66+
67+
- `id` (String) Resource identifier. Equal to `workspace_id`.
68+
69+
## Import
70+
71+
Import by workspace ID or slug:
72+
73+
```
74+
terraform import portkey_workspace_defaults.prod <workspace-id-or-slug>
75+
```

examples/resources/portkey_workspace/resource.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ resource "portkey_workspace" "with_limits" {
2525

2626
# To clear limits, simply remove the usage_limits or rate_limits blocks
2727
# from your config and re-apply.
28+
29+
# Default input/output guardrails for a workspace are configured via the
30+
# separate portkey_workspace_defaults resource. See
31+
# examples/resources/portkey_workspace_defaults/resource.tf.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
resource "portkey_workspace" "prod" {
2+
name = "Production"
3+
description = "Prod workspace"
4+
}
5+
6+
resource "portkey_guardrail" "pii" {
7+
name = "pii-check"
8+
workspace_id = portkey_workspace.prod.id
9+
checks = jsonencode([{
10+
id = "default.wordCount"
11+
parameters = {
12+
minWords = 1
13+
maxWords = 4000
14+
}
15+
}])
16+
actions = jsonencode({
17+
onFail = "log"
18+
message = "guardrail triggered"
19+
})
20+
}
21+
22+
resource "portkey_workspace_defaults" "prod" {
23+
workspace_id = portkey_workspace.prod.id
24+
input_guardrails = [portkey_guardrail.pii.slug]
25+
output_guardrails = [portkey_guardrail.pii.slug]
26+
}
27+
28+
# Setting input_guardrails = [] or output_guardrails = [] ALWAYS clears them on
29+
# the next apply.
30+
#
31+
# Omitting an attribute behaves differently depending on the phase:
32+
# - On create it is not sent to the API, so any guardrails already on the
33+
# workspace (e.g. attached via the Portkey UI) are PRESERVED, not cleared.
34+
# Set the attribute to [] if you want to clear on create.
35+
# - On update, removing an attribute you previously managed clears it.
36+
#
37+
# Destroying this resource clears both lists via the workspace update endpoint.

internal/client/client.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,29 @@ func (c *Client) doRequest(ctx context.Context, method, path string, body interf
196196
}
197197

198198
// WorkspaceDefaults represents the defaults configuration for a workspace
199+
// as returned by GET /admin/workspaces/{id} and accepted by Create.
200+
//
201+
// GET returns guardrails as plain string arrays — slugs under admin-API-key
202+
// auth (see albus getAWorkspace controller), UUIDs for system users. Writes
203+
// (via UpdateWorkspaceDefaults) accept either UUIDs or slugs.
199204
type WorkspaceDefaults struct {
200-
Metadata map[string]string `json:"metadata,omitempty"`
205+
Metadata map[string]string `json:"metadata,omitempty"`
206+
InputGuardrails []string `json:"input_guardrails,omitempty"`
207+
OutputGuardrails []string `json:"output_guardrails,omitempty"`
208+
}
209+
210+
// UpdateWorkspaceDefaults is the write-side of workspace defaults used by
211+
// UpdateWorkspaceRequest. Input/OutputGuardrails use json.RawMessage so
212+
// callers can distinguish three states:
213+
// - nil: field omitted → preserve existing guardrails
214+
// - marshaled `[]`: clears all guardrails
215+
// - marshaled array: replaces guardrails with the given IDs/slugs
216+
//
217+
// Metadata is passed through unchanged.
218+
type UpdateWorkspaceDefaults struct {
219+
Metadata map[string]string `json:"metadata,omitempty"`
220+
InputGuardrails json.RawMessage `json:"input_guardrails,omitempty"`
221+
OutputGuardrails json.RawMessage `json:"output_guardrails,omitempty"`
201222
}
202223

203224
// Workspace represents a Portkey workspace
@@ -287,7 +308,7 @@ type UpdateWorkspaceRequest struct {
287308
Name string `json:"name,omitempty"`
288309
Icon json.RawMessage `json:"icon,omitempty"`
289310
Description string `json:"description,omitempty"`
290-
Defaults *WorkspaceDefaults `json:"defaults,omitempty"`
311+
Defaults *UpdateWorkspaceDefaults `json:"defaults,omitempty"`
291312
RateLimits json.RawMessage `json:"rate_limits,omitempty"`
292313
UsageLimits json.RawMessage `json:"usage_limits,omitempty"`
293314
SecuritySettings *WorkspaceSecuritySettings `json:"security_settings,omitempty"`

0 commit comments

Comments
 (0)