terraform-provider-docker/internal/provider/data_source_docker_registry_image_manifests.go

201 lines
6.3 KiB
Go

package provider
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"github.com/docker/docker/api/types/registry"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func dataSourceDockerRegistryImageManifests() *schema.Resource {
return &schema.Resource{
Description: "Reads the image metadata for each manifest in a Docker multi-arch image from a Docker Registry.",
ReadContext: dataSourceDockerRegistryImageManifestsRead,
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "The name of the Docker image, including any tags. e.g. `alpine:latest`",
Required: true,
},
"auth_config": AuthConfigSchema,
"manifests": {
Type: schema.TypeSet,
Description: "The metadata for each manifest in the image",
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"media_type": {
Type: schema.TypeString,
Description: "The media type of the manifest.",
Computed: true,
},
"sha256_digest": {
Type: schema.TypeString,
Description: "The content digest of the manifest, as stored in the registry.",
Computed: true,
},
"architecture": {
Type: schema.TypeString,
Description: "The platform architecture supported by the manifest.",
Computed: true,
},
"os": {
Type: schema.TypeString,
Description: "The operating system supported by the manifest.",
Computed: true,
},
},
},
},
"insecure_skip_verify": {
Type: schema.TypeBool,
Description: "If `true`, the verification of TLS certificates of the server/registry is disabled. Defaults to `false`",
Optional: true,
Default: false,
},
},
}
}
func dataSourceDockerRegistryImageManifestsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
pullOpts := parseImageOptions(d.Get("name").(string))
var authConfig registry.AuthConfig
if v, ok := d.GetOk("auth_config"); ok {
log.Printf("[INFO] Using auth config from resource: %s", v)
authConfig = buildAuthConfigFromResource(v)
} else {
log.Printf("[INFO] Using auth config from provider: %s", v)
var err error
authConfig, err = getAuthConfigForRegistry(pullOpts.Registry, meta.(*ProviderConfig))
if err != nil {
// The user did not provide a credential for this registry.
// But there are many registries where you can pull without a credential.
// We are setting default values for the authConfig here.
authConfig.Username = ""
authConfig.Password = ""
authConfig.ServerAddress = "https://" + pullOpts.Registry
}
}
insecureSkipVerify := d.Get("insecure_skip_verify").(bool)
manifest, err := getImageManifest(pullOpts.Registry, authConfig.ServerAddress, pullOpts.Repository, pullOpts.Tag, authConfig.Username, authConfig.Password, insecureSkipVerify, false)
if err != nil {
manifest, err = getImageManifest(pullOpts.Registry, authConfig.ServerAddress, pullOpts.Repository, pullOpts.Tag, authConfig.Username, authConfig.Password, insecureSkipVerify, true)
if err != nil {
return diag.Errorf("Got error when attempting to fetch image version %s:%s from registry: %s", pullOpts.Repository, pullOpts.Tag, err)
}
}
d.SetId(fmt.Sprintf("%s:%s", pullOpts.Repository, pullOpts.Tag))
if err = d.Set("manifests", flattenManifests(manifest.Manifests)); err != nil {
log.Printf("[WARN] failed to set manifests from API: %s", err)
}
return nil
}
func getImageManifest(registry, registryWithProtocol, image, tag, username, password string, insecureSkipVerify, fallback bool) (*ManifestResponse, error) {
client := buildHttpClientForRegistry(registryWithProtocol, insecureSkipVerify)
req, err := setupHTTPRequestForRegistry("GET", registry, registryWithProtocol, image, tag, username, password, fallback)
if err != nil {
return nil, err
}
return doManifestRequest(req, client, username, password, "repository:"+image+":push,pull", true)
}
func doManifestRequest(req *http.Request, client *http.Client, username string, password string, fallbackScope string, retryUnauthorized bool) (*ManifestResponse, error) {
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("Error during registry request: %s", err)
}
switch resp.StatusCode {
// Basic auth was valid or not needed
case http.StatusOK:
return getManifestsFromResponse(resp)
default:
if resp.StatusCode == http.StatusUnauthorized && retryUnauthorized {
auth, err := parseAuthHeader(resp.Header.Get("www-authenticate"))
if err != nil {
return nil, fmt.Errorf("bad credentials: %s", resp.Status)
}
token, err := getAuthToken(auth, username, password, fallbackScope, client)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
return doManifestRequest(req, client, username, password, fallbackScope, false)
}
return nil, fmt.Errorf("got bad response from registry: %s", resp.Status)
}
}
func getManifestsFromResponse(response *http.Response) (*ManifestResponse, error) {
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("Error reading response body: %s", err)
}
manifest := &ManifestResponse{}
err = json.Unmarshal(body, manifest)
if err != nil {
return nil, fmt.Errorf("Error parsing manifest response: %s", err)
}
if len(manifest.Manifests) == 0 {
log.Printf("[DEBUG] Manifest response was not for list: %s", string(body))
return nil, fmt.Errorf("Error unsupported manifest response")
}
return manifest, nil
}
func flattenManifests(in []Manifest) []manifestMap {
manifests := make([]manifestMap, len(in))
for i, m := range in {
manifests[i] = manifestMap{
"media_type": m.MediaType,
"sha256_digest": m.Digest,
"architecture": m.Platform.Architecture,
"os": m.Platform.OS,
}
}
return manifests
}
type ManifestResponse struct {
Manifests []Manifest `json:"manifests"`
}
type Manifest struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Platform ManifestPlatform `json:"platform"`
}
type ManifestPlatform struct {
Architecture string `json:"architecture"`
OS string `json:"os"`
}
type manifestMap map[string]interface{}