mirror of
https://github.com/kreuzwerker/terraform-provider-docker.git
synced 2025-12-18 23:06:10 -05:00
feat: Implement registry_image_manifests data source (#714)
* feat: Implement registry_image_manifests data source * fix: Golint errors
This commit is contained in:
parent
d638dddb1e
commit
62970c2d5a
11 changed files with 466 additions and 150 deletions
52
docs/data-sources/registry_image_manifests.md
Normal file
52
docs/data-sources/registry_image_manifests.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "docker_registry_image_manifests 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_image_manifests (Data Source)
|
||||
|
||||
Reads the image metadata for each manifest in a Docker multi-arch image from a Docker Registry.
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `name` (String) The name of the Docker image, including any tags. e.g. `alpine:latest`
|
||||
|
||||
### Optional
|
||||
|
||||
- `auth_config` (Block List, Max: 1) Authentication configuration for the Docker registry. It is only used for this resource. (see [below for nested schema](#nestedblock--auth_config))
|
||||
- `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))
|
||||
|
||||
<a id="nestedblock--auth_config"></a>
|
||||
### Nested Schema for `auth_config`
|
||||
|
||||
Required:
|
||||
|
||||
- `address` (String) The address of the Docker registry.
|
||||
- `password` (String, Sensitive) The password for the Docker registry.
|
||||
- `username` (String) The username for the Docker registry.
|
||||
|
||||
|
||||
<a id="nestedatt--manifests"></a>
|
||||
### Nested Schema for `manifests`
|
||||
|
||||
Read-Only:
|
||||
|
||||
- `architecture` (String)
|
||||
- `media_type` (String)
|
||||
- `os` (String)
|
||||
- `sha256_digest` (String)
|
||||
|
||||
|
||||
|
|
@ -2,10 +2,17 @@ package provider
|
|||
|
||||
import (
|
||||
b64 "encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
)
|
||||
|
||||
// ECR HTTP authentication needs Bearer token in the Authorization header
|
||||
|
|
@ -72,3 +79,126 @@ 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 isECRPublicRepositoryURL(registry) {
|
||||
password = normalizeECRPasswordForHTTPUsage(password)
|
||||
req.Header.Add("Authorization", "Bearer "+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
|
||||
}
|
||||
|
||||
// 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, does not start with 'Bearer'")
|
||||
}
|
||||
|
||||
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 := io.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"`
|
||||
}
|
||||
|
||||
var AuthConfigSchema = &schema.Schema{
|
||||
Type: schema.TypeList,
|
||||
Description: "Authentication configuration for the Docker registry. It is only used for this resource.",
|
||||
Optional: true,
|
||||
MaxItems: 1,
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"address": {
|
||||
Type: schema.TypeString,
|
||||
Description: "The address of the Docker registry.",
|
||||
Required: true,
|
||||
},
|
||||
"username": {
|
||||
Type: schema.TypeString,
|
||||
Description: "The username for the Docker registry.",
|
||||
Required: true,
|
||||
},
|
||||
"password": {
|
||||
Type: schema.TypeString,
|
||||
Description: "The password for the Docker registry.",
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,15 +29,26 @@ func TestIsECRRepositoryURL(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseAuthHeaders(t *testing.T) {
|
||||
_, err := parseAuthHeader("")
|
||||
if err == nil || err.Error() != "missing or invalid www-authenticate header, does not start with 'Bearer'" {
|
||||
t.Fatalf("wanted \"missing or invalid www-authenticate header, does not start with 'Bearer'\", got nil")
|
||||
}
|
||||
|
||||
header := "Bearer realm=\"https://gcr.io/v2/token\",service=\"gcr.io\",scope=\"repository:<owner>/:<repo>/<name>:pull\""
|
||||
result := parseAuthHeader(header)
|
||||
result, err := parseAuthHeader(header)
|
||||
if err != nil {
|
||||
t.Errorf("wanted no error, got %s", err)
|
||||
}
|
||||
wantScope := "repository:<owner>/:<repo>/<name>: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:<owner>/:<repo>/<name>:push,pull\""
|
||||
result = parseAuthHeader(header)
|
||||
result, err = parseAuthHeader(header)
|
||||
if err != nil {
|
||||
t.Errorf("wanted no error, got %s", err)
|
||||
}
|
||||
wantScope = "repository:<owner>/:<repo>/<name>:push,pull"
|
||||
if result["scope"] != wantScope {
|
||||
t.Errorf("want: %#v, got: %#v", wantScope, result["scope"])
|
||||
|
|
|
|||
|
|
@ -3,14 +3,9 @@ package provider
|
|||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
b64 "encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
|
|
@ -76,29 +71,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)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
if registry != "ghcr.io" && !isECRRepositoryURL(registry) && !isAzureCRRepositoryURL(registry) && registry != "gcr.io" {
|
||||
req.SetBasicAuth(username, password)
|
||||
} else {
|
||||
if isECRPublicRepositoryURL(registry) {
|
||||
password = normalizeECRPasswordForHTTPUsage(password)
|
||||
req.Header.Add("Authorization", "Bearer "+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)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error during registry request: %s", err)
|
||||
|
|
@ -111,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
|
||||
}
|
||||
|
|
@ -149,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")
|
||||
|
||||
|
|
@ -185,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 := io.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 {
|
||||
|
|
|
|||
201
internal/provider/data_source_docker_registry_image_manifests.go
Normal file
201
internal/provider/data_source_docker_registry_image_manifests.go
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
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, 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 := 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{}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
|
||||
)
|
||||
|
||||
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 TestAccDockerRegistryMultiarchImageDataSource_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_image_manifests", "testAccDockerImageManifestsDataSourceConfig"),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestMatchTypeSetElemNestedAttrs("data.docker_registry_image_manifests.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_image_manifests", "testAccDockerImageManifestsDataSourcePrivateConfig"),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestMatchTypeSetElemNestedAttrs("data.docker_registry_image_manifests.bar", "manifests.*", manifestRegexSet),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -169,11 +169,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_image_manifests": dataSourceDockerRegistryImageManifests(),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,32 +48,7 @@ func resourceDockerRegistryImage() *schema.Resource {
|
|||
Computed: true,
|
||||
},
|
||||
|
||||
"auth_config": {
|
||||
Type: schema.TypeList,
|
||||
Description: "Authentication configuration for the Docker registry. It is only used for this resource.",
|
||||
Optional: true,
|
||||
MaxItems: 1,
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"address": {
|
||||
Type: schema.TypeString,
|
||||
Description: "The address of the Docker registry.",
|
||||
Required: true,
|
||||
},
|
||||
"username": {
|
||||
Type: schema.TypeString,
|
||||
Description: "The username for the Docker registry.",
|
||||
Required: true,
|
||||
},
|
||||
"password": {
|
||||
Type: schema.TypeString,
|
||||
Description: "The password for the Docker registry.",
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"auth_config": AuthConfigSchema,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,29 +296,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)
|
||||
return err
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
if pushOpts.Registry != "ghcr.io" && !isECRRepositoryURL(pushOpts.Registry) && !isAzureCRRepositoryURL(pushOpts.Registry) && pushOpts.Registry != "gcr.io" {
|
||||
req.SetBasicAuth(username, password)
|
||||
} else {
|
||||
if isECRPublicRepositoryURL(pushOpts.Registry) {
|
||||
password = normalizeECRPasswordForHTTPUsage(password)
|
||||
req.Header.Add("Authorization", "Bearer "+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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupHTTPHeadersForRegistryRequests(req, fallback)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error during registry request: %s", err)
|
||||
|
|
@ -331,11 +313,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
data "docker_registry_image_manifests" "foo" {
|
||||
name = "alpine:latest"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
data "docker_registry_image_manifests" "bar" {
|
||||
name = "gcr.io:443/google_containers/pause:3.2"
|
||||
}
|
||||
Loading…
Reference in a new issue