Skip to content

Commit 5b2d453

Browse files
authored
fix: add UseStateForUnknown to Optional+Computed attributes (#5)
Optional+Computed attributes without a UseStateForUnknown plan modifier are planned as 'unknown' on any in-place update when the config omits them, and the resource then sends the Go zero value (false / "" / 0), silently clobbering server-managed state. The concrete incident: changing an aigateway_provider's api_version disabled the whole provider (enabled went unknown -> false -> 'Provider is disabled' 503 on every request). Add UseStateForUnknown to the affected Optional+Computed attributes so they keep their prior state value across unrelated updates: - provider: enabled, auth_type, api_version - model: enabled, is_default, capability, model_type, and the three pricing microdollar fields (an unrelated edit would otherwise zero gateway-fetched prices) - cost_center: currency - deployment_group: strategy, and nested weight/priority/enabled go build / vet / test / gofmt all clean.
1 parent 8034863 commit 5b2d453

4 files changed

Lines changed: 74 additions & 39 deletions

File tree

internal/provider/cost_center_resource.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ func (r *costCenterResource) Schema(_ context.Context, _ resource.SchemaRequest,
5252
Description: "Cost center (budget) name, unique per tenant.",
5353
},
5454
"currency": schema.StringAttribute{
55-
Optional: true,
56-
Computed: true,
57-
Description: "ISO 4217 currency code. Defaults to USD.",
55+
Optional: true,
56+
Computed: true,
57+
Description: "ISO 4217 currency code. Defaults to USD.",
58+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
5859
},
5960
"description": schema.StringAttribute{
6061
Optional: true,

internal/provider/deployment_group_resource.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"github.com/hashicorp/terraform-plugin-framework/path"
77
"github.com/hashicorp/terraform-plugin-framework/resource"
88
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
911
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1012
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1113
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -75,9 +77,10 @@ func (r *deploymentGroupResource) Schema(_ context.Context, _ resource.SchemaReq
7577
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
7678
},
7779
"strategy": schema.StringAttribute{
78-
Optional: true,
79-
Computed: true,
80-
Description: "Load-balancer strategy: round_robin | weighted_random | latency_based | least_busy. Defaults to round_robin.",
80+
Optional: true,
81+
Computed: true,
82+
Description: "Load-balancer strategy: round_robin | weighted_random | latency_based | least_busy. Defaults to round_robin.",
83+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
8184
},
8285
"deployments": schema.ListNestedAttribute{
8386
Required: true,
@@ -97,19 +100,22 @@ func (r *deploymentGroupResource) Schema(_ context.Context, _ resource.SchemaReq
97100
Description: "Azure deployment name (azure_openai).",
98101
},
99102
"weight": schema.Int64Attribute{
100-
Optional: true,
101-
Computed: true,
102-
Description: "Traffic weight (1-100, higher = more traffic). Defaults to 100.",
103+
Optional: true,
104+
Computed: true,
105+
Description: "Traffic weight (1-100, higher = more traffic). Defaults to 100.",
106+
PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()},
103107
},
104108
"priority": schema.Int64Attribute{
105-
Optional: true,
106-
Computed: true,
107-
Description: "Priority tier (0 = highest). Lower tiers used only when higher are exhausted.",
109+
Optional: true,
110+
Computed: true,
111+
Description: "Priority tier (0 = highest). Lower tiers used only when higher are exhausted.",
112+
PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()},
108113
},
109114
"enabled": schema.BoolAttribute{
110-
Optional: true,
111-
Computed: true,
112-
Description: "Whether this deployment is enabled. Defaults to true.",
115+
Optional: true,
116+
Computed: true,
117+
Description: "Whether this deployment is enabled. Defaults to true.",
118+
PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()},
113119
},
114120
"timeout_seconds": schema.Int64Attribute{
115121
Optional: true,

internal/provider/model_resource.go

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"github.com/hashicorp/terraform-plugin-framework/path"
77
"github.com/hashicorp/terraform-plugin-framework/resource"
88
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
911
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1012
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1113
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -72,40 +74,53 @@ func (r *modelResource) Schema(_ context.Context, _ resource.SchemaRequest, resp
7274
Optional: true,
7375
Description: "Azure deployment name (required for azure_openai).",
7476
},
77+
// All of the following are Optional+Computed. UseStateForUnknown keeps
78+
// the prior state value when the config omits the attribute, instead
79+
// of planning "unknown" — which on an in-place update would be sent
80+
// as the zero value ("" / 0 / false), clobbering server-managed data.
81+
// Without it, e.g. editing display_name would zero the gateway-fetched
82+
// pricing, blank the capability, or disable the model.
7583
"capability": schema.StringAttribute{
76-
Optional: true,
77-
Computed: true,
78-
Description: "chat | embedding | image | audio. Defaults to chat.",
84+
Optional: true,
85+
Computed: true,
86+
Description: "chat | embedding | image | audio. Defaults to chat.",
87+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
7988
},
8089
"model_type": schema.StringAttribute{
81-
Optional: true,
82-
Computed: true,
83-
Description: "chat | embedding | image | audio. Defaults to chat.",
90+
Optional: true,
91+
Computed: true,
92+
Description: "chat | embedding | image | audio. Defaults to chat.",
93+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
8494
},
8595
"input_per_1m_tokens_microdollars": schema.Int64Attribute{
86-
Optional: true,
87-
Computed: true,
88-
Description: "Input token price per 1M tokens in microdollars.",
96+
Optional: true,
97+
Computed: true,
98+
Description: "Input token price per 1M tokens in microdollars.",
99+
PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()},
89100
},
90101
"output_per_1m_tokens_microdollars": schema.Int64Attribute{
91-
Optional: true,
92-
Computed: true,
93-
Description: "Output token price per 1M tokens in microdollars.",
102+
Optional: true,
103+
Computed: true,
104+
Description: "Output token price per 1M tokens in microdollars.",
105+
PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()},
94106
},
95107
"cached_input_per_1m_tokens_microdollars": schema.Int64Attribute{
96-
Optional: true,
97-
Computed: true,
98-
Description: "Cached input token price per 1M tokens in microdollars.",
108+
Optional: true,
109+
Computed: true,
110+
Description: "Cached input token price per 1M tokens in microdollars.",
111+
PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()},
99112
},
100113
"enabled": schema.BoolAttribute{
101-
Optional: true,
102-
Computed: true,
103-
Description: "Whether the model is enabled. Defaults to true.",
114+
Optional: true,
115+
Computed: true,
116+
Description: "Whether the model is enabled. Defaults to true.",
117+
PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()},
104118
},
105119
"is_default": schema.BoolAttribute{
106-
Optional: true,
107-
Computed: true,
108-
Description: "Whether this is the tenant default model.",
120+
Optional: true,
121+
Computed: true,
122+
Description: "Whether this is the tenant default model.",
123+
PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()},
109124
},
110125
"price_region": schema.StringAttribute{
111126
Optional: true,

internal/provider/provider_resource.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/hashicorp/terraform-plugin-framework/path"
77
"github.com/hashicorp/terraform-plugin-framework/resource"
88
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
910
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1011
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1112
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -69,6 +70,11 @@ func (r *providerResource) Schema(_ context.Context, _ resource.SchemaRequest, r
6970
Optional: true,
7071
Computed: true,
7172
Description: "Authentication type (apiKey | managedIdentity | none). Defaults to apiKey.",
73+
// Optional+Computed: when the config omits it, keep the prior
74+
// state value instead of planning "unknown". Without this the
75+
// planned value goes unknown on any in-place update and is sent
76+
// as "" (the zero value), clobbering the server default.
77+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
7278
},
7379
"credential": schema.StringAttribute{
7480
Optional: true,
@@ -84,9 +90,10 @@ func (r *providerResource) Schema(_ context.Context, _ resource.SchemaRequest, r
8490
Description: "GCP project id (Vertex AI).",
8591
},
8692
"api_version": schema.StringAttribute{
87-
Optional: true,
88-
Computed: true,
89-
Description: "API version (Azure OpenAI).",
93+
Optional: true,
94+
Computed: true,
95+
Description: "API version (Azure OpenAI).",
96+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
9097
},
9198
"managed_by": schema.StringAttribute{
9299
Optional: true,
@@ -96,6 +103,12 @@ func (r *providerResource) Schema(_ context.Context, _ resource.SchemaRequest, r
96103
Optional: true,
97104
Computed: true,
98105
Description: "Whether the provider is enabled. Defaults to true.",
106+
// CRITICAL: without UseStateForUnknown, an in-place update of any
107+
// other attribute (e.g. api_version) plans `enabled` as unknown,
108+
// so Update sends enabled=false (the bool zero value) and silently
109+
// DISABLES the provider. Keeping the prior state value prevents
110+
// that — a provider stays enabled across unrelated updates.
111+
PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()},
99112
},
100113
},
101114
}

0 commit comments

Comments
 (0)