Skip to content
Closed
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
8 changes: 8 additions & 0 deletions docs/data-sources/sfs_share.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ data "stackit_sfs_share" "example" {
### Optional

- `region` (String) The resource region. Read-only attribute that reflects the provider region.
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))

### Read-Only

Expand All @@ -49,3 +50,10 @@ You can also assign a Share Export Policy after creating the Share
- `space_hard_limit_gigabytes` (Number) Space hard limit for the Share.
If zero, the Share will have access to the full space of the Resource Pool it lives in.
(unit: gigabytes)

<a id="nestedatt--timeouts"></a>
### Nested Schema for `timeouts`

Optional:

- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).
11 changes: 11 additions & 0 deletions docs/resources/sfs_share.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,24 @@ clients with IPs matching the IP ACL of the Resource Pool hosting this Share.
You can also assign a Share Export Policy after creating the Share
- `labels` (Map of String) Labels are key-value string pairs which can be attached to the resource.
- `region` (String) The resource region. If not defined, the provider region is used.
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))

### Read-Only

- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`resource_pool_id`,`share_id`".
- `mount_path` (String) Mount path of the Share, used to mount the Share
- `share_id` (String) share ID

<a id="nestedatt--timeouts"></a>
### Nested Schema for `timeouts`

Optional:

- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).
- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs.
- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled.
- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).

## Import

Import is supported using the following syntax:
Expand Down
4 changes: 4 additions & 0 deletions stackit/internal/services/sfs/sfs_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,10 @@ func TestAccShareResourceMax(t *testing.T) {
project_id = stackit_sfs_resource_pool.resourcepool.project_id
resource_pool_id = stackit_sfs_resource_pool.resourcepool.resource_pool_id
share_id = stackit_sfs_share.share.share_id

timeouts = {
read = "20m"
}
}
`,
testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), resourceShareMaxConfig,
Expand Down
53 changes: 53 additions & 0 deletions stackit/internal/services/sfs/sfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,56 @@ resource "stackit_sfs_resource_pool" "resourcepool" {
},
})
}

// TestSfsShareCreateTimeout asserts that the configured `timeouts.create` bounds the create. Only the create timeout
// is covered: read, update and delete would each need a successful create beforehand, which makes those tests slow
// and flaky.
func TestSfsShareCreateTimeout(t *testing.T) {
projectId := uuid.NewString()
resourcePoolId := uuid.NewString()
s := testutil.NewMockServer(t)
defer s.Server.Close()
tfConfig := fmt.Sprintf(`
provider "stackit" {
default_region = "eu01"
sfs_custom_endpoint = "%s"
service_account_token = "mock-server-needs-no-auth"
enable_beta_resources = true
}
resource "stackit_sfs_share" "example" {
project_id = "%s"
resource_pool_id = "%s"
name = "my-nfs-share"
export_policy = "high-performance-class"
space_hard_limit_gigabytes = 32

timeouts = {
create = "10ms"
read = "10m"
update = "10m"
delete = "10m"
}
}
`, s.Server.URL, projectId, resourcePoolId)

resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
PreConfig: func() {
s.Reset(testutil.MockResponse{
Description: "answers later than the configured timeout allows",
Handler: func(_ http.ResponseWriter, r *http.Request) {
select {
case <-r.Context().Done():
case <-time.After(20 * time.Millisecond):
}
},
})
},
Config: tfConfig,
ExpectError: regexp.MustCompile("deadline exceeded"),
},
},
})
}
34 changes: 23 additions & 11 deletions stackit/internal/services/sfs/share/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net/http"

"github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
Expand All @@ -28,16 +29,17 @@ var (
)

type dataSourceModel struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
ResourcePoolId types.String `tfsdk:"resource_pool_id"`
ShareId types.String `tfsdk:"share_id"`
Name types.String `tfsdk:"name"`
MountPath types.String `tfsdk:"mount_path"`
SpaceHardLimitGigabytes types.Int32 `tfsdk:"space_hard_limit_gigabytes"`
ExportPolicyName types.String `tfsdk:"export_policy"`
Region types.String `tfsdk:"region"`
Labels types.Map `tfsdk:"labels"`
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
ResourcePoolId types.String `tfsdk:"resource_pool_id"`
ShareId types.String `tfsdk:"share_id"`
Name types.String `tfsdk:"name"`
MountPath types.String `tfsdk:"mount_path"`
SpaceHardLimitGigabytes types.Int32 `tfsdk:"space_hard_limit_gigabytes"`
ExportPolicyName types.String `tfsdk:"export_policy"`
Region types.String `tfsdk:"region"`
Labels types.Map `tfsdk:"labels"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}
type shareDataSource struct {
client *sfs.APIClient
Expand Down Expand Up @@ -82,6 +84,15 @@ func (r *shareDataSource) Read(ctx context.Context, req datasource.ReadRequest,
if resp.Diagnostics.HasError() {
return
}

readTimeout, diags := model.Timeouts.Read(ctx, core.DefaultOperationTimeout)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, readTimeout)
defer cancel()

projectId := model.ProjectId.ValueString()
resourcePoolId := model.ResourcePoolId.ValueString()
shareId := model.ShareId.ValueString()
Expand Down Expand Up @@ -124,7 +135,7 @@ func (r *shareDataSource) Read(ctx context.Context, req datasource.ReadRequest,
}

// Schema implements datasource.DataSource.
func (r *shareDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
func (r *shareDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
description := "SFS Share schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
MarkdownDescription: features.AddBetaDescription(description, core.Datasource),
Expand Down Expand Up @@ -189,6 +200,7 @@ You can also assign a Share Export Policy after creating the Share`,
ElementType: types.StringType,
Computed: true,
},
"timeouts": timeouts.Attributes(ctx),
},
}
}
Expand Down
73 changes: 59 additions & 14 deletions stackit/internal/services/sfs/share/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"strings"

"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
Expand Down Expand Up @@ -35,16 +36,17 @@ var (
)

type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
ResourcePoolId types.String `tfsdk:"resource_pool_id"`
ShareId types.String `tfsdk:"share_id"`
Name types.String `tfsdk:"name"`
Labels types.Map `tfsdk:"labels"`
ExportPolicyName types.String `tfsdk:"export_policy"`
SpaceHardLimitGigabytes types.Int32 `tfsdk:"space_hard_limit_gigabytes"`
Region types.String `tfsdk:"region"`
MountPath types.String `tfsdk:"mount_path"`
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
ResourcePoolId types.String `tfsdk:"resource_pool_id"`
ShareId types.String `tfsdk:"share_id"`
Name types.String `tfsdk:"name"`
Labels types.Map `tfsdk:"labels"`
ExportPolicyName types.String `tfsdk:"export_policy"`
SpaceHardLimitGigabytes types.Int32 `tfsdk:"space_hard_limit_gigabytes"`
Region types.String `tfsdk:"region"`
MountPath types.String `tfsdk:"mount_path"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}

// NewShareResource is a helper function to simplify the provider implementation.
Expand Down Expand Up @@ -114,7 +116,7 @@ func (r *shareResource) Configure(ctx context.Context, req resource.ConfigureReq
}

// Schema defines the schema for the resource.
func (r *shareResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
func (r *shareResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := "SFS Share schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
MarkdownDescription: features.AddBetaDescription(description, core.Resource),
Expand Down Expand Up @@ -203,6 +205,7 @@ You can also assign a Share Export Policy after creating the Share`,
stringplanmodifier.UseStateForUnknown(),
},
},
"timeouts": timeouts.AttributesAll(ctx),
},
}
}
Expand All @@ -217,6 +220,17 @@ func (r *shareResource) Create(ctx context.Context, req resource.CreateRequest,
return
}

// The wait handler only enforces its own timeout when the context carries no deadline,
// so the context deadline set here is what actually bounds the polling.
waiterTimeout := wait.CreateShareWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to read the default wait handler timeout
createTimeout, diags := model.Timeouts.Create(ctx, waiterTimeout+core.DefaultTimeoutMargin)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, createTimeout)
defer cancel()

projectId := model.ProjectId.ValueString()
region := model.Region.ValueString()
resourcePoolId := model.ResourcePoolId.ValueString()
Expand Down Expand Up @@ -261,7 +275,8 @@ func (r *shareResource) Create(ctx context.Context, req resource.CreateRequest,
response, err := wait.CreateShareWaitHandler(ctx, r.client.DefaultAPI, projectId, region, resourcePoolId, *share.Share.Id).
WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating share", fmt.Sprintf("share creation waiting: %v", err))
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating share",
fmt.Sprintf("share creation waiting: %v%s", err, utils.TimeoutHint(ctx, "create", createTimeout)))
return
}
ctx = tflog.SetField(ctx, "share_id", response.Share.Id)
Expand Down Expand Up @@ -303,6 +318,15 @@ func (r *shareResource) Read(ctx context.Context, req resource.ReadRequest, resp
if resp.Diagnostics.HasError() {
return
}

readTimeout, diags := model.Timeouts.Read(ctx, core.DefaultOperationTimeout)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, readTimeout)
defer cancel()

projectId := model.ProjectId.ValueString()
resourcePoolId := model.ResourcePoolId.ValueString()
shareId := model.ShareId.ValueString()
Expand Down Expand Up @@ -358,6 +382,16 @@ func (r *shareResource) Update(ctx context.Context, req resource.UpdateRequest,
if resp.Diagnostics.HasError() {
return
}

waiterTimeout := wait.UpdateShareWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() //nolint:tfctxinit // false positive - only called to read the default wait handler timeout
updateTimeout, diags := model.Timeouts.Update(ctx, waiterTimeout+core.DefaultTimeoutMargin)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, updateTimeout)
defer cancel()

projectId := model.ProjectId.ValueString()
shareId := model.ShareId.ValueString()
region := model.Region.ValueString()
Expand Down Expand Up @@ -410,7 +444,8 @@ func (r *shareResource) Update(ctx context.Context, req resource.UpdateRequest,

getResponse, err := wait.UpdateShareWaitHandler(ctx, r.client.DefaultAPI, projectId, region, resourcePoolId, shareId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating share", fmt.Sprintf("share get: %v", err))
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating share",
fmt.Sprintf("share get: %v%s", err, utils.TimeoutHint(ctx, "update", updateTimeout)))
return
}
err = mapFields(ctx, getResponse.Share, region, &model)
Expand All @@ -436,6 +471,15 @@ func (r *shareResource) Delete(ctx context.Context, req resource.DeleteRequest,
return
}

waiterTimeout := wait.DeleteShareWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() //nolint:tfctxinit // false positive - only called to read the default wait handler timeout
deleteTimeout, diags := model.Timeouts.Delete(ctx, waiterTimeout+core.DefaultTimeoutMargin)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, deleteTimeout)
defer cancel()

projectId := model.ProjectId.ValueString()
shareId := model.ShareId.ValueString()
region := model.Region.ValueString()
Expand Down Expand Up @@ -465,7 +509,8 @@ func (r *shareResource) Delete(ctx context.Context, req resource.DeleteRequest,
// only delete, if no error occurred
_, err = wait.DeleteShareWaitHandler(ctx, r.client.DefaultAPI, projectId, region, resourcePoolId, shareId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting share", fmt.Sprintf("share deletion waiting: %v", err))
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting share",
fmt.Sprintf("share deletion waiting: %v%s", err, utils.TimeoutHint(ctx, "delete", deleteTimeout)))
return
}

Expand Down
7 changes: 7 additions & 0 deletions stackit/internal/services/sfs/testdata/share-max.tf
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ resource "stackit_sfs_share" "share" {
labels = {
label = var.label
}

timeouts = {
create = "20m"
read = "20m"
update = "20m"
delete = "20m"
}
}