diff --git a/docs/resources/vpcep_approval_v1.md b/docs/resources/vpcep_approval_v1.md new file mode 100644 index 000000000..99adcadbe --- /dev/null +++ b/docs/resources/vpcep_approval_v1.md @@ -0,0 +1,96 @@ +--- +subcategory: "VPC Endpoint (VPCEP)" +layout: "opentelekomcloud" +page_title: "OpenTelekomCloud: opentelekomcloud_vpcep_approval_v1" +sidebar_current: "docs-opentelekomcloud-resource-vpcep-approval-v1" +description: |- + Manages a VPCEP Endpoint resource within OpenTelekomCloud. +--- + + +# opentelekomcloud_vpcep_approval_v1 + +Provides a resource to manage the VPC endpoint connections. + +## Example Usage + +```hcl +variable "service_vpc_id" {} +variable "vm_port" {} +variable "vpc_id" {} +variable "subnet_id" {} + +resource "opentelekomcloud_vpcep_service_v1" "srv" { + name = "demo-service" + server_type = "VM" + vpc_id = var.service_vpc_id + port_id = var.vm_port + + approval_enabled = true + + port { + server_port = 8080 + client_port = 80 + } +} + +resource "opentelekomcloud_vpcep_endpoint_v1" "ep" { + service_id = opentelekomcloud_vpcep_service_v1.srv.id + vpc_id = var.vpc_id + subnet_id = var.subnet_id + enable_dns = true + + lifecycle { + # enable_dns and ip_address are not assigned until connecting to the service + ignore_changes = [ + enable_dns, + ip_address + ] + } +} + +resource "opentelekomcloud_vpcep_approval_v1" "approval" { + service_id = opentelekomcloud_vpcep_service_v1.srv.id + endpoints = [opentelekomcloud_vpcep_endpoint_v1.ep.id] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `service_id` - (Required, String, ForceNew) Specifies the ID of the VPC endpoint service. Changing this creates a new + resource. + +* `endpoints` - (Required, List) Specifies the list of VPC endpoint IDs which accepted to connect to VPC endpoint + service. The VPC endpoints will be rejected when the resource was destroyed. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The unique ID in UUID format which equals to the ID of the VPC endpoint service. + +* `connections` - An array of VPC endpoints connect to the VPC endpoint service. Structure is documented below. + + `endpoint_id` - The unique ID of the VPC endpoint. + + `packet_id` - The packet ID of the VPC endpoint. + + `domain_id` - The user's domain ID. + + `status` - The connection status of the VPC endpoint. + + `description` - The description of the VPC endpoint service connection. + +* `region` - The VPC endpoint service region. + +## Timeouts + +This resource provides the following timeouts configuration options: + +* `create` - Default is 10 minute. +* `delete` - Default is 3 minute. + +## Import + +VPC endpoint approval can be imported using the `id`, e.g. + +```bash +$ terraform import opentelekomcloud_vpcep_approval_v1.apr +``` diff --git a/opentelekomcloud/acceptance/vpcep/resource_opentelekomcloud_vpcep_approval_v1_test.go b/opentelekomcloud/acceptance/vpcep/resource_opentelekomcloud_vpcep_approval_v1_test.go new file mode 100644 index 000000000..67097951b --- /dev/null +++ b/opentelekomcloud/acceptance/vpcep/resource_opentelekomcloud_vpcep_approval_v1_test.go @@ -0,0 +1,121 @@ +package vpcep + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/opentelekomcloud/gophertelekomcloud/acceptance/tools" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/vpcep/v1/endpoints" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/acceptance/common" +) + +func TestAccVPCEndpointApproval_Basic(t *testing.T) { + var endpoint endpoints.Endpoint + rName := tools.RandomString("tf-test-ep-", 4) + resourceName := "opentelekomcloud_vpcep_approval_v1.approval" + + rc := common.InitResourceCheck( + "opentelekomcloud_vpcep_endpoint_v1.endpoint", + &endpoint, + getVPCEndpointFunc, + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { common.TestAccPreCheck(t) }, + ProviderFactories: common.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccVPCEndpointApproval_Basic(rName), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttrPair(resourceName, "id", "opentelekomcloud_vpcep_service_v1.service", "id"), + resource.TestCheckResourceAttrPair(resourceName, "connections.0.endpoint_id", + "opentelekomcloud_vpcep_endpoint_v1.endpoint", "id"), + resource.TestCheckResourceAttr(resourceName, "connections.0.status", "accepted"), + ), + }, + { + Config: testAccVPCEndpointApproval_Update(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "connections.0.endpoint_id", + "opentelekomcloud_vpcep_endpoint_v1.endpoint", "id"), + resource.TestCheckResourceAttr(resourceName, "connections.0.status", "rejected"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccVPCEndpointApproval_Base(name string) string { + return fmt.Sprintf(` +%s + +resource "opentelekomcloud_lb_loadbalancer_v2" "lb_1" { + vip_subnet_id = data.opentelekomcloud_vpc_subnet_v1.shared_subnet.subnet_id +} + +resource "opentelekomcloud_vpcep_service_v1" "service" { + name = "%s" + port_id = opentelekomcloud_lb_loadbalancer_v2.lb_1.vip_port_id + vpc_id = data.opentelekomcloud_vpc_subnet_v1.shared_subnet.vpc_id + server_type = "LB" + description = "test description" + + approval_enabled = true + + port { + client_port = 80 + server_port = 8080 + } + + tags = { + "key" : "value", + } + whitelist = ["698f9bf85ca9437a9b2f41132ab3aa0e"] +} + +resource "opentelekomcloud_vpcep_endpoint_v1" "endpoint" { + service_id = opentelekomcloud_vpcep_service_v1.service.id + vpc_id = opentelekomcloud_vpcep_service_v1.service.vpc_id + subnet_id = data.opentelekomcloud_vpc_subnet_v1.shared_subnet.id + enable_dns = true + + tags = { + "fizz" : "buzz" + } + + lifecycle { + ignore_changes = [enable_dns] + } +} +`, common.DataSourceSubnet, name) +} + +func testAccVPCEndpointApproval_Basic(rName string) string { + return fmt.Sprintf(` +%s + +resource "opentelekomcloud_vpcep_approval_v1" "approval" { + service_id = opentelekomcloud_vpcep_service_v1.service.id + endpoints = [opentelekomcloud_vpcep_endpoint_v1.endpoint.id] +} +`, testAccVPCEndpointApproval_Base(rName)) +} + +func testAccVPCEndpointApproval_Update(rName string) string { + return fmt.Sprintf(` +%s + +resource "opentelekomcloud_vpcep_approval_v1" "approval" { + service_id = opentelekomcloud_vpcep_service_v1.service.id + endpoints = [] +} +`, testAccVPCEndpointApproval_Base(rName)) +} diff --git a/opentelekomcloud/provider.go b/opentelekomcloud/provider.go index 1cd58bf42..0e303181b 100644 --- a/opentelekomcloud/provider.go +++ b/opentelekomcloud/provider.go @@ -583,6 +583,7 @@ func Provider() *schema.Provider { "opentelekomcloud_vpc_peering_connection_v2": vpc.ResourceVpcPeeringConnectionV2(), "opentelekomcloud_vpc_peering_connection_accepter_v2": vpc.ResourceVpcPeeringConnectionAccepterV2(), "opentelekomcloud_vpc_route_table_v1": vpc.ResourceVPCRouteTableV1(), + "opentelekomcloud_vpcep_approval_v1": vpcep.ResourceVPCEPApprovalV1(), "opentelekomcloud_vpcep_endpoint_v1": vpcep.ResourceVPCEPEndpointV1(), "opentelekomcloud_vpcep_service_v1": vpcep.ResourceVPCEPServiceV1(), "opentelekomcloud_vpc_route_v2": vpc.ResourceVPCRouteV2(), diff --git a/opentelekomcloud/services/vpcep/common.go b/opentelekomcloud/services/vpcep/common.go index 268fc8864..cae8908c7 100644 --- a/opentelekomcloud/services/vpcep/common.go +++ b/opentelekomcloud/services/vpcep/common.go @@ -1,4 +1,13 @@ package vpcep -const ErrClientCreate = "error creating VPC Endpoint v1 client: %w" -const keyClient = "vpcep-client" +const ( + ErrClientCreate = "error creating VPC Endpoint v1 client: %w" + keyClient = "vpcep-client" + actionReceive string = "receive" + actionReject string = "reject" +) + +var approvalActionStatusMap = map[string]string{ + actionReceive: "accepted", + actionReject: "rejected", +} diff --git a/opentelekomcloud/services/vpcep/resource_opentelekomcloud_vpcep_approval_v1.go b/opentelekomcloud/services/vpcep/resource_opentelekomcloud_vpcep_approval_v1.go new file mode 100644 index 000000000..246403847 --- /dev/null +++ b/opentelekomcloud/services/vpcep/resource_opentelekomcloud_vpcep_approval_v1.go @@ -0,0 +1,258 @@ +package vpcep + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/vpcep/v1/services" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/common" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/common/cfg" + "github.com/opentelekomcloud/terraform-provider-opentelekomcloud/opentelekomcloud/common/fmterr" +) + +func ResourceVPCEPApprovalV1() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceVPCEndpointApprovalCreate, + ReadContext: resourceVPCEndpointApprovalRead, + UpdateContext: resourceVPCEndpointApprovalUpdate, + DeleteContext: resourceVPCEndpointApprovalDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(3 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "service_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "endpoints": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "connections": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint_id": { + Type: schema.TypeString, + Computed: true, + }, + "packet_id": { + Type: schema.TypeInt, + Computed: true, + }, + "domain_id": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "region": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceVPCEndpointApprovalCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*cfg.Config) + client, err := common.ClientFromCtx(ctx, keyClient, func() (*golangsdk.ServiceClient, error) { + return config.VpcEpV1Client(config.GetRegion(d)) + }) + if err != nil { + return fmterr.Errorf(ErrClientCreate, err) + } + + // check status of the VPC endpoint service + serviceID := d.Get("service_id").(string) + n, err := services.Get(client, serviceID) + if err != nil { + return diag.Errorf("error retrieving VPC endpoint service %s: %s", serviceID, err) + } + if n.Status != "available" { + return diag.Errorf("the status of VPC endpoint service is %s, expected to be available", n.Status) + } + + raw := d.Get("endpoints").(*schema.Set).List() + err = doConnectionAction(ctx, d, client, serviceID, actionReceive, raw) + if err != nil { + return diag.Errorf("error receiving connections to VPC endpoint service %s: %s", serviceID, err) + } + + d.SetId(serviceID) + + clientCtx := common.CtxWithClient(ctx, client, keyClient) + return resourceVPCEndpointApprovalRead(clientCtx, d, meta) +} + +func resourceVPCEndpointApprovalRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*cfg.Config) + client, err := common.ClientFromCtx(ctx, keyClient, func() (*golangsdk.ServiceClient, error) { + return config.VpcEpV1Client(config.GetRegion(d)) + }) + if err != nil { + return fmterr.Errorf(ErrClientCreate, err) + } + + serviceID := d.Id() + connections, err := flattenVPCEndpointConnections(client, serviceID) + if err != nil { + return common.CheckDeletedDiag(d, err, "VPC endpoint service connection") + } + mErr := multierror.Append(nil, + d.Set("region", config.GetRegion(d)), + d.Set("connections", connections), + d.Set("service_id", serviceID), + ) + return diag.FromErr(mErr.ErrorOrNil()) +} + +func resourceVPCEndpointApprovalUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*cfg.Config) + client, err := common.ClientFromCtx(ctx, keyClient, func() (*golangsdk.ServiceClient, error) { + return config.VpcEpV1Client(config.GetRegion(d)) + }) + if err != nil { + return fmterr.Errorf(ErrClientCreate, err) + } + + if d.HasChange("endpoints") { + oldVal, newVal := d.GetChange("endpoints") + oldConnSet := oldVal.(*schema.Set) + newConnSet := newVal.(*schema.Set) + received := newConnSet.Difference(oldConnSet) + rejected := oldConnSet.Difference(newConnSet) + + serviceID := d.Get("service_id").(string) + err = doConnectionAction(ctx, d, client, serviceID, actionReceive, received.List()) + if err != nil { + return diag.Errorf("error receiving connections to VPC endpoint service %s: %s", serviceID, err) + } + + err = doConnectionAction(ctx, d, client, serviceID, actionReject, rejected.List()) + if err != nil { + return diag.Errorf("error rejecting connections to VPC endpoint service %s: %s", serviceID, err) + } + } + + clientCtx := common.CtxWithClient(ctx, client, keyClient) + return resourceVPCEndpointApprovalRead(clientCtx, d, meta) +} + +func resourceVPCEndpointApprovalDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*cfg.Config) + client, err := common.ClientFromCtx(ctx, keyClient, func() (*golangsdk.ServiceClient, error) { + return config.VpcEpV1Client(config.GetRegion(d)) + }) + if err != nil { + return fmterr.Errorf(ErrClientCreate, err) + } + + serviceID := d.Get("service_id").(string) + raw := d.Get("endpoints").(*schema.Set).List() + err = doConnectionAction(ctx, d, client, serviceID, actionReject, raw) + if err != nil { + return diag.Errorf("error rejecting connections to VPC endpoint service %s: %s", serviceID, err) + } + + return nil +} + +func doConnectionAction(ctx context.Context, d *schema.ResourceData, client *golangsdk.ServiceClient, serviceID, + action string, raw []interface{}) error { + if len(raw) == 0 { + return nil + } + + if _, ok := approvalActionStatusMap[action]; !ok { + return fmt.Errorf("approval action(%s) is invalid, only support %s or %s", action, actionReceive, + actionReject) + } + + targetStatus := approvalActionStatusMap[action] + for _, v := range raw { + // Each request accepts or rejects only one VPC endpoint + epID := v.(string) + connOpts := services.ActionOpts{ + Action: action, + Endpoints: []string{epID}, + } + + log.Printf("[DEBUG] %s to endpoint %s from VPC endpoint service %s", action, epID, serviceID) + _, err := services.Action(client, serviceID, connOpts) + if err != nil { + return fmt.Errorf("error %s to endpoint %s from VPC endpoint service %s: %s", action, epID, serviceID, err) + } + + log.Printf("[INFO] Waiting for VPC endpoint(%s) to become %s", epID, targetStatus) + stateConf := &resource.StateChangeConf{ + Pending: []string{"creating", "pendingAcceptance"}, + Target: []string{targetStatus}, + Refresh: waitForVPCEndpointConnected(client, serviceID, epID), + Timeout: d.Timeout(schema.TimeoutCreate), + Delay: 3 * time.Second, + PollInterval: 3 * time.Second, + } + + _, stateErr := stateConf.WaitForStateContext(ctx) + if stateErr != nil { + return fmt.Errorf("error waiting for VPC endpoint(%s) to become %s: %s", epID, targetStatus, + stateErr) + } + } + + return nil +} + +func waitForVPCEndpointConnected(client *golangsdk.ServiceClient, serviceId, endpointId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + listOpts := services.ListConnectionsOpts{ + ID: endpointId, + } + connections, err := services.ListConnections(client, serviceId, listOpts) + if err != nil { + if _, ok := err.(golangsdk.ErrDefault404); ok { + return connections, "deleted", nil + } + return connections, "error", err + } + if len(connections) == 1 && connections[0].ID == endpointId { + return connections, connections[0].Status, nil + } + return connections, "deleted", nil + } +} diff --git a/releasenotes/notes/vpcep-approval-5039ae3a4ba6a0f8.yaml b/releasenotes/notes/vpcep-approval-5039ae3a4ba6a0f8.yaml new file mode 100644 index 000000000..ce5acf323 --- /dev/null +++ b/releasenotes/notes/vpcep-approval-5039ae3a4ba6a0f8.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + **New Resource:** ``opentelekomcloud_vpcep_approval_v1`` (`#2764 `_)