Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HCCP91 hvn routes data source #115

Merged
merged 12 commits into from
May 3, 2021
Merged
Show file tree
Hide file tree
Changes from 11 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
49 changes: 49 additions & 0 deletions docs/data-sources/hvn_route.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "hcp_hvn_route Data Source - terraform-provider-hcp"
subcategory: ""
description: |-
The HVN route data source provides information about an existing HVN route.
---

# hcp_hvn_route (Data Source)

The HVN route data source provides information about an existing HVN route.

## Example Usage

```terraform
data "hcp_hvn_route" "example" {
hvn = var.hvn
destination_cidr = var.destination_cidr
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- **destination_cidr** (String) The destination CIDR of the HVN route
- **hvn** (String) The `self_link` of the HashiCorp Virtual Network (HVN).

### Optional

- **id** (String) The ID of this resource.
- **timeouts** (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))

### Read-Only

- **created_at** (String) The time that the HVN route was created.
- **self_link** (String) A unique URL identifying the HVN route.
- **state** (String) The state of the HVN route.
- **target_link** (String) A unique URL identifying the target of the HVN route.

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

Optional:

- **default** (String)


4 changes: 4 additions & 0 deletions examples/data-sources/hcp_hvn_route/data-source.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
data "hcp_hvn_route" "example" {
hvn = var.hvn
destination_cidr = var.destination_cidr
}
9 changes: 9 additions & 0 deletions examples/data-sources/hcp_hvn_route/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
variable "hvn" {
description = "The `self_link` of the HashiCorp Virtual Network (HVN)."
type = string
}

variable "destination_cidr" {
description = "The destination CIDR of the HVN route."
type = string
}
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ module github.com/hashicorp/terraform-provider-hcp
go 1.15

require (
github.com/armon/go-radix v1.0.0 // indirect
github.com/aws/aws-sdk-go v1.37.0 // indirect
github.com/go-openapi/runtime v0.19.28
github.com/go-openapi/strfmt v0.20.1
github.com/google/uuid v1.2.0
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
github.com/hashicorp/hcp-sdk-go v0.6.0
github.com/hashicorp/hcl/v2 v2.8.2 // indirect
github.com/hashicorp/hcp-sdk-go v0.7.0
github.com/hashicorp/terraform-exec v0.13.3 // indirect
github.com/hashicorp/terraform-plugin-docs v0.4.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.5.0
github.com/posener/complete v1.2.1 // indirect
github.com/stretchr/testify v1.7.0
golang.org/x/tools v0.0.0-20201028111035-eafbe7b904eb // indirect
google.golang.org/api v0.34.0 // indirect
)
106 changes: 77 additions & 29 deletions go.sum

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions internal/clients/hvn_route.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package clients

import (
"context"

"github.com/hashicorp/hcp-sdk-go/clients/cloud-network/preview/2020-09-07/client/network_service"
networkmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-network/preview/2020-09-07/models"
sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models"
)

// ListHVNRoutes lists the routes for an HVN.
func ListHVNRoutes(ctx context.Context, client *Client, hvnID string,
destination string, targetID string, targetType string,
loc *sharedmodels.HashicorpCloudLocationLocation) ([]*networkmodels.HashicorpCloudNetwork20200907HVNRoute, error) {
listHVNRoutesParams := network_service.NewListHVNRoutesParams()
listHVNRoutesParams.Context = ctx
listHVNRoutesParams.HvnID = hvnID
listHVNRoutesParams.HvnLocationOrganizationID = loc.OrganizationID
listHVNRoutesParams.HvnLocationProjectID = loc.ProjectID
listHVNRoutesParams.Destination = &destination
listHVNRoutesParams.TargetID = &targetID
listHVNRoutesParams.TargetType = &targetType

listHVNRoutesResponse, err := client.Network.ListHVNRoutes(listHVNRoutesParams, nil)
if err != nil {
return nil, err
}

return listHVNRoutesResponse.Payload.Routes, nil
}
102 changes: 102 additions & 0 deletions internal/provider/data_source_hvn_route.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package provider

import (
"context"
"log"

sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-hcp/internal/clients"
)

func dataSourceHVNRoute() *schema.Resource {
return &schema.Resource{
Description: "The HVN route data source provides information about an existing HVN route.",
ReadContext: dataSourceHVNRouteRead,
Timeouts: &schema.ResourceTimeout{
Default: &hvnRouteDefaultTimeout,
},
Schema: map[string]*schema.Schema{
// Required inputs
"hvn": {
Description: "The `self_link` of the HashiCorp Virtual Network (HVN).",
Type: schema.TypeString,
Required: true,
},
"destination_cidr": {
nombiezinja marked this conversation as resolved.
Show resolved Hide resolved
Description: "The destination CIDR of the HVN route",
Type: schema.TypeString,
Required: true,
ValidateDiagFunc: validateIsStartOfPrivateCIDRRange,
Copy link
Contributor Author

@nombiezinja nombiezinja Apr 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added validation for destination_cidr. Originally I was planning to use the IsCIDR helper as @bcmdarroch had suggested, and use validation.All following the example here(https://github.com/hashicorp/terraform-provider-aws/blob/main/aws/resource_aws_eks_cluster.go#L133). However, I went with a custom validator for 3 reasons:

1- terraform-provider-hcp only uses ValidateDiagFunc, and validation.All(https://github.com/hashicorp/terraform-plugin-sdk/blob/v1.17.2/helper/validation/meta.go#L30) would not work
2- ValidateFunc is deprecated (https://github.com/hashicorp/terraform-plugin-sdk/blob/main/helper/schema/schema.go#L308) but most importantly
3- HCP create HVN route endpoint validation (https://github.com/hashicorp/cloud-network/blob/master/service/network/private/service_create_hvn_route.go#L218) uses the validation rule here: https://github.com/hashicorp/cloud-sdk/blob/master/validation/rule_cidrblock.go#L0-L1; this validation is reused in several HCP APIs taking a CIDR parameter.

validateIsStartOfPrivateCIDRRange validates based on the same rules as the cloud-sdk CIDR validation:

a) CIDR is valid
b) CIDR is private CIDR block
c) Address is at beginning of CIDR range

We could reuse this validator elsewhere in the provider too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to remove validation here (and probably in other places too), the least duplicated logic we have in Tf provider the easier it will be to maintain. Basic request validation will be done by the backend very quickly and won't impact UX of the TF provider.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding client-side validation for this! Looks great, and agreed that this validator should be used for our other CIDRs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, just missed @smaant 's response on this. IMO it is reasonable to have this be validated client-side (I'm assuming it is generic and unlikely to change over time), and another slight benefit is that the user will get feedback on the plan step instead of seeing it later during apply. But otherwise, I do agree that this could be deferred to the backend and the experience would be similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the custom validator with IsCIDR as we are looking to gain some traction on this work, and merged this PR

Also spoke to provider team (@bcmdarroch & @roaks3 ) as well as some Terraform practitioners; my recommendation is to ensure the custom validator is added back at some point. The aforementioned 3 rules are rudimentary and are unlikely to be removed from the cloud-sdk validation package, hence drift is less of a concern. If the validation rules in cloud-sdk were to change, we are unlikely to remove rules, and only likely to add additional rules, in which case the custom validator covering 3 existing rules will be a good idea in terms of improving the practitioner experience for this provider.

},
// Computed outputs
"self_link": {
Description: "A unique URL identifying the HVN route.",
Type: schema.TypeString,
Computed: true,
},
"target_link": {
Description: "A unique URL identifying the target of the HVN route.",
Type: schema.TypeString,
nombiezinja marked this conversation as resolved.
Show resolved Hide resolved
Computed: true,
},
"state": {
Description: "The state of the HVN route.",
Type: schema.TypeString,
Computed: true,
},
"created_at": {
Description: "The time that the HVN route was created.",
Type: schema.TypeString,
Computed: true,
},
},
}
}

func dataSourceHVNRouteRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client)

hvn := d.Get("hvn").(string)
var hvnLink *sharedmodels.HashicorpCloudLocationLink

hvnLink, err := parseLinkURL(hvn, HvnResourceType)
if err != nil {
return diag.FromErr(err)
}

loc := &sharedmodels.HashicorpCloudLocationLocation{
OrganizationID: client.Config.OrganizationID,
ProjectID: client.Config.ProjectID,
}
destination := d.Get("destination_cidr").(string)

log.Printf("[INFO] Reading HVN route for HVN (%s) with destination_cidr=%s ", hvn, destination)
route, err := clients.ListHVNRoutes(ctx, client, hvnLink.ID, destination, "", "", loc)
if err != nil {
return diag.Errorf("unable to retrieve HVN route for HVN (%s) with destination_cidr=%s: %v",
hvn, destination, err)
}

// ListHVNRoutes call should return 1 and only 1 HVN route.
if len(route) > 1 {
return diag.Errorf("Unexpected number of HVN routes returned for destination_cidr=%s: %d", destination, len(route))
nombiezinja marked this conversation as resolved.
Show resolved Hide resolved
}
if len(route) == 0 {
return diag.Errorf("No HVN route found for destionation_cidr=%s", destination)
}

link := newLink(loc, HVNRouteResourceType, route[0].ID)
url, err := linkURL(link)
if err != nil {
return diag.FromErr(err)
}
d.SetId(url)

if err := setHVNRouteResourceData(d, route[0], loc); err != nil {
return diag.FromErr(err)
}

return nil
}
3 changes: 3 additions & 0 deletions internal/provider/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const (
// TgwAttachmentResourceType is the resource type of a TGW attachment
TgwAttachmentResourceType = "hashicorp.network.tgw-attachment"

// HVNRouteResourceType is the resource type of an HVN route
HVNRouteResourceType = "hashicorp.network.route"

// ConsulSnapshotResourceType is the resource type of a Consul snapshot
ConsulSnapshotResourceType = "hashicorp.consul.snapshot"

Expand Down
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func New() func() *schema.Provider {
"hcp_consul_cluster": dataSourceConsulCluster(),
"hcp_consul_versions": dataSourceConsulVersions(),
"hcp_hvn": dataSourceHvn(),
"hcp_hvn_route": dataSourceHVNRoute(),
"hcp_vault_cluster": dataSourceVaultCluster(),
},
ResourcesMap: map[string]*schema.Resource{
Expand Down
51 changes: 51 additions & 0 deletions internal/provider/resource_hvn_route.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package provider

import (
"time"

networkmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-network/preview/2020-09-07/models"
sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var hvnRouteDefaultTimeout = time.Minute * 1

func setHVNRouteResourceData(d *schema.ResourceData, route *networkmodels.HashicorpCloudNetwork20200907HVNRoute,
loc *sharedmodels.HashicorpCloudLocationLocation) error {

// Set self_link for the HVN route.
link := newLink(loc, HVNRouteResourceType, route.ID)
selfLink, err := linkURL(link)
if err != nil {
return err
}

if err := d.Set("self_link", selfLink); err != nil {
return err
}

// Set self_link identifying the target of the HVN route.
hvnLink := newLink(loc, route.Target.HvnConnection.Type, route.Target.HvnConnection.ID)
targetLink, err := linkURL(hvnLink)
if err != nil {
return err
}

if err := d.Set("target_link", targetLink); err != nil {
return err
}

if err := d.Set("destination_cidr", route.Destination); err != nil {
return err
}

if err := d.Set("state", route.State); err != nil {
return err
}

if err := d.Set("created_at", route.CreatedAt.String()); err != nil {
return err
}

return nil
}
58 changes: 58 additions & 0 deletions internal/provider/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package provider

import (
"fmt"
"net"
"regexp"
"strings"

Expand Down Expand Up @@ -148,3 +149,60 @@ func validateConsulClusterSize(v interface{}, path cty.Path) diag.Diagnostics {

return diagnostics
}

// validateIsStartOfPrivateCIDRRange validates that a value passed is (a)a valid
// CIDR, (b)a valid RFC 1819 address, and (c)at the start of the CIDR range.
func validateIsStartOfPrivateCIDRRange(v interface{}, path cty.Path) diag.Diagnostics {
var diagnostics diag.Diagnostics

// Validation fails if value is not string.
s, ok := v.(string)
if !ok {
msg := fmt.Sprintf("expected type of %v to be string", v)
diagnostics = append(diagnostics, diag.Diagnostic{
Severity: diag.Error,
Summary: msg,
Detail: msg,
AttributePath: path,
})
return diagnostics
}

// Validation fails if string cannot be parsed as CIDR.
ip, net, err := net.ParseCIDR(s)
if err != nil {
msg := fmt.Sprintf("expected \"%v\" to be a valid IPv4 value", v)
diagnostics = append(diagnostics, diag.Diagnostic{
Severity: diag.Error,
Summary: msg,
Detail: msg,
AttributePath: path,
})
return diagnostics
}

// Validation fails if address is not within valid RFC 1819 ranges
if !regexp.MustCompile(`^(10|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\..*`).MatchString(v.(string)) {
msg := "must be within 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16"
diagnostics = append(diagnostics, diag.Diagnostic{
Severity: diag.Error,
Summary: msg,
Detail: msg,
AttributePath: path,
})
return diagnostics
}

// Validation fails if value is not at the beginning of the CIDR range.
if !ip.Equal(net.IP) {
msg := fmt.Sprintf("invalid CIDR range start %v, should have been %v", ip, net.IP)
diagnostics = append(diagnostics, diag.Diagnostic{
Severity: diag.Error,
Summary: msg,
Detail: msg + "; CIDR value must be at the start of the range",
AttributePath: path,
})
}

return diagnostics
}
Loading