-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Add artifact docker image data source #9521
Changes from all commits
4edfeb0
96482a4
8e20357
4939bb2
572129d
d32c2a9
4853435
05493d3
25bf55e
ff71c94
cbb92dd
4e7438b
5831254
71cce66
073bef8
6bd1c9d
07ea9ff
fe5bf1a
09b7719
0334707
c416487
d9c4979
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 |
---|---|---|
@@ -0,0 +1,320 @@ | ||
package artifactregistry | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
"strings" | ||
|
||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
"github.com/hashicorp/terraform-provider-google/google/tpgresource" | ||
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" | ||
) | ||
|
||
// https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories.dockerImages#DockerImage | ||
type DockerImage struct { | ||
name string | ||
uri string | ||
tags []string | ||
imageSizeBytes string | ||
mediaType string | ||
uploadTime string | ||
buildTime string | ||
updateTime string | ||
} | ||
|
||
func DataSourceArtifactRegistryDockerImage() *schema.Resource { | ||
|
||
return &schema.Resource{ | ||
Read: DataSourceArtifactRegistryDockerImageRead, | ||
|
||
Schema: map[string]*schema.Schema{ | ||
"project": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Description: `Project ID of the project.`, | ||
}, | ||
"location": { | ||
Type: schema.TypeString, | ||
Required: true, | ||
Description: `The region of the artifact registry repository. For example, "us-west1".`, | ||
}, | ||
"repository_id": { | ||
Type: schema.TypeString, | ||
Required: true, | ||
Description: `The last part of the repository name to fetch from.`, | ||
}, | ||
"image_name": { | ||
Type: schema.TypeString, | ||
Required: true, | ||
Description: `The image name to fetch.`, | ||
}, | ||
"name": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
Description: `The fully qualified name of the fetched image.`, | ||
}, | ||
"self_link": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
Description: `The URI to access the image.`, | ||
}, | ||
"tags": { | ||
Type: schema.TypeList, | ||
Computed: true, | ||
Elem: &schema.Schema{Type: schema.TypeString}, | ||
Description: `All tags associated with the image.`, | ||
}, | ||
"image_size_bytes": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
Description: `Calculated size of the image in bytes.`, | ||
}, | ||
"media_type": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
Description: `Media type of this image.`, | ||
}, | ||
"upload_time": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
Description: `The time, as a RFC 3339 string, the image was uploaded.`, | ||
}, | ||
"build_time": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
Description: `The time, as a RFC 3339 string, this image was built.`, | ||
}, | ||
"update_time": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
Description: `The time, as a RFC 3339 string, this image was updated.`, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func DataSourceArtifactRegistryDockerImageRead(d *schema.ResourceData, meta interface{}) error { | ||
config := meta.(*transport_tpg.Config) | ||
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var res DockerImage | ||
|
||
imageName, tag, digest := parseImage(d.Get("image_name").(string)) | ||
|
||
if digest != "" { | ||
// fetch image by digest | ||
// https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories.dockerImages/get | ||
imageUrlSafe := url.QueryEscape(imageName) | ||
urlRequest, err := tpgresource.ReplaceVars(d, config, fmt.Sprintf("{{ArtifactRegistryBasePath}}projects/{{project}}/locations/{{location}}/repositories/{{repository_id}}/dockerImages/%s@%s", imageUrlSafe, digest)) | ||
if err != nil { | ||
return fmt.Errorf("Error setting api endpoint") | ||
} | ||
|
||
resGet, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ | ||
Config: config, | ||
Method: "GET", | ||
RawURL: urlRequest, | ||
UserAgent: userAgent, | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
res = convertResponseToStruct(resGet) | ||
} else { | ||
// fetch the list of images, ordered by update time | ||
// https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories.dockerImages/list | ||
urlRequest, err := tpgresource.ReplaceVars(d, config, "{{ArtifactRegistryBasePath}}projects/{{project}}/locations/{{location}}/repositories/{{repository_id}}/dockerImages") | ||
if err != nil { | ||
return fmt.Errorf("Error setting api endpoint") | ||
} | ||
|
||
urlRequest, err = transport_tpg.AddQueryParams(urlRequest, map[string]string{"orderBy": "update_time desc"}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
res, err = retrieveAndFilterImages(d, config, urlRequest, userAgent, imageName, tag) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
// set the schema data using the response | ||
if err := d.Set("name", res.name); err != nil { | ||
return fmt.Errorf("Error setting name: %s", err) | ||
} | ||
|
||
if err := d.Set("self_link", res.uri); err != nil { | ||
return fmt.Errorf("Error setting self_link: %s", err) | ||
} | ||
|
||
if err := d.Set("tags", res.tags); err != nil { | ||
return fmt.Errorf("Error setting tags: %s", err) | ||
} | ||
|
||
if err := d.Set("image_size_bytes", res.imageSizeBytes); err != nil { | ||
return fmt.Errorf("Error setting image_size_bytes: %s", err) | ||
} | ||
|
||
if err := d.Set("media_type", res.mediaType); err != nil { | ||
return fmt.Errorf("Error setting media_type: %s", err) | ||
} | ||
|
||
if err := d.Set("upload_time", res.uploadTime); err != nil { | ||
return fmt.Errorf("Error setting upload_time: %s", err) | ||
} | ||
|
||
if err := d.Set("build_time", res.buildTime); err != nil { | ||
return fmt.Errorf("Error setting build_time: %s", err) | ||
} | ||
|
||
if err := d.Set("update_time", res.updateTime); err != nil { | ||
return fmt.Errorf("Error setting update_time: %s", err) | ||
} | ||
|
||
id, err := tpgresource.ReplaceVars(d, config, "{{ArtifactRegistryBasePath}}projects/{{project}}/locations/{{location}}/repositories/{{repository_id}}/dockerImages/{{image_name}}") | ||
if err != nil { | ||
return fmt.Errorf("Error constructing the data source id: %s", err) | ||
} | ||
|
||
d.SetId(id) | ||
|
||
return nil | ||
} | ||
|
||
func parseImage(image string) (imageName string, tag string, digest string) { | ||
splitByAt := strings.Split(image, "@") | ||
splitByColon := strings.Split(image, ":") | ||
|
||
if len(splitByAt) == 2 { | ||
imageName = splitByAt[0] | ||
digest = splitByAt[1] | ||
} else if len(splitByColon) == 2 { | ||
imageName = splitByColon[0] | ||
tag = splitByColon[1] | ||
} else { | ||
imageName = image | ||
} | ||
|
||
return imageName, tag, digest | ||
} | ||
|
||
func retrieveAndFilterImages(d *schema.ResourceData, config *transport_tpg.Config, urlRequest string, userAgent string, imageName string, tag string) (DockerImage, error) { | ||
// Paging through the list method until either: | ||
// if a tag was provided, the matching image name and tag pair | ||
// otherwise, return the first matching image name | ||
|
||
for { | ||
resListImages, token, err := retrieveListOfDockerImages(config, urlRequest, userAgent) | ||
if err != nil { | ||
return DockerImage{}, err | ||
} | ||
|
||
var resFiltered []DockerImage | ||
for _, image := range resListImages { | ||
if strings.Contains(image.name, "/"+url.QueryEscape(imageName)+"@") { | ||
resFiltered = append(resFiltered, image) | ||
} | ||
} | ||
|
||
if tag != "" { | ||
for _, image := range resFiltered { | ||
for _, iterTag := range image.tags { | ||
if iterTag == tag { | ||
return image, nil | ||
} | ||
} | ||
} | ||
} else if len(resFiltered) > 0 { | ||
return resFiltered[0], nil | ||
} | ||
|
||
if token == "" { | ||
return DockerImage{}, fmt.Errorf("Requested image was not found.") | ||
} | ||
|
||
urlRequest, err = transport_tpg.AddQueryParams(urlRequest, map[string]string{"pageToken": token}) | ||
if err != nil { | ||
return DockerImage{}, err | ||
} | ||
} | ||
} | ||
|
||
func retrieveListOfDockerImages(config *transport_tpg.Config, urlRequest string, userAgent string) ([]DockerImage, string, error) { | ||
resList, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ | ||
Config: config, | ||
Method: "GET", | ||
RawURL: urlRequest, | ||
UserAgent: userAgent, | ||
}) | ||
if err != nil { | ||
return make([]DockerImage, 0), "", err | ||
} | ||
|
||
if nextPageToken, ok := resList["nextPageToken"].(string); ok { | ||
return flattenDataSourceListResponse(resList), nextPageToken, nil | ||
} else { | ||
return flattenDataSourceListResponse(resList), "", nil | ||
} | ||
} | ||
|
||
func flattenDataSourceListResponse(res map[string]interface{}) []DockerImage { | ||
var dockerImages []DockerImage | ||
|
||
resDockerImages, _ := res["dockerImages"].([]interface{}) | ||
|
||
for _, resImage := range resDockerImages { | ||
image, _ := resImage.(map[string]interface{}) | ||
dockerImages = append(dockerImages, convertResponseToStruct(image)) | ||
} | ||
|
||
return dockerImages | ||
} | ||
|
||
func convertResponseToStruct(res map[string]interface{}) DockerImage { | ||
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. On first read, I'm wondering if this function is needed. If you didn't have it, and tweak the 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 added it, because on a previous iteration I was getting compiler errors when iterating through the list of tags from the REST API response -- unmarshaling the response into a struct made the compiler happy, so that's what I went with. 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 think you could do a 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'm not sure I follow what you're suggesting? Isn't |
||
var dockerImage DockerImage | ||
|
||
if name, ok := res["name"].(string); ok { | ||
dockerImage.name = name | ||
} | ||
|
||
if uri, ok := res["uri"].(string); ok { | ||
dockerImage.uri = uri | ||
} | ||
|
||
if tags, ok := res["tags"].([]interface{}); ok { | ||
var stringTags []string | ||
|
||
for _, tag := range tags { | ||
strTag := tag.(string) | ||
stringTags = append(stringTags, strTag) | ||
} | ||
dockerImage.tags = stringTags | ||
} | ||
|
||
if imageSizeBytes, ok := res["imageSizeBytes"].(string); ok { | ||
dockerImage.imageSizeBytes = imageSizeBytes | ||
} | ||
|
||
if mediaType, ok := res["mediaType"].(string); ok { | ||
dockerImage.mediaType = mediaType | ||
} | ||
|
||
if uploadTime, ok := res["uploadTime"].(string); ok { | ||
dockerImage.uploadTime = uploadTime | ||
} | ||
|
||
if buildTime, ok := res["buildTime"].(string); ok { | ||
dockerImage.buildTime = buildTime | ||
} | ||
|
||
if updateTime, ok := res["updateTime"].(string); ok { | ||
dockerImage.updateTime = updateTime | ||
} | ||
|
||
return dockerImage | ||
} |
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 verifying, is this sorting going to be relevant for all (or most) use cases?
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.
It's more of an optimization: since there can be only one image with a particular label, it's most likely going to be one that was updated recently.
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.
Ah, that makes sense.
I think I was also asking about this case (from your doc change):
If a digest or tag is not provided, then the last updated version of the image will be fetched.
. Is the most recently updated going to typically be what users want?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.
@roaks3 Yes, I think that's a reasonable expectation that users have. It's also the behavior of other API calls, for example when calling the image of a family, the latest image is returned.