Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/resources/machine_configuration_apply.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ resource "talos_machine_configuration_apply" "this" {

> Note: Any changes to *on_destroy* block has to be applied first by running *terraform apply* first,
then a subsequent *terraform destroy* for the changes to take effect due to limitations in Terraform provider framework. (see [below for nested schema](#nestedatt--on_destroy))
- `prevent_uncontrolled_reboots` (Boolean) Only applicable when apply_mode is auto (default). When enabled, prevents uncontrolled reboots by automatically switching to staged mode when a reboot is required. A manual reboot will then be necessary
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))

### Read-Only
Expand Down
114 changes: 104 additions & 10 deletions pkg/talos/talos_machine_configuration_apply_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"crypto/tls"
"errors"
"fmt"
"strings"
"time"

Expand Down Expand Up @@ -58,16 +59,17 @@ type talosMachineConfigurationApplyResourceModelV0 struct {
}

type talosMachineConfigurationApplyResourceModelV1 struct { //nolint:govet
ID types.String `tfsdk:"id"`
ApplyMode types.String `tfsdk:"apply_mode"`
Node types.String `tfsdk:"node"`
Endpoint types.String `tfsdk:"endpoint"`
ClientConfiguration clientConfiguration `tfsdk:"client_configuration"`
MachineConfigurationInput types.String `tfsdk:"machine_configuration_input"`
OnDestroy *onDestroyOptions `tfsdk:"on_destroy"`
MachineConfiguration types.String `tfsdk:"machine_configuration"`
ConfigPatches []types.String `tfsdk:"config_patches"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
ID types.String `tfsdk:"id"`
ApplyMode types.String `tfsdk:"apply_mode"`
PreventUncontrolledReboots types.Bool `tfsdk:"prevent_uncontrolled_reboots"`
Node types.String `tfsdk:"node"`
Endpoint types.String `tfsdk:"endpoint"`
ClientConfiguration clientConfiguration `tfsdk:"client_configuration"`
MachineConfigurationInput types.String `tfsdk:"machine_configuration_input"`
OnDestroy *onDestroyOptions `tfsdk:"on_destroy"`
MachineConfiguration types.String `tfsdk:"machine_configuration"`
ConfigPatches []types.String `tfsdk:"config_patches"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}

type onDestroyOptions struct {
Expand Down Expand Up @@ -106,6 +108,13 @@ func (p *talosMachineConfigurationApplyResource) Schema(ctx context.Context, _ r
},
Default: stringdefault.StaticString("auto"),
},
"prevent_uncontrolled_reboots": schema.BoolAttribute{
Optional: true,
Computed: true,
Description: "Only applicable when apply_mode is auto (default). When enabled, prevents uncontrolled reboots " +
"by automatically switching to staged mode when a reboot is required. A manual reboot will then be necessary",
Default: booldefault.StaticBool(false),
},
"node": schema.StringAttribute{
Required: true,
Description: "The name of the node to bootstrap",
Expand Down Expand Up @@ -434,6 +443,89 @@ func (p *talosMachineConfigurationApplyResource) Delete(ctx context.Context, req
}
}

func (p *talosMachineConfigurationApplyResource) handleRebootPrevention(
ctx context.Context,
req resource.ModifyPlanRequest,
resp *resource.ModifyPlanResponse,
planState *talosMachineConfigurationApplyResourceModelV1,
cfgBytes []byte,
) {
if planState.PreventUncontrolledReboots.IsNull() ||
!planState.PreventUncontrolledReboots.ValueBool() ||
req.State.Raw.IsNull() {
return
}

applyMode := strings.ToLower(planState.ApplyMode.ValueString())
if applyMode == "" || planState.ApplyMode.IsNull() || planState.ApplyMode.IsUnknown() {
applyMode = "auto"
}

if applyMode != "auto" || planState.Node.IsUnknown() {
return
}

endpoint := planState.Endpoint.ValueString()
if endpoint == "" || planState.Endpoint.IsNull() || planState.Endpoint.IsUnknown() {
endpoint = planState.Node.ValueString()
}

talosClientConfig, err := talosClientTFConfigToTalosClientConfig(
"dynamic",
planState.ClientConfiguration.CA.ValueString(),
planState.ClientConfiguration.Cert.ValueString(),
planState.ClientConfiguration.Key.ValueString(),
)
if err != nil {
resp.Diagnostics.AddWarning(
"Cannot check reboot requirement",
fmt.Sprintf("Node %s: Failed to create Talos client config: %v. Will use requested mode 'auto' (may reboot).",
planState.Node.ValueString(), err),
)

return
}

var needsReboot bool

err = talosClientOp(ctx, endpoint, planState.Node.ValueString(), talosClientConfig,
func(nodeCtx context.Context, c *client.Client) error {
applyResp, applyErr := c.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
Mode: machineapi.ApplyConfigurationRequest_AUTO,
Data: cfgBytes,
DryRun: true,
})
if applyErr != nil {
return applyErr
}

if len(applyResp.Messages) > 0 {
needsReboot = (applyResp.Messages[0].Mode == machineapi.ApplyConfigurationRequest_REBOOT)
}

return nil
},
)
if err != nil {
resp.Diagnostics.AddWarning(
"Cannot check reboot requirement",
fmt.Sprintf("Node %s: Dry-run API call failed: %v. Will use requested mode 'auto' (may reboot).",
planState.Node.ValueString(), err),
)

return
}

if needsReboot {
resp.Plan.SetAttribute(ctx, path.Root("apply_mode"), "staged")
resp.Diagnostics.AddWarning(
"Reboot prevented - switched to staged mode",
fmt.Sprintf("Node %s: Configuration requires reboot. Mode automatically changed to 'staged'. Manually reboot with: talosctl reboot --nodes %s",
planState.Node.ValueString(), planState.Node.ValueString()),
)
}
}

func (p *talosMachineConfigurationApplyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocyclo,cyclop
// delete is a no-op
if req.Plan.Raw.IsNull() {
Expand Down Expand Up @@ -546,6 +638,8 @@ func (p *talosMachineConfigurationApplyResource) ModifyPlan(ctx context.Context,
if diags.HasError() {
return
}

p.handleRebootPrevention(ctx, req, resp, &planState, cfgBytes)
}
}

Expand Down
51 changes: 51 additions & 0 deletions pkg/talos/talos_machine_configuration_apply_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,28 @@ func TestAccTalosMachineConfigurationApplyResource(t *testing.T) {
})
}

func TestAccTalosMachineConfigurationApplyResourcePreventReboots(t *testing.T) {
rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)

resource.ParallelTest(t, resource.TestCase{
ExternalProviders: map[string]resource.ExternalProvider{
"libvirt": {
Source: "dmacvicar/libvirt",
},
},
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccTalosMachineConfigurationApplyResourceConfigWithPreventReboots("talos", rName),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("talos_machine_configuration_apply.prevent_reboots", "id", "machine_configuration_apply"),
resource.TestCheckResourceAttr("talos_machine_configuration_apply.prevent_reboots", "prevent_uncontrolled_reboots", "true"),
),
},
},
})
}

func TestAccTalosMachineConfigurationApplyResourceUpgrade(t *testing.T) {
// ref: https://github.com/hashicorp/terraform-plugin-testing/pull/118
t.Skip("skipping until TF test framework has a way to remove state resource")
Expand Down Expand Up @@ -142,3 +164,32 @@ func testAccTalosMachineConfigurationApplyResourceConfigV1(providerName, rName s

return config.render()
}

func testAccTalosMachineConfigurationApplyResourceConfigWithPreventReboots(providerName, rName string) string {
config := dynamicConfig{
Provider: providerName,
ResourceName: rName,
WithApplyConfig: true,
WithBootstrap: false,
}

baseConfig := config.render()

return baseConfig + `
resource "talos_machine_configuration_apply" "prevent_reboots" {
client_configuration = talos_machine_secrets.this.client_configuration
machine_configuration_input = data.talos_machine_configuration.this.machine_configuration
node = libvirt_domain.cp.network_interface[0].addresses[0]
prevent_uncontrolled_reboots = true
config_patches = [
yamlencode({
machine = {
install = {
disk = data.talos_machine_disks.this.disks[0].dev_path
}
}
}),
]
}
`
}