terraform-provider-docker/internal/provider/data_source_docker_registry_image.go
Manuel Vogel 6c796e15a5
feat/doc generation (#193)
* chore: add tfplugindocs tool

* feat: add tfplugin doc dependency and make target

* chore: apply documentation generation

* docs(contributing): update for documentation generation

* fix: adapt website-lint target to new do folder

* docs(network): update ds descriptions

* docs: add template for index.md

* docs: add network resource generation

* chore(ci): updates paths for website checks

* docs: add plugin data source generation

* docs: add import cmd for network resource

* docs: add plugin resource generation

* feat: outlines remaining resources with example and import cmd

* feat: add descriptions to docs

* chore: add DevSkim ignores and fix capitalized errors

* docs: complete ds registry image

* docs: add container resource generation

* docs: add lables description to missing resources

* docs: remove computed:true from network data

so the list is rendered in the description

* Revert "docs: remove computed:true from network data"

This reverts commit dce9b7a5a2.

* docs: add docker image descriptions to generate the docs

* docs: add docker registry image descriptions to generate the docs

* docs: add docker service descriptions to generate the docs

* docs: add docker volume descriptions to generate the docs

* docs(index): clarifies description

so more docker resources are mentioned

* docs(network): fixes required and read-only attributes

so the ds can only be read by-name

* docs(plugin): clarifies the ds docs attributes

* docs: fix typo registry image ds

* docs(config): clarifies attributes and enhances examples

Provide a long example and import command

* fix(config): make data non-sensitive

Because only secrets data is

* docs(containter): clarifies attributes

and enhances examples with import

* docs(config): fix typo

* docs(image): clarifies attributes and remove import

* docs(network): clarifies attributes and adapts import

* docs(plugin): clarifies attributes and import

* docs(registry_image): clarifies attributes and removes import

* chore(secret): remove typo

* docs(service): clarifies attributes and import

* docs(volume): clarifies attributes and import

* fix: correct md linter rules after doc gen

* docs(volume): regenerated

* docs: add config custom template

* docs: add templates for all resources

* docs(config): templates all sections and examples

for better redability and structure

* docs(config): fix md linter

* docs(container): templates all sections and examples

* docs(image): templates all sections and examples

* docs(image): fix import resource by renaming

* docs(network): templates all sections and examples

* docs(service): templates all sections and examples

* docs(volume): templates all sections and examples

* fix(lint): replace website with doc directory

* fix(ci): link check file extension check

* fix: markdown links

* chore: remove old website folder

* chore: fix website-lint terrafmr dir and pattern

* fix: lint fix target website folder

* fix: website links

* docs(provider): update examples

with templates on auth and certs

* docs(provider): add tf-plugin-docs line

* docs(contributing): split doc generation section

* docs: final brush up for readability and structure

* chore(ci): add website-generation job

to see if files changed and it should run locally again

* chore(ci): remove explicit docker setup

from website lint because it's installed by default
2021-05-21 21:30:56 +09:00

224 lines
6.5 KiB
Go

package provider
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func dataSourceDockerRegistryImage() *schema.Resource {
return &schema.Resource{
Description: "Reads the image metadata from a Docker Registry. Used in conjunction with the [docker_image](../resources/image.md) resource to keep an image up to date on the latest available version of the tag.",
ReadContext: dataSourceDockerRegistryImageRead,
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,
},
"sha256_digest": {
Type: schema.TypeString,
Description: "The content digest of the image, as stored in the registry.",
Computed: true,
},
},
}
}
func dataSourceDockerRegistryImageRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
pullOpts := parseImageOptions(d.Get("name").(string))
authConfig := meta.(*ProviderConfig).AuthConfigs
// Use the official Docker Hub if a registry isn't specified
if pullOpts.Registry == "" {
pullOpts.Registry = "registry.hub.docker.com"
} else {
// Otherwise, filter the registry name out of the repo name
pullOpts.Repository = strings.Replace(pullOpts.Repository, pullOpts.Registry+"/", "", 1)
}
if pullOpts.Registry == "registry.hub.docker.com" {
// Docker prefixes 'library' to official images in the path; 'consul' becomes 'library/consul'
if !strings.Contains(pullOpts.Repository, "/") {
pullOpts.Repository = "library/" + pullOpts.Repository
}
}
if pullOpts.Tag == "" {
pullOpts.Tag = "latest"
}
username := ""
password := ""
if auth, ok := authConfig.Configs[normalizeRegistryAddress(pullOpts.Registry)]; ok {
username = auth.Username
password = auth.Password
}
digest, err := getImageDigest(pullOpts.Registry, pullOpts.Repository, pullOpts.Tag, username, password, false)
if err != nil {
digest, err = getImageDigest(pullOpts.Registry, pullOpts.Repository, pullOpts.Tag, username, password, true)
if err != nil {
return diag.Errorf("Got error when attempting to fetch image version from registry: %s", err)
}
}
d.SetId(digest)
d.Set("sha256_digest", digest)
return nil
}
func getImageDigest(registry, image, tag, username, password string, fallback bool) (string, error) {
client := http.DefaultClient
// Allow insecure registries only for ACC tests
// cuz we don't have a valid certs for this case
if env, okEnv := os.LookupEnv("TF_ACC"); okEnv {
if i, errConv := strconv.Atoi(env); errConv == nil && i >= 1 {
// DevSkim: ignore DS440000
cfg := &tls.Config{
InsecureSkipVerify: true,
}
client.Transport = &http.Transport{
TLSClientConfig: cfg,
}
}
}
req, err := http.NewRequest("GET", "https://"+registry+"/v2/"+image+"/manifests/"+tag, nil)
if err != nil {
return "", fmt.Errorf("Error creating registry request: %s", err)
}
if username != "" {
req.SetBasicAuth(username, password)
}
// We accept schema v2 manifests and manifest lists, and also OCI types
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json")
req.Header.Add("Accept", "application/vnd.oci.image.manifest.v1+json")
req.Header.Add("Accept", "application/vnd.oci.image.index.v1+json")
if fallback {
// Fallback to this header if the registry does not support the v2 manifest like gcr.io
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v1+prettyjws")
}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("Error during registry request: %s", err)
}
switch resp.StatusCode {
// Basic auth was valid or not needed
case http.StatusOK:
return getDigestFromResponse(resp)
// Either OAuth is required or the basic auth creds were invalid
case http.StatusUnauthorized:
if strings.HasPrefix(resp.Header.Get("www-authenticate"), "Bearer") {
auth := parseAuthHeader(resp.Header.Get("www-authenticate"))
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)
}
req.Header.Set("Authorization", "Bearer "+token.Token)
digestResponse, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("Error during registry request: %s", err)
}
if digestResponse.StatusCode != http.StatusOK {
return "", fmt.Errorf("Got bad response from registry: " + digestResponse.Status)
}
return getDigestFromResponse(digestResponse)
}
return "", fmt.Errorf("Bad credentials: " + resp.Status)
// Some unexpected status was given, return an error
default:
return "", fmt.Errorf("Got bad response from registry: " + resp.Status)
}
}
type TokenResponse struct {
Token string
}
// Parses key/value pairs from a WWW-Authenticate header
func parseAuthHeader(header string) map[string]string {
parts := strings.SplitN(header, " ", 2)
parts = strings.Split(parts[1], ",")
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")
if header == "" {
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", fmt.Errorf("Error reading registry response body: %s", err)
}
return fmt.Sprintf("sha256:%x", sha256.Sum256(body)), nil
}
return header, nil
}