Skip to content

Commit 2fd48f1

Browse files
Add support for in-place updates of application type versions and enhance error handling for existing application types
1 parent 62ecf62 commit 2fd48f1

File tree

6 files changed

+106
-21
lines changed

6 files changed

+106
-21
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ provider "servicefabric" {
4646
client_id = "22222222-2222-2222-2222-222222222222"
4747
client_secret = var.service_principal_secret
4848
# default_credential_type = "azure_cli" # Optional override of the DefaultAzureCredential chain
49+
# allow_application_type_version_updates = true
4950
}
5051
```
5152

5253
Optional provider argument `application_recreate_on_upgrade` (default `true`) controls whether replacing an existing application triggers a Service Fabric upgrade with ForceRestart instead of deleting the application.
54+
Set `allow_application_type_version_updates = true` to enable in-place updates of `servicefabric_application_type` versions during Terraform apply (the previous version remains registered in the cluster unless you unprovision it manually).
5355

5456
### Authentication Notes
5557

@@ -150,6 +152,7 @@ provider "servicefabric" {
150152
client_id = "22222222-2222-2222-2222-222222222222"
151153
client_secret = var.service_principal_secret
152154
# default_credential_type = "managed_identity"
155+
# allow_application_type_version_updates = true
153156
}
154157
155158
resource "servicefabric_application_type" "sample" {

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ The following arguments are supported in the provider block:
5454
- `tenant_id`, `client_id`, `client_secret` (Optional) Entra credential details.
5555
- `default_credential_type` (Optional) Restrict the DefaultAzureCredential chain to a single credential (`default`, `environment`, `workload_identity`, `managed_identity`, `azure_cli`, `azure_developer_cli`, `azure_powershell`).
5656
- `application_recreate_on_upgrade` (Optional) When true, replacements of existing applications trigger an upgrade with ForceRestart instead of deleting and recreating the application.
57+
- `allow_application_type_version_updates` (Optional) Permit in-place updates to `servicefabric_application_type` versions. When true, Terraform will show an update instead of a replacement, even though the previous version remains registered unless manually unprovisioned.
5758

5859
## Resources
5960

docs/resources/application_type.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ The following arguments are supported:
2121
- `name` (Required) - Application type name as defined in the application
2222
manifest. Changing this recreates the resource.
2323
- `version` (Required) - Application type version. Changing this recreates the
24-
resource.
24+
resource unless the provider option `allow_application_type_version_updates`
25+
is enabled.
2526
- `package_uri` (Required) - HTTPS URI to the `.sfpkg` package. Usually a SAS
2627
URL in Azure Blob Storage. Changing this recreates the resource.
2728
- `retain_versions` (Optional) - Defaults to `false`. When enabled the resource
@@ -35,6 +36,11 @@ In addition to the arguments above, the following attributes are exported:
3536
- `id` - Combination of `name/version`.
3637
- `status` - Provisioning status reported by the cluster.
3738

39+
> **Note:** Enabling the provider option `allow_application_type_version_updates`
40+
> allows Terraform to update the `version` and `package_uri` in place. The
41+
> previous version remains registered in the cluster unless you unprovision it
42+
> manually or disable `retain_versions` and destroy the resource.
43+
3844
## Import
3945

4046
Application types can be imported using `name/version`, for example:

internal/provider/provider.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"crypto/tls"
66
"fmt"
77
"net/http"
8+
"sync/atomic"
89
"time"
910

1011
"github.com/hashicorp/terraform-plugin-framework/datasource"
@@ -39,14 +40,18 @@ type serviceFabricProviderModel struct {
3940
ClientCertificatePath types.String `tfsdk:"client_certificate_path"`
4041
ClientCertificatePassword types.String `tfsdk:"client_certificate_password"`
4142
ApplicationRecreateOnUpgrade types.Bool `tfsdk:"application_recreate_on_upgrade"`
43+
AllowApplicationTypeUpdates types.Bool `tfsdk:"allow_application_type_version_updates"`
4244
}
4345

4446
type serviceFabricProvider struct{}
4547

4648
type providerFeatures struct {
4749
ApplicationRecreateOnUpgrade bool
50+
AllowApplicationTypeUpdates bool
4851
}
4952

53+
var allowApplicationTypeUpdatesFlag atomic.Bool
54+
5055
type providerData struct {
5156
Client *servicefabric.Client
5257
Features providerFeatures
@@ -106,6 +111,10 @@ func (p *serviceFabricProvider) Schema(_ context.Context, _ provider.SchemaReque
106111
Optional: true,
107112
Description: "When true, replacements of existing applications trigger a Service Fabric upgrade with ForceRestart instead of deleting and recreating the application. Defaults to true.",
108113
},
114+
"allow_application_type_version_updates": providerschema.BoolAttribute{
115+
Optional: true,
116+
Description: "When true, version changes for servicefabric_application_type are applied in-place instead of forcing Terraform replacement. Use with caution: Terraform will treat the existing resource as updated even though the old version may remain registered in the cluster.",
117+
},
109118
},
110119
}
111120
}
@@ -243,6 +252,10 @@ func (p *serviceFabricProvider) Configure(ctx context.Context, req provider.Conf
243252
if !config.ApplicationRecreateOnUpgrade.IsNull() && !config.ApplicationRecreateOnUpgrade.IsUnknown() {
244253
features.ApplicationRecreateOnUpgrade = config.ApplicationRecreateOnUpgrade.ValueBool()
245254
}
255+
if !config.AllowApplicationTypeUpdates.IsNull() && !config.AllowApplicationTypeUpdates.IsUnknown() {
256+
features.AllowApplicationTypeUpdates = config.AllowApplicationTypeUpdates.ValueBool()
257+
}
258+
allowApplicationTypeUpdatesFlag.Store(features.AllowApplicationTypeUpdates)
246259

247260
providerData := &providerData{
248261
Client: client,
@@ -253,6 +266,10 @@ func (p *serviceFabricProvider) Configure(ctx context.Context, req provider.Conf
253266
resp.ResourceData = providerData
254267
}
255268

269+
func allowApplicationTypeUpdatesEnabled() bool {
270+
return allowApplicationTypeUpdatesFlag.Load()
271+
}
272+
256273
// Resources returns the resources implemented by the provider.
257274
func (p *serviceFabricProvider) Resources(_ context.Context) []func() resource.Resource {
258275
return []func() resource.Resource{

internal/provider/resource_application_type.go

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,32 @@ type applicationTypeResource struct {
2626
features providerFeatures
2727
}
2828

29+
func (r *applicationTypeResource) computeID(name, version string) string {
30+
if allowApplicationTypeUpdatesEnabled() || r.features.AllowApplicationTypeUpdates {
31+
return name
32+
}
33+
return fmt.Sprintf("%s/%s", name, version)
34+
}
35+
36+
type featureAwareVersionPlanModifier struct {
37+
resource *applicationTypeResource
38+
}
39+
40+
func (m featureAwareVersionPlanModifier) Description(_ context.Context) string {
41+
return "Requires replacement when provider feature allow_application_type_version_updates is disabled."
42+
}
43+
44+
func (m featureAwareVersionPlanModifier) MarkdownDescription(_ context.Context) string {
45+
return "Requires replacement when provider feature `allow_application_type_version_updates` is disabled."
46+
}
47+
48+
func (m featureAwareVersionPlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
49+
if allowApplicationTypeUpdatesEnabled() || (m.resource != nil && m.resource.features.AllowApplicationTypeUpdates) {
50+
return
51+
}
52+
stringplanmodifier.RequiresReplace().PlanModifyString(ctx, req, resp)
53+
}
54+
2955
type applicationTypeResourceModel struct {
3056
ID types.String `tfsdk:"id"`
3157
Name types.String `tfsdk:"name"`
@@ -58,16 +84,16 @@ func (r *applicationTypeResource) Schema(_ context.Context, _ resource.SchemaReq
5884
stringplanmodifier.RequiresReplace(),
5985
},
6086
},
61-
"version": rschema.StringAttribute{
62-
Required: true,
63-
Validators: []validator.String{
64-
stringvalidator.LengthAtLeast(1),
65-
},
66-
Description: "Application type version.",
67-
PlanModifiers: []planmodifier.String{
68-
stringplanmodifier.RequiresReplace(),
69-
},
87+
"version": rschema.StringAttribute{
88+
Required: true,
89+
Validators: []validator.String{
90+
stringvalidator.LengthAtLeast(1),
91+
},
92+
Description: "Application type version.",
93+
PlanModifiers: []planmodifier.String{
94+
featureAwareVersionPlanModifier{resource: r},
7095
},
96+
},
7197
"package_uri": rschema.StringAttribute{
7298
Required: true,
7399
Description: "Service Fabric package URI (SAS URL) pointing to the SFPKG.",
@@ -110,16 +136,23 @@ func (r *applicationTypeResource) Create(ctx context.Context, req resource.Creat
110136
}
111137

112138
if err := r.client.ProvisionApplicationType(ctx, plan.Name.ValueString(), plan.Version.ValueString(), plan.PackageURI.ValueString()); err != nil {
113-
resp.Diagnostics.AddError("Provisioning failed", err.Error())
114-
return
139+
if servicefabric.IsApplicationTypeAlreadyExistsError(err) {
140+
tflog.Info(ctx, "Application type version already provisioned", map[string]any{
141+
"name": plan.Name.ValueString(),
142+
"version": plan.Version.ValueString(),
143+
})
144+
} else {
145+
resp.Diagnostics.AddError("Provisioning failed", err.Error())
146+
return
147+
}
115148
}
116149

117150
tflog.Info(ctx, "Provisioned Service Fabric application type", map[string]any{
118151
"name": plan.Name.ValueString(),
119152
"version": plan.Version.ValueString(),
120153
})
121154

122-
plan.ID = types.StringValue(fmt.Sprintf("%s/%s", plan.Name.ValueString(), plan.Version.ValueString()))
155+
plan.ID = types.StringValue(r.computeID(plan.Name.ValueString(), plan.Version.ValueString()))
123156

124157
if err := r.readIntoState(ctx, &plan); err != nil {
125158
resp.Diagnostics.AddError("Failed to read application type", err.Error())
@@ -154,9 +187,7 @@ func (r *applicationTypeResource) readIntoState(ctx context.Context, state *appl
154187
return err
155188
}
156189
state.Status = types.StringValue(info.Status)
157-
if state.ID.IsNull() || state.ID.ValueString() == "" {
158-
state.ID = types.StringValue(fmt.Sprintf("%s/%s", state.Name.ValueString(), state.Version.ValueString()))
159-
}
190+
state.ID = types.StringValue(r.computeID(state.Name.ValueString(), state.Version.ValueString()))
160191
if state.RetainVersions.IsNull() || state.RetainVersions.IsUnknown() {
161192
state.RetainVersions = types.BoolValue(false)
162193
}
@@ -172,13 +203,23 @@ func (r *applicationTypeResource) Update(ctx context.Context, req resource.Updat
172203
return
173204
}
174205

206+
if plan.RetainVersions.IsNull() || plan.RetainVersions.IsUnknown() {
207+
plan.RetainVersions = state.RetainVersions
208+
}
209+
if plan.Status.IsNull() || plan.Status.IsUnknown() {
210+
plan.Status = state.Status
211+
}
212+
if plan.ID.IsNull() || plan.ID.IsUnknown() {
213+
plan.ID = state.ID
214+
}
215+
175216
versionChanged := plan.Version.ValueString() != state.Version.ValueString()
176217
packageChanged := plan.PackageURI.ValueString() != state.PackageURI.ValueString()
177218

178-
if versionChanged {
219+
if versionChanged && !r.features.AllowApplicationTypeUpdates {
179220
resp.Diagnostics.AddError(
180221
"Application type version change requires replacement",
181-
"Terraform planned an in-place update but version changes are handled via resource replacement. Set `lifecycle { create_before_destroy = true }` if you need zero-downtime upgrades.",
222+
"Terraform planned an in-place update but version changes are handled via resource replacement. Enable provider setting `allow_application_type_version_updates` to permit in-place updates, or set `lifecycle { create_before_destroy = true }` for zero-downtime upgrades.",
182223
)
183224
return
184225
}
@@ -189,11 +230,18 @@ func (r *applicationTypeResource) Update(ctx context.Context, req resource.Updat
189230
}
190231

191232
if err := r.client.ProvisionApplicationType(ctx, plan.Name.ValueString(), plan.Version.ValueString(), plan.PackageURI.ValueString()); err != nil {
192-
resp.Diagnostics.AddError("Provisioning failed", err.Error())
193-
return
233+
if servicefabric.IsApplicationTypeAlreadyExistsError(err) {
234+
tflog.Info(ctx, "Application type version already provisioned", map[string]any{
235+
"name": plan.Name.ValueString(),
236+
"version": plan.Version.ValueString(),
237+
})
238+
} else {
239+
resp.Diagnostics.AddError("Provisioning failed", err.Error())
240+
return
241+
}
194242
}
195243

196-
plan.ID = types.StringValue(fmt.Sprintf("%s/%s", plan.Name.ValueString(), plan.Version.ValueString()))
244+
plan.ID = types.StringValue(r.computeID(plan.Name.ValueString(), plan.Version.ValueString()))
197245

198246
if err := r.readIntoState(ctx, &plan); err != nil {
199247
resp.Diagnostics.AddError("Failed to read application type", err.Error())

internal/servicefabric/errors.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ func IsApplicationTypeInUseError(err error) bool {
4141
return false
4242
}
4343

44+
// IsApplicationTypeAlreadyExistsError reports whether the error corresponds to an
45+
// attempt to provision an application type version that already exists.
46+
func IsApplicationTypeAlreadyExistsError(err error) bool {
47+
var apiErr *APIError
48+
if errors.As(err, &apiErr) {
49+
return apiErr.StatusCode == http.StatusConflict && apiErr.Code == "FABRIC_E_APPLICATION_TYPE_ALREADY_EXISTS"
50+
}
51+
return false
52+
}
53+
4454
// IsApplicationUpgradeInProgressError reports whether an upgrade is already in progress.
4555
func IsApplicationUpgradeInProgressError(err error) bool {
4656
var apiErr *APIError

0 commit comments

Comments
 (0)