From b6d000e3cc573edec40869ecdcc646e855c87764 Mon Sep 17 00:00:00 2001 From: Vijay Prakash Date: Tue, 13 Jun 2023 15:48:09 +0200 Subject: [PATCH 1/5] feat: refactor common HTTP request logic to shared method --- internal/provider/authentication_helpers.go | 25 +++++++++++++++++++ .../data_source_docker_registry_image.go | 20 ++------------- .../resource_docker_registry_image_funcs.go | 19 ++------------ 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/internal/provider/authentication_helpers.go b/internal/provider/authentication_helpers.go index 50662020a..95bce7e4b 100644 --- a/internal/provider/authentication_helpers.go +++ b/internal/provider/authentication_helpers.go @@ -2,6 +2,7 @@ package provider import ( b64 "encoding/base64" + "fmt" "log" "net/http" "regexp" @@ -68,3 +69,27 @@ func setupHTTPHeadersForRegistryRequests(req *http.Request, fallback bool) { req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v1+prettyjws") } } + +func setupHTTPRequestForRegistry(method, registry, registryWithProtocol, image, tag, username, password string, fallback bool) (*http.Request, error) { + req, err := http.NewRequest(method, registryWithProtocol+"/v2/"+image+"/manifests/"+tag, nil) + if err != nil { + return nil, fmt.Errorf("Error creating registry request: %s", err) + } + + if username != "" { + if registry != "ghcr.io" && !isECRRepositoryURL(registry) && !isAzureCRRepositoryURL(registry) && registry != "gcr.io" { + req.SetBasicAuth(username, password) + } else { + if isECRRepositoryURL(registry) { + password = normalizeECRPasswordForHTTPUsage(password) + req.Header.Add("Authorization", "Basic "+password) + } else { + req.Header.Add("Authorization", "Bearer "+b64.StdEncoding.EncodeToString([]byte(password))) + } + } + } + + setupHTTPHeadersForRegistryRequests(req, fallback) + + return req, nil +} diff --git a/internal/provider/data_source_docker_registry_image.go b/internal/provider/data_source_docker_registry_image.go index 3bab2f3e6..a4bcb3c05 100644 --- a/internal/provider/data_source_docker_registry_image.go +++ b/internal/provider/data_source_docker_registry_image.go @@ -3,7 +3,6 @@ package provider import ( "context" "crypto/sha256" - b64 "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -76,26 +75,11 @@ func dataSourceDockerRegistryImageRead(ctx context.Context, d *schema.ResourceDa func getImageDigest(registry string, registryWithProtocol string, image, tag, username, password string, insecureSkipVerify, fallback bool) (string, error) { client := buildHttpClientForRegistry(registryWithProtocol, insecureSkipVerify) - req, err := http.NewRequest("HEAD", registryWithProtocol+"/v2/"+image+"/manifests/"+tag, nil) + req, err := setupHTTPRequestForRegistry("HEAD", registry, registryWithProtocol, image, tag, username, password, fallback) if err != nil { - return "", fmt.Errorf("Error creating registry request: %s", err) - } - - if username != "" { - if registry != "ghcr.io" && !isECRRepositoryURL(registry) && !isAzureCRRepositoryURL(registry) && registry != "gcr.io" { - req.SetBasicAuth(username, password) - } else { - if isECRRepositoryURL(registry) { - password = normalizeECRPasswordForHTTPUsage(password) - req.Header.Add("Authorization", "Basic "+password) - } else { - req.Header.Add("Authorization", "Bearer "+b64.StdEncoding.EncodeToString([]byte(password))) - } - } + return "", err } - setupHTTPHeadersForRegistryRequests(req, fallback) - resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("Error during registry request: %s", err) diff --git a/internal/provider/resource_docker_registry_image_funcs.go b/internal/provider/resource_docker_registry_image_funcs.go index 33ba4740e..6919ddd11 100644 --- a/internal/provider/resource_docker_registry_image_funcs.go +++ b/internal/provider/resource_docker_registry_image_funcs.go @@ -261,26 +261,11 @@ func buildHttpClientForRegistry(registryAddressWithProtocol string, insecureSkip func deleteDockerRegistryImage(pushOpts internalPushImageOptions, registryWithProtocol string, sha256Digest, username, password string, insecureSkipVerify, fallback bool) error { client := buildHttpClientForRegistry(registryWithProtocol, insecureSkipVerify) - req, err := http.NewRequest("DELETE", registryWithProtocol+"/v2/"+pushOpts.Repository+"/manifests/"+sha256Digest, nil) + req, err := setupHTTPRequestForRegistry("DELETE", pushOpts.Registry, registryWithProtocol, pushOpts.Repository, sha256Digest, username, password, fallback) if err != nil { - return fmt.Errorf("Error deleting registry image: %s", err) - } - - if username != "" { - if pushOpts.Registry != "ghcr.io" && !isECRRepositoryURL(pushOpts.Registry) && !isAzureCRRepositoryURL(pushOpts.Registry) && pushOpts.Registry != "gcr.io" { - req.SetBasicAuth(username, password) - } else { - if isECRRepositoryURL(pushOpts.Registry) { - password = normalizeECRPasswordForHTTPUsage(password) - req.Header.Add("Authorization", "Basic "+password) - } else { - req.Header.Add("Authorization", "Bearer "+base64.StdEncoding.EncodeToString([]byte(password))) - } - } + return err } - setupHTTPHeadersForRegistryRequests(req, fallback) - resp, err := client.Do(req) if err != nil { return fmt.Errorf("Error during registry request: %s", err) From 1458646762dd2a1bec7ef3efdd46a01ea2673781 Mon Sep 17 00:00:00 2001 From: Vijay Prakash Date: Tue, 13 Jun 2023 17:08:13 +0200 Subject: [PATCH 2/5] feat: add docker_registry_multiarch_image data source --- internal/provider/authentication_helpers.go | 73 +++++++ .../data_source_docker_registry_image.go | 77 +------ ..._source_docker_registry_multiarch_image.go | 189 ++++++++++++++++++ internal/provider/provider.go | 11 +- .../resource_docker_registry_image_funcs.go | 7 +- 5 files changed, 276 insertions(+), 81 deletions(-) create mode 100644 internal/provider/data_source_docker_registry_multiarch_image.go diff --git a/internal/provider/authentication_helpers.go b/internal/provider/authentication_helpers.go index 95bce7e4b..b91f5c925 100644 --- a/internal/provider/authentication_helpers.go +++ b/internal/provider/authentication_helpers.go @@ -2,9 +2,13 @@ package provider import ( b64 "encoding/base64" + "encoding/json" + "errors" "fmt" + "io/ioutil" "log" "net/http" + "net/url" "regexp" "strings" ) @@ -93,3 +97,72 @@ func setupHTTPRequestForRegistry(method, registry, registryWithProtocol, image, return req, nil } + +// Checks for and parses key/value pairs from a WWW-Authenticate header +func parseAuthHeader(header string) (map[string]string, error) { + if !strings.HasPrefix(header, "Bearer") { + return nil, errors.New("missing or invalid www-authenticate header") + } + + parts := strings.SplitN(header, " ", 2) + parts = regexp.MustCompile(`\w+\=\".*?\"|\w+[^\s\"]+?`).FindAllString(parts[1], -1) // expression to match auth headers. + opts := make(map[string]string) + + for _, part := range parts { + vals := strings.SplitN(part, "=", 2) + key := vals[0] + val := strings.Trim(vals[1], "\", ") + opts[key] = val + } + + return opts, nil +} + +func getAuthToken(auth map[string]string, username string, password string, client *http.Client) (string, error) { + params := url.Values{} + params.Set("service", auth["service"]) + params.Set("scope", auth["scope"]) + tokenRequest, err := http.NewRequest("GET", auth["realm"]+"?"+params.Encode(), nil) + if err != nil { + return "", fmt.Errorf("Error creating registry request: %s", err) + } + + if username != "" { + tokenRequest.SetBasicAuth(username, password) + } + + tokenResponse, err := client.Do(tokenRequest) + if err != nil { + return "", fmt.Errorf("Error during registry request: %s", err) + } + + if tokenResponse.StatusCode != http.StatusOK { + return "", fmt.Errorf("Got bad response from registry: " + tokenResponse.Status) + } + + body, err := ioutil.ReadAll(tokenResponse.Body) + if err != nil { + return "", fmt.Errorf("Error reading response body: %s", err) + } + + token := &TokenResponse{} + err = json.Unmarshal(body, token) + if err != nil { + return "", fmt.Errorf("Error parsing OAuth token response: %s", err) + } + + if token.Token != "" { + return token.Token, nil + } + + if token.AccessToken != "" { + return token.AccessToken, nil + } + + return "", fmt.Errorf("Error unsupported OAuth response") +} + +type TokenResponse struct { + Token string + AccessToken string `json:"access_token"` +} diff --git a/internal/provider/data_source_docker_registry_image.go b/internal/provider/data_source_docker_registry_image.go index a4bcb3c05..ae82175e0 100644 --- a/internal/provider/data_source_docker_registry_image.go +++ b/internal/provider/data_source_docker_registry_image.go @@ -3,13 +3,9 @@ package provider import ( "context" "crypto/sha256" - "encoding/json" "fmt" "io/ioutil" "net/http" - "net/url" - "regexp" - "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -92,11 +88,12 @@ func getImageDigest(registry string, registryWithProtocol string, image, tag, us // Either OAuth is required or the basic auth creds were invalid case http.StatusUnauthorized: - if !strings.HasPrefix(resp.Header.Get("www-authenticate"), "Bearer") { - return "", fmt.Errorf("Bad credentials: " + resp.Status) + auth, err := parseAuthHeader(resp.Header.Get("www-authenticate")) + if err != nil { + return "", fmt.Errorf("Bad credentials: %s", resp.Status) } - token, err := getAuthToken(resp.Header.Get("www-authenticate"), username, password, client) + token, err := getAuthToken(auth, username, password, client) if err != nil { return "", err } @@ -130,27 +127,6 @@ func getImageDigest(registry string, registryWithProtocol string, image, tag, us } } -type TokenResponse struct { - Token string - AccessToken string `json:"access_token"` -} - -// Parses key/value pairs from a WWW-Authenticate header -func parseAuthHeader(header string) map[string]string { - parts := strings.SplitN(header, " ", 2) - parts = regexp.MustCompile(`\w+\=\".*?\"|\w+[^\s\"]+?`).FindAllString(parts[1], -1) // expression to match auth headers. - opts := make(map[string]string) - - for _, part := range parts { - vals := strings.SplitN(part, "=", 2) - key := vals[0] - val := strings.Trim(vals[1], "\", ") - opts[key] = val - } - - return opts -} - func getDigestFromResponse(response *http.Response) (string, error) { header := response.Header.Get("Docker-Content-Digest") @@ -166,51 +142,6 @@ func getDigestFromResponse(response *http.Response) (string, error) { return header, nil } -func getAuthToken(authHeader string, username string, password string, client *http.Client) (string, error) { - auth := parseAuthHeader(authHeader) - params := url.Values{} - params.Set("service", auth["service"]) - params.Set("scope", auth["scope"]) - tokenRequest, err := http.NewRequest("GET", auth["realm"]+"?"+params.Encode(), nil) - if err != nil { - return "", fmt.Errorf("Error creating registry request: %s", err) - } - - if username != "" { - tokenRequest.SetBasicAuth(username, password) - } - - tokenResponse, err := client.Do(tokenRequest) - if err != nil { - return "", fmt.Errorf("Error during registry request: %s", err) - } - - if tokenResponse.StatusCode != http.StatusOK { - return "", fmt.Errorf("Got bad response from registry: " + tokenResponse.Status) - } - - body, err := ioutil.ReadAll(tokenResponse.Body) - if err != nil { - return "", fmt.Errorf("Error reading response body: %s", err) - } - - token := &TokenResponse{} - err = json.Unmarshal(body, token) - if err != nil { - return "", fmt.Errorf("Error parsing OAuth token response: %s", err) - } - - if token.Token != "" { - return token.Token, nil - } - - if token.AccessToken != "" { - return token.AccessToken, nil - } - - return "", fmt.Errorf("Error unsupported OAuth response") -} - func doDigestRequest(req *http.Request, client *http.Client) (*http.Response, error) { digestResponse, err := client.Do(req) if err != nil { diff --git a/internal/provider/data_source_docker_registry_multiarch_image.go b/internal/provider/data_source_docker_registry_multiarch_image.go new file mode 100644 index 000000000..c705c284e --- /dev/null +++ b/internal/provider/data_source_docker_registry_multiarch_image.go @@ -0,0 +1,189 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceDockerRegistryMultiarchImage() *schema.Resource { + return &schema.Resource{ + Description: "Reads the image metadata for each manifest in a Docker multi-arch image from a Docker Registry.", + + ReadContext: dataSourceDockerRegistryMultiarchImageRead, + + 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, + }, + + "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 dataSourceDockerRegistryMultiarchImageRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + pullOpts := parseImageOptions(d.Get("name").(string)) + + 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, true) +} + +func doManifestRequest(req *http.Request, client *http.Client, username string, password 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, client) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+token) + + return doManifestRequest(req, client, username, password, false) + } + + return nil, fmt.Errorf("Got bad response from registry: %s", resp.Status) + } +} + +func getManifestsFromResponse(response *http.Response) (*ManifestResponse, error) { + body, err := ioutil.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 { + 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{} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b52deabbd..a11ce62f6 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -156,11 +156,12 @@ func New(version string) func() *schema.Provider { }, DataSourcesMap: map[string]*schema.Resource{ - "docker_registry_image": dataSourceDockerRegistryImage(), - "docker_network": dataSourceDockerNetwork(), - "docker_plugin": dataSourceDockerPlugin(), - "docker_image": dataSourceDockerImage(), - "docker_logs": dataSourceDockerLogs(), + "docker_registry_image": dataSourceDockerRegistryImage(), + "docker_network": dataSourceDockerNetwork(), + "docker_plugin": dataSourceDockerPlugin(), + "docker_image": dataSourceDockerImage(), + "docker_logs": dataSourceDockerLogs(), + "docker_registry_multiarch_image": dataSourceDockerRegistryMultiarchImage(), }, } diff --git a/internal/provider/resource_docker_registry_image_funcs.go b/internal/provider/resource_docker_registry_image_funcs.go index 6919ddd11..56c4b95a1 100644 --- a/internal/provider/resource_docker_registry_image_funcs.go +++ b/internal/provider/resource_docker_registry_image_funcs.go @@ -278,11 +278,12 @@ func deleteDockerRegistryImage(pushOpts internalPushImageOptions, registryWithPr // Either OAuth is required or the basic auth creds were invalid case http.StatusUnauthorized: - if !strings.HasPrefix(resp.Header.Get("www-authenticate"), "Bearer") { - return fmt.Errorf("Bad credentials: " + resp.Status) + auth, err := parseAuthHeader(resp.Header.Get("www-authenticate")) + if err != nil { + return fmt.Errorf("Bad credentials: %s", resp.Status) } - token, err := getAuthToken(resp.Header.Get("www-authenticate"), username, password, client) + token, err := getAuthToken(auth, username, password, client) if err != nil { return err } From c872bcf0c0de49bcca178293f6638a4e85f554f0 Mon Sep 17 00:00:00 2001 From: Vijay Prakash Date: Wed, 14 Jun 2023 12:24:24 +0200 Subject: [PATCH 3/5] fix: fix auth helper and docker_registry_image data source tests --- .../provider/authentication_helpers_test.go | 17 +++++++++++-- .../data_source_docker_registry_image_test.go | 8 +++--- ...ce_docker_registry_multiarch_image_test.go | 25 +++++++++++++++++++ ...ockerMultiarchImageDataSourceAuthConfig.tf | 11 ++++++++ 4 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 internal/provider/data_source_docker_registry_multiarch_image_test.go create mode 100644 testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceAuthConfig.tf diff --git a/internal/provider/authentication_helpers_test.go b/internal/provider/authentication_helpers_test.go index fef82429a..cfa2390e7 100644 --- a/internal/provider/authentication_helpers_test.go +++ b/internal/provider/authentication_helpers_test.go @@ -21,15 +21,28 @@ func TestIsECRRepositoryURL(t *testing.T) { } func TestParseAuthHeaders(t *testing.T) { + _, err := parseAuthHeader("") + if err == nil || err.Error() != "missing or invalid www-authenticate header" { + t.Fatalf("wanted \"missing or invalid www-authenticate header\", got nil") + } + header := "Bearer realm=\"https://gcr.io/v2/token\",service=\"gcr.io\",scope=\"repository:/:/:pull\"" - result := parseAuthHeader(header) + result, err := parseAuthHeader(header) + if err != nil { + t.Errorf("wanted no error, got %s", err) + } + wantScope := "repository:/:/:pull" if result["scope"] != wantScope { t.Errorf("want: %#v, got: %#v", wantScope, result["scope"]) } header = "Bearer realm=\"https://gcr.io/v2/token\",service=\"gcr.io\",scope=\"repository:/:/:push,pull\"" - result = parseAuthHeader(header) + result, err = parseAuthHeader(header) + if err != nil { + t.Errorf("wanted no error, got %s", err) + } + wantScope = "repository:/:/:push,pull" if result["scope"] != wantScope { t.Errorf("want: %#v, got: %#v", wantScope, result["scope"]) diff --git a/internal/provider/data_source_docker_registry_image_test.go b/internal/provider/data_source_docker_registry_image_test.go index 6d9044501..853694631 100644 --- a/internal/provider/data_source_docker_registry_image_test.go +++ b/internal/provider/data_source_docker_registry_image_test.go @@ -15,7 +15,7 @@ import ( var registryDigestRegexp = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`) -func TestAccDockerRegistryImage_basic(t *testing.T) { +func TestAccDockerRegistryImageDataSource_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: providerFactories, @@ -30,7 +30,7 @@ func TestAccDockerRegistryImage_basic(t *testing.T) { }) } -func TestAccDockerRegistryImage_private(t *testing.T) { +func TestAccDockerRegistryImageDataSource_private(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: providerFactories, @@ -45,7 +45,7 @@ func TestAccDockerRegistryImage_private(t *testing.T) { }) } -func TestAccDockerRegistryImage_auth(t *testing.T) { +func TestAccDockerRegistryImageDataSource_auth(t *testing.T) { registry := "127.0.0.1:15000" image := "127.0.0.1:15000/tftest-service:v1" ctx := context.Background() @@ -66,7 +66,7 @@ func TestAccDockerRegistryImage_auth(t *testing.T) { }) } -func TestAccDockerRegistryImage_httpAuth(t *testing.T) { +func TestAccDockerRegistryImageDataSource_httpAuth(t *testing.T) { registry := "http://127.0.0.1:15001" image := "127.0.0.1:15001/tftest-service:v1" ctx := context.Background() diff --git a/internal/provider/data_source_docker_registry_multiarch_image_test.go b/internal/provider/data_source_docker_registry_multiarch_image_test.go new file mode 100644 index 000000000..cbce9c014 --- /dev/null +++ b/internal/provider/data_source_docker_registry_multiarch_image_test.go @@ -0,0 +1,25 @@ +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +var bar = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`) + +func TestAccDockerRegistryMultiarchImage_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: loadTestConfiguration(t, DATA_SOURCE, "docker_registry_multiarch_image", "testAccDockerMultiarchImageDataSourceConfig"), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("data.docker_registry_multiarch_image.foo", "sha256_digest", bar), + ), + }, + }, + }) +} diff --git a/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceAuthConfig.tf b/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceAuthConfig.tf new file mode 100644 index 000000000..8d050596e --- /dev/null +++ b/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceAuthConfig.tf @@ -0,0 +1,11 @@ +provider "docker" { + alias = "private" + registry_auth { + address = "%s" + } +} +data "docker_registry_multiarch_image" "foobar" { + provider = "docker.private" + name = "%s" + insecure_skip_verify = true +} From ef1528a89a90cec55c35db87240b814ab556ab74 Mon Sep 17 00:00:00 2001 From: Vijay Prakash Date: Wed, 14 Jun 2023 16:39:13 +0200 Subject: [PATCH 4/5] feat: add acceptance tests --- ..._source_docker_registry_multiarch_image.go | 5 ++-- ...ce_docker_registry_multiarch_image_test.go | 26 ++++++++++++++++--- ...ockerMultiarchImageDataSourceAuthConfig.tf | 11 -------- ...AccDockerMultiarchImageDataSourceConfig.tf | 3 +++ ...erMultiarchImageDataSourcePrivateConfig.tf | 3 +++ 5 files changed, 32 insertions(+), 16 deletions(-) delete mode 100644 testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceAuthConfig.tf create mode 100644 testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceConfig.tf create mode 100644 testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourcePrivateConfig.tf diff --git a/internal/provider/data_source_docker_registry_multiarch_image.go b/internal/provider/data_source_docker_registry_multiarch_image.go index c705c284e..06bca3f0c 100644 --- a/internal/provider/data_source_docker_registry_multiarch_image.go +++ b/internal/provider/data_source_docker_registry_multiarch_image.go @@ -121,7 +121,7 @@ func doManifestRequest(req *http.Request, client *http.Client, username string, 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) + return nil, fmt.Errorf("bad credentials: %s", resp.Status) } token, err := getAuthToken(auth, username, password, client) @@ -134,7 +134,7 @@ func doManifestRequest(req *http.Request, client *http.Client, username string, return doManifestRequest(req, client, username, password, false) } - return nil, fmt.Errorf("Got bad response from registry: %s", resp.Status) + return nil, fmt.Errorf("got bad response from registry: %s", resp.Status) } } @@ -151,6 +151,7 @@ func getManifestsFromResponse(response *http.Response) (*ManifestResponse, error } 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") } diff --git a/internal/provider/data_source_docker_registry_multiarch_image_test.go b/internal/provider/data_source_docker_registry_multiarch_image_test.go index cbce9c014..4f9fa37ee 100644 --- a/internal/provider/data_source_docker_registry_multiarch_image_test.go +++ b/internal/provider/data_source_docker_registry_multiarch_image_test.go @@ -7,9 +7,14 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -var bar = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`) +var manifestRegexSet = map[string]*regexp.Regexp{ + "sha256_digest": regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`), + "architecture": regexp.MustCompile(`\A(?:amd|amd64|arm|arm64|386|ppc64le|s390x|unknown)\z`), + "os": regexp.MustCompile(`\A(?:linux|unknown)\z`), + "media_type": regexp.MustCompile(`\Aapplication\/vnd\..+\z`), +} -func TestAccDockerRegistryMultiarchImage_basic(t *testing.T) { +func TestAccDockerRegistryMultiarchImageDataSource_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: providerFactories, @@ -17,7 +22,22 @@ func TestAccDockerRegistryMultiarchImage_basic(t *testing.T) { { Config: loadTestConfiguration(t, DATA_SOURCE, "docker_registry_multiarch_image", "testAccDockerMultiarchImageDataSourceConfig"), Check: resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr("data.docker_registry_multiarch_image.foo", "sha256_digest", bar), + resource.TestMatchTypeSetElemNestedAttrs("data.docker_registry_multiarch_image.foo", "manifests.*", manifestRegexSet), + ), + }, + }, + }) +} + +func TestAccDockerRegistryMultiarchImageDataSource_private(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: loadTestConfiguration(t, DATA_SOURCE, "docker_registry_multiarch_image", "testAccDockerMultiarchImageDataSourcePrivateConfig"), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchTypeSetElemNestedAttrs("data.docker_registry_multiarch_image.bar", "manifests.*", manifestRegexSet), ), }, }, diff --git a/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceAuthConfig.tf b/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceAuthConfig.tf deleted file mode 100644 index 8d050596e..000000000 --- a/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceAuthConfig.tf +++ /dev/null @@ -1,11 +0,0 @@ -provider "docker" { - alias = "private" - registry_auth { - address = "%s" - } -} -data "docker_registry_multiarch_image" "foobar" { - provider = "docker.private" - name = "%s" - insecure_skip_verify = true -} diff --git a/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceConfig.tf b/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceConfig.tf new file mode 100644 index 000000000..ae1bb6886 --- /dev/null +++ b/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourceConfig.tf @@ -0,0 +1,3 @@ +data "docker_registry_multiarch_image" "foo" { + name = "alpine:latest" +} diff --git a/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourcePrivateConfig.tf b/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourcePrivateConfig.tf new file mode 100644 index 000000000..c9ba4eafb --- /dev/null +++ b/testdata/data-sources/docker_registry_multiarch_image/testAccDockerMultiarchImageDataSourcePrivateConfig.tf @@ -0,0 +1,3 @@ +data "docker_registry_multiarch_image" "bar" { + name = "gcr.io:443/google_containers/pause:3.2" +} From 1e4ca8918d03030be84ddadf5833f1835ddf3a8c Mon Sep 17 00:00:00 2001 From: Vijay Prakash Date: Thu, 15 Jun 2023 10:19:19 +0200 Subject: [PATCH 5/5] feat: add documentation for the new data source --- docs/data-sources/registry_multiarch_image.md | 46 +++++++++++++++++++ .../data-source.tf | 4 ++ 2 files changed, 50 insertions(+) create mode 100644 docs/data-sources/registry_multiarch_image.md create mode 100644 examples/data-sources/docker_registry_multiarch_image/data-source.tf diff --git a/docs/data-sources/registry_multiarch_image.md b/docs/data-sources/registry_multiarch_image.md new file mode 100644 index 000000000..65bea96d1 --- /dev/null +++ b/docs/data-sources/registry_multiarch_image.md @@ -0,0 +1,46 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "docker_registry_multiarch_image Data Source - terraform-provider-docker" +subcategory: "" +description: |- + Reads the image metadata for each manifest in a Docker multi-arch image from a Docker Registry. +--- + +# docker_registry_multiarch_image (Data Source) + +Reads the image metadata for each manifest in a [Docker multi-arch image](https://docs.docker.com/build/building/multi-platform/) from a Docker Registry. + +## Example Usage + +```terraform +### Must be a Docker multi-arch image +data "docker_registry_multiarch_image" "alpine" { + name = "alpine:latest" +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the Docker image, including any tags. e.g. `alpine:latest` + +### Optional + +- `insecure_skip_verify` (Boolean) If `true`, the verification of TLS certificates of the server/registry is disabled. Defaults to `false` + +### Read-Only + +- `id` (String) The ID of this resource. +- `manifests` (Set of Object) The metadata for each manifest in the image (see [below for nested schema](#nestedatt--manifests)) + + +### Nested Schema for `manifests` + +Read-Only: + +- `architecture` (String) +- `media_type` (String) +- `os` (String) +- `sha256_digest` (String) diff --git a/examples/data-sources/docker_registry_multiarch_image/data-source.tf b/examples/data-sources/docker_registry_multiarch_image/data-source.tf new file mode 100644 index 000000000..0354ac6b7 --- /dev/null +++ b/examples/data-sources/docker_registry_multiarch_image/data-source.tf @@ -0,0 +1,4 @@ +### Must be a Docker multi-arch image +data "docker_registry_multiarch_image" "alpine" { + name = "alpine:latest" +}