Skip to content

Commit 12eafd4

Browse files
authored
Merge pull request #297 from serturx/main
project: Add force_destroy attribute
2 parents 13c421e + fedc058 commit 12eafd4

File tree

3 files changed

+126
-5
lines changed

3 files changed

+126
-5
lines changed

docs/resources/project.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ resource "incus_instance" "instance" {
3434
* `remote` - *Optional* - The remote in which the resource will be created. If
3535
not provided, the provider's default remote will be used.
3636

37+
* `force_destroy` - *Optional* - Whether to delete everything the project contains on destroy so that it can be destroyed without any error.
38+
3739
## Attribute Reference
3840

3941
No attributes are exported.

internal/project/resource_project.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/hashicorp/terraform-plugin-framework/path"
1010
"github.com/hashicorp/terraform-plugin-framework/resource"
1111
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
1213
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault"
1314
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1415
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
@@ -25,10 +26,11 @@ import (
2526

2627
// ProjectModel resource data model that matches the schema.
2728
type ProjectModel struct {
28-
Name types.String `tfsdk:"name"`
29-
Description types.String `tfsdk:"description"`
30-
Remote types.String `tfsdk:"remote"`
31-
Config types.Map `tfsdk:"config"`
29+
Name types.String `tfsdk:"name"`
30+
Description types.String `tfsdk:"description"`
31+
Remote types.String `tfsdk:"remote"`
32+
Config types.Map `tfsdk:"config"`
33+
ForceDestroy types.Bool `tfsdk:"force_destroy"`
3234
}
3335

3436
// ProjectResource represent Incus project resource.
@@ -76,6 +78,12 @@ func (r ProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, res
7678
stringplanmodifier.RequiresReplace(),
7779
},
7880
},
81+
82+
"force_destroy": schema.BoolAttribute{
83+
Optional: true,
84+
Computed: true,
85+
Default: booldefault.StaticBool(false),
86+
},
7987
},
8088
}
8189
}
@@ -226,7 +234,12 @@ func (r ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest,
226234
return
227235
}
228236

229-
err = server.DeleteProject(projectName)
237+
if state.ForceDestroy.ValueBool() {
238+
err = server.DeleteProjectForce(projectName)
239+
} else {
240+
err = server.DeleteProject(projectName)
241+
}
242+
230243
if err != nil {
231244
resp.Diagnostics.AddError(fmt.Sprintf("Failed to remove project %q", projectName), err.Error())
232245
}
@@ -257,6 +270,10 @@ func (r *ProjectResource) ImportState(ctx context.Context, req resource.ImportSt
257270

258271
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(k), v)...)
259272
}
273+
274+
// Force a change on import if set to `true` to make it clear in the plan
275+
// that the resource has force_destroy set.
276+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("force_destroy"), types.BoolValue(false))...)
260277
}
261278

262279
// SyncState fetches the server's current state for a project and updates

internal/project/resource_project_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func TestAccProject_basic(t *testing.T) {
2828
Check: resource.ComposeTestCheckFunc(
2929
resource.TestCheckResourceAttr("incus_project.project0", "name", projectName),
3030
resource.TestCheckResourceAttr("incus_project.project0", "description", "Terraform provider test project"),
31+
resource.TestCheckResourceAttr("incus_project.project0", "force_destroy", "false"),
3132
// Ensure state of computed keys is not tracked.
3233
resource.TestCheckNoResourceAttr("incus_project.project0", "config.features.images"),
3334
resource.TestCheckNoResourceAttr("incus_project.project0", "config.features.profiles"),
@@ -52,6 +53,7 @@ func TestAccProject_config(t *testing.T) {
5253
resource.TestCheckResourceAttr("incus_project.project1", "name", projectName),
5354
resource.TestCheckResourceAttr("incus_project.project1", "config.features.images", "true"),
5455
resource.TestCheckResourceAttr("incus_project.project1", "config.features.profiles", "false"),
56+
resource.TestCheckResourceAttr("incus_project.project1", "force_destroy", "false"),
5557
// Ensure state of computed keys is not tracked.
5658
resource.TestCheckNoResourceAttr("incus_project.project1", "config.features.storage.volumes"),
5759
resource.TestCheckNoResourceAttr("incus_project.project1", "config.features.storage.buckets"),
@@ -75,6 +77,7 @@ func TestAccProject_updateConfig(t *testing.T) {
7577
resource.TestCheckResourceAttr("incus_project.project1", "description", "Old description"),
7678
resource.TestCheckResourceAttr("incus_project.project1", "config.features.images", "true"),
7779
resource.TestCheckResourceAttr("incus_project.project1", "config.features.profiles", "false"),
80+
resource.TestCheckResourceAttr("incus_project.project1", "force_destroy", "false"),
7881
// Ensure state of computed keys is not tracked.
7982
resource.TestCheckNoResourceAttr("incus_project.project1", "config.features.storage.volumes"),
8083
resource.TestCheckNoResourceAttr("incus_project.project1", "config.features.storage.buckets"),
@@ -87,6 +90,20 @@ func TestAccProject_updateConfig(t *testing.T) {
8790
resource.TestCheckResourceAttr("incus_project.project1", "description", "New description"),
8891
resource.TestCheckResourceAttr("incus_project.project1", "config.features.images", "false"),
8992
resource.TestCheckResourceAttr("incus_project.project1", "config.features.profiles", "true"),
93+
resource.TestCheckResourceAttr("incus_project.project1", "force_destroy", "false"),
94+
// Ensure state of computed keys is not tracked.
95+
resource.TestCheckNoResourceAttr("incus_project.project1", "config.features.storage.volumes"),
96+
resource.TestCheckNoResourceAttr("incus_project.project1", "config.features.storage.buckets"),
97+
),
98+
},
99+
{
100+
Config: testAccProject_updateConfig_3(projectName),
101+
Check: resource.ComposeTestCheckFunc(
102+
resource.TestCheckResourceAttr("incus_project.project1", "name", projectName),
103+
resource.TestCheckResourceAttr("incus_project.project1", "description", "New description"),
104+
resource.TestCheckResourceAttr("incus_project.project1", "force_destroy", "true"),
105+
resource.TestCheckResourceAttr("incus_project.project1", "config.features.images", "false"),
106+
resource.TestCheckResourceAttr("incus_project.project1", "config.features.profiles", "true"),
90107
// Ensure state of computed keys is not tracked.
91108
resource.TestCheckNoResourceAttr("incus_project.project1", "config.features.storage.volumes"),
92109
resource.TestCheckNoResourceAttr("incus_project.project1", "config.features.storage.buckets"),
@@ -96,6 +113,36 @@ func TestAccProject_updateConfig(t *testing.T) {
96113
})
97114
}
98115

116+
// force_destroy is tested by creating a project containing an untracked instance
117+
// and letting CheckDestroy verify that the project can be destroyed.
118+
func TestAccProject_forceDestroy(t *testing.T) {
119+
projectName := petname.Generate(2, "-")
120+
instanceName := petname.Generate(2, "-")
121+
122+
resource.Test(t, resource.TestCase{
123+
PreCheck: func() { acctest.PreCheck(t) },
124+
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
125+
Steps: []resource.TestStep{
126+
{
127+
Config: testAccProject_forceDestroy_config(projectName, instanceName),
128+
Check: resource.ComposeTestCheckFunc(
129+
resource.TestCheckResourceAttr("incus_project.project1", "name", projectName),
130+
resource.TestCheckResourceAttr("incus_project.project1", "force_destroy", "true"),
131+
resource.TestCheckResourceAttr("incus_instance.instance1", "name", instanceName),
132+
resource.TestCheckResourceAttr("incus_instance.instance1", "project", projectName),
133+
),
134+
},
135+
{
136+
Config: testAccProject_forceDestroy_danglingInstance(projectName),
137+
Check: resource.ComposeTestCheckFunc(
138+
resource.TestCheckResourceAttr("incus_project.project1", "name", projectName),
139+
resource.TestCheckResourceAttr("incus_project.project1", "force_destroy", "true"),
140+
),
141+
},
142+
},
143+
})
144+
}
145+
99146
func TestAccProject_importBasic(t *testing.T) {
100147
resourceName := "incus_project.project0"
101148

@@ -181,3 +228,58 @@ resource "incus_project" "project1" {
181228
}
182229
}`, name)
183230
}
231+
232+
func testAccProject_updateConfig_3(name string) string {
233+
return fmt.Sprintf(`
234+
resource "incus_project" "project1" {
235+
name = "%s"
236+
description = "New description"
237+
force_destroy = true
238+
config = {
239+
"features.images" = false
240+
"features.profiles" = true
241+
}
242+
}`, name)
243+
}
244+
245+
func testAccProject_forceDestroy_config(name string, instanceName string) string {
246+
return fmt.Sprintf(`
247+
resource "incus_project" "project1" {
248+
name = "%s"
249+
description = "Terraform provider test project"
250+
force_destroy = true
251+
config = {
252+
"features.images" = true
253+
"features.profiles" = false
254+
}
255+
}
256+
257+
resource "incus_instance" "instance1" {
258+
name = "%s"
259+
project = incus_project.project1.name
260+
type = "container"
261+
image = "%s"
262+
}
263+
`, name, instanceName, acctest.TestImage)
264+
}
265+
266+
func testAccProject_forceDestroy_danglingInstance(name string) string {
267+
return fmt.Sprintf(`
268+
resource "incus_project" "project1" {
269+
name = "%s"
270+
description = "Terraform provider test project"
271+
force_destroy = true
272+
config = {
273+
"features.images" = true
274+
"features.profiles" = false
275+
}
276+
}
277+
278+
removed {
279+
from = incus_instance.instance1
280+
lifecycle {
281+
destroy = false
282+
}
283+
}
284+
`, name)
285+
}

0 commit comments

Comments
 (0)