Skip to content

feat: add Anthropic integration resource and data source [ENG-8433] - #257

Merged
wgrant merged 1 commit into
mainfrom
wgrant/feat/anthropic-integration
Oct 23, 2026
Merged

wgrant merged 1 commit into
mainfrom
wgrant/feat/anthropic-integration

Conversation

@wgrant

@wgrant wgrant commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

ENG-8433

what

add stacklet_anthropic_integration resource and data source, which configure importing an Anthropic organization's cost and usage

  • org_id identifies the integration: changing it replaces the resource, and it's the import ID
  • admin_key_wo / admin_key_wo_version are write-only and both required, like the Jira profile's key; configured says whether a key is stored
  • the admin key is masked in debug logs, like the other write-only credentials
  • a key the API rejects fails the apply with the API's reason
  • a Conflict on remove (the integration is gone but its stored key isn't deleted yet) becomes a warning, since Terraform can't retry once the resource is gone; fromProblems now returns a typed Conflict
  • other resources' optional *_wo attributes whose version is also optional get an XXX: a key without a version is never sent

registered as unreleased (STACKLET_UNRELEASED_FEATURES) until the platform feature is generally available

why

allow configuring the Anthropic integration through Terraform

testing

acceptance tests for create, import, key rotation, rejected keys (including a failed rotation keeping the stored key), and the data source, recorded against a local platform with a fake Anthropic Admin API; unit test for fromProblems

docs

not yet, since it's unreleased

@wgrant
wgrant requested a review from a team as a code owner October 22, 2026 04:50

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 17 files

Confidence score: 2/5

  • internal/api/anthropic_integration.go can send the admin key to an http:// endpoint, exposing it in transit. Reject non-HTTPS endpoints before sending the mutation.
  • internal/api/anthropic_integration.go sends the write-only key in a GraphQL request body that is logged at TF_LOG=DEBUG or TRACE. Redact adminKey from request logs.
  • internal/resources/anthropic_integration.go can ignore a configured admin_key_wo after import when its version is null, leaving the existing key unchanged. Handle the null version so the configured key is sent.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/resources/anthropic_integration.go">

<violation number="1" location="internal/resources/anthropic_integration.go:141">
P2: An imported integration with `admin_key_wo` but no `admin_key_wo_version` never sends the configured key: import leaves both versions null, so this condition passes `nil` to `Upsert`, which preserves the old key. Treat a supplied key as a trigger for the initial post-import update, or require the version whenever a key is configured.</violation>
</file>

<file name="internal/api/anthropic_integration.go">

<violation number="1" location="internal/api/anthropic_integration.go:29">
P2: The new admin key is sent through a client that logs the complete GraphQL request body at `TF_LOG=DEBUG` or `TRACE`, so enabling provider debugging exposes this write-only credential in logs. Redact `adminKey` (and other secret variables) before logging request bodies.</violation>

<violation number="2" location="internal/api/anthropic_integration.go:78">
P1: Custom agent: **Flag Security Vulnerabilities**

This mutation sends `AdminKey` to the provider endpoint, but the shared client accepts `http://` endpoints and does not enforce TLS. Reject non-HTTPS endpoints before sending credentials.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant TF as Terraform Core
    participant P as Provider (Framework)
    participant R as AnthropicIntegration Resource
    participant DS as AnthropicIntegration Data Source
    participant A as API Client
    participant G as GraphQL API
    participant S as Stacklet Platform
    participant An as Anthropic Admin API

    Note over TF,An: Anthropic Integration Management Flow

    rect rgb(240, 248, 255)
    Note over TF,An: Create Operation
    TF->>R: Create(plan, config)
    R->>R: Validate admin_key_wo present
    alt Key Missing
        R-->>TF: Error: "Missing admin_key_wo"
    end
    R->>A: Upsert(orgID, adminKey)
    A->>G: Mutation upsertAnthropicIntegration
    G->>S: Validate key with Anthropic
    S->>An: Check admin key (401 if invalid)
    An-->>S: Auth result
    alt Key Valid
        S-->>G: integration {id, orgId, configured}
        G-->>A: Success response
        A-->>R: integration data
        R->>R: Update state (configured=true)
        R-->>TF: State with id, org_id, configured
    else Key Invalid
        S-->>G: Problems [{InvalidInput}]
        G-->>A: Error response
        A->>A: fromProblems() -> apiError
        A-->>R: Rejected key error
        R-->>TF: Error: "Failed to connect to Anthropic"
    end
    end

    rect rgb(255, 248, 220)
    Note over TF,An: Update Operation (Key Rotation)
    TF->>R: Update(plan, state, config)
    R->>R: Compare admin_key_wo_version
    alt Version Changed
        R->>R: Extract new key from config
        alt Key Missing
            R-->>TF: Error: "Missing admin_key_wo"
        end
        R->>A: Upsert(orgID, newKey)
        A->>G: Mutation upsertAnthropicIntegration
        G->>S: Validate with Anthropic
        S-->>G: Result
        alt Success
            G-->>A: Updated integration
            A-->>R: Success
            R-->>TF: Updated state (admin_key_wo_version)
        else Failure
            A-->>R: Error (previous key preserved)
            R-->>TF: Apply error
        end
    else Version Unchanged
        R->>A: Upsert(orgID, no key)
        A-->>R: Preserve existing key
        R-->>TF: State unchanged
    end
    end

    rect rgb(245, 245, 245)
    Note over TF,An: Read/Import Operation
    TF->>R: Read(state)
    R->>A: Read(orgID)
    A->>G: Query anthropicIntegration
    G-->>A: {id, orgId, configured}
    A-->>R: Integration data
    R->>R: Update state (configured only)
    R-->>TF: Refreshed state

    TF->>DS: Read(org_id)
    DS->>A: Read(orgID)
    A->>G: Query anthropicIntegration
    G-->>A: {id, orgId, configured}
    A-->>DS: Integration data
    DS-->>TF: Data source attributes
    end

    rect rgb(255, 240, 240)
    Note over TF,An: Delete Operation
    TF->>R: Delete(state)
    R->>A: Remove(orgID)
    A->>G: Mutation removeAnthropicIntegration
    G->>S: Delete integration
    S-->>G: Problems array
    alt No Problems
        G-->>A: Success
        A-->>R: nil error
        R-->>TF: State removed
    else Conflict Problem
        G-->>A: Conflict typed error
        A->>A: fromProblems() -> api.Conflict
        A-->>R: Conflict error
        R->>R: AddWarning (integration gone, key pending deletion)
        R-->>TF: Warning + state removed
    else Other Error
        A-->>R: General error
        R-->>TF: Error (state retained)
    end
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Problems []problem
} `graphql:"upsertAnthropicIntegration(input: $input)"`
}
if err := a.c.Mutate(ctx, &mutation, map[string]any{"input": input}); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

This mutation sends AdminKey to the provider endpoint, but the shared client accepts http:// endpoints and does not enforce TLS. Reject non-HTTPS endpoints before sending credentials.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/api/anthropic_integration.go, line 78:

<comment>This mutation sends `AdminKey` to the provider endpoint, but the shared client accepts `http://` endpoints and does not enforce TLS. Reject non-HTTPS endpoints before sending credentials.</comment>

<file context>
@@ -0,0 +1,103 @@
+			Problems             []problem
+		} `graphql:"upsertAnthropicIntegration(input: $input)"`
+	}
+	if err := a.c.Mutate(ctx, &mutation, map[string]any{"input": input}); err != nil {
+		return nil, err
+	}
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope here: the endpoint comes from the provider configuration, and enforcing https would apply to the whole client, not this resource. Leaving as is.

Comment thread internal/acceptance_tests/anthropic_integration_resource_test.go
// sent when its version says there is one.
input := api.AnthropicIntegrationInput{
OrgID: plan.OrgID.ValueString(),
AdminKey: credentialToSend(&resp.Diagnostics, !state.AdminKeyWOVersion.Equal(plan.AdminKeyWOVersion), "admin_key_wo", config.AdminKeyWO),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: An imported integration with admin_key_wo but no admin_key_wo_version never sends the configured key: import leaves both versions null, so this condition passes nil to Upsert, which preserves the old key. Treat a supplied key as a trigger for the initial post-import update, or require the version whenever a key is configured.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/resources/anthropic_integration.go, line 141:

<comment>An imported integration with `admin_key_wo` but no `admin_key_wo_version` never sends the configured key: import leaves both versions null, so this condition passes `nil` to `Upsert`, which preserves the old key. Treat a supplied key as a trigger for the initial post-import update, or require the version whenever a key is configured.</comment>

<file context>
@@ -0,0 +1,180 @@
+	// sent when its version says there is one.
+	input := api.AnthropicIntegrationInput{
+		OrgID:    plan.OrgID.ValueString(),
+		AdminKey: credentialToSend(&resp.Diagnostics, !state.AdminKeyWOVersion.Equal(plan.AdminKeyWOVersion), "admin_key_wo", config.AdminKeyWO),
+	}
+	if resp.Diagnostics.HasError() {
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

admin_key_wo now requires admin_key_wo_version, so a key without a version is rejected at plan time. The other resources' write-only attributes get the same treatment separately.

// that is never returned.
type AnthropicIntegrationInput struct {
OrgID string `json:"orgId"`
AdminKey *string `json:"adminKey,omitempty"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new admin key is sent through a client that logs the complete GraphQL request body at TF_LOG=DEBUG or TRACE, so enabling provider debugging exposes this write-only credential in logs. Redact adminKey (and other secret variables) before logging request bodies.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/api/anthropic_integration.go, line 29:

<comment>The new admin key is sent through a client that logs the complete GraphQL request body at `TF_LOG=DEBUG` or `TRACE`, so enabling provider debugging exposes this write-only credential in logs. Redact `adminKey` (and other secret variables) before logging request bodies.</comment>

<file context>
@@ -0,0 +1,103 @@
+// that is never returned.
+type AnthropicIntegrationInput struct {
+	OrgID    string  `json:"orgId"`
+	AdminKey *string `json:"adminKey,omitempty"`
+}
+
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real, and provider-wide: every write-only credential goes through the same logging. Fixed separately in "fix: mask write-only credentials in debug logs", which masks them via the request context; whichever of the two PRs lands second masks this key too.


func (r *anthropicIntegrationResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manage an Anthropic integration, which imports the daily cost and usage of one Anthropic organization.\n\n" +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would be a tad more readable as a backticked string with no explicit newlines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


// The API refuses to create an integration without a key; saying so here
// names the attribute to set.
if config.AdminKeyWO.IsNull() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just make the key required?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, much simpler. admin_key_wo and admin_key_wo_version are both required now, like the Jira profile's, so the create-time check, the validator and their tests are gone.

[ENG-8433](https://stacklet.atlassian.net/browse/ENG-8433)

### what

add `stacklet_anthropic_integration` resource and data source, which configure importing an Anthropic organization's cost and usage

- `org_id` identifies the integration: changing it replaces the resource, and it's the import ID
- `admin_key_wo` / `admin_key_wo_version` are write-only and both required, like the Jira profile's key; `configured` says whether a key is stored
- the admin key is masked in debug logs, like the other write-only credentials
- a key the API rejects fails the apply with the API's reason
- a `Conflict` on remove (the integration is gone but its stored key isn't deleted yet) becomes a warning, since Terraform can't retry once the resource is gone; `fromProblems` now returns a typed `Conflict`
- other resources' optional `*_wo` attributes whose version is also optional get an `XXX`: a key without a version is never sent

registered as unreleased (`STACKLET_UNRELEASED_FEATURES`) until the platform feature is generally available

### why

allow configuring the Anthropic integration through Terraform

### testing

acceptance tests for create, import, key rotation, rejected keys (including a failed rotation keeping the stored key), and the data source, recorded against a local platform with a fake Anthropic Admin API; unit test for `fromProblems`

### docs

not yet, since it's unreleased
@wgrant
wgrant force-pushed the wgrant/feat/anthropic-integration branch from f91bee3 to ed278eb Compare October 22, 2026 19:58

@albertodonato albertodonato left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@wgrant
wgrant merged commit 4ef34db into main Oct 23, 2026
4 checks passed
@wgrant
wgrant deleted the wgrant/feat/anthropic-integration branch October 23, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants