Skip to content

Commit 42beed9

Browse files
authored
Merge pull request #130 from maveonair/default-profile-creation
Edit default profile on new project
2 parents 8f92d50 + 96a49f0 commit 42beed9

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed

internal/profile/resource_profile.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,49 @@ func (r *ProfileResource) Configure(_ context.Context, req resource.ConfigureReq
145145
r.provider = provider
146146
}
147147

148+
func (r *ProfileResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
149+
priorState := req.State
150+
plannedState := req.Plan
151+
152+
// Detection creation: no prior state
153+
if priorState.Raw.IsNull() {
154+
var profileName types.String
155+
diags := plannedState.GetAttribute(ctx, path.Root("name"), &profileName)
156+
if diags.HasError() {
157+
resp.Diagnostics.Append(diags...)
158+
return
159+
}
160+
161+
// We only print the warning here and take care of the correct changes in the ProfileResource#Create
162+
if profileName.ValueString() == "default" {
163+
resp.Diagnostics.AddWarning(
164+
"Default Profile Creation Attempted",
165+
"'default' profile cannot be created in an Incus project. Instead, its config and devices will be updated accordingly.",
166+
)
167+
}
168+
return
169+
}
170+
171+
// Detect deletion: no planned state
172+
if plannedState.Raw.IsNull() {
173+
var profileName types.String
174+
diags := priorState.GetAttribute(ctx, path.Root("name"), &profileName)
175+
if diags.HasError() {
176+
resp.Diagnostics.Append(diags...)
177+
return
178+
}
179+
180+
// We only print the warning here and take care of the correct changes in the ProfileResource#Delete
181+
if profileName.ValueString() == "default" {
182+
resp.Diagnostics.AddWarning(
183+
"Default Profile Deletion Attempted",
184+
"'default' profile cannot be deleted in an Incus project. Instead, its config and devices will be set to the Incus default profile state.",
185+
)
186+
}
187+
return
188+
}
189+
}
190+
148191
func (r ProfileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
149192
var plan ProfileModel
150193

@@ -154,6 +197,11 @@ func (r ProfileResource) Create(ctx context.Context, req resource.CreateRequest,
154197
return
155198
}
156199

200+
if plan.Name.ValueString() == "default" {
201+
r.importAndUpdateDefaultProfile(ctx, plan, resp)
202+
return
203+
}
204+
157205
remote := plan.Remote.ValueString()
158206
project := plan.Project.ValueString()
159207
server, err := r.provider.InstanceServer(remote, project, "")
@@ -276,6 +324,11 @@ func (r ProfileResource) Delete(ctx context.Context, req resource.DeleteRequest,
276324
return
277325
}
278326

327+
if state.Name.ValueString() == "default" {
328+
r.resetDefaultProfile(ctx, state, resp)
329+
return
330+
}
331+
279332
remote := state.Remote.ValueString()
280333
project := state.Project.ValueString()
281334
server, err := r.provider.InstanceServer(remote, project, "")
@@ -344,3 +397,126 @@ func (r ProfileResource) SyncState(ctx context.Context, tfState *tfsdk.State, se
344397

345398
return tfState.Set(ctx, &m)
346399
}
400+
401+
func (r ProfileResource) importAndUpdateDefaultProfile(ctx context.Context, plan ProfileModel, resp *resource.CreateResponse) {
402+
remote := plan.Remote.ValueString()
403+
project := plan.Project.ValueString()
404+
profileName := plan.Name.ValueString()
405+
406+
// 1. We import the default profile.
407+
var id string
408+
if project != "" {
409+
id = fmt.Sprintf("%s/%s", project, profileName)
410+
} else {
411+
id = profileName
412+
}
413+
414+
meta := common.ImportMetadata{
415+
ResourceName: "profile",
416+
RequiredFields: []string{"name"},
417+
}
418+
419+
fields, diag := meta.ParseImportID(id)
420+
if diag != nil {
421+
resp.Diagnostics.Append(diag)
422+
return
423+
}
424+
425+
for k, v := range fields {
426+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(k), v)...)
427+
}
428+
429+
// 2. After we have imported the default profile, we can now update it.
430+
server, err := r.provider.InstanceServer(remote, project, "")
431+
if err != nil {
432+
resp.Diagnostics.Append(errors.NewInstanceServerError(err))
433+
return
434+
}
435+
436+
_, etag, err := server.GetProfile(profileName)
437+
if err != nil {
438+
resp.Diagnostics.AddError(fmt.Sprintf("Failed to retrieve existing profile %q", profileName), err.Error())
439+
return
440+
}
441+
442+
config, diags := common.ToConfigMap(ctx, plan.Config)
443+
resp.Diagnostics.Append(diags...)
444+
445+
devices, diags := common.ToDeviceMap(ctx, plan.Devices)
446+
resp.Diagnostics.Append(diags...)
447+
448+
if resp.Diagnostics.HasError() {
449+
return
450+
}
451+
452+
profile := api.ProfilePut{
453+
Description: plan.Description.ValueString(),
454+
Config: config,
455+
Devices: devices,
456+
}
457+
458+
err = server.UpdateProfile(profileName, profile, etag)
459+
if err != nil {
460+
resp.Diagnostics.AddError(fmt.Sprintf("Failed to update profile %q", profileName), err.Error())
461+
return
462+
}
463+
464+
// 3. Sync Terraform state.
465+
diags = r.SyncState(ctx, &resp.State, server, plan)
466+
resp.Diagnostics.Append(diags...)
467+
}
468+
469+
func (r ProfileResource) resetDefaultProfile(ctx context.Context, plan ProfileModel, resp *resource.DeleteResponse) {
470+
remote := plan.Remote.ValueString()
471+
projectName := plan.Project.ValueString()
472+
profileName := plan.Name.ValueString()
473+
474+
server, err := r.provider.InstanceServer(remote, projectName, "")
475+
if err != nil {
476+
resp.Diagnostics.Append(errors.NewInstanceServerError(err))
477+
return
478+
}
479+
480+
_, etag, err := server.GetProfile(profileName)
481+
if err != nil {
482+
resp.Diagnostics.AddError(fmt.Sprintf("Failed to retrieve existing profile %q", profileName), err.Error())
483+
return
484+
}
485+
486+
devices, diags := common.ToDeviceMap(ctx, plan.Devices)
487+
resp.Diagnostics.Append(diags...)
488+
if resp.Diagnostics.HasError() {
489+
return
490+
}
491+
492+
defaultDevices := make(map[string]map[string]string)
493+
defaultConfig := make(map[string]string)
494+
495+
// We must keep the root device if features.profiles is set to false.
496+
project, _, err := server.GetProject(projectName)
497+
if err != nil {
498+
resp.Diagnostics.AddError(fmt.Sprintf("Failed to retrieve project %q", projectName), err.Error())
499+
return
500+
}
501+
502+
if project.Config["features.profiles"] == "false" {
503+
if value, exists := devices["root"]; exists {
504+
defaultDevices["root"] = value
505+
}
506+
}
507+
508+
profile := api.ProfilePut{
509+
Description: plan.Description.ValueString(),
510+
Config: defaultConfig,
511+
Devices: defaultDevices,
512+
}
513+
514+
err = server.UpdateProfile(profileName, profile, etag)
515+
if err != nil {
516+
resp.Diagnostics.AddError(fmt.Sprintf("Failed to update profile %q", profileName), err.Error())
517+
return
518+
}
519+
520+
diags = r.SyncState(ctx, &resp.State, server, plan)
521+
resp.Diagnostics.Append(diags...)
522+
}

internal/profile/resource_profile_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,27 @@ func TestAccProfile_importDevice(t *testing.T) {
301301
})
302302
}
303303

304+
func TestAccProfile_default(t *testing.T) {
305+
projectName := petname.Name()
306+
307+
resource.Test(t, resource.TestCase{
308+
PreCheck: func() { acctest.PreCheck(t) },
309+
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
310+
Steps: []resource.TestStep{
311+
{
312+
Config: testAccProfile_default(projectName),
313+
Check: resource.ComposeTestCheckFunc(
314+
resource.TestCheckResourceAttr("incus_project.project1", "name", projectName),
315+
resource.TestCheckResourceAttr("incus_profile.profile1", "name", "default"),
316+
resource.TestCheckResourceAttr("incus_profile.profile1", "project", projectName),
317+
resource.TestCheckResourceAttr("incus_profile.profile1", "device.#", "1"),
318+
resource.TestCheckResourceAttr("incus_profile.profile1", "device.0.name", "foo"),
319+
),
320+
},
321+
},
322+
})
323+
}
324+
304325
func testAccProfile_basic(name string) string {
305326
return fmt.Sprintf(`
306327
resource "incus_profile" "profile1" {
@@ -496,3 +517,30 @@ resource "incus_profile" "profile1" {
496517
}
497518
`, projectName, profileName)
498519
}
520+
521+
func testAccProfile_default(projectName string) string {
522+
return fmt.Sprintf(`
523+
resource "incus_project" "project1" {
524+
name = "%s"
525+
config = {
526+
"features.images" = false
527+
"features.profiles" = true
528+
}
529+
}
530+
531+
resource "incus_profile" "profile1" {
532+
name = "default"
533+
project = incus_project.project1.name
534+
535+
device {
536+
name = "foo"
537+
type = "nic"
538+
properties = {
539+
name = "bar"
540+
nictype = "bridged"
541+
parent = "incusbr0"
542+
}
543+
}
544+
}
545+
`, projectName)
546+
}

0 commit comments

Comments
 (0)