-
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
Changes from 6 commits
59e6324
95e01ad
73565af
90254b9
6868ba7
4c41729
72bfc43
ec93fb5
32c1bb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,96 +2,193 @@ package google | |
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strings" | ||
|
||
"google.golang.org/api/googleapi" | ||
) | ||
|
||
// If the given name is a URL, return it. | ||
// If it is of the form project/name, search the specified project first, then | ||
// search image families in the specified project. | ||
// If it is of the form name then look in the configured project, then hosted | ||
// image projects, and lastly at image families in hosted image projects. | ||
func resolveImage(c *Config, name string) (string, error) { | ||
const ( | ||
resolveImageProjectRegex = "[-_a-zA-Z0-9]*" | ||
resolveImageFamilyRegex = "[-_a-zA-Z0-9]*" | ||
resolveImageImageRegex = "[-_a-zA-Z0-9]*" | ||
) | ||
|
||
if strings.HasPrefix(name, "https://www.googleapis.com/compute/v1/") { | ||
return name, nil | ||
var ( | ||
resolveImageProjectImage = regexp.MustCompile(fmt.Sprintf("^projects/(%s)/global/images/(%s)$", resolveImageProjectRegex, resolveImageImageRegex)) | ||
resolveImageProjectFamily = regexp.MustCompile(fmt.Sprintf("^projects/(%s)/global/images/family/(%s)$", resolveImageProjectRegex, resolveImageFamilyRegex)) | ||
resolveImageGlobalImage = regexp.MustCompile(fmt.Sprintf("^global/images/(%s)$", resolveImageImageRegex)) | ||
resolveImageGlobalFamily = regexp.MustCompile(fmt.Sprintf("^global/images/family/(%s)$", resolveImageFamilyRegex)) | ||
resolveImageFamilyFamily = regexp.MustCompile(fmt.Sprintf("^family/(%s)$", resolveImageFamilyRegex)) | ||
resolveImageProjectImageShorthand = regexp.MustCompile(fmt.Sprintf("^(%s)/(%s)$", resolveImageProjectRegex, resolveImageImageRegex)) | ||
resolveImageProjectFamilyShorthand = regexp.MustCompile(fmt.Sprintf("^(%s)/(%s)$", resolveImageProjectRegex, resolveImageFamilyRegex)) | ||
resolveImageFamily = regexp.MustCompile(fmt.Sprintf("^(%s)$", resolveImageFamilyRegex)) | ||
resolveImageImage = regexp.MustCompile(fmt.Sprintf("^(%s)$", resolveImageImageRegex)) | ||
resolveImageLink = regexp.MustCompile(fmt.Sprintf("^https://www.googleapis.com/compute/v1/projects/(%s)/global/images/(%s)", resolveImageProjectRegex, resolveImageImageRegex)) | ||
) | ||
|
||
func resolveImageImageExists(c *Config, project, name string) (bool, error) { | ||
if _, err := c.clientCompute.Images.Get(project, name).Do(); err == nil { | ||
return true, nil | ||
} else if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { | ||
return false, nil | ||
} else { | ||
splitName := strings.Split(name, "/") | ||
if len(splitName) == 1 { | ||
|
||
// Must infer the project name: | ||
|
||
// First, try the configured project for a specific image: | ||
image, err := c.clientCompute.Images.Get(c.Project, name).Do() | ||
if err == nil { | ||
return image.SelfLink, nil | ||
} | ||
return false, fmt.Errorf("Error checking if image %s exists: %s", name, err) | ||
} | ||
} | ||
|
||
// If it doesn't exist, try to see if it works as an image family: | ||
image, err = c.clientCompute.Images.GetFromFamily(c.Project, name).Do() | ||
if err == nil { | ||
return image.SelfLink, nil | ||
} | ||
func resolveImageFamilyExists(c *Config, project, name string) (bool, error) { | ||
if _, err := c.clientCompute.Images.GetFromFamily(project, name).Do(); err == nil { | ||
return true, nil | ||
} else if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { | ||
return false, nil | ||
} else { | ||
return false, fmt.Errorf("Error checking if family %s exists: %s", name, err) | ||
} | ||
} | ||
|
||
// If we match a lookup for an alternate project, then try that next. | ||
// If not, we return the original 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) | ||
} | ||
return nil | ||
} | ||
|
||
// If the image name contains the left hand side, we use the project from | ||
// the right hand side. | ||
imageMap := map[string]string{ | ||
"centos": "centos-cloud", | ||
"coreos": "coreos-cloud", | ||
"debian": "debian-cloud", | ||
"opensuse": "opensuse-cloud", | ||
"rhel": "rhel-cloud", | ||
"sles": "suse-cloud", | ||
"ubuntu": "ubuntu-os-cloud", | ||
"windows": "windows-cloud", | ||
} | ||
var project string | ||
for k, v := range imageMap { | ||
if strings.Contains(name, k) { | ||
project = v | ||
break | ||
} | ||
} | ||
if project == "" { | ||
// If the given name is a URL, return it. | ||
// If it's in the form projects/{project}/global/images/{image}, return it | ||
// If it's in the form projects/{project}/global/images/family/{family}, return it | ||
// If it's in the form global/images/{image}, return it | ||
// If it's in the form global/images/family/{family}, return it | ||
// If it's in the form family/{family}, check if it's a family in the current project. If it is, return it as global/images/family/{family}. | ||
// If not, check if it could be a GCP-provided family, and if it exists. If it does, return it as projects/{project}/global/images/family/{family}. | ||
// If it's in the form {project}/{family-or-image}, check if it's an image in the named project. If it is, return it as projects/{project}/global/images/{image}. | ||
// If not, check if it's a family in the named project. If it is, return it as projects/{project}/global/images/family/{family}. | ||
// If it's in the form {family-or-image}, check if it's an image in the current project. If it is, return it as global/images/{image}. | ||
// If not, check if it could be a GCP-provided image, and if it exists. If it does, return it as projects/{project}/global/images/{image}. | ||
// If not, check if it's a family in the current project. If it is, return it as global/images/family/{family}. | ||
// If not, check if it could be a GCP-provided family, and if it exists. If it does, return it as projects/{project}/global/images/family/{family} | ||
func resolveImage(c *Config, name string) (string, error) { | ||
// built-in projects to look for images/families containing the string | ||
// on the left in | ||
imageMap := map[string]string{ | ||
"centos": "centos-cloud", | ||
"coreos": "coreos-cloud", | ||
"debian": "debian-cloud", | ||
"opensuse": "opensuse-cloud", | ||
"rhel": "rhel-cloud", | ||
"sles": "suse-cloud", | ||
"ubuntu": "ubuntu-os-cloud", | ||
"windows": "windows-cloud", | ||
} | ||
var builtInProject string | ||
for k, v := range imageMap { | ||
if strings.Contains(name, k) { | ||
builtInProject = v | ||
break | ||
} | ||
} | ||
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 commentThe 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 commentThe 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 commentThe 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. |
||
res := resolveImageProjectImage.FindStringSubmatch(name) | ||
if err := sanityTestRegexMatches(2, res, "project image", name); err != nil { | ||
return "", err | ||
} | ||
return fmt.Sprintf("projects/%s/global/images/%s", res[1], res[2]), nil | ||
case resolveImageProjectFamily.MatchString(name): // projects/xyz/global/images/family/xyz | ||
res := resolveImageProjectFamily.FindStringSubmatch(name) | ||
if err := sanityTestRegexMatches(2, res, "project family", name); err != nil { | ||
return "", err | ||
} | ||
return fmt.Sprintf("projects/%s/global/images/family/%s", res[1], res[2]), nil | ||
case resolveImageGlobalImage.MatchString(name): // global/images/xyz | ||
res := resolveImageGlobalImage.FindStringSubmatch(name) | ||
if err := sanityTestRegexMatches(1, res, "global image", name); err != nil { | ||
return "", err | ||
} | ||
return fmt.Sprintf("global/images/%s", res[1]), nil | ||
case resolveImageGlobalFamily.MatchString(name): // global/images/family/xyz | ||
res := resolveImageGlobalFamily.FindStringSubmatch(name) | ||
if err := sanityTestRegexMatches(1, res, "global family", name); err != nil { | ||
return "", err | ||
} | ||
return fmt.Sprintf("global/images/family/%s", res[1]), nil | ||
case resolveImageFamilyFamily.MatchString(name): // family/xyz | ||
res := resolveImageFamilyFamily.FindStringSubmatch(name) | ||
if err := sanityTestRegexMatches(1, res, "family family", name); err != nil { | ||
return "", err | ||
} | ||
if ok, err := resolveImageFamilyExists(c, c.Project, res[1]); err != nil { | ||
return "", err | ||
} else if ok { | ||
return fmt.Sprintf("global/images/family/%s", res[1]), nil | ||
} | ||
if builtInProject != "" { | ||
if ok, err := resolveImageFamilyExists(c, builtInProject, res[1]); err != nil { | ||
return "", err | ||
} else if ok { | ||
return fmt.Sprintf("projects/%s/global/images/family/%s", builtInProject, res[1]), nil | ||
} | ||
|
||
// There was a match, but the image still may not exist, so check it: | ||
image, err = c.clientCompute.Images.Get(project, name).Do() | ||
if err == nil { | ||
return image.SelfLink, nil | ||
} | ||
|
||
// If it doesn't exist, try to see if it works as an image family: | ||
image, err = c.clientCompute.Images.GetFromFamily(project, name).Do() | ||
if err == nil { | ||
return image.SelfLink, nil | ||
} | ||
|
||
} | ||
case resolveImageProjectImageShorthand.MatchString(name): // xyz/xyz | ||
res := resolveImageProjectImageShorthand.FindStringSubmatch(name) | ||
if err := sanityTestRegexMatches(2, res, "project image shorthand", name); err != nil { | ||
return "", err | ||
|
||
} else if len(splitName) == 2 { | ||
|
||
// Check if image exists in the specified project: | ||
image, err := c.clientCompute.Images.Get(splitName[0], splitName[1]).Do() | ||
if err == nil { | ||
return image.SelfLink, nil | ||
} | ||
|
||
// If it doesn't, check if it exists as an image family: | ||
image, err = c.clientCompute.Images.GetFromFamily(splitName[0], splitName[1]).Do() | ||
if err == nil { | ||
return image.SelfLink, nil | ||
} | ||
if ok, err := resolveImageImageExists(c, res[1], res[2]); err != nil { | ||
return "", err | ||
} else if ok { | ||
return fmt.Sprintf("projects/%s/global/images/%s", res[1], res[2]), nil | ||
} | ||
fallthrough // check if it's a family | ||
case resolveImageProjectFamilyShorthand.MatchString(name): // xyz/xyz | ||
res := resolveImageProjectFamilyShorthand.FindStringSubmatch(name) | ||
if err := sanityTestRegexMatches(2, res, "project family shorthand", name); err != nil { | ||
return "", err | ||
} | ||
if ok, err := resolveImageFamilyExists(c, res[1], res[2]); err != nil { | ||
return "", err | ||
} else if ok { | ||
return fmt.Sprintf("projects/%s/global/images/family/%s", res[1], res[2]), nil | ||
} | ||
case resolveImageImage.MatchString(name): // xyz | ||
res := resolveImageImage.FindStringSubmatch(name) | ||
if err := sanityTestRegexMatches(1, res, "image", name); err != nil { | ||
return "", err | ||
} | ||
if ok, err := resolveImageImageExists(c, c.Project, res[1]); err != nil { | ||
return "", err | ||
} else if ok { | ||
return fmt.Sprintf("global/images/%s", res[1]), nil | ||
} | ||
if builtInProject != "" { | ||
// check the images GCP provides | ||
if ok, err := resolveImageImageExists(c, builtInProject, res[1]); err != nil { | ||
return "", err | ||
} else if ok { | ||
return fmt.Sprintf("projects/%s/global/images/%s", builtInProject, res[1]), nil | ||
} | ||
|
||
} | ||
fallthrough // check if the name is a family, instead of an image | ||
case resolveImageFamily.MatchString(name): // xyz | ||
res := resolveImageFamily.FindStringSubmatch(name) | ||
if err := sanityTestRegexMatches(1, res, "family", name); err != nil { | ||
return "", err | ||
|
||
} else { | ||
return "", fmt.Errorf("Invalid image name, require URL, project/name, or just name: %s", name) | ||
} | ||
if ok, err := resolveImageFamilyExists(c, c.Project, res[1]); err != nil { | ||
return "", err | ||
} else if ok { | ||
return fmt.Sprintf("global/images/family/%s", res[1]), nil | ||
} | ||
if builtInProject != "" { | ||
// check the families GCP provides | ||
if ok, err := resolveImageFamilyExists(c, builtInProject, res[1]); err != nil { | ||
return "", err | ||
} else if ok { | ||
return fmt.Sprintf("projects/%s/global/images/family/%s", builtInProject, res[1]), nil | ||
} | ||
} | ||
} | ||
|
||
return "", fmt.Errorf("Could not find image or family %s", name) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package google | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
compute "google.golang.org/api/compute/v1" | ||
|
||
"github.com/hashicorp/terraform/helper/acctest" | ||
"github.com/hashicorp/terraform/helper/resource" | ||
"github.com/hashicorp/terraform/terraform" | ||
) | ||
|
||
func TestAccComputeImage_resolveImage(t *testing.T) { | ||
var image compute.Image | ||
rand := acctest.RandString(10) | ||
name := fmt.Sprintf("test-image-%s", rand) | ||
fam := fmt.Sprintf("test-image-family-%s", rand) | ||
|
||
resource.Test(t, resource.TestCase{ | ||
PreCheck: func() { testAccPreCheck(t) }, | ||
Providers: testAccProviders, | ||
CheckDestroy: testAccCheckComputeImageDestroy, | ||
Steps: []resource.TestStep{ | ||
{ | ||
Config: testAccComputeImage_resolving(name, fam), | ||
Check: resource.ComposeTestCheckFunc( | ||
testAccCheckComputeImageExists( | ||
"google_compute_image.foobar", &image), | ||
testAccCheckComputeImageResolution("google_compute_image.foobar"), | ||
), | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
func testAccCheckComputeImageResolution(n string) resource.TestCheckFunc { | ||
return func(s *terraform.State) error { | ||
config := testAccProvider.Meta().(*Config) | ||
project := config.Project | ||
|
||
rs, ok := s.RootModule().Resources[n] | ||
if !ok { | ||
return fmt.Errorf("Resource not found: %s", n) | ||
} | ||
|
||
if rs.Primary.ID == "" { | ||
return fmt.Errorf("No ID is set") | ||
} | ||
if rs.Primary.Attributes["name"] == "" { | ||
return fmt.Errorf("No image name is set") | ||
} | ||
if rs.Primary.Attributes["family"] == "" { | ||
return fmt.Errorf("No image family is set") | ||
} | ||
if rs.Primary.Attributes["self_link"] == "" { | ||
return fmt.Errorf("No self_link is set") | ||
} | ||
|
||
name := rs.Primary.Attributes["name"] | ||
family := rs.Primary.Attributes["family"] | ||
link := rs.Primary.Attributes["self_link"] | ||
|
||
images := map[string]string{ | ||
"family/debian-8": "projects/debian-cloud/global/images/family/debian-8", | ||
"projects/debian-cloud/global/images/debian-8-jessie-v20170110": "projects/debian-cloud/global/images/debian-8-jessie-v20170110", | ||
"debian-8": "projects/debian-cloud/global/images/family/debian-8", | ||
"debian-8-jessie-v20170110": "projects/debian-cloud/global/images/debian-8-jessie-v20170110", | ||
"https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-8-jessie-v20170110": "https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-8-jessie-v20170110", | ||
|
||
"global/images/" + name: "global/images/" + name, | ||
"global/images/family/" + family: "global/images/family/" + family, | ||
name: "global/images/" + name, | ||
family: "global/images/family/" + family, | ||
"family/" + family: "global/images/family/" + family, | ||
project + "/" + name: "projects/" + project + "/global/images/" + name, | ||
project + "/" + family: "projects/" + project + "/global/images/family/" + family, | ||
link: link, | ||
} | ||
|
||
for input, expectation := range images { | ||
result, err := resolveImage(config, input) | ||
if err != nil { | ||
return fmt.Errorf("Error resolving input %s to image: %+v\n", input, err) | ||
} | ||
if result != expectation { | ||
return fmt.Errorf("Expected input '%s' to resolve to '%s', it resolved to '%s' instead.\n", input, expectation, result) | ||
} | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
func testAccComputeImage_resolving(name, family string) string { | ||
return fmt.Sprintf(` | ||
resource "google_compute_disk" "foobar" { | ||
name = "%s" | ||
zone = "us-central1-a" | ||
image = "debian-8-jessie-v20160803" | ||
} | ||
resource "google_compute_image" "foobar" { | ||
name = "%s" | ||
family = "%s" | ||
source_disk = "${google_compute_disk.foobar.self_link}" | ||
} | ||
`, name, name, family) | ||
} |
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
beexpected
?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.