From 990316fdd74d05118cd51a5c81c0c5aa42c148df Mon Sep 17 00:00:00 2001 From: Kim Date: Wed, 17 Jan 2024 11:51:09 +0100 Subject: [PATCH] Implement VPC CRUD (#140) * Implement VPC shell import Create global scenario test file --- docs/resources/vpcs.md | 35 ++ examples/resources/timescale_vpcs/vpcs.tf | 27 ++ internal/client/client.go | 20 +- .../queries/attach_service_to_vpc.graphql | 2 +- internal/client/queries/create_vpc.graphql | 31 ++ internal/client/queries/delete_vpc.graphql | 6 + .../queries/detach_service_from_vpc.graphql | 2 +- internal/client/queries/rename_vpc.graphql | 7 + internal/client/queries/vpc_by_id.graphql | 25 ++ internal/client/queries/vpc_by_name.graphql | 28 ++ internal/client/service.go | 6 +- internal/client/vpc.go | 157 ++++++- internal/provider/general_test.go | 157 +++++++ internal/provider/provider.go | 1 + internal/provider/provider_test.go | 80 +++- internal/provider/service_data_source.go | 8 +- internal/provider/service_resource.go | 13 +- internal/provider/service_resource_test.go | 138 ++---- internal/provider/vpc_resource.go | 412 ++++++++++++++++++ internal/provider/vpc_resource_test.go | 82 ++++ internal/provider/vpcs_data_source.go | 12 +- 21 files changed, 1109 insertions(+), 140 deletions(-) create mode 100644 docs/resources/vpcs.md create mode 100644 examples/resources/timescale_vpcs/vpcs.tf create mode 100644 internal/client/queries/create_vpc.graphql create mode 100644 internal/client/queries/delete_vpc.graphql create mode 100644 internal/client/queries/rename_vpc.graphql create mode 100644 internal/client/queries/vpc_by_id.graphql create mode 100644 internal/client/queries/vpc_by_name.graphql create mode 100644 internal/provider/general_test.go create mode 100644 internal/provider/vpc_resource.go create mode 100644 internal/provider/vpc_resource_test.go diff --git a/docs/resources/vpcs.md b/docs/resources/vpcs.md new file mode 100644 index 0000000..bb2bae6 --- /dev/null +++ b/docs/resources/vpcs.md @@ -0,0 +1,35 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "timescale_vpcs Resource - terraform-provider-timescale" +subcategory: "" +description: |- + +--- + +# timescale_vpcs (Resource) + + + + + + +## Schema + +### Required + +- `cidr` (String) The IPv4 CIDR block +- `region_code` (String) The region for this service. Currently supported regions are us-east-1, eu-west-1, us-west-2, eu-central-1, ap-southeast-2 + +### 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. + +### Read-Only + +- `created` (String) +- `error_message` (String) +- `id` (Number) The ID of this resource. +- `project_id` (String) +- `provisioned_id` (String) +- `status` (String) +- `updated` (String) diff --git a/examples/resources/timescale_vpcs/vpcs.tf b/examples/resources/timescale_vpcs/vpcs.tf new file mode 100644 index 0000000..b6cbf4b --- /dev/null +++ b/examples/resources/timescale_vpcs/vpcs.tf @@ -0,0 +1,27 @@ +terraform { + required_providers { + timescale = { + source = "registry.terraform.io/providers/timescale" + version = "~> 1.0" + } + } +} + +variable "ts_access_token" { + type = string +} + +variable "ts_project_id" { + type = string +} + +provider "timescale" { + access_token = var.ts_access_token + project_id = var.ts_project_id +} + +resource "timescale_vpcs" "new_vpc" { + name = "test-vpc" + cidr = "10.0.0.0/19" + region_code = "us-east-1" +} \ No newline at end of file diff --git a/internal/client/client.go b/internal/client/client.go index ee9a1c0..c65fc6b 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -28,18 +28,30 @@ var ( GetAllServicesQuery string //go:embed queries/get_service.graphql GetServiceQuery string - //go:embed queries/vpcs.graphql - GetVPCsQuery string //go:embed queries/products.graphql ProductsQuery string //go:embed queries/jwt_cc.graphql JWTFromCCQuery string + //go:embed queries/set_replica_count.graphql + SetReplicaCountMutation string + + // VCPs /////////////////////////////// + //go:embed queries/vpcs.graphql + GetVPCsQuery string + //go:embed queries/vpc_by_name.graphql + GetVPCByNameQuery string + //go:embed queries/vpc_by_id.graphql + GetVPCByIDQuery string //go:embed queries/attach_service_to_vpc.graphql AttachServiceToVPCMutation string //go:embed queries/detach_service_from_vpc.graphql DetachServiceFromVPCMutation string - //go:embed queries/set_replica_count.graphql - SetReplicaCountMutation string + //go:embed queries/create_vpc.graphql + CreateVPCMutation string + //go:embed queries/delete_vpc.graphql + DeleteVPCMutation string + //go:embed queries/rename_vpc.graphql + RenameVPCMutation string ) type Client struct { diff --git a/internal/client/queries/attach_service_to_vpc.graphql b/internal/client/queries/attach_service_to_vpc.graphql index edde8fb..6c40fe0 100644 --- a/internal/client/queries/attach_service_to_vpc.graphql +++ b/internal/client/queries/attach_service_to_vpc.graphql @@ -1,4 +1,4 @@ -mutation AttachServiceToVpc($projectId: ID!, $serviceId: ID!, $vpcId: ID!) { +mutation AttachServiceToVPC($projectId: ID!, $serviceId: ID!, $vpcId: ID!) { attachServiceToVpc (data:{ serviceId: $serviceId, projectId: $projectId, diff --git a/internal/client/queries/create_vpc.graphql b/internal/client/queries/create_vpc.graphql new file mode 100644 index 0000000..132ef5e --- /dev/null +++ b/internal/client/queries/create_vpc.graphql @@ -0,0 +1,31 @@ +mutation CreateVPC($projectId: ID!, $name: String!, $cidr: String!, $regionCode: String!) { + createVpc(data:{ + projectId:$projectId, + name:$name, + cidr:$cidr, + cloudProvider: AWS, + regionCode:$regionCode + }){ + id + projectId + cidr + name + created + updated + # peeringConnections { + # id + # vpcId + # peerVpc { + # id + # accountId + # regionCode + # cidr + # } + # errorMessage + # status + # } + errorMessage + status + regionCode + } +} diff --git a/internal/client/queries/delete_vpc.graphql b/internal/client/queries/delete_vpc.graphql new file mode 100644 index 0000000..0731bb0 --- /dev/null +++ b/internal/client/queries/delete_vpc.graphql @@ -0,0 +1,6 @@ +mutation DeleteVPC($projectId: ID!, $vpcId: ID!) { + deleteVpc (data:{ + vpcId: $vpcId, + projectId: $projectId + }) +} diff --git a/internal/client/queries/detach_service_from_vpc.graphql b/internal/client/queries/detach_service_from_vpc.graphql index eed3bdc..3e90295 100644 --- a/internal/client/queries/detach_service_from_vpc.graphql +++ b/internal/client/queries/detach_service_from_vpc.graphql @@ -1,4 +1,4 @@ -mutation DetachServiceFromVpc($projectId: ID!, $serviceId: ID!, $vpcId: ID!) { +mutation DetachServiceFromVPC($projectId: ID!, $serviceId: ID!, $vpcId: ID!) { detachServiceFromVpc (data:{ serviceId: $serviceId, projectId: $projectId, diff --git a/internal/client/queries/rename_vpc.graphql b/internal/client/queries/rename_vpc.graphql new file mode 100644 index 0000000..53a9d95 --- /dev/null +++ b/internal/client/queries/rename_vpc.graphql @@ -0,0 +1,7 @@ +mutation RenameVPC($projectId: ID!, $forgeVpcId: ID!, $newName: String!) { + renameVpc (data:{ + projectId: $projectId, + forgeVpcId: $forgeVpcId, + newName: $newName + }) +} diff --git a/internal/client/queries/vpc_by_id.graphql b/internal/client/queries/vpc_by_id.graphql new file mode 100644 index 0000000..176affe --- /dev/null +++ b/internal/client/queries/vpc_by_id.graphql @@ -0,0 +1,25 @@ + query GetVPC($vpcId: ID!) { + getVpc(vpcId: $vpcId) { + id + projectId + cidr + name + created + updated + # peeringConnections { + # id + # vpcId + # peerVpc { + # id + # accountId + # regionCode + # cidr + # } + # errorMessage + # status + # } + errorMessage + status + regionCode + } +} diff --git a/internal/client/queries/vpc_by_name.graphql b/internal/client/queries/vpc_by_name.graphql new file mode 100644 index 0000000..fa602f5 --- /dev/null +++ b/internal/client/queries/vpc_by_name.graphql @@ -0,0 +1,28 @@ +query GetVPCByName($projectId: ID!, $name: String!) { + getVpcByName (data:{ + projectId: $projectId + vpcName: $name, + }) { + id + projectId + cidr + name + created + updated + # peeringConnections { + # id + # vpcId + # peerVpc { + # id + # accountId + # regionCode + # cidr + # } + # errorMessage + # status + # } + errorMessage + status + regionCode + } +} diff --git a/internal/client/service.go b/internal/client/service.go index 63fd184..3e3ded6 100644 --- a/internal/client/service.go +++ b/internal/client/service.go @@ -23,7 +23,7 @@ type Service struct { Resources []ResourceSpec `json:"resources"` Created string `json:"created"` ReplicaStatus string `json:"replicaStatus"` - VpcEndpoint *VpcEndpoint `json:"vpcEndpoint"` + VPCEndpoint *VPCEndpoint `json:"vpcEndpoint"` ForkSpec *ForkSpec `json:"forkedFromId"` } @@ -33,10 +33,10 @@ type ServiceSpec struct { Port int64 `json:"port"` } -type VpcEndpoint struct { +type VPCEndpoint struct { Host string `json:"host"` Port int64 `json:"port"` - VpcId string `json:"vpcId"` + VPCId string `json:"vpcId"` } type ResourceSpec struct { diff --git a/internal/client/vpc.go b/internal/client/vpc.go index 67c190b..19b26ba 100644 --- a/internal/client/vpc.go +++ b/internal/client/vpc.go @@ -3,6 +3,9 @@ package client import ( "context" "errors" + "fmt" + "math/rand" + "time" "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -23,33 +26,41 @@ type VPC struct { type PeeringConnection struct { ID string `json:"id"` - VpcID string `json:"vpcId"` + VPCID string `json:"vpcId"` Status string `json:"status"` ErrorMessage string `json:"errorMessage"` - PeerVpcs []*PeerVpc `json:"peerVpc"` + PeerVPCs []*PeerVPC `json:"peerVPC"` } -type PeerVpc struct { +type PeerVPC struct { ID string `json:"id"` CIDR string `json:"cidr"` AccountID string `json:"accountId"` RegionCode string `json:"regionCode"` } -type VpcsResponse struct { - Vpcs []*VPC `json:"getAllVpcs"` +type VPCsResponse struct { + VPCs []*VPC `json:"getAllVPCs"` +} + +type CreateVPCResponse struct { + VPC *VPC `json:"createVPC"` +} + +type VPCResponse struct { + VPC *VPC `json:"getVPCByName"` } func (c *Client) GetVPCs(ctx context.Context) ([]*VPC, error) { tflog.Trace(ctx, "Client.GetVPCs") req := map[string]interface{}{ - "operationName": "GetAllVpcs", + "operationName": "GetAllVPCs", "query": GetVPCsQuery, "variables": map[string]string{ "projectId": c.projectID, }, } - var resp Response[VpcsResponse] + var resp Response[VPCsResponse] if err := c.do(ctx, req, &resp); err != nil { return nil, err } @@ -59,14 +70,59 @@ func (c *Client) GetVPCs(ctx context.Context) ([]*VPC, error) { if resp.Data == nil { return nil, errors.New("no response found") } - return resp.Data.Vpcs, nil + return resp.Data.VPCs, nil } -func (c *Client) AttachServiceToVpc(ctx context.Context, serviceID string, vpcID int64) error { - tflog.Trace(ctx, "Client.AttachServiceToVpc") +func (c *Client) GetVPCByName(ctx context.Context, name string) (*VPC, error) { + tflog.Trace(ctx, "Client.GetVPCByName") + req := map[string]interface{}{ + "operationName": "GetVPCByName", + "query": GetVPCByNameQuery, + "variables": map[string]string{ + "projectId": c.projectID, + "name": name, + }, + } + var resp Response[VPCResponse] + if err := c.do(ctx, req, &resp); err != nil { + return nil, err + } + if len(resp.Errors) > 0 { + return nil, resp.Errors[0] + } + if resp.Data == nil { + return nil, errors.New("no vpc found") + } + return resp.Data.VPC, nil +} +func (c *Client) GetVPCByID(ctx context.Context, vpcID int64) (*VPC, error) { + tflog.Trace(ctx, "Client.GetVPCByID") req := map[string]interface{}{ - "operationName": "AttachServiceToVpc", + "operationName": "GetVPCByID", + "query": GetVPCByIDQuery, + "variables": map[string]any{ + "vpcId": vpcID, + }, + } + var resp Response[VPCResponse] + if err := c.do(ctx, req, &resp); err != nil { + return nil, err + } + if len(resp.Errors) > 0 { + return nil, resp.Errors[0] + } + if resp.Data == nil { + return nil, errors.New("no vpc found") + } + return resp.Data.VPC, nil +} + +func (c *Client) AttachServiceToVPC(ctx context.Context, serviceID string, vpcID int64) error { + tflog.Trace(ctx, "Client.AttachServiceToVPC") + + req := map[string]interface{}{ + "operationName": "AttachServiceToVPC", "query": AttachServiceToVPCMutation, "variables": map[string]any{ "projectId": c.projectID, @@ -87,11 +143,11 @@ func (c *Client) AttachServiceToVpc(ctx context.Context, serviceID string, vpcID return nil } -func (c *Client) DetachServiceFromVpc(ctx context.Context, serviceID string, vpcID int64) error { - tflog.Trace(ctx, "Client.DetachServiceFromVpc") +func (c *Client) DetachServiceFromVPC(ctx context.Context, serviceID string, vpcID int64) error { + tflog.Trace(ctx, "Client.DetachServiceFromVPC") req := map[string]interface{}{ - "operationName": "DetachServiceFromVpc", + "operationName": "DetachServiceFromVPC", "query": DetachServiceFromVPCMutation, "variables": map[string]any{ "projectId": c.projectID, @@ -111,3 +167,76 @@ func (c *Client) DetachServiceFromVpc(ctx context.Context, serviceID string, vpc } return nil } + +func (c *Client) CreateVPC(ctx context.Context, name, cidr, regionCode string) (*VPC, error) { + tflog.Trace(ctx, "Client.CreateVPC") + + if name == "" { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + name = fmt.Sprintf("vpc-%d", 10000+r.Intn(90000)) + } + + req := map[string]interface{}{ + "operationName": "CreateVPC", + "query": CreateVPCMutation, + "variables": map[string]string{ + "projectId": c.projectID, + "name": name, + "cidr": cidr, + "regionCode": regionCode, + }, + } + var resp Response[CreateVPCResponse] + if err := c.do(ctx, req, &resp); err != nil { + return nil, err + } + if len(resp.Errors) > 0 { + return nil, resp.Errors[0] + } + if resp.Data == nil { + return nil, errors.New("no response found") + } + return resp.Data.VPC, nil +} + +func (c *Client) RenameVPC(ctx context.Context, vpcId int64, newName string) error { + tflog.Trace(ctx, "Client.GetVPCs") + req := map[string]interface{}{ + "operationName": "RenameVPC", + "query": RenameVPCMutation, + "variables": map[string]any{ + "projectId": c.projectID, + "forgeVpcId": vpcId, + "newName": newName, + }, + } + var resp Response[any] + if err := c.do(ctx, req, &resp); err != nil { + return err + } + if len(resp.Errors) > 0 { + return resp.Errors[0] + } + return nil +} + +func (c *Client) DeleteVPC(ctx context.Context, vpcId int64) error { + tflog.Trace(ctx, "Client.DeleteVPC") + + req := map[string]interface{}{ + "operationName": "DeleteVPC", + "query": DeleteVPCMutation, + "variables": map[string]any{ + "projectId": c.projectID, + "vpcId": vpcId, + }, + } + var resp Response[any] + if err := c.do(ctx, req, &resp); err != nil { + return err + } + if len(resp.Errors) > 0 { + return resp.Errors[0] + } + return nil +} diff --git a/internal/provider/general_test.go b/internal/provider/general_test.go new file mode 100644 index 0000000..aca3486 --- /dev/null +++ b/internal/provider/general_test.go @@ -0,0 +1,157 @@ +package provider + +// func TestGeneralScenario(t *testing.T) { +// const ( +// primaryName = "primary" +// extraName = "extra" +// replicaName = "read_replica" +// primaryFQID = "timescale_service." + primaryName +// extraFQID = "timescale_service." + extraName +// replicaFQID = "timescale_service." + replicaName +// ) +// var ( +// // primaryConfig = &ServiceConfig{ +// // ResourceName: primaryName, +// // Name: "service resource test init", +// // } +// // replicaConfig = &ServiceConfig{ +// // ResourceName: replicaName, +// // ReadReplicaSource: primaryFQID + ".id", +// // MilliCPU: 500, +// // MemoryGB: 2, +// // } +// config = &ServiceConfig{ +// ResourceName: "resource", +// } +// vpcConfig = &VPCConfig{ +// ResourceName: "resource", +// } +// ) +// var vpcID int64 +// var vpcIDStr string +// resource.ParallelTest(t, resource.TestCase{ +// ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, +// PreCheck: func() { testAccPreCheck(t) }, +// Steps: []resource.TestStep{ +// // Create VPC +// { +// Config: getVPCConfig(t, vpcConfig.WithName("global-test").WithCIDR("10.0.0.0/21").WithRegionCode("us-east-1")), +// Check: func(s *terraform.State) error { +// time.Sleep(10 * time.Second) +// rs, ok := s.RootModule().Resources["timescale_vpcs.resource"] +// if !ok { +// return fmt.Errorf("Not found: %s", "timescale_vpcs.resource") +// } +// if rs.Primary.ID == "" { +// return fmt.Errorf("Widget ID is not set") +// } +// var err error +// vpcIDStr = rs.Primary.ID +// vpcID, err = strconv.ParseInt(rs.Primary.ID, 10, 64) +// if err != nil { +// return fmt.Errorf("Could not parse ID") +// } +// return nil +// fmt.Printf("%v\n", getVPCConfig(t, vpcConfig.WithName("global-test").WithCIDR("10.0.0.0/21").WithRegionCode("us-east-1"))+getServiceNoProviderConfig(t, config.WithName("create default").WithVPC(vpcID))) +// }, +// }, +// // // Create with HA and VPC attached +// // { +// // Config: newServiceCustomVpcConfig("hareplica", ServiceConfig{ +// // Name: "service resource test HA", +// // RegionCode: "us-east-1", +// // MilliCPU: 500, +// // MemoryGB: 2, +// // EnableHAReplica: true, +// // VpcID: vpcID, +// // }), +// // Check: resource.ComposeAggregateTestCheckFunc( +// // resource.TestCheckResourceAttr("timescale_service.hareplica", "name", "service resource test HA"), +// // resource.TestCheckResourceAttr("timescale_service.hareplica", "enable_ha_replica", "true"), +// // resource.TestCheckResourceAttr("timescale_service.hareplica", "vpc_id", vpcIDStr), +// // ), +// // }, +// // Create default and Read testing +// // { +// // Config: getServiceConfig(t, config.WithName("create default")), +// // Check: resource.ComposeAggregateTestCheckFunc( +// // // Verify the name is set. +// // resource.TestCheckResourceAttrSet("timescale_service.resource", "name"), +// // // Verify ID value is set in state. +// // resource.TestCheckResourceAttrSet("timescale_service.resource", "id"), +// // resource.TestCheckResourceAttrSet("timescale_service.resource", "password"), +// // resource.TestCheckResourceAttrSet("timescale_service.resource", "hostname"), +// // resource.TestCheckResourceAttrSet("timescale_service.resource", "username"), +// // resource.TestCheckResourceAttrSet("timescale_service.resource", "port"), +// // resource.TestCheckResourceAttr("timescale_service.resource", "milli_cpu", "500"), +// // resource.TestCheckResourceAttr("timescale_service.resource", "memory_gb", "2"), +// // resource.TestCheckResourceAttr("timescale_service.resource", "region_code", "us-east-1"), +// // resource.TestCheckResourceAttr("timescale_service.resource", "enable_ha_replica", "false"), +// // resource.TestCheckNoResourceAttr("timescale_service.resource", "vpc_id"), +// // ), +// // }, + +// // Add VPC +// { +// Config: getVPCConfig(t, vpcConfig.WithName("global-test").WithCIDR("10.0.0.0/21").WithRegionCode("us-east-1")) + getServiceNoProviderConfig(t, config.WithName("create default").WithVPC(vpcID)), +// Check: resource.ComposeAggregateTestCheckFunc( +// resource.TestCheckResourceAttr("timescale_service.resource", "vpc_id", vpcIDStr), +// ), +// }, +// // // Add HA replica and remove VPC +// // { +// // Config: getServiceConfig(t, config.WithVPC(0).WithHAReplica(true)), +// // Check: resource.ComposeAggregateTestCheckFunc( +// // resource.TestCheckNoResourceAttr("timescale_service.resource", "vpc_id"), +// // resource.TestCheckResourceAttr("timescale_service.resource", "enable_ha_replica", "true"), +// // ), +// // }, +// // // Create with read replica +// // { +// // Config: getServiceConfig(t, primaryConfig, replicaConfig), +// // Check: resource.ComposeAggregateTestCheckFunc( +// // // Verify service attributes +// // resource.TestCheckResourceAttr(primaryFQID, "name", "service resource test init"), +// // resource.TestCheckResourceAttrSet(primaryFQID, "id"), +// // resource.TestCheckResourceAttrSet(primaryFQID, "password"), +// // resource.TestCheckResourceAttrSet(primaryFQID, "hostname"), +// // resource.TestCheckResourceAttrSet(primaryFQID, "username"), +// // resource.TestCheckResourceAttrSet(primaryFQID, "port"), +// // resource.TestCheckResourceAttr(primaryFQID, "milli_cpu", "500"), +// // resource.TestCheckResourceAttr(primaryFQID, "memory_gb", "2"), +// // resource.TestCheckResourceAttr(primaryFQID, "region_code", "us-east-1"), +// // resource.TestCheckResourceAttr(primaryFQID, "enable_ha_replica", "false"), +// // resource.TestCheckNoResourceAttr(primaryFQID, "vpc_id"), + +// // // Verify read replica attributes +// // resource.TestCheckResourceAttr(replicaFQID, "name", "replica-service resource test init"), +// // resource.TestCheckResourceAttrSet(replicaFQID, "id"), +// // resource.TestCheckResourceAttrSet(replicaFQID, "password"), +// // resource.TestCheckResourceAttrSet(replicaFQID, "hostname"), +// // resource.TestCheckResourceAttrSet(replicaFQID, "username"), +// // resource.TestCheckResourceAttrSet(replicaFQID, "port"), +// // resource.TestCheckResourceAttr(replicaFQID, "milli_cpu", "500"), +// // resource.TestCheckResourceAttr(replicaFQID, "memory_gb", "2"), +// // resource.TestCheckResourceAttr(replicaFQID, "region_code", "us-east-1"), +// // resource.TestCheckResourceAttr(replicaFQID, "enable_ha_replica", "false"), +// // resource.TestCheckResourceAttrSet(replicaFQID, "read_replica_source"), +// // resource.TestCheckNoResourceAttr(replicaFQID, "vpc_id"), +// // ), +// // }, +// // // Add VPC to the read replica +// // { +// // Config: getServiceConfig(t, primaryConfig, replicaConfig.WithVPC(vpcID)), +// // Check: resource.ComposeAggregateTestCheckFunc( +// // resource.TestCheckResourceAttr(replicaFQID, "vpc_id", vpcIDStr), +// // ), +// // }, +// // // Remove VPC +// // { +// // Config: getServiceConfig(t, primaryConfig, replicaConfig.WithVPC(0)), +// // Check: resource.ComposeAggregateTestCheckFunc( +// // resource.TestCheckNoResourceAttr(primaryFQID, "vpc_id"), +// // ), +// // }, +// }, +// }) +// } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 24fe2a4..6ab6aef 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -121,6 +121,7 @@ func (p *TimescaleProvider) Resources(ctx context.Context) []func() resource.Res tflog.Trace(ctx, "TimescaleProvider.Resources") return []func() resource.Resource{ NewServiceResource, + NewVpcsResource, } } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 6a4394c..eeb7780 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -43,7 +43,58 @@ var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServe "timescale": providerserver.NewProtocol6WithError(New("test")()), } -type Config struct { +type VPCConfig struct { + ResourceName string + Name string + CIDR string + RegionCode string +} + +func (vc *VPCConfig) WithName(s string) *VPCConfig { + vc.Name = s + return vc +} +func (vc *VPCConfig) WithCIDR(s string) *VPCConfig { + vc.CIDR = s + return vc +} +func (vc *VPCConfig) WithRegionCode(s string) *VPCConfig { + vc.RegionCode = s + return vc +} + +func (vc *VPCConfig) String(t *testing.T) string { + b := &strings.Builder{} + write := func(format string, a ...any) { + _, err := fmt.Fprintf(b, format, a...) + require.NoError(t, err) + } + _, err := fmt.Fprintf(b, "\n\n resource timescale_vpcs %q { \n", vc.ResourceName) + require.NoError(t, err) + if vc.Name != "" { + write("name = %q \n", vc.Name) + } + if vc.CIDR != "" { + write("cidr = %q \n", vc.CIDR) + } + if vc.RegionCode != "" { + write("region_code = %q \n", vc.RegionCode) + } + write("}") + return b.String() +} + +// getServiceConfig returns a configuration for a test step +func getVPCConfig(t *testing.T, cfgs ...*VPCConfig) string { + res := strings.Builder{} + res.WriteString(providerConfig) + for _, cfg := range cfgs { + res.WriteString(cfg.String(t)) + } + return res.String() +} + +type ServiceConfig struct { ResourceName string Name string Timeouts Timeouts @@ -55,33 +106,33 @@ type Config struct { ReadReplicaSource string } -func (c *Config) WithName(name string) *Config { +func (c *ServiceConfig) WithName(name string) *ServiceConfig { c.Name = name return c } -func (c *Config) WithSpec(milliCPU, memoryGB int64) *Config { +func (c *ServiceConfig) WithSpec(milliCPU, memoryGB int64) *ServiceConfig { c.MilliCPU = milliCPU c.MemoryGB = memoryGB return c } -func (c *Config) WithVPC(ID int64) *Config { +func (c *ServiceConfig) WithVPC(ID int64) *ServiceConfig { c.VpcID = ID return c } -func (c *Config) WithHAReplica(enableHAReplica bool) *Config { +func (c *ServiceConfig) WithHAReplica(enableHAReplica bool) *ServiceConfig { c.EnableHAReplica = enableHAReplica return c } -func (c *Config) WithReadReplica(source string) *Config { +func (c *ServiceConfig) WithReadReplica(source string) *ServiceConfig { c.ReadReplicaSource = source return c } -func (c *Config) String(t *testing.T) string { +func (c *ServiceConfig) String(t *testing.T) string { c.setDefaults() b := &strings.Builder{} write := func(format string, a ...any) { @@ -116,7 +167,7 @@ func (c *Config) String(t *testing.T) string { return b.String() } -func (c *Config) setDefaults() { +func (c *ServiceConfig) setDefaults() { if c.MilliCPU == 0 { c.MilliCPU = 500 } @@ -128,8 +179,8 @@ func (c *Config) setDefaults() { } } -// getConfig returns a configuration for a test step -func getConfig(t *testing.T, cfgs ...*Config) string { +// getServiceConfig returns a configuration for a test step +func getServiceConfig(t *testing.T, cfgs ...*ServiceConfig) string { res := strings.Builder{} res.WriteString(providerConfig) for _, cfg := range cfgs { @@ -138,6 +189,15 @@ func getConfig(t *testing.T, cfgs ...*Config) string { return res.String() } +// // getServiceConfig returns a configuration for a test step +// func getServiceNoProviderConfig(t *testing.T, cfgs ...*ServiceConfig) string { +// res := strings.Builder{} +// for _, cfg := range cfgs { +// res.WriteString(cfg.String(t)) +// } +// return res.String() +// } + type Timeouts struct { Create string } diff --git a/internal/provider/service_data_source.go b/internal/provider/service_data_source.go index 0378bb3..365d294 100644 --- a/internal/provider/service_data_source.go +++ b/internal/provider/service_data_source.go @@ -201,14 +201,14 @@ func serviceToDataModel(diag diag.Diagnostics, s *tsClient.Service) ServiceDataS }, Created: types.StringValue(s.Created), } - if s.VpcEndpoint != nil { - if vpcId, err := strconv.ParseInt(s.VpcEndpoint.VpcId, 10, 64); err != nil { + if s.VPCEndpoint != nil { + if vpcId, err := strconv.ParseInt(s.VPCEndpoint.VPCId, 10, 64); err != nil { diag.AddError("Parse Error", "could not parse vpcID") } else { serviceModel.VpcId = types.Int64Value(vpcId) } - serviceModel.Spec.Hostname = types.StringValue(s.VpcEndpoint.Host) - serviceModel.Spec.Port = types.Int64Value(s.VpcEndpoint.Port) + serviceModel.Spec.Hostname = types.StringValue(s.VPCEndpoint.Host) + serviceModel.Spec.Port = types.Int64Value(s.VPCEndpoint.Port) } for _, resource := range s.Resources { serviceModel.Resources = append(serviceModel.Resources, ResourceModel{ diff --git a/internal/provider/service_resource.go b/internal/provider/service_resource.go index 73d1660..04ef674 100644 --- a/internal/provider/service_resource.go +++ b/internal/provider/service_resource.go @@ -449,14 +449,14 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest if !plan.VpcId.Equal(state.VpcId) { // if state.VpcId is known and different from plan.VpcId, we must detach first if !state.VpcId.IsNull() && !state.VpcId.IsUnknown() { - if err := r.client.DetachServiceFromVpc(ctx, serviceID, state.VpcId.ValueInt64()); err != nil { + if err := r.client.DetachServiceFromVPC(ctx, serviceID, state.VpcId.ValueInt64()); err != nil { resp.Diagnostics.AddError("Failed to detach service from VPC", err.Error()) return } } // if plan.VpcId is known, it must be attached if !plan.VpcId.IsNull() && !plan.VpcId.IsUnknown() { - if err := r.client.AttachServiceToVpc(ctx, serviceID, plan.VpcId.ValueInt64()); err != nil { + if err := r.client.AttachServiceToVPC(ctx, serviceID, plan.VpcId.ValueInt64()); err != nil { resp.Diagnostics.AddError("Failed to attach service to VPC", err.Error()) return } @@ -504,7 +504,6 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest tflog.Error(ctx, fmt.Sprintf("error updating terraform state %v", resp.Diagnostics.Errors())) return } - } func (r *ServiceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { @@ -549,14 +548,14 @@ func serviceToResource(diag diag.Diagnostics, s *tsClient.Service, state service EnableHAReplica: types.BoolValue(s.ReplicaStatus != ""), ReadReplicaSource: state.ReadReplicaSource, } - if s.VpcEndpoint != nil { - if vpcId, err := strconv.ParseInt(s.VpcEndpoint.VpcId, 10, 64); err != nil { + if s.VPCEndpoint != nil { + if vpcId, err := strconv.ParseInt(s.VPCEndpoint.VPCId, 10, 64); err != nil { diag.AddError("Parse Error", "could not parse vpcID") } else { model.VpcId = types.Int64Value(vpcId) } - model.Hostname = types.StringValue(s.VpcEndpoint.Host) - model.Port = types.Int64Value(s.VpcEndpoint.Port) + model.Hostname = types.StringValue(s.VPCEndpoint.Host) + model.Port = types.Int64Value(s.VPCEndpoint.Port) } return model diff --git a/internal/provider/service_resource_test.go b/internal/provider/service_resource_test.go index 159935a..4547a27 100644 --- a/internal/provider/service_resource_test.go +++ b/internal/provider/service_resource_test.go @@ -5,16 +5,15 @@ import ( "fmt" "regexp" "testing" + "time" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) -const DEFAULT_VPC_ID = 2074 // Default vpc id for test acc - func TestServiceResource_Default_Success(t *testing.T) { // Test resource creation succeeds - config := &Config{ + config := &ServiceConfig{ ResourceName: "resource", } resource.ParallelTest(t, resource.TestCase{ @@ -23,7 +22,7 @@ func TestServiceResource_Default_Success(t *testing.T) { Steps: []resource.TestStep{ // Create default and Read testing { - Config: getConfig(t, config), + Config: getServiceConfig(t, config), Check: resource.ComposeAggregateTestCheckFunc( // Verify the name is set. resource.TestCheckResourceAttrSet("timescale_service.resource", "name"), @@ -42,7 +41,7 @@ func TestServiceResource_Default_Success(t *testing.T) { }, // Do a compute resize { - Config: getConfig(t, config.WithSpec(1000, 4)), + Config: getServiceConfig(t, config.WithSpec(1000, 4)), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("timescale_service.resource", "milli_cpu", "1000"), resource.TestCheckResourceAttr("timescale_service.resource", "memory_gb", "4"), @@ -50,26 +49,11 @@ func TestServiceResource_Default_Success(t *testing.T) { }, // Update service name { - Config: getConfig(t, config.WithName("service resource test update")), + Config: getServiceConfig(t, config.WithName("service resource test update")), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("timescale_service.resource", "name", "service resource test update"), ), }, - // Add VPC - { - Config: getConfig(t, config.WithVPC(DEFAULT_VPC_ID)), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("timescale_service.resource", "vpc_id", "2074"), - ), - }, - // Add HA replica and remove VPC - { - Config: getConfig(t, config.WithVPC(0).WithHAReplica(true)), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckNoResourceAttr("timescale_service.resource", "vpc_id"), - resource.TestCheckResourceAttr("timescale_service.resource", "enable_ha_replica", "true"), - ), - }, }, }) } @@ -84,20 +68,20 @@ func TestServiceResource_Read_Replica(t *testing.T) { replicaFQID = "timescale_service." + replicaName ) var ( - primaryConfig = &Config{ + primaryConfig = &ServiceConfig{ ResourceName: primaryName, Name: "service resource test init", } - extraConfig = &Config{ + extraConfig = &ServiceConfig{ ResourceName: extraName, } - replicaConfig = &Config{ + replicaConfig = &ServiceConfig{ ResourceName: replicaName, ReadReplicaSource: primaryFQID + ".id", MilliCPU: 500, MemoryGB: 2, } - extraReplicaConfig = &Config{ + extraReplicaConfig = &ServiceConfig{ ResourceName: replicaName + "_2", ReadReplicaSource: primaryFQID + ".id", } @@ -108,7 +92,7 @@ func TestServiceResource_Read_Replica(t *testing.T) { PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { - Config: getConfig(t, primaryConfig, replicaConfig), + Config: getServiceConfig(t, primaryConfig, replicaConfig), Check: resource.ComposeAggregateTestCheckFunc( // Verify service attributes resource.TestCheckResourceAttr(primaryFQID, "name", "service resource test init"), @@ -140,66 +124,52 @@ func TestServiceResource_Read_Replica(t *testing.T) { }, // Update replica name { - Config: getConfig(t, primaryConfig, replicaConfig.WithName("replica")), + Config: getServiceConfig(t, primaryConfig, replicaConfig.WithName("replica")), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(replicaFQID, "name", "replica"), ), }, // Do a compute resize { - Config: getConfig(t, primaryConfig, replicaConfig.WithSpec(1000, 4)), + Config: getServiceConfig(t, primaryConfig, replicaConfig.WithSpec(1000, 4)), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(replicaFQID, "milli_cpu", "1000"), resource.TestCheckResourceAttr(replicaFQID, "memory_gb", "4"), ), }, - // Add VPC to the read replica - { - Config: getConfig(t, primaryConfig, replicaConfig.WithVPC(DEFAULT_VPC_ID)), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(replicaFQID, "vpc_id", "2074"), - ), - }, - // Remove VPC - { - Config: getConfig(t, primaryConfig, replicaConfig.WithVPC(0)), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckNoResourceAttr(primaryFQID, "vpc_id"), - ), - }, // Check adding HA returns an error { - Config: getConfig(t, primaryConfig, replicaConfig.WithHAReplica(true)), + Config: getServiceConfig(t, primaryConfig, replicaConfig.WithHAReplica(true)), ExpectError: regexp.MustCompile(errReplicaWithHA), }, // Check removing read_replica_source returns an error { - Config: getConfig(t, primaryConfig, replicaConfig.WithHAReplica(false).WithReadReplica("")), + Config: getServiceConfig(t, primaryConfig, replicaConfig.WithHAReplica(false).WithReadReplica("")), ExpectError: regexp.MustCompile(errUpdateReplicaSource), }, // Check changing read_replica_source returns an error { - Config: getConfig(t, primaryConfig, extraConfig, replicaConfig.WithReadReplica(extraFQID+".id")), + Config: getServiceConfig(t, primaryConfig, extraConfig, replicaConfig.WithReadReplica(extraFQID+".id")), ExpectError: regexp.MustCompile(errUpdateReplicaSource), }, // Check enabling read_replica_source returns an error { - Config: getConfig(t, primaryConfig.WithReadReplica(extraFQID+".id"), extraConfig, replicaConfig.WithReadReplica(primaryFQID+".id")), + Config: getServiceConfig(t, primaryConfig.WithReadReplica(extraFQID+".id"), extraConfig, replicaConfig.WithReadReplica(primaryFQID+".id")), ExpectError: regexp.MustCompile(errUpdateReplicaSource), }, // Check creating multiple read replicas returns an error { - Config: getConfig(t, primaryConfig.WithReadReplica(""), replicaConfig, extraReplicaConfig), + Config: getServiceConfig(t, primaryConfig.WithReadReplica(""), replicaConfig, extraReplicaConfig), ExpectError: regexp.MustCompile(errMultipleReadReplicas), }, // Test creating a read replica from a read replica returns an error { - Config: getConfig(t, primaryConfig, replicaConfig, extraReplicaConfig.WithReadReplica(replicaFQID+".id")), + Config: getServiceConfig(t, primaryConfig, replicaConfig, extraReplicaConfig.WithReadReplica(replicaFQID+".id")), ExpectError: regexp.MustCompile(errReplicaFromFork), }, // Remove Replica { - Config: getConfig(t, primaryConfig), + Config: getServiceConfig(t, primaryConfig), Check: func(state *terraform.State) error { resources := state.RootModule().Resources if _, ok := resources[replicaFQID]; ok { @@ -218,7 +188,7 @@ func TestServiceResource_Timeout(t *testing.T) { PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { - Config: newServiceConfig(Config{ + Config: newServiceConfig(ServiceConfig{ Name: "service resource test timeout", Timeouts: Timeouts{ Create: "1s", @@ -238,7 +208,7 @@ func TestServiceResource_CustomConf(t *testing.T) { Steps: []resource.TestStep{ // Invalid conf millicpu & memory invalid ratio { - Config: newServiceCustomConfig("invalid", Config{ + Config: newServiceCustomConfig("invalid", ServiceConfig{ Name: "service resource test conf", MilliCPU: 2000, MemoryGB: 2, @@ -247,7 +217,7 @@ func TestServiceResource_CustomConf(t *testing.T) { }, // Invalid conf storage invalid value { - Config: newServiceCustomConfig("invalid", Config{ + Config: newServiceCustomConfig("invalid", ServiceConfig{ Name: "service resource test conf", MilliCPU: 500, MemoryGB: 3, @@ -256,14 +226,14 @@ func TestServiceResource_CustomConf(t *testing.T) { }, // Invalid conf storage invalid region { - Config: newServiceCustomConfig("invalid", Config{ + Config: newServiceCustomConfig("invalid", ServiceConfig{ RegionCode: "test-invalid-region", }), ExpectError: regexp.MustCompile(ErrInvalidAttribute), }, // Create with custom conf and region { - Config: newServiceCustomConfig("custom", Config{ + Config: newServiceCustomConfig("custom", ServiceConfig{ Name: "service resource test conf", RegionCode: "eu-central-1", MilliCPU: 1000, @@ -275,28 +245,12 @@ func TestServiceResource_CustomConf(t *testing.T) { resource.TestCheckNoResourceAttr("timescale_service.custom", "vpc_id"), ), }, - // Create with HA and VPC attached - { - Config: newServiceCustomVpcConfig("hareplica", Config{ - Name: "service resource test HA", - RegionCode: "us-east-1", - MilliCPU: 500, - MemoryGB: 2, - EnableHAReplica: true, - VpcID: DEFAULT_VPC_ID, - }), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("timescale_service.hareplica", "name", "service resource test HA"), - resource.TestCheckResourceAttr("timescale_service.hareplica", "enable_ha_replica", "true"), - resource.TestCheckResourceAttr("timescale_service.hareplica", "vpc_id", "2074"), - ), - }, }, }) } func TestServiceResource_Import(t *testing.T) { - config := newServiceConfig(Config{Name: "import test"}) + config := newServiceConfig(ServiceConfig{Name: "import test"}) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, PreCheck: func() { testAccPreCheck(t) }, @@ -308,6 +262,10 @@ func TestServiceResource_Import(t *testing.T) { // Import the resource. This step compares the resource attributes for "test" defined above with the imported resource // "test_import" defined in the config for this step. This check is done by specifying the ImportStateVerify configuration option. { + Check: func(s *terraform.State) error { + time.Sleep(10 * time.Second) + return nil + }, ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"password"}, @@ -329,7 +287,7 @@ func TestServiceResource_Import(t *testing.T) { }) } -func newServiceConfig(config Config) string { +func newServiceConfig(config ServiceConfig) string { if config.Timeouts.Create == "" { config.Timeouts.Create = "10m" } @@ -342,7 +300,7 @@ func newServiceConfig(config Config) string { }`, config.Name, config.Timeouts.Create) } -func newServiceCustomConfig(resourceName string, config Config) string { +func newServiceCustomConfig(resourceName string, config ServiceConfig) string { if config.Timeouts.Create == "" { config.Timeouts.Create = "30m" } @@ -359,20 +317,20 @@ func newServiceCustomConfig(resourceName string, config Config) string { }`, resourceName, config.Name, config.Timeouts.Create, config.MilliCPU, config.MemoryGB, config.RegionCode, config.EnableHAReplica) } -func newServiceCustomVpcConfig(resourceName string, config Config) string { - if config.Timeouts.Create == "" { - config.Timeouts.Create = "30m" - } - return providerConfig + fmt.Sprintf(` - resource "timescale_service" "%s" { - name = %q - timeouts = { - create = %q - } - milli_cpu = %d - memory_gb = %d - region_code = %q - vpc_id = %d - enable_ha_replica = %t - }`, resourceName, config.Name, config.Timeouts.Create, config.MilliCPU, config.MemoryGB, config.RegionCode, config.VpcID, config.EnableHAReplica) -} +// func newServiceCustomVpcConfig(resourceName string, config ServiceConfig) string { +// if config.Timeouts.Create == "" { +// config.Timeouts.Create = "30m" +// } +// return providerConfig + fmt.Sprintf(` +// resource "timescale_service" "%s" { +// name = %q +// timeouts = { +// create = %q +// } +// milli_cpu = %d +// memory_gb = %d +// region_code = %q +// vpc_id = %d +// enable_ha_replica = %t +// }`, resourceName, config.Name, config.Timeouts.Create, config.MilliCPU, config.MemoryGB, config.RegionCode, config.VpcID, config.EnableHAReplica) +// } diff --git a/internal/provider/vpc_resource.go b/internal/provider/vpc_resource.go new file mode 100644 index 0000000..250f7c2 --- /dev/null +++ b/internal/provider/vpc_resource.go @@ -0,0 +1,412 @@ +package provider + +import ( + "context" + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + tsClient "github.com/timescale/terraform-provider-timescale/internal/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &vpcResource{} + _ resource.ResourceWithConfigure = &vpcResource{} + + ErrVPCRead = "Error reading VPC" + ErrVPCCreate = "Error creating VPC" + ErrVPCUpdate = "Error updating VPC" + + regionCodes = []string{"us-east-1", "eu-west-1", "us-west-2", "eu-central-1", "ap-southeast-2"} +) + +// NewVpcsResource is a helper function to simplify the provider implementation. +func NewVpcsResource() resource.Resource { + return &vpcResource{} +} + +// vpcResource is the data source implementation. +type vpcResource struct { + client *tsClient.Client +} + +// vpcResourceModel maps vpcs schema data. +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"` + // PeeringConnections types.List `tfsdk:"peering_connections"` +} + +// type peeringConnectionResourceModel struct { +// ID types.Int64 `tfsdk:"id"` +// VpcID types.Int64 `tfsdk:"vpc_id"` +// Status types.String `tfsdk:"status"` +// ErrorMessage types.String `tfsdk:"error_message"` +// PeerVpcs types.List `tfsdk:"peer_vpc"` +// } + +var ( + PeerVpcType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.Int64Type, + "cidr": types.StringType, + "account_id": types.StringType, + "region_code": types.StringType, + }, + } + + PeeringConnectionsType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.Int64Type, + "vpc_id": types.Int64Type, + "status": types.StringType, + "error_message": types.StringType, + "peer_vpc": types.ListType{ElemType: PeerVpcType}, + }, + } +) + +// Metadata returns the data source type name. +func (r *vpcResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vpcs" +} + +// Read refreshes the Terraform state with the latest data. +func (r *vpcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Trace(ctx, "VpcResource.Read") + var state vpcResourceModel + var vpc *tsClient.VPC + var err error + // Read Terraform prior state plan into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + if !state.Name.IsNull() { + tflog.Info(ctx, "Getting VPC by name: "+state.Name.ValueString()) + vpc, err = r.client.GetVPCByName(ctx, state.Name.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to Read vpc, got error: %s, %s", state.Name.ValueString(), err)) + return + } + } else { + resp.Diagnostics.AddError(ErrVPCRead, "error must provide Name") + return + } + vpcId, err := strconv.ParseInt(vpc.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("Parse Error", "could not parse vpcID") + } + state.ID = types.Int64Value(vpcId) + state.Created = types.StringValue(vpc.Created) + state.ProjectID = types.StringValue(vpc.ProjectID) + state.CIDR = types.StringValue(vpc.CIDR) + state.RegionCode = types.StringValue(vpc.RegionCode) + resourceModel := vpcToResource(resp.Diagnostics, vpc, state) + // Save updated plan into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, resourceModel)...) + if resp.Diagnostics.HasError() { + tflog.Error(ctx, fmt.Sprintf("error updating terraform state %v", resp.Diagnostics.Errors())) + return + } +} + +func vpcToResource(diag diag.Diagnostics, s *tsClient.VPC, state vpcResourceModel) vpcResourceModel { + model := vpcResourceModel{ + ID: state.ID, + ProjectID: state.ProjectID, + Created: state.Created, + RegionCode: state.RegionCode, + CIDR: state.CIDR, + Name: types.StringValue(s.Name), + ProvisionedID: types.StringValue(s.ProvisionedID), + Status: types.StringValue(s.Status), + ErrorMessage: types.StringValue(s.ErrorMessage), + Updated: types.StringValue(s.Updated), + // PeeringConnections: types.ListUnknown(PeeringConnectionsType), + } + // for _, peeringConn := range vpc.PeeringConnections { + // peeringConnID, err := strconv.ParseInt(peeringConn.ID, 10, 64) + // if err != nil { + // resp.Diagnostics.AddError("Unable to Convert Vpc ID", err.Error()) + // return + // } + // peeringConnVpcID, err := strconv.ParseInt(peeringConn.VpcID, 10, 64) + // if err != nil { + // resp.Diagnostics.AddError("Unable to Convert Vpc ID", err.Error()) + // return + // } + // peerConn := peeringConnectionModel{ + // ID: types.Int64Value(peeringConnID), + // VpcID: types.Int64Value(peeringConnVpcID), + // Status: types.StringValue(peeringConn.Status), + // ErrorMessage: types.StringValue(peeringConn.ErrorMessage), + // } + // for _, peerVpc := range peeringConn.PeerVpcs { + // peerVpcId, err := strconv.ParseInt(peerVpc.ID, 10, 64) + // if err != nil { + // resp.Diagnostics.AddError("Unable to Convert Vpc ID", err.Error()) + // return + // } + // peerConn.PeerVpcs = append(peerConn.PeerVpcs, &peerVpcModel{ + // ID: types.Int64Value(peerVpcId), + // AccountID: types.StringValue(peerVpc.AccountID), + // CIDR: types.StringValue(peerVpc.CIDR), + // RegionCode: types.StringValue(peerVpc.RegionCode), + // }) + // } + // vpcState.PeeringConnections = append(vpcState.PeeringConnections, peerConn) + // } + return model +} + +// Create creates a VPC shell +func (r *vpcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + tflog.Trace(ctx, "VpcResource.Create") + var plan vpcResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if plan.CIDR.IsNull() { + resp.Diagnostics.AddError(ErrVPCCreate, "CIDR is required") + return + } + if plan.RegionCode.IsNull() { + resp.Diagnostics.AddError(ErrVPCCreate, "Region code is required") + return + } + vpc, err := r.client.CreateVPC(ctx, plan.Name.ValueString(), plan.CIDR.ValueString(), plan.RegionCode.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to Create Vpc %v", plan), + err.Error(), + ) + return + } + vpcId, err := strconv.ParseInt(vpc.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("Parse Error", "could not parse vpcID") + } + plan.ID = types.Int64Value(vpcId) + plan.Created = types.StringValue(vpc.Created) + plan.ProjectID = types.StringValue(vpc.ProjectID) + plan.CIDR = types.StringValue(vpc.CIDR) + plan.RegionCode = types.StringValue(vpc.RegionCode) + resourceModel := vpcToResource(resp.Diagnostics, vpc, plan) + + // Set state + resp.Diagnostics.Append(resp.State.Set(ctx, resourceModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes a VPC shell +func (r *vpcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + tflog.Trace(ctx, "VpcsResource.Delete") + var state vpcResourceModel + // // TODO: find a way to have this before automated test deletion + // time.Sleep(10 * time.Second) + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, fmt.Sprintf("Deleting Vpc: %v", state.ID.ValueInt64())) + + err := r.client.DeleteVPC(ctx, state.ID.ValueInt64()) + if err != nil { + resp.Diagnostics.AddError( + "Error Deleting Timescale Vpc", + "Could not delete vpc, unexpected error: "+err.Error(), + ) + return + } +} + +// Update updates a VPC shell +func (r *vpcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + tflog.Trace(ctx, "VpcsResource.Update") + var plan, state vpcResourceModel + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if plan.RegionCode != state.RegionCode { + resp.Diagnostics.AddError(ErrVPCUpdate, "Do not support region code change") + return + } + if plan.CIDR != state.CIDR { + resp.Diagnostics.AddError(ErrVPCUpdate, "Do not support cidr change") + return + } + + if !plan.Name.Equal(state.Name) { + if err := r.client.RenameVPC(ctx, state.ID.ValueInt64(), plan.Name.ValueString()); err != nil { + resp.Diagnostics.AddError(ErrVPCUpdate, err.Error()) + return + } + } + state.Name = plan.Name + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *vpcResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp) +} + +// Configure adds the provider configured client to the data source. +func (r *vpcResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + tflog.Trace(ctx, "vpcsResource.Configure") + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*tsClient.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *tsClient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +// Schema defines the schema for the data source. +func (r *vpcResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "provisioned_id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "cidr": schema.StringAttribute{ + Description: `The IPv4 CIDR block`, + MarkdownDescription: "The IPv4 CIDR block", + Required: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "VPC Name is the configurable name assigned to this vpc. If none is provided, a default will be generated by the provider.", + Description: "Vpc name", + Optional: true, + // If the name attribute is absent, the provider will generate a default. + Computed: true, + }, + "region_code": schema.StringAttribute{ + Description: `The region for this service`, + MarkdownDescription: "The region for this service. Currently supported regions are us-east-1, eu-west-1, us-west-2, eu-central-1, ap-southeast-2", + Required: true, + Validators: []validator.String{stringvalidator.OneOf(regionCodes...)}, + }, + "status": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "error_message": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "created": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "updated": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + // "peering_connections": schema.ListNestedAttribute{ + // Optional: true, + // Computed: true, + // NestedObject: schema.NestedAttributeObject{ + // Attributes: map[string]schema.Attribute{ + // "id": schema.Int64Attribute{ + // Computed: true, + // }, + // "vpc_id": schema.Int64Attribute{ + // Computed: true, + // }, + // "status": schema.StringAttribute{ + // Computed: true, + // }, + // "error_message": schema.StringAttribute{ + // Computed: true, + // }, + // "peer_vpc": schema.ListNestedAttribute{ + // Computed: true, + // NestedObject: schema.NestedAttributeObject{ + // Attributes: map[string]schema.Attribute{ + // "id": schema.Int64Attribute{ + // Computed: true, + // }, + // "cidr": schema.StringAttribute{ + // Computed: true, + // }, + // "region_code": schema.StringAttribute{ + // Computed: true, + // }, + // "account_id": schema.StringAttribute{ + // Computed: true, + // }, + // }, + // }, + // }, + // }, + // }, + // }, + }, + } +} diff --git a/internal/provider/vpc_resource_test.go b/internal/provider/vpc_resource_test.go new file mode 100644 index 0000000..26042dd --- /dev/null +++ b/internal/provider/vpc_resource_test.go @@ -0,0 +1,82 @@ +package provider + +import ( + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +var ( + config = &VPCConfig{ + ResourceName: "resource", + } +) + +func TestVPCResource_Default_Success(t *testing.T) { + // Test resource creation succeeds + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + // Create the VPC + { + Config: getVPCConfig(t, config.WithName("vpc-1").WithCIDR("10.0.0.0/21").WithRegionCode("us-east-1")), + Check: resource.ComposeAggregateTestCheckFunc( + func(s *terraform.State) error { + time.Sleep(10 * time.Second) + return nil + }, + 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", "name", "vpc-1"), + // resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "updated"), + // resource.TestCheckNoResourceAttr("timescale_vpcs.resource", "created"), + ), + }, + // Rename + { + Config: getVPCConfig(t, config.WithName("vpc-renamed").WithCIDR("10.0.0.0/21").WithRegionCode("us-east-1")), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "project_id"), + resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "cidr"), + resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "created"), + resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "id"), + resource.TestCheckResourceAttrSet("timescale_vpcs.resource", "status"), + resource.TestCheckResourceAttr("timescale_vpcs.resource", "name", "vpc-renamed"), + // resource.TestCheckNoResourceAttr("timescale_vpcs.resource", "updated"), // rename returns a success and not a vpc so we only get this at refresh + ), + }, + }, + }) +} + +func TestVPCResource_Import(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + // Create the VPC to import + { + Config: getVPCConfig(t, config.WithName("import-test").WithCIDR("10.0.0.0/21").WithRegionCode("us-east-1")), + Check: func(s *terraform.State) error { + time.Sleep(10 * time.Second) + return nil + }, + }, + { + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"created", "status"}, + ImportStateId: "import-test", + ResourceName: "timescale_vpcs.resource_import", + Config: getVPCConfig(t, config.WithName("import-test").WithCIDR("10.0.0.0/21").WithRegionCode("us-east-1")) + ` + resource "timescale_vpcs" "resource_import" {} + `, + }, + }, + }) +} diff --git a/internal/provider/vpcs_data_source.go b/internal/provider/vpcs_data_source.go index 7e5c625..4a081fa 100644 --- a/internal/provider/vpcs_data_source.go +++ b/internal/provider/vpcs_data_source.go @@ -30,13 +30,13 @@ type vpcsDataSource struct { // vpcsDataSourceModel maps the data source schema data. type vpcsDataSourceModel struct { - Vpcs []vpcsModel `tfsdk:"vpcs"` + Vpcs []vpcDataSourceModel `tfsdk:"vpcs"` // following is a placeholder, required by terraform to run test suite ID types.String `tfsdk:"id"` } -// vpcsModel maps vpcs schema data. -type vpcsModel struct { +// vpcDataSourceModel maps vpcs schema data. +type vpcDataSourceModel struct { ID types.Int64 `tfsdk:"id"` ProvisionedID types.String `tfsdk:"provisioned_id"` ProjectID types.String `tfsdk:"project_id"` @@ -89,7 +89,7 @@ func (d *vpcsDataSource) Read(ctx context.Context, req datasource.ReadRequest, r resp.Diagnostics.AddError("Unable to Convert Vpc ID", err.Error()) return } - vpcState := vpcsModel{ + vpcState := vpcDataSourceModel{ ID: types.Int64Value(vpcId), Name: types.StringValue(vpc.Name), ProvisionedID: types.StringValue(vpc.ProvisionedID), @@ -108,7 +108,7 @@ func (d *vpcsDataSource) Read(ctx context.Context, req datasource.ReadRequest, r resp.Diagnostics.AddError("Unable to Convert Vpc ID", err.Error()) return } - peeringConnVpcID, err := strconv.ParseInt(peeringConn.VpcID, 10, 64) + peeringConnVpcID, err := strconv.ParseInt(peeringConn.VPCID, 10, 64) if err != nil { resp.Diagnostics.AddError("Unable to Convert Vpc ID", err.Error()) return @@ -119,7 +119,7 @@ func (d *vpcsDataSource) Read(ctx context.Context, req datasource.ReadRequest, r Status: types.StringValue(peeringConn.Status), ErrorMessage: types.StringValue(peeringConn.ErrorMessage), } - for _, peerVpc := range peeringConn.PeerVpcs { + for _, peerVpc := range peeringConn.PeerVPCs { peerVpcId, err := strconv.ParseInt(peerVpc.ID, 10, 64) if err != nil { resp.Diagnostics.AddError("Unable to Convert Vpc ID", err.Error())