Skip to content

Commit 2047f7b

Browse files
Add a new openai_integration resource (#657)
* Add a new openai_integration resource * Fix comments in PR
1 parent ee60a92 commit 2047f7b

11 files changed

Lines changed: 1104 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Changes
2+
body: Added openai_integration resource
3+
time: 2026-04-07T09:43:22.103746+03:00
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
page_title: "dbtcloud_openai_integration Resource - dbtcloud"
3+
subcategory: ""
4+
description: |-
5+
Manages a bring-your-own-key OpenAI integration for a dbt Cloud account, enabling AI-powered features such as dbt Copilot.
6+
Two key types are supported:
7+
openai — your own OpenAI API keyazure_openai — your own Azure OpenAI deployment
8+
Lifecycle note: dbt Cloud defaults to a dbt Labs-managed OpenAI key when no integration record exists. Creating this resource switches the account to a customer-managed key. Destroying it (or removing it from the Terraform config) deletes the record and automatically reverts the account to the dbt Labs-managed key — no additional steps are required.
9+
Secret handling: the API key is write-only and never returned after creation. Use key_value_wo with key_value_wo_version (Terraform 1.11+) to keep the secret out of state entirely. Use key_value for older Terraform versions — it is stored as a sensitive value in state.
10+
---
11+
12+
# dbtcloud_openai_integration (Resource)
13+
14+
15+
Manages a bring-your-own-key OpenAI integration for a dbt Cloud account, enabling AI-powered features such as dbt Copilot.
16+
17+
Two key types are supported:
18+
- `openai` — your own OpenAI API key
19+
- `azure_openai` — your own Azure OpenAI deployment
20+
21+
**Lifecycle note:** dbt Cloud defaults to a dbt Labs-managed OpenAI key when no integration record exists. Creating this resource switches the account to a customer-managed key. Destroying it (or removing it from the Terraform config) deletes the record and automatically reverts the account to the dbt Labs-managed key — no additional steps are required.
22+
23+
**Secret handling:** the API key is write-only and never returned after creation. Use `key_value_wo` with `key_value_wo_version` (Terraform 1.11+) to keep the secret out of state entirely. Use `key_value` for older Terraform versions — it is stored as a sensitive value in state.
24+
25+
## Example Usage
26+
27+
```terraform
28+
# Use a native OpenAI API key.
29+
# Recommended: write-only (Terraform 1.11+) — the key is never stored in state.
30+
resource "dbtcloud_openai_integration" "openai" {
31+
key_type = "openai"
32+
key_value_wo = var.openai_api_key
33+
key_value_wo_version = 1
34+
}
35+
36+
# For older Terraform versions, use key_value instead — stored as a sensitive value in state.
37+
# resource "dbtcloud_openai_integration" "openai" {
38+
# key_type = "openai"
39+
# key_value = var.openai_api_key
40+
# }
41+
42+
# Use an Azure OpenAI deployment.
43+
resource "dbtcloud_openai_integration" "azure" {
44+
key_type = "azure_openai"
45+
key_value_wo = var.azure_openai_api_key
46+
key_value_wo_version = 1
47+
azure_endpoint = "https://my-deployment.openai.azure.com/"
48+
azure_deployment_name = "gpt-4o"
49+
azure_api_version = "2024-02-01"
50+
}
51+
52+
# To revert to the dbt Labs-managed key, remove this resource from your config
53+
# and run `terraform apply`, or run `terraform destroy`. Deleting the record is
54+
# all that is needed — dbt Cloud automatically falls back to the managed key
55+
# when no integration exists.
56+
```
57+
58+
<!-- schema generated by tfplugindocs -->
59+
## Schema
60+
61+
### Required
62+
63+
- `key_type` (String) The type of OpenAI key. One of: ~~~openai~~~, ~~~azure_openai~~~. To revert to the dbt Labs-managed key, destroy this resource.
64+
65+
### Optional
66+
67+
- `azure_api_version` (String) The Azure OpenAI API version (e.g. ~~~2024-02-01~~~). Required when ~~~key_type~~~ is ~~~azure_openai~~~.
68+
- `azure_deployment_name` (String) The Azure OpenAI deployment name. Required when ~~~key_type~~~ is ~~~azure_openai~~~.
69+
- `azure_endpoint` (String) The Azure OpenAI endpoint URL. Required when ~~~key_type~~~ is ~~~azure_openai~~~.
70+
- `key_value` (String, Sensitive) The OpenAI or Azure OpenAI API key. Stored as a sensitive value in Terraform state. Conflicts with ~~~key_value_wo~~~. For Terraform 1.11+, prefer ~~~key_value_wo~~~ to avoid storing secrets in state.
71+
- `key_value_wo` (String) Write-only variant of the API key (Terraform 1.11+). Never stored in state. Increment ~~~key_value_wo_version~~~ to rotate the key. Conflicts with ~~~key_value~~~.
72+
- `key_value_wo_version` (Number) Increment this value to rotate the key when using ~~~key_value_wo~~~.
73+
74+
### Read-Only
75+
76+
- `account_id` (Number) The ID of the dbt Cloud account.
77+
- `created_at` (String) Timestamp when the integration was created.
78+
- `id` (Number) The ID of the OpenAI integration.
79+
- `updated_at` (String) Timestamp when the integration was last updated.
80+
81+
## Import
82+
83+
Import is supported using the following syntax:
84+
85+
```shell
86+
# Import an existing OpenAI integration by its numeric ID.
87+
# Note: key_value will be absent after import — the API never returns it.
88+
# Use key_value_wo or key_value_wo_version to manage the key going forward.
89+
terraform import dbtcloud_openai_integration.openai 12345
90+
```
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
terraform {
2+
required_providers {
3+
dbtcloud = {
4+
source = "dbt-labs/dbtcloud"
5+
version = ">= 0.3"
6+
}
7+
}
8+
}
9+
10+
provider "dbtcloud" {
11+
account_id = 1234
12+
token = "xxx"
13+
host_url = "https://dbt.com/api"
14+
}
15+
16+
# ── Case 1: native OpenAI key (key_value, stored in state) ────────────────────
17+
# resource "dbtcloud_openai_integration" "test" {
18+
# key_type = "openai"
19+
# key_value = "sk-test-placeholder"
20+
# }
21+
22+
# ── Case 2: rotate the key ────────────────────────────────────────────────────
23+
# Uncomment and re-apply to test PATCH updating key_value.
24+
# resource "dbtcloud_openai_integration" "test" {
25+
# key_type = "openai"
26+
# key_value = "sk-test-rotated"
27+
# }
28+
29+
# ── Case 4: azure_openai ──────────────────────────────────────────────────────
30+
# Uncomment and re-apply to test Azure OpenAI integration.
31+
# resource "dbtcloud_openai_integration" "test" {
32+
# key_type = "azure_openai"
33+
# key_value = "az-test-placeholder"
34+
# azure_endpoint = "https://my-deployment.openai.azure.com/"
35+
# azure_deployment_name = "gpt-4o"
36+
# azure_api_version = "2024-02-01"
37+
# }
38+
39+
# ── Case 5: write-only key (Terraform 1.11+) ──────────────────────────────────
40+
# Uncomment and re-apply to test key_value_wo (not stored in state).
41+
# resource "dbtcloud_openai_integration" "test" {
42+
# key_type = "openai"
43+
# key_value_wo = "sk-wo-placeholder"
44+
# key_value_wo_version = 1
45+
# }
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Import an existing OpenAI integration by its numeric ID.
2+
# Note: key_value will be absent after import — the API never returns it.
3+
# Use key_value_wo or key_value_wo_version to manage the key going forward.
4+
terraform import dbtcloud_openai_integration.openai 12345
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Use a native OpenAI API key.
2+
# Recommended: write-only (Terraform 1.11+) — the key is never stored in state.
3+
resource "dbtcloud_openai_integration" "openai" {
4+
key_type = "openai"
5+
key_value_wo = var.openai_api_key
6+
key_value_wo_version = 1
7+
}
8+
9+
# For older Terraform versions, use key_value instead — stored as a sensitive value in state.
10+
# resource "dbtcloud_openai_integration" "openai" {
11+
# key_type = "openai"
12+
# key_value = var.openai_api_key
13+
# }
14+
15+
# Use an Azure OpenAI deployment.
16+
resource "dbtcloud_openai_integration" "azure" {
17+
key_type = "azure_openai"
18+
key_value_wo = var.azure_openai_api_key
19+
key_value_wo_version = 1
20+
azure_endpoint = "https://my-deployment.openai.azure.com/"
21+
azure_deployment_name = "gpt-4o"
22+
azure_api_version = "2024-02-01"
23+
}
24+
25+
# To revert to the dbt Labs-managed key, remove this resource from your config
26+
# and run `terraform apply`, or run `terraform destroy`. Deleting the record is
27+
# all that is needed — dbt Cloud automatically falls back to the managed key
28+
# when no integration exists.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package dbt_cloud
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
type OpenAIIntegration struct {
12+
ID *int64 `json:"id,omitempty"`
13+
AccountID int64 `json:"account_id,omitempty"`
14+
KeyType string `json:"key_type"`
15+
KeyValue *string `json:"key_value,omitempty"`
16+
AzureEndpoint *string `json:"azure_endpoint,omitempty"`
17+
AzureDeploymentName *string `json:"azure_deployment_name,omitempty"`
18+
AzureAPIVersion *string `json:"azure_api_version,omitempty"`
19+
CreatedAt *string `json:"created_at,omitempty"`
20+
UpdatedAt *string `json:"updated_at,omitempty"`
21+
}
22+
23+
type OpenAIIntegrationResponse struct {
24+
Data OpenAIIntegration `json:"data"`
25+
Status ResponseStatus `json:"status"`
26+
}
27+
28+
type OpenAIIntegrationListResponse struct {
29+
Data []OpenAIIntegration `json:"data"`
30+
Status ResponseStatus `json:"status"`
31+
}
32+
33+
func (c *Client) GetOpenAIIntegration(integrationID int64) (*OpenAIIntegration, error) {
34+
req, err := http.NewRequest(
35+
"GET",
36+
fmt.Sprintf(
37+
"%s/v3/accounts/%s/integrations/open-ai/%d/",
38+
c.HostURL,
39+
strconv.FormatInt(c.AccountID, 10),
40+
integrationID,
41+
),
42+
nil,
43+
)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
body, err := c.doRequestWithRetry(req)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
resp := OpenAIIntegrationResponse{}
54+
if err = json.Unmarshal(body, &resp); err != nil {
55+
return nil, err
56+
}
57+
58+
return &resp.Data, nil
59+
}
60+
61+
func (c *Client) CreateOpenAIIntegration(integration OpenAIIntegration) (*OpenAIIntegration, error) {
62+
// account_id is passed via the URL path — the API rejects it in the body.
63+
integration.AccountID = 0
64+
65+
payload, err := json.Marshal(integration)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
req, err := http.NewRequest(
71+
"POST",
72+
fmt.Sprintf(
73+
"%s/v3/accounts/%s/integrations/open-ai/",
74+
c.HostURL,
75+
strconv.FormatInt(c.AccountID, 10),
76+
),
77+
strings.NewReader(string(payload)),
78+
)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
body, err := c.doRequestWithRetry(req)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
resp := OpenAIIntegrationResponse{}
89+
if err = json.Unmarshal(body, &resp); err != nil {
90+
return nil, err
91+
}
92+
93+
return &resp.Data, nil
94+
}
95+
96+
func (c *Client) UpdateOpenAIIntegration(integrationID int64, integration OpenAIIntegration) (*OpenAIIntegration, error) {
97+
// account_id is passed via the URL path; zero it out to avoid API rejection.
98+
integration.AccountID = 0
99+
100+
payload, err := json.Marshal(integration)
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
req, err := http.NewRequest(
106+
"PATCH",
107+
fmt.Sprintf(
108+
"%s/v3/accounts/%s/integrations/open-ai/%d/",
109+
c.HostURL,
110+
strconv.FormatInt(c.AccountID, 10),
111+
integrationID,
112+
),
113+
strings.NewReader(string(payload)),
114+
)
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
body, err := c.doRequestWithRetry(req)
120+
if err != nil {
121+
return nil, err
122+
}
123+
124+
resp := OpenAIIntegrationResponse{}
125+
if err = json.Unmarshal(body, &resp); err != nil {
126+
return nil, err
127+
}
128+
129+
return &resp.Data, nil
130+
}
131+
132+
func (c *Client) DeleteOpenAIIntegration(integrationID int64) error {
133+
req, err := http.NewRequest(
134+
"DELETE",
135+
fmt.Sprintf(
136+
"%s/v3/accounts/%s/integrations/open-ai/%d/",
137+
c.HostURL,
138+
strconv.FormatInt(c.AccountID, 10),
139+
integrationID,
140+
),
141+
nil,
142+
)
143+
if err != nil {
144+
return err
145+
}
146+
147+
_, err = c.doRequestWithRetry(req)
148+
return err
149+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package openai_integration
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework/types"
5+
)
6+
7+
type OpenAIIntegrationResourceModel struct {
8+
ID types.Int64 `tfsdk:"id"`
9+
AccountID types.Int64 `tfsdk:"account_id"`
10+
KeyType types.String `tfsdk:"key_type"`
11+
KeyValue types.String `tfsdk:"key_value"`
12+
KeyValueWO types.String `tfsdk:"key_value_wo"`
13+
KeyValueWOVersion types.Int64 `tfsdk:"key_value_wo_version"`
14+
AzureEndpoint types.String `tfsdk:"azure_endpoint"`
15+
AzureDeploymentName types.String `tfsdk:"azure_deployment_name"`
16+
AzureAPIVersion types.String `tfsdk:"azure_api_version"`
17+
CreatedAt types.String `tfsdk:"created_at"`
18+
UpdatedAt types.String `tfsdk:"updated_at"`
19+
}

0 commit comments

Comments
 (0)