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

provider/google: Cloud router resource #12411

Merged
merged 5 commits into from
May 25, 2017

Conversation

drebes
Copy link
Contributor

@drebes drebes commented Mar 3, 2017

This adds support for Cloud Router, which allows BGP announcement of routes through Cloud VPN tunnels.

It's still missing documentation, I'll provide that once the code is reviewed.

It adds the following resources:

  • google_compute_router
  • google_compute_router_interface
  • google_compute_router_peer

It also modifies google_compute_vpn_tunnel to support Cloud Router.

A full test is provided on resource_compute_router_peer_test.go

$ make testacc TEST=./builtin/providers/google TESTARGS='-run=TestAccComputeRouter'
==> Checking that code complies with gofmt requirements...
go generate $(go list ./... | grep -v /terraform/vendor/)
2017/03/03 15:38:06 Generated command/internal_plugin_list.go
TF_ACC=1 go test ./builtin/providers/google -v -run=TestAccComputeRouter -timeout 120m
=== RUN   TestAccComputeRouterInterface_import
--- PASS: TestAccComputeRouterInterface_import (197.04s)
=== RUN   TestAccComputeRouterPeer_import
--- PASS: TestAccComputeRouterPeer_import (218.85s)
=== RUN   TestAccComputeRouter_import
--- PASS: TestAccComputeRouter_import (83.30s)
=== RUN   TestAccComputeRouterInterface_basic
--- PASS: TestAccComputeRouterInterface_basic (378.11s)
=== RUN   TestAccComputeRouterPeer_basic
--- PASS: TestAccComputeRouterPeer_basic (410.42s)
=== RUN   TestAccComputeRouter_basic
--- PASS: TestAccComputeRouter_basic (96.71s)
=== RUN   TestAccComputeRouter_noRegion
--- PASS: TestAccComputeRouter_noRegion (62.47s)
=== RUN   TestAccComputeRouter_networkLink
--- PASS: TestAccComputeRouter_networkLink (84.34s)
PASS
ok  	github.com/hashicorp/terraform/builtin/providers/google	1531.337s

@drebes drebes force-pushed the cloud_router branch 2 times, most recently from 95318ad to 05b2f10 Compare March 25, 2017 05:21
@drebes drebes force-pushed the cloud_router branch 2 times, most recently from 641aceb to cc38e07 Compare April 26, 2017 19:32
Copy link
Contributor

@paddycarver paddycarver left a comment

Choose a reason for hiding this comment

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

Hey @drebes, thanks for opening this PR! Sorry for the long wait on the review. A PR this size is pretty intimidating and takes a lot of time to review. In the future, a PR that tackled maybe one of these resources would probably get a faster review on it, just as a heads up.

I know there are a lot of comments, but a lot of them are repetitive and are pretty minor. You did a great job at the design level, and I don't see much here that would require a refactor or major shift in how you're doing things. You get bonus points for supporting import right out of the gate! I was hoping to see Exists support, as well, but I won't hold this up over it.

That being said, it'd also be great if you could add some docs for the new resources/update the docs for the existing resource you've modified so people know how to use this great new functionality. If you need an example of that, let me know.

I know the amount of comments here is intimidating, but I think you're super close to having this be merge-ready; just some find-and-replaces and a few tweaks away.

Thanks for all the effort! If you've moved on/don't have time anymore to push this over the finish line, let me know, and I'll tackle it. :)

@@ -5,12 +5,17 @@ import (
"fmt"
"strings"

"github.com/hashicorp/terraform/helper/mutexkv"
//"github.com/hashicorp/terraform/helper/pathorcontents"
Copy link
Contributor

Choose a reason for hiding this comment

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

Would love to remove commented out code.

if err != nil {
return err
}
routersService := compute.NewRoutersService(config.clientCompute)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a benefit to doing this instead of using config.clientCompute.Routers?

return fmt.Errorf("Error Inserting Router %s into network %s: %s", name, network, err)
}

err = computeOperationWaitRegion(config, op, project, region, "Inserting Router")
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd love to see a call to d.SetId prior to the computeOperationWaitRegion, which just helps Terraform recover in case the operation times out or Terraform crashes, or something. Keep in mind this means that if there's an error inserting, d.SetId("") should be called, to clear out the ID. See an example in google_compute_instance.

}

name := d.Get("name").(string)
routersService := compute.NewRoutersService(config.clientCompute)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, I think we want config.clientCompute.Routers, just to be consistent, unless there's a difference or benefit I'm missing.


if err != nil {
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 {
log.Printf("[WARN] Removing Router %q because it's gone", d.Get("name").(string))
Copy link
Contributor

Choose a reason for hiding this comment

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

Are routers unique in name, or only unique per-region? If the latter, I think we would want to log the region here, too, right? Just so the router can be uniquely identified?

}
`, acctest.RandString(10), acctest.RandString(10), acctest.RandString(10))

var testAccComputeRouter_noRegion = fmt.Sprintf(`
Copy link
Contributor

Choose a reason for hiding this comment

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

Would love to see this as a function instead of a variable.

}
`, acctest.RandString(10), acctest.RandString(10), acctest.RandString(10))

var testAccComputeRouter_networkLink = fmt.Sprintf(`
Copy link
Contributor

Choose a reason for hiding this comment

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

Would love to see this as a function instead of a variable.

@@ -77,6 +78,7 @@ func resourceComputeVpnTunnel() *schema.Resource {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Computed: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

For posterity, why does this need to be computed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When a tunnel is created attached to a router, it will have a default policy (local and remote traffic selectors) of "0.0.0.0/0". We already had local_traffic_selector as computed, but would not get a default remote_traffic_selector from the server until now. With router based tunnels, we get them, so the need for the Computed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks!

@@ -154,6 +173,67 @@ resource "google_compute_vpn_tunnel" "foobar" {
acctest.RandString(10), acctest.RandString(10), acctest.RandString(10),
acctest.RandString(10), acctest.RandString(10))

var testAccComputeVpnTunnel_router = fmt.Sprintf(`
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be great to turn this into a function.

target = "${google_compute_vpn_gateway.foobar.self_link}"
}
resource "google_compute_router" "foobar"{
name = "tunnel-test-router"
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't this have to be unique? If so, for resiliency, I'd like to have it randomized so we don't have collisions if the test runner fails and leaves dangling resources.

@drebes
Copy link
Contributor Author

drebes commented Apr 28, 2017

Hi, thanks for the feedback! I'll need some time to go through all the comments but will definitely do it.

@paddycarver
Copy link
Contributor

Awesome, thanks! Feel free to tag off or ask for help or clarification if we can assist somehow. :)

@drebes drebes force-pushed the cloud_router branch 6 times, most recently from e0b682b to 97ef205 Compare May 1, 2017 14:35
@drebes
Copy link
Contributor Author

drebes commented May 1, 2017

Hi @paddycarver , I've implemented most of the changes, and replied to the comments where just clarification was needed.

Sorry for the large PR, creating a router without peers and interfaces is not functional so I thought it made more sense to send all as a single PR. Will send smaller, incremental ones in the future.

What did you mean by "Exists" support? Can you point me to an example?

I still need to add some docs, will work on it from now.

@drebes drebes force-pushed the cloud_router branch 4 times, most recently from 7a7bfaa to 9968166 Compare May 3, 2017 19:15
- config.clientCompute.Routers
- peer fields renamed
- more consistent logging
- better handling of SetId for error handling
- function for router locks
- test configs as functions
- simplify exists logic
- use getProject, getRegion logic on acceptance tests
- CheckDestroy for peers an interfaces
- dynamic router name for tunnel test
- extra fields for BgpPeer
- resource documentation
@drebes
Copy link
Contributor Author

drebes commented May 3, 2017

Done with the docs, just missing feedback on the points replied above.

@paddycarver
Copy link
Contributor

Resolved merge conflicts, will review now. :)

Copy link
Contributor

@paddycarver paddycarver left a comment

Choose a reason for hiding this comment

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

OK, this sat open in my browser for literally weeks, accruing comments slowly over time. Sorry @drebes! Only a handful more minor comments. If you want, I'm happy to schedule a short chunk of time to sit and pair with you on this and we'll get it pushed over the barrier and merged. If you're (understandably!) tired of this PR, I'm happy to carry the torch, if you'd like.

For me, the last big open question re: design is why the mutexes are being used.

udp4500Rule := fmt.Sprintf("router-interface-import-test-%s", acctest.RandString(10))
router := fmt.Sprintf("router-interface-import-test-%s", acctest.RandString(10))
tunnel := fmt.Sprintf("router-interface-import-test-%s", acctest.RandString(10))
iface := fmt.Sprintf("router-interface-import-test-%s", acctest.RandString(10))
Copy link
Contributor

Choose a reason for hiding this comment

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

Do all of these need to be controlled on a per-test basis? If not, it may be clearer to just put them in the function, instead of passing them in as args. Sorry if my last review wasn't super clear; generally, my rule of thumb is if the test would want to control the value, it should be an arg, otherwise it should be defined in the function. The important part is that we're not using fmt.Sprintf as a templating function (you're not, here), and that each test case gets a unique value for things that require uniqueness. That can be achieved by just moving the variable declaration into the function, or moving the fmt.Sprintf into the function (but it has to be inside a function, not just one big fmt.Sprintf call assigned to a variable, otherwise it only gets defined once).

router := fmt.Sprintf("router-peer-import-test-%s", acctest.RandString(10))
tunnel := fmt.Sprintf("router-peer-import-test-%s", acctest.RandString(10))
iface := fmt.Sprintf("router-peer-import-test-%s", acctest.RandString(10))
peer := fmt.Sprintf("router-peer-import-test-%s", acctest.RandString(10))
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thing here.

resourceName := "google_compute_router.foobar"
network := fmt.Sprintf("router-import-test-%s", acctest.RandString(10))
subnet := fmt.Sprintf("router-import-test-%s", acctest.RandString(10))
router := fmt.Sprintf("router-import-test-%s", acctest.RandString(10))
Copy link
Contributor

Choose a reason for hiding this comment

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

This one, on the other hand, feels reasonable and appropriate.

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
)

// Global MutexKV
var mutexKV = mutexkv.NewMutexKV()
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still unclear, sorry, why we're using a mutex. Could you explain? I see that it seems to restrict it so only one API call for the same router (or its interfaces or peers) can happen at once, but why? Is that an API limitation? Could we comment the code with why this is necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interfaces and Peers are not first class objects in the API, but attributes of the routers. As they don't have their own create/delete methods, but instead are created/destroyed with updates to the router that hosts them, the mutex is to ensure that no concurrent access on the router due to different peers/interfaces breaks the read -> add/remove -> write create/delete logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

BTW, that's the same reason I test deletion of peers/interfaces by using a second test step with similar config. (with the omitted resource, but same testID). If I just tested destroy, this would delete the hosted router, which of course would delete the peer/interface, but not test the proper deletion of the peer/interface only.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense, thanks!

Worth noting that this isn't entirely safe--if people are modifying things outside of Terraform, the read/modify/write loop would still overwrite the changes. But our stance is people shouldn't be doing that, so I'm fine accepting this limitation.

return config.Project, nil
}
return "", fmt.Errorf("%q: required field is not set", "project")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

👏💯

d.Set("peer_ip_address", peer.PeerIpAddress)
d.Set("peer_asn", peer.PeerAsn)
d.Set("advertised_route_priority", peer.AdvertisedRoutePriority)
d.Set("ip_address", peer.IpAddress)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would love to see project and region persisted here, in case they change in the provider config.

func resourceComputeRouterPeerImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), "/")
if len(parts) != 3 {
return nil, fmt.Errorf("Invalid router specifier. Expecting {region}/{router}")
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I'm missing something. Why are we expecting three parts (line 281) if we're expecting only a single / as input? Where in that expecting string is the "name" portion that's being set in state below?

router := fmt.Sprintf("router-peer-test-%s", acctest.RandString(10))
tunnel := fmt.Sprintf("router-peer-test-%s", acctest.RandString(10))
iface := fmt.Sprintf("router-peer-test-%s", acctest.RandString(10))
peer := fmt.Sprintf("router-peer-test-%s", acctest.RandString(10))
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thing here.

@@ -77,6 +78,7 @@ func resourceComputeVpnTunnel() *schema.Resource {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Computed: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks!

udp500Rule := fmt.Sprintf("router-interface-test-%s", acctest.RandString(10))
udp4500Rule := fmt.Sprintf("router-interface-test-%s", acctest.RandString(10))
router := fmt.Sprintf("router-interface-test-%s", acctest.RandString(10))
tunnel := fmt.Sprintf("router-interface-test-%s", acctest.RandString(10))
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thing here.

@paddycarver
Copy link
Contributor

What did you mean by "Exists" support? Can you point me to an example?

We have an Exists lifecycle method (like Read, Update, Destroy) that runs before some of the other methods, allowing Terraform to check if a resource with the given ID exists or not.

// Exists is a function that is called to check if a resource still
// exists. If this returns false, then this will affect the diff
// accordingly. If this function isn't set, it will not be called. It
// is highly recommended to set it. The *ResourceData passed to Exists
// should not be modified.

(from https://godoc.org/github.com/hashicorp/terraform/helper/schema#Resource)

An example of it being used is here:

func resourceContainerNodePoolExists(d *schema.ResourceData, meta interface{}) (bool, error) {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return false, err
}
zone := d.Get("zone").(string)
name := d.Get("name").(string)
cluster := d.Get("cluster").(string)
_, err = config.clientContainer.Projects.Zones.Clusters.NodePools.Get(
project, zone, cluster, name).Do()
if err != nil {
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 {
log.Printf("[WARN] Removing Container NodePool %q because it's gone", name)
// The resource doesn't exist anymore
return false, err
}
// There was some other error in reading the resource
return true, err
}
return true, nil
}

It's not necessary, but is nice to have. At this point in the review of this resource, I'd be happy to accept it without Exists, and iterate Exists into... existence later. But if you feel the urge, there's the info. :)

Thanks for your patience and your hard work on this.

@drebes drebes force-pushed the cloud_router branch 2 times, most recently from 7f48364 to 5785f15 Compare May 19, 2017 20:57
 - test arguments
 - set region, project in state
 - fix import error messages
 - get rid of peerFound
 - linkDiffSuppress
@drebes
Copy link
Contributor Author

drebes commented May 19, 2017

Thanks for the clarification on the Exists support. Still not clear to me is when the function is called during the resource lifecycle, but I can check that. If that would give me reasonable confidence that I can get rid of the multiple checks for existence I do on read/create/delete, I see the value in adding it.

The other changes should be implemented, and the mutex question I've answered above.

Copy link
Contributor

@paddycarver paddycarver left a comment

Choose a reason for hiding this comment

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

LGTM, tests pass. Merging!

Thanks @drebes, for your Herculean patience and endurance on this one. This is great new functionality to add, and I'm excited for it!

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
)

// Global MutexKV
var mutexKV = mutexkv.NewMutexKV()
Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense, thanks!

Worth noting that this isn't entirely safe--if people are modifying things outside of Terraform, the read/modify/write loop would still overwrite the changes. But our stance is people shouldn't be doing that, so I'm fine accepting this limitation.

@paddycarver paddycarver merged commit 63ce0da into hashicorp:master May 25, 2017
@ghost
Copy link

ghost commented Apr 12, 2020

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@ghost ghost locked and limited conversation to collaborators Apr 12, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants