-
Notifications
You must be signed in to change notification settings - Fork 9.6k
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: upgrade our image resolution logic #12223
Conversation
Add tests that show what we want image input strings to resolve to, so we can test that behaviour.
Add tests that ensure that image syntax resolves to API input the way we want it to. Add a lot of different input forms for images, to more closely map to what the API accepts, so anything that's valid input to the API should also be valid input in a config. Stop resolving image families to specific image URLs, allowing things like instance templates to evolve over time as new images are pushed.
Update the explanation of the logic being followed in resolveImage.
builtin/providers/google/image.go
Outdated
func resolveImage(c *Config, name string) (string, error) { | ||
const ( | ||
resolveImageProjectRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?" // TODO(paddy): this isn't based on any documentation; we're just copying the image name restrictions. Need to follow up with @danawillow and/or @evandbrown and see if there's an actual limit to this | ||
resolveImageFamilyRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?" // TODO(paddy): this isn't based on any documentation; we're just copying the image name restrictions. Need to follow up with @danawillow and/or @evandbrown and see if there's an actual limit to this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to https://cloud.google.com/compute/docs/reference/latest/images, it has to conform with https://www.ietf.org/rfc/rfc1035.txt. I'm not sure what people tend to actually do in practice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I saw that. I just didn't know which specific part of RFC 1035 it meant. Does it mean it must be a valid domain? A valid subdomain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just took a closer look and it's the same validation as images, so this looks like the right regex to use.
builtin/providers/google/image.go
Outdated
// image projects, and lastly at image families in hosted image projects. | ||
func resolveImage(c *Config, name string) (string, error) { | ||
const ( | ||
resolveImageProjectRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?" // TODO(paddy): this isn't based on any documentation; we're just copying the image name restrictions. Need to follow up with @danawillow and/or @evandbrown and see if there's an actual limit to this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From https://cloud.google.com/resource-manager/reference/rest/v1/projects:
It must be 6 to 30 lowercase letters, digits, or hyphens. It must start with a letter. Trailing hyphens are prohibited.
However, is it worth it to be strict here? It seems like what we really care about is just that it isn't a slash, and we can let the API handle whether things are actually well-formed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So my thought on this is that I'd like to try to keep my future options open and not trap myself into a forced backwards incompatibility. In this case, that means accepting the smallest set of strings I can while still accepting every valid string.
On the other hand, however, the typical Terraform approach is to avoid client-side validation, as it is a headache keeping it in sync with the server-side validation.
So I could see both sides on this one. Do you have a preference either way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, though I don't 100% agree with your logic. If we let the server handle it, then we also don't have to worry about backwards incompatibility (that's Google's problem). Sure, we could try to predict it by being extra strict now just in case, but we can't really tell in what way the Google API would become stricter. The win by doing client-side validation is more so that problems show up faster to the user, which I think is a valid win. If you do keep it this way though, let's get that regex changed to the actual requirements, since the ones for projects are stricter than for images.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we let the server handle it, then we also don't have to worry about backwards incompatibility (that's Google's problem).
Where this comes into play is that we have several custom shorthands for this. The one I'm thinking of specifically is {project}/{image}
and {project}/{family}
; if, in the future, we wanted to do something like {family}/{image}
for whatever reason, we'd find ourselves in a tough spot, as we wouldn't be able to tell between {project}/{image}
and {family}/{image}
. But the more I'm thinking about this, the more I'm agreeing with you; we try to avoid doing too much client-side validation because it can be brittle, and in this case, guarding against the vagaries of the future doesn't feel like it's yielding enough concrete benefits.
My proposal: Let's accept anything that matches /[a-zA-Z0-9_-]*/
for projects, images, and families. Sound like a good way forward?
builtin/providers/google/image.go
Outdated
return name, nil | ||
case resolveImageProjectImage.MatchString(name): // projects/xyz/global/images/xyz | ||
res := resolveImageProjectImage.FindStringSubmatch(name) | ||
if len(res)-1 != 2 { // subtract one, index zero is the entire matched expression |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any cases where the length wouldn't match? It feels like this is just confirming that the go regex library works as expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you do decide to keep it, since you do this check a whole bunch of times, you could add a function that does it instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm mostly just sanity-checking to make sure that if the regex string changes and the logic handling it isn't updated, or some other type of human error, it's an error, not a crash. I'm open to removing the code, if you think that's overdoing it. :) In terms of a helper function, you mean just something like replacing
if len(res)-1 != 2 {
return "", fmt.Errorf("Expected %d project image regex matches, got %d for %s", 2, len(res)-1, name)
}
with
if err := checkRegexMatches(res, 2); err != nil {
return "", err
}
?
I could see a case for that, and how it would clean up some code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's necessary, but I'm not opposed to having it there either. I do like your helper function though- I think one of the reasons this part of the code feels messy to me is because all of the repeated -1
s and their associated // subtract one, index zero is the entire matched expression
comments, so this would help add more whitespace into that section.
switch { | ||
case resolveImageLink.MatchString(name): // https://www.googleapis.com/compute/v1/projects/xyz/global/images/xyz | ||
return name, nil | ||
case resolveImageProjectImage.MatchString(name): // projects/xyz/global/images/xyz |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and the following three checks all follow the same format. It might be a bit cleaner to loop over a slice of structs containing the regex, the expected length, and the result URL (although if we can't do a similar thing elsewhere it might look out of place- use your judgment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought of doing that, but wasn't super convinced of the benefit--the indirection didn't make it feel any clearer, and because the format of the code would be essentially the same (define a case, find the matches, check the lengths, handle errors, build a string) it seemed to me like it would be adding more code, not removing code.
I'm happy to reconsider on this one, though, if you have any suggestions or pointers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a look to see if I could quickly do my own suggestion and it looks like you're right that it would add rather than remove code.
* Make our regexes more permissive (though still separated out for readability, despite being identical) * Add a helper that will improve readability while sanity testing our regex results.
Latest push should address all feedback. Unit test still runs:
|
builtin/providers/google/image.go
Outdated
@@ -47,6 +47,13 @@ func resolveImageFamilyExists(c *Config, project, name string) (bool, error) { | |||
} | |||
} | |||
|
|||
func sanityTestRegexMatches(expected int, got []string, regexType, name string) error { | |||
if len(got)-1 != expected { // subtract one, index zero is the entire matched expression | |||
return fmt.Errorf("Expected %d %s regex matches, got %d for %s", 2, regexType, len(got)-1, name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this 2
be expected
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be addressed now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good (aside from the small comment), merge whenever you're ready.
We never updated the error to use the expectation, not hardcode it to 2.
Document the acceptable inputs everywhere that uses the function.
Updated docs to document the new acceptable inputs and updated the copy/paste error you noticed. Still look good? :) |
* `image` - (Optional) The image from which to initialize this disk. Either the | ||
full URL, a contraction of the form "project/name", or just a name (in which | ||
case the current project is used). | ||
* `image` - (Optional) The image from which to initialize this disk. This can be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can each of the image choices be code font?
[image family](https://cloud.google.com/compute/docs/images#image_families), | ||
or simple the name of an image or image family (in which case the current | ||
project is used). | ||
* `image` - The image from which to initialize this disk. This can be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(here too)
* `source_image` - (Required if source not set) The name of the image to base | ||
this disk off of. Accepts same arguments as a [google_compute_instance image](https://www.terraform.io/docs/providers/google/r/compute_instance.html#image). | ||
* `source_image` - (Required if source not set) The image from which to | ||
initialize this disk. This can be one of: the image self_link, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(here too)
Still looks good! Feel free to merge when ready (after the code font changes if you agree with that comment) |
Update our docs to code format all the image template strings that a disk can use.
Updated! Once Travis goes green, I'll merge. |
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. |
Prior to now, GCP images could be specified as:
{project}/{name}
, which would be resolved as an image in the named project, and if found, passed as a URL to the API. If not found, it would be interpreted as an image family in the named project, and if found, resolved to the latest image in that family, which would be passed through to the API as a URL.{name}
, which would be resolved as an image in the authenticated project, and if found, passed as a URL to the API. If not found, it would be interpreted as an image family in the authenticated project, and if found, resolved to the latest image in that family, which would be passed through to the API as a URL. If still not found, it would be checked against a static map of GCP-provided projects, and if it had a match, would be interpreted as an image in that project. If that image was found, it would be passed through to the API as a URL. Finally, if it still wasn't found, it would be interpreted as a family in the GCP-provided project, and if it was found, if would be resolved to the latest image in that family, which would be passed through to the API as a URL.This code was used uniformly across all our disk input modes (as a sub-resource of instances, as standalone disks, as instance templates), but it had some shortcomings:
projects/{project}/global/images/family/{family}
projects/{project}/global/images/{image}
global/images/{image}
global/images/family/{family}
This PR updates the resolveImage helper in two ways:
{project}/family
specifies the project, but not an image, so it gets converted toprojects/{project}/global/images/family/{family}
.family/{family}
does not specify a project or an image, so it gets converted toglobal/images/family/{family}
, rather than interpolating the authenticating project in.family/{family}
projects/{project}/global/images/{image}
{family}
{image}
{URL to image}
global/images/{image}
global/images/family/{family}
{project}/{image}
{project}/{family}
I also added an acceptance test that checks that each of those inputs gets converted to the API values we expect, including for images and families provided by GCP.
This change was prompted by #10984 and is half the solution to it. The other half is to provide a data source which will return the latest image in a family, which will allow users to pin disks to the latest image at
terraform apply
-time, and useterraform apply
to roll out updates to the disk image.