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

Add artifact docker image data source #9521

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var handwrittenDatasources = map[string]*schema.Resource{
"google_active_folder": resourcemanager.DataSourceGoogleActiveFolder(),
"google_alloydb_locations": alloydb.DataSourceAlloydbLocations(),
"google_alloydb_supported_database_flags": alloydb.DataSourceAlloydbSupportedDatabaseFlags(),
"google_artifact_registry_docker_image": artifactregistry.DataSourceArtifactRegistryDockerImage(),
"google_artifact_registry_repository": artifactregistry.DataSourceArtifactRegistryRepository(),
"google_apphub_discovered_workload": apphub.DataSourceApphubDiscoveredWorkload(),
"google_app_engine_default_service_account": appengine.DataSourceGoogleAppEngineDefaultServiceAccount(),
Expand Down
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"})
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 d.Set(...) calls, that might be slightly more straight-forward / consistent with other resources.

Copy link
Contributor Author

@dibunker dibunker Dec 7, 2023

Choose a reason for hiding this comment

The 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.

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 you could do a d.Set() call instead of the dockerImage.tags = stringTags line you have, but keep the rest.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I follow what you're suggesting? Isn't d.Set() specific to the *schema.ResourceData type? Here I'm setting fields in the DockerImage struct.

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
}
Loading