Skip to content

Commit f369e49

Browse files
committed
fix: add configurable domain update shutdown timeout
Resolves: #1298
1 parent b26618a commit f369e49

4 files changed

Lines changed: 294 additions & 76 deletions

File tree

TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [ ] Review/adjust the generated docs YAML once the new guidance is in place to confirm correctness
88
- [x] Improve the docgen prompt to surface valid values/examples using RNG context and the IR paths (plan drafted in `internal/codegen/README.md`)
99
- [x] Keep the codegen README focused on tricky cases and exceptions rather than repeating the main README
10+
- [x] Add a shared domain stop helper and configurable update shutdown timeout for running-domain updates
1011
- [ ] Upgrade docgen/docindex prompts and inputs:
1112
- Enrich FieldContext with optional/required/computed flags, presence/string-to-bool/flattening info, valid values/patterns, and union notes (one-of branches).
1213
- Include docindex URLs as `reference` instead of forcing empty refs; only leave blank when nothing is known.

internal/provider/domain_resource.go

Lines changed: 138 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type DomainResourceModel struct {
4040
Running types.Bool `tfsdk:"running"`
4141
Autostart types.Bool `tfsdk:"autostart"`
4242
Create types.Object `tfsdk:"create"`
43+
Update types.Object `tfsdk:"update"`
4344
Destroy types.Object `tfsdk:"destroy"`
4445
}
4546

@@ -67,21 +68,27 @@ type DomainCreateModel struct {
6768
ResetNVRAM types.Bool `tfsdk:"reset_nvram"`
6869
}
6970

71+
// DomainUpdateModel describes domain update stop behavior.
72+
type DomainUpdateModel struct {
73+
Shutdown types.Object `tfsdk:"shutdown"`
74+
}
75+
7076
// DomainDestroyModel describes domain shutdown behavior
7177
type DomainDestroyModel struct {
7278
Graceful types.Bool `tfsdk:"graceful"`
7379
Shutdown types.Object `tfsdk:"shutdown"`
7480
}
7581

76-
// DomainDestroyShutdownModel describes optional shutdown wait behavior.
77-
type DomainDestroyShutdownModel struct {
82+
// DomainShutdownModel describes optional shutdown wait behavior.
83+
type DomainShutdownModel struct {
7884
Timeout types.Int64 `tfsdk:"timeout"`
7985
}
8086

81-
type domainDestroyOptions struct {
87+
type domainStopOptions struct {
8288
Flags golibvirt.DomainDestroyFlagsValues
8389
ShutdownEnabled bool
8490
ShutdownTimeout time.Duration
91+
ForceOnTimeout bool
8592
}
8693

8794
type domainPlanData struct {
@@ -458,9 +465,59 @@ func domainDestroyFlagsFromDestroy(ctx context.Context, destroyVal types.Object)
458465
return flags, nil
459466
}
460467

461-
func domainDestroyOptionsFromDestroy(ctx context.Context, destroyVal types.Object) (domainDestroyOptions, diag.Diagnostics) {
468+
func domainShutdownTimeoutFromObject(ctx context.Context, shutdownVal types.Object, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) {
469+
if shutdownVal.IsNull() || shutdownVal.IsUnknown() {
470+
return defaultTimeout, nil
471+
}
472+
473+
var shutdownModel DomainShutdownModel
474+
diags := shutdownVal.As(ctx, &shutdownModel, basetypes.ObjectAsOptions{})
475+
if diags.HasError() {
476+
return defaultTimeout, diags
477+
}
478+
479+
if !shutdownModel.Timeout.IsNull() && !shutdownModel.Timeout.IsUnknown() {
480+
timeout := shutdownModel.Timeout.ValueInt64()
481+
if timeout > 0 {
482+
return time.Duration(timeout) * time.Second, nil
483+
}
484+
}
485+
486+
return defaultTimeout, nil
487+
}
488+
489+
func domainUpdateOptionsFromUpdate(ctx context.Context, updateVal types.Object) (domainStopOptions, diag.Diagnostics) {
490+
options := domainStopOptions{
491+
ShutdownEnabled: true,
492+
ShutdownTimeout: 30 * time.Second,
493+
ForceOnTimeout: true,
494+
}
495+
496+
if updateVal.IsNull() || updateVal.IsUnknown() {
497+
return options, nil
498+
}
499+
500+
var updateModel DomainUpdateModel
501+
diags := updateVal.As(ctx, &updateModel, basetypes.ObjectAsOptions{})
502+
if diags.HasError() {
503+
return options, diags
504+
}
505+
506+
if !updateModel.Shutdown.IsNull() && !updateModel.Shutdown.IsUnknown() {
507+
timeout, timeoutDiags := domainShutdownTimeoutFromObject(ctx, updateModel.Shutdown, options.ShutdownTimeout)
508+
diags.Append(timeoutDiags...)
509+
if diags.HasError() {
510+
return options, diags
511+
}
512+
options.ShutdownTimeout = timeout
513+
}
514+
515+
return options, diags
516+
}
517+
518+
func domainDestroyOptionsFromDestroy(ctx context.Context, destroyVal types.Object) (domainStopOptions, diag.Diagnostics) {
462519
flags, diags := domainDestroyFlagsFromDestroy(ctx, destroyVal)
463-
options := domainDestroyOptions{
520+
options := domainStopOptions{
464521
Flags: flags,
465522
}
466523
if diags.HasError() || destroyVal.IsNull() || destroyVal.IsUnknown() {
@@ -476,19 +533,12 @@ func domainDestroyOptionsFromDestroy(ctx context.Context, destroyVal types.Objec
476533
if !destroyModel.Shutdown.IsNull() && !destroyModel.Shutdown.IsUnknown() {
477534
options.ShutdownEnabled = true
478535
options.ShutdownTimeout = 30 * time.Second
479-
480-
var shutdownModel DomainDestroyShutdownModel
481-
diags = destroyModel.Shutdown.As(ctx, &shutdownModel, basetypes.ObjectAsOptions{})
536+
timeout, timeoutDiags := domainShutdownTimeoutFromObject(ctx, destroyModel.Shutdown, options.ShutdownTimeout)
537+
diags.Append(timeoutDiags...)
482538
if diags.HasError() {
483539
return options, diags
484540
}
485-
486-
if !shutdownModel.Timeout.IsNull() && !shutdownModel.Timeout.IsUnknown() {
487-
timeout := shutdownModel.Timeout.ValueInt64()
488-
if timeout > 0 {
489-
options.ShutdownTimeout = time.Duration(timeout) * time.Second
490-
}
491-
}
541+
options.ShutdownTimeout = timeout
492542
}
493543

494544
return options, nil
@@ -523,6 +573,22 @@ func (r *DomainResource) Schema(ctx context.Context, req resource.SchemaRequest,
523573
"reset_nvram": schema.BoolAttribute{Optional: true},
524574
},
525575
},
576+
"update": schema.SingleNestedAttribute{
577+
Description: "Update behavior when Terraform must stop the domain before redefining it.",
578+
Optional: true,
579+
Attributes: map[string]schema.Attribute{
580+
"shutdown": schema.SingleNestedAttribute{
581+
Description: "Experimental: request a guest shutdown and wait for shutoff before forcing a stop during update. Subject to change in future releases.",
582+
Optional: true,
583+
Attributes: map[string]schema.Attribute{
584+
"timeout": schema.Int64Attribute{
585+
Description: "Experimental: seconds to wait for guest shutdown before forcing a stop during update. Defaults to 30.",
586+
Optional: true,
587+
},
588+
},
589+
},
590+
},
591+
},
526592
"destroy": schema.SingleNestedAttribute{
527593
Description: "Destroy behavior when Terraform removes the domain.",
528594
Optional: true,
@@ -574,6 +640,41 @@ func (r *DomainResource) Configure(ctx context.Context, req resource.ConfigureRe
574640
r.client = client
575641
}
576642

643+
func (r *DomainResource) stopDomainIfRunning(domain golibvirt.Domain, options domainStopOptions) (bool, error) {
644+
domainState, _, err := r.client.Libvirt().DomainGetState(domain, 0)
645+
if err != nil {
646+
return false, fmt.Errorf("check domain state: %w", err)
647+
}
648+
649+
if uint32(domainState) != uint32(golibvirt.DomainRunning) {
650+
return false, nil
651+
}
652+
653+
if options.ShutdownEnabled {
654+
if err := r.client.Libvirt().DomainShutdown(domain); err != nil {
655+
return false, fmt.Errorf("request guest shutdown: %w", err)
656+
}
657+
658+
if err := waitForDomainState(r.client, domain, uint32(golibvirt.DomainShutoff), options.ShutdownTimeout); err != nil {
659+
if !options.ForceOnTimeout {
660+
return true, fmt.Errorf("wait for shutdown: %w", err)
661+
}
662+
663+
if destroyErr := r.client.Libvirt().DomainDestroyFlags(domain, options.Flags); destroyErr != nil {
664+
return false, fmt.Errorf("force stop after shutdown timeout: %w", destroyErr)
665+
}
666+
}
667+
668+
return false, nil
669+
}
670+
671+
if err := r.client.Libvirt().DomainDestroyFlags(domain, options.Flags); err != nil {
672+
return false, fmt.Errorf("force stop running domain: %w", err)
673+
}
674+
675+
return false, nil
676+
}
677+
577678
// Create creates a new domain
578679
func (r *DomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
579680
var plan DomainResourceModel
@@ -709,6 +810,7 @@ func (r *DomainResource) Create(ctx context.Context, req resource.CreateRequest,
709810
Running: plan.Running,
710811
Autostart: plan.Autostart,
711812
Create: plan.Create,
813+
Update: plan.Update,
712814
Destroy: plan.Destroy,
713815
}
714816

@@ -882,33 +984,18 @@ func (r *DomainResource) Update(ctx context.Context, req resource.UpdateRequest,
882984
return
883985
}
884986

885-
domainState, _, err := r.client.Libvirt().DomainGetState(existingDomain, 0)
886-
if err != nil {
887-
resp.Diagnostics.AddError(
888-
"Failed to Get Domain State",
889-
"Failed to check if domain is running: "+err.Error(),
890-
)
987+
updateOptions, updateDiags := domainUpdateOptionsFromUpdate(ctx, plan.Update)
988+
resp.Diagnostics.Append(updateDiags...)
989+
if resp.Diagnostics.HasError() {
891990
return
892991
}
893992

894-
if uint32(domainState) == uint32(golibvirt.DomainRunning) {
895-
if err := r.client.Libvirt().DomainShutdown(existingDomain); err != nil {
896-
resp.Diagnostics.AddError(
897-
"Failed to Shutdown Domain",
898-
"Domain must be stopped before updating. Failed to shutdown: "+err.Error(),
899-
)
900-
return
901-
}
902-
903-
if err := waitForDomainState(r.client, existingDomain, uint32(golibvirt.DomainShutoff), 5*time.Second); err != nil {
904-
if err := r.client.Libvirt().DomainDestroy(existingDomain); err != nil {
905-
resp.Diagnostics.AddError(
906-
"Failed to Stop Domain",
907-
"Domain must be stopped before updating. Failed to force stop: "+err.Error(),
908-
)
909-
return
910-
}
911-
}
993+
if _, err := r.stopDomainIfRunning(existingDomain, updateOptions); err != nil {
994+
resp.Diagnostics.AddError(
995+
"Failed to Stop Domain",
996+
"Domain must be stopped before updating: "+err.Error(),
997+
)
998+
return
912999
}
9131000

9141001
domainXML, err := generated.DomainToXML(ctx, &planData.SanitizedModel)
@@ -1036,6 +1123,7 @@ func (r *DomainResource) Update(ctx context.Context, req resource.UpdateRequest,
10361123
Running: plan.Running,
10371124
Autostart: plan.Autostart,
10381125
Create: plan.Create,
1126+
Update: plan.Update,
10391127
Destroy: plan.Destroy,
10401128
}
10411129

@@ -1089,48 +1177,23 @@ func (r *DomainResource) Delete(ctx context.Context, req resource.DeleteRequest,
10891177
return
10901178
}
10911179

1092-
// Destroy (stop) the domain if it's running
1093-
domainState, _, err := r.client.Libvirt().DomainGetState(domain, 0)
1180+
timedOut, err := r.stopDomainIfRunning(domain, destroyOptions)
10941181
if err != nil {
1182+
if timedOut {
1183+
resp.Diagnostics.AddError(
1184+
"Timeout Waiting for Domain Shutdown",
1185+
fmt.Sprintf("Domain did not reach shutoff state within %s: %s", destroyOptions.ShutdownTimeout, err),
1186+
)
1187+
return
1188+
}
1189+
10951190
resp.Diagnostics.AddError(
1096-
"Failed to Get Domain State",
1097-
"Failed to check domain state: "+err.Error(),
1191+
"Failed to Destroy Domain",
1192+
"Failed to stop running domain: "+err.Error(),
10981193
)
10991194
return
11001195
}
11011196

1102-
// DomainState values: 0=nostate, 1=running, 2=blocked, 3=paused, 4=shutdown, 5=shutoff, 6=crashed, 7=pmsuspended
1103-
if uint32(domainState) == uint32(golibvirt.DomainRunning) {
1104-
if destroyOptions.ShutdownEnabled {
1105-
err = r.client.Libvirt().DomainShutdown(domain)
1106-
if err != nil {
1107-
resp.Diagnostics.AddError(
1108-
"Failed to Shutdown Domain",
1109-
"Failed to request guest shutdown: "+err.Error(),
1110-
)
1111-
return
1112-
}
1113-
1114-
err = waitForDomainState(r.client, domain, uint32(golibvirt.DomainShutoff), destroyOptions.ShutdownTimeout)
1115-
if err != nil {
1116-
resp.Diagnostics.AddError(
1117-
"Timeout Waiting for Domain Shutdown",
1118-
fmt.Sprintf("Domain did not reach shutoff state within %s: %s", destroyOptions.ShutdownTimeout, err),
1119-
)
1120-
return
1121-
}
1122-
} else {
1123-
err = r.client.Libvirt().DomainDestroyFlags(domain, destroyOptions.Flags)
1124-
if err != nil {
1125-
resp.Diagnostics.AddError(
1126-
"Failed to Destroy Domain",
1127-
"Failed to stop running domain: "+err.Error(),
1128-
)
1129-
return
1130-
}
1131-
}
1132-
}
1133-
11341197
libvirtVersion, err := r.client.Libvirt().ConnectGetLibVersion()
11351198
if err != nil {
11361199
resp.Diagnostics.AddError(

0 commit comments

Comments
 (0)