Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ make testacc # TF_ACC=1 go test ./... -timeout 120m

**Naming.** Terraform resource types are prefixed with the Coder feature area they belong to so related resources cluster together in configs and docs. Prefer an existing prefix over inventing a new one:

- `coderd_agents_*` — Coder Agents feature area (`coderd_agents_mcp_server`, `coderd_agents_model`, `coderd_agents_system_prompt`, `coderd_default_agents_model`).
- `coderd_agents_*` — Coder Agents feature area (`coderd_agents_default_model`, `coderd_agents_mcp_server`, `coderd_agents_model`, `coderd_agents_system_prompt`).
- `coderd_organization_*` — organization-scoped settings (`coderd_organization_group_sync`, `coderd_organization_sync_settings`).
- `coderd_oauth2_*`, `coderd_workspace_*`, etc. follow the same rule.

Expand Down
4 changes: 0 additions & 4 deletions docs/resources/agents_default_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,6 @@ resource "coderd_agents_default_model" "default" {
- `model_id` (String) ID of the `coderd_agents_model` to mark as the organization's default. Usually this is `coderd_agents_model.<name>.id`.
- `organization_id` (String) Organization ID whose default Agents model is managed.

### Read-Only

- `id` (String) Organization ID that identifies this organization's default Agents model selection.

## Import

Import is supported using the following syntax:
Expand Down
4 changes: 0 additions & 4 deletions docs/resources/ai_model_price.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,6 @@ resource "coderd_ai_model_price" "sonnet" {
- `input_price` (Number) Price per 1M input tokens, in integer micro-units: `1000000` is $1.00 per 1M tokens. Omit it if the price is not known, which AI Gateway counts as zero cost. Set `0` to declare the tokens free of charge.
- `output_price` (Number) Price per 1M output tokens, in integer micro-units: `1000000` is $1.00 per 1M tokens. Omit it if the price is not known, which AI Gateway counts as zero cost. Set `0` to declare the tokens free of charge.

### Read-Only

- `id` (String) Model price ID, in the form `<provider_type>/<model>`.

## Import

Import is supported using the following syntax:
Expand Down
19 changes: 1 addition & 18 deletions internal/provider/agents_default_model_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,13 @@ func (r *AgentsDefaultModelResource) experimentalClient() *codersdk.Experimental
}

type AgentsDefaultModelResourceModel struct {
ID UUID `tfsdk:"id"`
OrganizationID UUID `tfsdk:"organization_id"`
ModelID UUID `tfsdk:"model_id"`
}

// legacyDefaultAgentsModelResourceModel is the v0 state shape published by
// coderd_default_agents_model in provider v0.0.23.
type legacyDefaultAgentsModelResourceModel struct {
ID types.String `tfsdk:"id"`
ModelID types.String `tfsdk:"model_id"`
}

Expand Down Expand Up @@ -86,14 +84,6 @@ func (r *AgentsDefaultModelResource) Schema(ctx context.Context, req resource.Sc
"and promotes a replacement when the current default is removed, so deleting this resource only stops " +
"Terraform from managing which model is default.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "Organization ID that identifies this organization's default Agents model selection.",
CustomType: UUIDType,
Computed: true,
PlanModifiers: []planmodifier.String{
useStateForUnknownUnlessChanged("organization_id"),
},
},
"organization_id": schema.StringAttribute{
MarkdownDescription: "Organization ID whose default Agents model is managed.",
CustomType: UUIDType,
Expand All @@ -120,9 +110,6 @@ func (r *AgentsDefaultModelResource) MoveState(ctx context.Context) []resource.S
{
SourceSchema: &schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"model_id": schema.StringAttribute{
Required: true,
},
Expand Down Expand Up @@ -161,7 +148,6 @@ func (r *AgentsDefaultModelResource) MoveState(ctx context.Context) []resource.S
// default organization cannot be resolved here. Leave it null and
// let the subsequent apply adopt the configured organization_id.
resp.Diagnostics.Append(resp.TargetState.Set(ctx, AgentsDefaultModelResourceModel{
ID: NewUUIDNull(),
OrganizationID: NewUUIDNull(),
ModelID: UUIDValue(modelID),
})...)
Expand Down Expand Up @@ -284,7 +270,6 @@ func (r *AgentsDefaultModelResource) ImportState(ctx context.Context, req resour
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get organization %q: %s", req.ID, err))
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), org.ID.String())...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), org.ID.String())...)
}

Expand All @@ -303,11 +288,9 @@ func (r *AgentsDefaultModelResource) setDefault(ctx context.Context, organizatio
}

// stateFromAgentsDefaultModelConfig maps the model config that Coder reports as the
// default into resource state. The organization UUID is the natural identity
// because each organization has at most one default model.
// default into resource state.
func stateFromAgentsDefaultModelConfig(config codersdk.ChatModel) AgentsDefaultModelResourceModel {
return AgentsDefaultModelResourceModel{
ID: UUIDValue(config.OrganizationID),
OrganizationID: UUIDValue(config.OrganizationID),
ModelID: UUIDValue(config.ID),
}
Expand Down
104 changes: 5 additions & 99 deletions internal/provider/agents_default_model_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import (
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
fwresource "github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
Expand All @@ -35,24 +33,6 @@ import (
"github.com/stretchr/testify/require"
)

func TestAgentsDefaultModelStateFromModelConfig(t *testing.T) {
t.Parallel()

organizationID := uuid.New()
modelID := uuid.New()
state := stateFromAgentsDefaultModelConfig(codersdk.ChatModel{
ID: modelID,
OrganizationID: organizationID,
IsDefault: true,
})
require.Equal(t, organizationID, state.ID.ValueUUID())
require.Equal(t, organizationID.String(), state.ID.ValueString())
require.Equal(t, organizationID, state.OrganizationID.ValueUUID())
require.Equal(t, organizationID.String(), state.OrganizationID.ValueString())
require.Equal(t, modelID, state.ModelID.ValueUUID())
require.Equal(t, modelID.String(), state.ModelID.ValueString())
}

func TestAgentsDefaultModelMoveState(t *testing.T) {
t.Parallel()

Expand All @@ -69,7 +49,6 @@ func TestAgentsDefaultModelMoveState(t *testing.T) {
Raw: tftypes.NewValue(sourceSchema.Type().TerraformType(ctx), nil),
}
require.False(t, sourceState.Set(ctx, legacyDefaultAgentsModelResourceModel{
ID: types.StringValue("default"),
ModelID: types.StringValue(modelID.String()),
}).HasError())

Expand All @@ -93,7 +72,6 @@ func TestAgentsDefaultModelMoveState(t *testing.T) {

var got AgentsDefaultModelResourceModel
require.False(t, resp.TargetState.Get(ctx, &got).HasError())
require.True(t, got.ID.IsNull())
require.True(t, got.OrganizationID.IsNull())
require.Equal(t, modelID, got.ModelID.ValueUUID())

Expand Down Expand Up @@ -145,74 +123,6 @@ func TestAgentsDefaultModelMoveState(t *testing.T) {
}
}

func TestAgentsDefaultModelIDPlanModifier(t *testing.T) {
t.Parallel()

ctx := t.Context()
oldOrganizationID := uuid.New()
newOrganizationID := uuid.New()
modelID := uuid.New()

r := &AgentsDefaultModelResource{}
var schemaResp fwresource.SchemaResponse
r.Schema(ctx, fwresource.SchemaRequest{}, &schemaResp)
require.False(t, schemaResp.Diagnostics.HasError(), schemaResp.Diagnostics)

idAttribute, ok := schemaResp.Schema.Attributes["id"].(schema.StringAttribute)
require.True(t, ok)
require.Len(t, idAttribute.PlanModifiers, 1)
modifier := idAttribute.PlanModifiers[0]

raw := func(id tftypes.Value, organizationID uuid.UUID) tftypes.Value {
return tftypes.NewValue(schemaResp.Schema.Type().TerraformType(ctx), map[string]tftypes.Value{
"id": id,
"organization_id": tftypes.NewValue(tftypes.String, organizationID.String()),
"model_id": tftypes.NewValue(tftypes.String, modelID.String()),
})
}
state := tfsdk.State{
Schema: schemaResp.Schema,
Raw: raw(tftypes.NewValue(tftypes.String, oldOrganizationID.String()), oldOrganizationID),
}

for _, tc := range []struct {
name string
plannedOrganization uuid.UUID
want types.String
}{
{
name: "retains id when organization is unchanged",
plannedOrganization: oldOrganizationID,
want: types.StringValue(oldOrganizationID.String()),
},
{
name: "leaves id unknown when organization changes",
plannedOrganization: newOrganizationID,
want: types.StringUnknown(),
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

plan := tfsdk.Plan{
Schema: schemaResp.Schema,
Raw: raw(tftypes.NewValue(tftypes.String, tftypes.UnknownValue), tc.plannedOrganization),
}
resp := &planmodifier.StringResponse{PlanValue: types.StringUnknown()}
modifier.PlanModifyString(ctx, planmodifier.StringRequest{
ConfigValue: types.StringNull(),
PlanValue: types.StringUnknown(),
StateValue: types.StringValue(oldOrganizationID.String()),
Plan: plan,
State: state,
}, resp)

require.False(t, resp.Diagnostics.HasError(), resp.Diagnostics)
require.Equal(t, tc.want, resp.PlanValue)
})
}
}

func TestAgentsDefaultModelPatch404Diagnostics(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -351,7 +261,6 @@ func TestAgentsDefaultModelReadCollection404(t *testing.T) {

r := newAgentsDefaultModelTestResource(t, srv.URL, defaultOrganizationID)
state := agentsDefaultModelTestState(t, r, AgentsDefaultModelResourceModel{
ID: UUIDValue(targetOrganizationID),
OrganizationID: UUIDValue(targetOrganizationID),
ModelID: UUIDValue(modelID),
})
Expand Down Expand Up @@ -586,7 +495,6 @@ resource "coderd_agents_default_model" "this" {
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("coderd_agents_default_model.this", tfjsonpath.New("id"), knownvalue.StringExact(orgID.String())),
statecheck.ExpectKnownValue("coderd_agents_default_model.this", tfjsonpath.New("organization_id"), knownvalue.StringExact(orgID.String())),
statecheck.ExpectKnownValue("coderd_agents_default_model.this", tfjsonpath.New("model_id"), knownvalue.StringExact(model.ID.String())),
statecheck.ExpectKnownValue("coderd_agents_model.test", tfjsonpath.New("organization_id"), knownvalue.StringExact(orgID.String())),
Expand Down Expand Up @@ -699,7 +607,6 @@ resource "coderd_agents_default_model" "default" {
{
Config: cfg("sonnet"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("coderd_agents_default_model.default", "id", organizationID.String()),
resource.TestCheckResourceAttr("coderd_agents_default_model.default", "organization_id", organizationID.String()),
resource.TestCheckResourceAttrPair("coderd_agents_default_model.default", "model_id", "coderd_agents_model.sonnet", "id"),
checkServerDefaultMatchesResource(ctx, t, client, organizationID, "coderd_agents_default_model.default"),
Expand All @@ -721,10 +628,11 @@ resource "coderd_agents_default_model" "default" {
},
{
// Import by organization name; Read resolves its current default.
ResourceName: "coderd_agents_default_model.default",
ImportState: true,
ImportStateVerify: true,
ImportStateId: organization.Name,
ResourceName: "coderd_agents_default_model.default",
ImportState: true,
ImportStateVerify: true,
ImportStateId: organization.Name,
ImportStateVerifyIdentifierAttribute: "organization_id",
},
},
})
Expand Down Expand Up @@ -864,10 +772,8 @@ resource "coderd_agents_default_model" "other_org" {
{
Config: cfg,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("coderd_agents_default_model.default_org", "id", defaultOrganizationID.String()),
resource.TestCheckResourceAttr("coderd_agents_default_model.default_org", "organization_id", defaultOrganizationID.String()),
resource.TestCheckResourceAttr("coderd_agents_default_model.default_org", "model_id", defaultOrgSecond.ID.String()),
resource.TestCheckResourceAttr("coderd_agents_default_model.other_org", "id", otherOrganization.ID.String()),
resource.TestCheckResourceAttr("coderd_agents_default_model.other_org", "organization_id", otherOrganization.ID.String()),
resource.TestCheckResourceAttr("coderd_agents_default_model.other_org", "model_id", otherOrgFirst.ID.String()),
checkServerDefaultMatchesResource(ctx, t, client, defaultOrganizationID, "coderd_agents_default_model.default_org"),
Expand Down
14 changes: 0 additions & 14 deletions internal/provider/ai_model_price_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ type AIModelPriceResource struct {
}

type AIModelPriceResourceModel struct {
ID types.String `tfsdk:"id"`
ProviderType types.String `tfsdk:"provider_type"`
Model types.String `tfsdk:"model"`
InputPrice types.Int64 `tfsdk:"input_price"`
Expand Down Expand Up @@ -188,13 +187,6 @@ func (r *AIModelPriceResource) Schema(ctx context.Context, req resource.SchemaRe
"-> If Coder already has a different custom price for the same model, the plan fails. Import that price to manage it with Terraform.\n\n" +
"~> **Warning**\nDestroying this resource only removes it from the Terraform state. The price stays in effect, because Coder has no API to delete a custom price.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "Model price ID, in the form `<provider_type>/<model>`.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"provider_type": schema.StringAttribute{
MarkdownDescription: "AI provider type the price applies to. Valid values are `openai`, `anthropic`, `azure`, `bedrock`, `google`, `openrouter`, `vercel`, and `copilot`. " +
"`openai-compat` is not currently supported. " +
Expand Down Expand Up @@ -314,8 +306,6 @@ func (r *AIModelPriceResource) Create(ctx context.Context, req resource.CreateRe
resp.Diagnostics.Append(aiModelPriceDiag("set", id, err)...)
return
}

plan.ID = types.StringValue(id)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

Expand All @@ -340,7 +330,6 @@ func (r *AIModelPriceResource) Read(ctx context.Context, req resource.ReadReques
return
}

state.ID = types.StringValue(id)
state.InputPrice = types.Int64PointerValue(price.InputPrice)
state.OutputPrice = types.Int64PointerValue(price.OutputPrice)
state.CacheReadPrice = types.Int64PointerValue(price.CacheReadPrice)
Expand All @@ -364,8 +353,6 @@ func (r *AIModelPriceResource) Update(ctx context.Context, req resource.UpdateRe
resp.Diagnostics.Append(aiModelPriceDiag("update", id, err)...)
return
}

plan.ID = types.StringValue(id)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

Expand Down Expand Up @@ -394,7 +381,6 @@ func (r *AIModelPriceResource) ImportState(ctx context.Context, req resource.Imp
return
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("provider_type"), providerType)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("model"), model)...)
}
19 changes: 10 additions & 9 deletions internal/provider/ai_model_price_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,6 @@ resource "coderd_ai_model_price" "conflict" {
Steps: []resource.TestStep{
{
Config: cfg(100, 200, "null"),
Check: resource.TestCheckResourceAttr("coderd_ai_model_price.slash", "id", "openrouter/"+slashModel),
},
{
Config: cfg(150, 250, "0"),
Expand All @@ -262,16 +261,18 @@ resource "coderd_ai_model_price" "conflict" {
},
},
{
ResourceName: "coderd_ai_model_price.test",
ImportState: true,
ImportStateId: "anthropic/" + model,
ImportStateVerify: true,
ResourceName: "coderd_ai_model_price.test",
ImportState: true,
ImportStateId: "anthropic/" + model,
ImportStateVerify: true,
ImportStateVerifyIdentifierAttribute: "model",
},
{
ResourceName: "coderd_ai_model_price.slash",
ImportState: true,
ImportStateId: "openrouter/" + slashModel,
ImportStateVerify: true,
ResourceName: "coderd_ai_model_price.slash",
ImportState: true,
ImportStateId: "openrouter/" + slashModel,
ImportStateVerify: true,
ImportStateVerifyIdentifierAttribute: "model",
},
{
PreConfig: func() { upsert("openai", conflictModel, 1000000) },
Expand Down