Skip to content

Commit

Permalink
Wait for VPC on creation
Browse files Browse the repository at this point in the history
The VPC is not immediately ready when being created. This can result in errors when setting up a peering connection. If a peering connection is requested befor the VPC is ready, the Timescale VPC will not be found. Now the provider will wait for the VPC to be created as part of the creation workflow.
  • Loading branch information
aaronblevy committed Apr 12, 2024
1 parent f3da925 commit ad79304
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 12 deletions.
8 changes: 8 additions & 0 deletions docs/resources/vpcs.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Schema for a VPC. Import can be done using your VPCs name
### Optional

- `name` (String) VPC Name is the configurable name assigned to this vpc. If none is provided, a default will be generated by the provider.
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))

### Read-Only

Expand All @@ -33,3 +34,10 @@ Schema for a VPC. Import can be done using your VPCs name
- `provisioned_id` (String)
- `status` (String)
- `updated` (String)

<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).
97 changes: 86 additions & 11 deletions internal/provider/vpc_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ import (
"context"
"fmt"
"strconv"
"time"

"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"

tsClient "github.com/timescale/terraform-provider-timescale/internal/client"
)
Expand All @@ -38,16 +43,17 @@ type vpcResource struct {
}

type vpcResourceModel struct {
ID types.Int64 `tfsdk:"id"`
ProvisionedID types.String `tfsdk:"provisioned_id"`
ProjectID types.String `tfsdk:"project_id"`
CIDR types.String `tfsdk:"cidr"`
Name types.String `tfsdk:"name"`
RegionCode types.String `tfsdk:"region_code"`
Status types.String `tfsdk:"status"`
ErrorMessage types.String `tfsdk:"error_message"`
Created types.String `tfsdk:"created"`
Updated types.String `tfsdk:"updated"`
ID types.Int64 `tfsdk:"id"`
ProvisionedID types.String `tfsdk:"provisioned_id"`
ProjectID types.String `tfsdk:"project_id"`
CIDR types.String `tfsdk:"cidr"`
Name types.String `tfsdk:"name"`
RegionCode types.String `tfsdk:"region_code"`
Status types.String `tfsdk:"status"`
ErrorMessage types.String `tfsdk:"error_message"`
Created types.String `tfsdk:"created"`
Updated types.String `tfsdk:"updated"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}

// Metadata returns the data source type name.
Expand Down Expand Up @@ -110,6 +116,7 @@ func vpcToResource(s *tsClient.VPC, state vpcResourceModel) vpcResourceModel {
Status: types.StringValue(s.Status),
ErrorMessage: types.StringValue(s.ErrorMessage),
Updated: types.StringValue(s.Updated),
Timeouts: state.Timeouts,
}
return model
}
Expand Down Expand Up @@ -141,6 +148,12 @@ func (r *vpcResource) Create(ctx context.Context, req resource.CreateRequest, re
vpcID, err := strconv.ParseInt(vpc.ID, 10, 64)
if err != nil {
resp.Diagnostics.AddError("Parse Error", "could not parse vpcID")
return
}
vpc, err = r.waitForVPCReadiness(ctx, vpcID, plan.Timeouts)
if err != nil {
resp.Diagnostics.AddError("Create VPC Error", "error waiting for VPC readiness: "+err.Error())
return
}
plan.ID = types.Int64Value(vpcID)
plan.Created = types.StringValue(vpc.Created)
Expand All @@ -157,6 +170,41 @@ func (r *vpcResource) Create(ctx context.Context, req resource.CreateRequest, re
}
}

func (r *vpcResource) waitForVPCReadiness(ctx context.Context, id int64, timeouts timeouts.Value) (*tsClient.VPC, error) {
tflog.Trace(ctx, "VPCResource.waitForServiceReadiness")

defaultTimeout := 5 * time.Minute
timeout, diags := timeouts.Create(ctx, defaultTimeout)
if diags != nil && diags.HasError() {
tflog.Error(ctx, fmt.Sprintf("found errs %v", diags.Errors()))
return nil, fmt.Errorf("unable to get timeout from config %v", diags.Errors())
}
conf := retry.StateChangeConf{
Pending: []string{"CREATING"},
Target: []string{"CREATED"},
Delay: 5 * time.Second,
Timeout: timeout,
PollInterval: 5 * time.Second,
ContinuousTargetOccurence: 1,
Refresh: func() (result interface{}, state string, err error) {
vpc, err := r.client.GetVPCByID(ctx, id)
if err != nil {
return nil, "", err
}
return vpc, vpc.Status, nil
},
}
res, err := conf.WaitForStateContext(ctx)
if err != nil {
return nil, err
}
vpc, ok := res.(*tsClient.VPC)
if !ok {
return nil, fmt.Errorf("unexpected type found, expected VPC but got %T", res)
}
return vpc, nil
}

// Delete deletes a VPC shell
func (r *vpcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
tflog.Trace(ctx, "VpcsResource.Delete")
Expand Down Expand Up @@ -233,7 +281,7 @@ func (r *vpcResource) Configure(ctx context.Context, req resource.ConfigureReque
}

// Schema defines the schema for the data source.
func (r *vpcResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
func (r *vpcResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: `Schema for a VPC. Import can be done using your VPCs name`,
Attributes: map[string]schema.Attribute{
Expand Down Expand Up @@ -296,6 +344,33 @@ func (r *vpcResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *
stringplanmodifier.UseStateForUnknown(),
},
},
"timeouts": timeoutSchema(ctx, timeouts.Opts{
Create: true,
}),
},
}
}

// adding timeouts can break earlier versions of the provider since the field is not set.
// reference: https://github.com/hashicorp/terraform-plugin-framework-timeouts/issues/49#issuecomment-1511027690
func timeoutSchema(ctx context.Context, opts timeouts.Opts) schema.SingleNestedAttribute {
timeout := timeouts.Attributes(ctx, opts).(schema.SingleNestedAttribute)
at := map[string]attr.Type{}
if opts.Create {
at["create"] = types.StringType
}
if opts.Read {
at["read"] = types.StringType
}
if opts.Update {
at["update"] = types.StringType
}
if opts.Delete {
at["delete"] = types.StringType
}
timeout.Computed = true
timeout.Default = objectdefault.StaticValue(
types.ObjectNull(at),
)
return timeout
}
2 changes: 1 addition & 1 deletion internal/provider/vpc_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestVPCResource_Default_Success(t *testing.T) {
resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "project_id"),
resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "cidr"),
resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "id"),
resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "status"),
resource.TestCheckResourceAttr("timescale_vpcs.resource", "status", "CREATED"),
resource.TestCheckResourceAttr("timescale_vpcs.resource", "name", "vpc-1"),
// resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "updated"),
// resource.TestCheckNoResourceAttr("timescale_vpcs.resource", "created"),
Expand Down

0 comments on commit ad79304

Please sign in to comment.