Skip to content

Commit 143ad7c

Browse files
authored
Merge pull request #85 from PostHog/kefapps/external-data-source
feat: add posthog_external_data_source resource
2 parents 684538a + 8f2eccd commit 143ad7c

20 files changed

Lines changed: 1522 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "posthog_external_data_source Resource - posthog"
4+
subcategory: ""
5+
description: |-
6+
PostHog external data warehouse source (e.g. Stripe, Hubspot, Postgres, MySQL, MSSQL, Snowflake, BigQuery, Salesforce, Zendesk, Vitally, Chargebee). source_type, prefix, and schemas are immutable: changes trigger replacement. Sync cadence is managed by PostHog (per-schema) and is not configurable on this resource.
7+
---
8+
9+
# posthog_external_data_source (Resource)
10+
11+
PostHog external data warehouse source (e.g. Stripe, Hubspot, Postgres, MySQL, MSSQL, Snowflake, BigQuery, Salesforce, Zendesk, Vitally, Chargebee). `source_type`, `prefix`, and `schemas` are immutable: changes trigger replacement. Sync cadence is managed by PostHog (per-schema) and is not configurable on this resource.
12+
13+
## Example Usage
14+
15+
```terraform
16+
variable "stripe_secret_key" {
17+
type = string
18+
sensitive = true
19+
}
20+
21+
variable "pg_password" {
22+
type = string
23+
sensitive = true
24+
}
25+
26+
# Stripe warehouse source.
27+
# `source_type`, `prefix`, and `schemas` are immutable — changing any of them
28+
# replaces the resource. Sync cadence is managed by PostHog (per-schema).
29+
resource "posthog_external_data_source" "stripe" {
30+
source_type = "Stripe"
31+
prefix = "stripe_"
32+
schemas = ["charges", "customers", "invoices"]
33+
34+
job_inputs_json = jsonencode({
35+
stripe_account_id = "acct_123"
36+
stripe_secret_key = var.stripe_secret_key
37+
})
38+
}
39+
40+
# Postgres warehouse source
41+
resource "posthog_external_data_source" "prod_pg" {
42+
source_type = "Postgres"
43+
schemas = ["users", "orders"]
44+
45+
job_inputs_json = jsonencode({
46+
host = "db.internal"
47+
port = 5432
48+
database = "app"
49+
user = "readonly"
50+
password = var.pg_password
51+
schema = "public"
52+
})
53+
}
54+
```
55+
56+
<!-- schema generated by tfplugindocs -->
57+
## Schema
58+
59+
### Required
60+
61+
- `job_inputs_json` (String, Sensitive) JSON-encoded connection configuration for the source. Shape depends on `source_type`. For example Postgres expects `{host, port, database, user, password, schema}`; Stripe expects `{stripe_account_id, stripe_secret_key}`. PostHog redacts secret values when reading, so the plan value is preserved in state. On import, redacted secrets will appear in state until the next apply with real values.
62+
- `schemas` (List of String) List of table names to sync from the source (e.g. `["users", "orders"]`). PostHog discovers available tables from the source; these must match discovered table names. The source-level update endpoint cannot edit the schema list, so changes force destroy+recreate.
63+
- `source_type` (String) Source type recognised by the PostHog data warehouse (e.g. `Stripe`, `Postgres`, `Snowflake`, `BigQuery`, `Hubspot`). PostHog defines the set of accepted values and may add new types over time; see the PostHog data warehouse docs for the current list. Cannot be changed after creation.
64+
65+
### Optional
66+
67+
- `prefix` (String) Optional prefix for synced table names (e.g. `stripe_prod_`). Useful when connecting multiple sources of the same type. The PostHog update endpoint silently ignores prefix changes for non-direct-postgres sources, so this resource treats it as RequiresReplace.
68+
- `project_id` (String) Project ID (environment) for this resource. Overrides the provider-level project_id.
69+
70+
### Read-Only
71+
72+
- `created_at` (String) Timestamp when the source was created.
73+
- `id` (String) External data source ID (UUID).
74+
- `last_run_at` (String) Timestamp of the most recent sync run.
75+
- `status` (String) Current status reported by PostHog (e.g. `Running`, `Completed`, `Error`).
76+
- `sync_frequency` (String) Sync cadence reported by PostHog. Defaults to 6 hours (5 minutes for CDC schemas) and is not configurable on this resource — the create endpoint does not accept a sync frequency. Reports the value of the first schema; sources with mixed schedules will see this flap. Modify per-schema cadence via the PostHog UI or schemas API.
77+
78+
## Import
79+
80+
Import is supported using the following syntax:
81+
82+
The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example:
83+
84+
```shell
85+
# Import using: project_id/external_data_source_id
86+
terraform import posthog_external_data_source.example 12345/01900000-0000-7000-8000-000000000000
87+
88+
# If project_id is configured at the provider level, you can omit it:
89+
terraform import posthog_external_data_source.example 01900000-0000-7000-8000-000000000000
90+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Import using: project_id/external_data_source_id
2+
terraform import posthog_external_data_source.example 12345/01900000-0000-7000-8000-000000000000
3+
4+
# If project_id is configured at the provider level, you can omit it:
5+
terraform import posthog_external_data_source.example 01900000-0000-7000-8000-000000000000
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
variable "stripe_secret_key" {
2+
type = string
3+
sensitive = true
4+
}
5+
6+
variable "pg_password" {
7+
type = string
8+
sensitive = true
9+
}
10+
11+
# Stripe warehouse source.
12+
# `source_type`, `prefix`, and `schemas` are immutable — changing any of them
13+
# replaces the resource. Sync cadence is managed by PostHog (per-schema).
14+
resource "posthog_external_data_source" "stripe" {
15+
source_type = "Stripe"
16+
prefix = "stripe_"
17+
schemas = ["charges", "customers", "invoices"]
18+
19+
job_inputs_json = jsonencode({
20+
stripe_account_id = "acct_123"
21+
stripe_secret_key = var.stripe_secret_key
22+
})
23+
}
24+
25+
# Postgres warehouse source
26+
resource "posthog_external_data_source" "prod_pg" {
27+
source_type = "Postgres"
28+
schemas = ["users", "orders"]
29+
30+
job_inputs_json = jsonencode({
31+
host = "db.internal"
32+
port = 5432
33+
database = "app"
34+
user = "readonly"
35+
password = var.pg_password
36+
schema = "public"
37+
})
38+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package httpclient
2+
3+
import (
4+
"context"
5+
"fmt"
6+
)
7+
8+
// ExternalDataSource represents a PostHog data warehouse source (Stripe, Postgres,
9+
// Snowflake, BigQuery, Hubspot, etc.) that syncs external data into PostHog.
10+
type ExternalDataSource struct {
11+
ID string `json:"id"`
12+
SourceType string `json:"source_type"`
13+
Prefix *string `json:"prefix,omitempty"`
14+
JobInputs map[string]any `json:"job_inputs,omitempty"`
15+
Schemas []ExternalDataSourceSchema `json:"schemas,omitempty"`
16+
Status *string `json:"status,omitempty"`
17+
LastRunAt *string `json:"last_run_at,omitempty"`
18+
CreatedAt *string `json:"created_at,omitempty"`
19+
}
20+
21+
type ExternalDataSourceSchema struct {
22+
Name string `json:"name"`
23+
ShouldSync bool `json:"should_sync"`
24+
SyncFrequency *string `json:"sync_frequency,omitempty"`
25+
Status *string `json:"status,omitempty"`
26+
}
27+
28+
func (c *PosthogClient) CreateExternalDataSource(ctx context.Context, projectID string, input any) (ExternalDataSource, error) {
29+
path := fmt.Sprintf("/api/projects/%s/external_data_sources/", projectID)
30+
result, _, err := doPost[ExternalDataSource](c, ctx, path, input)
31+
return result, err
32+
}
33+
34+
func (c *PosthogClient) GetExternalDataSource(ctx context.Context, projectID, id string) (ExternalDataSource, HTTPStatusCode, error) {
35+
path := fmt.Sprintf("/api/projects/%s/external_data_sources/%s/", projectID, id)
36+
return doGet[ExternalDataSource](c, ctx, path)
37+
}
38+
39+
func (c *PosthogClient) UpdateExternalDataSource(ctx context.Context, projectID, id string, input any) (ExternalDataSource, HTTPStatusCode, error) {
40+
path := fmt.Sprintf("/api/projects/%s/external_data_sources/%s/", projectID, id)
41+
return doPatch[ExternalDataSource](c, ctx, path, input)
42+
}
43+
44+
func (c *PosthogClient) DeleteExternalDataSource(ctx context.Context, projectID, id string) (HTTPStatusCode, error) {
45+
path := fmt.Sprintf("/api/projects/%s/external_data_sources/%s/", projectID, id)
46+
return doDelete(c, ctx, path)
47+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package httpclient
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/posthog/terraform-provider/internal/util"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
const (
16+
testEDSID = "01900000-0000-7000-8000-000000000000"
17+
testEDSProject = "proj-1"
18+
externalSourceAPI = "/api/projects/"
19+
externalSourceSub = "/external_data_sources/"
20+
)
21+
22+
func externalDataSourceCollectionPath() string {
23+
return externalSourceAPI + testEDSProject + externalSourceSub
24+
}
25+
26+
func externalDataSourceItemPath() string {
27+
return externalDataSourceCollectionPath() + testEDSID + "/"
28+
}
29+
30+
func TestCreateExternalDataSource(t *testing.T) {
31+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32+
assert.Equal(t, http.MethodPost, r.Method)
33+
assert.Equal(t, externalDataSourceCollectionPath(), r.URL.Path)
34+
35+
var body map[string]any
36+
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
37+
assert.Equal(t, "Stripe", body["source_type"])
38+
assert.Equal(t, "stripe_", body["prefix"])
39+
40+
writeJSONResponse(t, w, ExternalDataSource{
41+
ID: testEDSID,
42+
SourceType: "Stripe",
43+
Prefix: util.StringPtr("stripe_"),
44+
})
45+
}))
46+
defer server.Close()
47+
48+
client := newTestPosthogClient(server)
49+
resp, err := client.CreateExternalDataSource(context.Background(), testEDSProject, map[string]any{
50+
"source_type": "Stripe",
51+
"prefix": "stripe_",
52+
"payload": map[string]any{
53+
"stripe_account_id": "acct_123",
54+
"schemas": []map[string]any{{"name": "charges", "should_sync": true}},
55+
},
56+
})
57+
58+
require.NoError(t, err)
59+
assert.Equal(t, testEDSID, resp.ID)
60+
require.NotNil(t, resp.Prefix)
61+
assert.Equal(t, "stripe_", *resp.Prefix)
62+
}
63+
64+
func TestGetExternalDataSource(t *testing.T) {
65+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66+
assert.Equal(t, http.MethodGet, r.Method)
67+
assert.Equal(t, externalDataSourceItemPath(), r.URL.Path)
68+
writeJSONResponse(t, w, ExternalDataSource{
69+
ID: testEDSID,
70+
SourceType: "Stripe",
71+
Schemas: []ExternalDataSourceSchema{
72+
{Name: "charges", ShouldSync: true, SyncFrequency: util.StringPtr("6hour")},
73+
},
74+
})
75+
}))
76+
defer server.Close()
77+
78+
client := newTestPosthogClient(server)
79+
resp, status, err := client.GetExternalDataSource(context.Background(), testEDSProject, testEDSID)
80+
81+
require.NoError(t, err)
82+
assert.Equal(t, http.StatusOK, int(status))
83+
require.Len(t, resp.Schemas, 1)
84+
assert.Equal(t, "charges", resp.Schemas[0].Name)
85+
require.NotNil(t, resp.Schemas[0].SyncFrequency)
86+
assert.Equal(t, "6hour", *resp.Schemas[0].SyncFrequency)
87+
}
88+
89+
func TestUpdateExternalDataSource(t *testing.T) {
90+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
91+
assert.Equal(t, http.MethodPatch, r.Method)
92+
assert.Equal(t, externalDataSourceItemPath(), r.URL.Path)
93+
94+
var body map[string]any
95+
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
96+
// PATCH must use job_inputs at top level — that's what the PostHog
97+
// update serializer reads. Sending under `payload` would silently no-op.
98+
_, ok := body["job_inputs"].(map[string]any)
99+
assert.True(t, ok, "PATCH body must include job_inputs")
100+
101+
writeJSONResponse(t, w, ExternalDataSource{ID: testEDSID, SourceType: "Stripe"})
102+
}))
103+
defer server.Close()
104+
105+
client := newTestPosthogClient(server)
106+
resp, status, err := client.UpdateExternalDataSource(context.Background(), testEDSProject, testEDSID, map[string]any{
107+
"job_inputs": map[string]any{"stripe_account_id": "acct_999"},
108+
})
109+
110+
require.NoError(t, err)
111+
assert.Equal(t, http.StatusOK, int(status))
112+
assert.Equal(t, testEDSID, resp.ID)
113+
}
114+
115+
func TestDeleteExternalDataSource(t *testing.T) {
116+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
117+
assert.Equal(t, http.MethodDelete, r.Method)
118+
assert.Equal(t, externalDataSourceItemPath(), r.URL.Path)
119+
w.WriteHeader(http.StatusOK)
120+
}))
121+
defer server.Close()
122+
123+
client := newTestPosthogClient(server)
124+
status, err := client.DeleteExternalDataSource(context.Background(), testEDSProject, testEDSID)
125+
126+
require.NoError(t, err)
127+
assert.Equal(t, http.StatusOK, int(status))
128+
}

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ func (p *PostHogProvider) Resources(_ context.Context) []func() frameworkresourc
141141
posthogresource.NewAlert,
142142
posthogresource.NewDashboard,
143143
posthogresource.NewDashboardLayout,
144+
posthogresource.NewExternalDataSource,
144145
posthogresource.NewFeatureFlag,
145146
posthogresource.NewHogFunction,
146147
posthogresource.NewInsight,

0 commit comments

Comments
 (0)