Skip to content

Commit

Permalink
config: Add source_file to incus_instance
Browse files Browse the repository at this point in the history
Signed-off-by: Lucas Bremgartner <[email protected]>
  • Loading branch information
breml committed Dec 3, 2024
1 parent bd33baa commit fe05afd
Showing 1 changed file with 158 additions and 2 deletions.
160 changes: 158 additions & 2 deletions internal/instance/resource_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package instance
import (
"context"
"fmt"
"os"
"sort"
"strings"
"time"
Expand All @@ -29,6 +30,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
incus "github.com/lxc/incus/v6/client"
"github.com/lxc/incus/v6/shared/api"
"github.com/mitchellh/go-homedir"

"github.com/lxc/terraform-provider-incus/internal/common"
"github.com/lxc/terraform-provider-incus/internal/errors"
Expand All @@ -52,6 +54,7 @@ type InstanceModel struct {
Remote types.String `tfsdk:"remote"`
Target types.String `tfsdk:"target"`
SourceInstance types.Object `tfsdk:"source_instance"`
SourceFile types.String `tfsdk:"source_file"`

// Computed.
IPv4 types.String `tfsdk:"ipv4_address"`
Expand Down Expand Up @@ -204,6 +207,16 @@ func (r InstanceResource) Schema(_ context.Context, _ resource.SchemaRequest, re
},
},

"source_file": schema.StringAttribute{
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},

// Computed.

"ipv4_address": schema.StringAttribute{
Expand Down Expand Up @@ -370,13 +383,76 @@ func (r InstanceResource) ValidateConfig(ctx context.Context, req resource.Valid
)
}

if !config.Image.IsNull() && !config.SourceInstance.IsNull() {
if !atMostOneOf(config.Image, config.SourceFile, config.SourceInstance) {
resp.Diagnostics.AddError(
"Invalid Configuration",
"Only image or source_instance can be set.",
"At most one of image, source_file and source_instance can be set.",
)
return
}

if !config.SourceFile.IsNull() {
// Instances from source_file are mutually exclusive with a series of other attributes.
if !config.Description.IsNull() ||
!config.Type.IsNull() ||
!config.Ephemeral.IsNull() ||
!config.Profiles.IsNull() ||
!config.Files.IsNull() ||
!config.Config.IsNull() {
resp.Diagnostics.AddError(
"Invalid Configuration",
"Attribute source_file is mutually exclusive with description, type, ephemeral, profiles, file and config.",
)
return
}

// With `incus import`, a storage pool can be provided optionally.
// In order to support the same behavior with source_file,
// a single device entry of type `disk` is allowed with exactly two properties
// `path` and `pool` being set. For `path`, the only accepted value is `/`.
if len(config.Devices.Elements()) > 0 {
if len(config.Devices.Elements()) > 1 {
resp.Diagnostics.AddError(
"Invalid Configuration",
"Only one device entry is supported with source_file.",
)
return
}

deviceList := make([]common.DeviceModel, 0, 1)
diags = config.Devices.ElementsAs(ctx, &deviceList, false)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

if len(deviceList[0].Properties.Elements()) != 2 {
resp.Diagnostics.AddError(
"Invalid Configuration",
`Exactly two device properties named "path" and "pool" need to be provided with source_file.`,
)
return
}

properties := make(map[string]string, 2)
diags = deviceList[0].Properties.ElementsAs(ctx, &properties, false)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

_, poolOK := properties["pool"]
path, pathOK := properties["path"]

if !poolOK || !pathOK || path != "/" {
resp.Diagnostics.AddError(
"Invalid Configuration",
`Exactly two device properties named "path" and "pool" need to be provided with source_file. For "path", the only accepted value is "/".`,
)
return
}
}
}
}

func (r InstanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
Expand All @@ -400,6 +476,9 @@ func (r InstanceResource) Create(ctx context.Context, req resource.CreateRequest
if !plan.Image.IsNull() {
diags = r.createInstanceFromImage(ctx, server, plan)
resp.Diagnostics.Append(diags...)
} else if !plan.SourceFile.IsNull() {
diags = r.createInstanceFromSourceFile(ctx, server, plan)
resp.Diagnostics.Append(diags...)
} else if !plan.SourceInstance.IsNull() {
diags = r.createInstanceFromSourceInstance(ctx, server, plan)
resp.Diagnostics.Append(diags...)
Expand Down Expand Up @@ -794,6 +873,13 @@ func (r InstanceResource) SyncState(ctx context.Context, tfState *tfsdk.State, s
return respDiags
}

if !m.SourceFile.IsNull() && !m.Devices.IsNull() {
// Using device to signal the storage pool is a special case, which is not
// reflected on the instance state and therefore we need to compensate here
// in order to prevent inconsistent provider results.
devices = m.Devices
}

m.Name = types.StringValue(instance.Name)
m.Type = types.StringValue(instance.Type)
m.Description = types.StringValue(instance.Description)
Expand Down Expand Up @@ -888,6 +974,66 @@ func (r InstanceResource) createInstanceFromImage(ctx context.Context, server in
return diags
}

func (r InstanceResource) createInstanceFromSourceFile(ctx context.Context, server incus.InstanceServer, plan InstanceModel) diag.Diagnostics {
var diags diag.Diagnostics

name := plan.Name.ValueString()

var poolName string

if len(plan.Devices.Elements()) > 0 {
// Only one device is expected, this is ensured by ValidateConfig.
deviceList := make([]common.DeviceModel, 0, 1)
diags = plan.Devices.ElementsAs(ctx, &deviceList, false)
if diags.HasError() {
return diags
}

// Exactly two properties named "path" and "pool" are expected, this is ensured by ValidateConfig.
properties := make(map[string]string, 2)
diags = deviceList[0].Properties.ElementsAs(ctx, &properties, false)
if diags.HasError() {
return diags
}

poolName = properties["pool"]
}

srcFile := plan.SourceFile.ValueString()

path, err := homedir.Expand(srcFile)
if err != nil {
diags.AddError(fmt.Sprintf("Failed to determine source_file: %q", srcFile), err.Error())
return diags
}

file, err := os.Open(path)
if err != nil {
diags.AddError(fmt.Sprintf("Failed to open source_file: %q", path), err.Error())
return diags
}

defer func() { _ = file.Close() }()

createArgs := incus.InstanceBackupArgs{
BackupFile: file,
PoolName: poolName,
Name: name,
}

op, err := server.CreateInstanceFromBackup(createArgs)
if err == nil {
err = op.Wait()
}

if err != nil {
diags.AddError(fmt.Sprintf("Failed to create instance: %q", name), err.Error())
return diags
}

return diags
}

func (r InstanceResource) createInstanceFromSourceInstance(ctx context.Context, destServer incus.InstanceServer, plan InstanceModel) diag.Diagnostics {
var diags diag.Diagnostics
var sourceInstanceModel SourceInstanceModel
Expand Down Expand Up @@ -1426,3 +1572,13 @@ func getAddresses(name string, entry api.InstanceStateNetwork) (string, string,

return ipv4, ipv6, entry.Hwaddr, name, true
}

func atMostOneOf(in ...interface{ IsNull() bool }) bool {
var count int
for _, v := range in {
if !v.IsNull() {
count++
}
}
return count <= 1
}

0 comments on commit fe05afd

Please sign in to comment.