Feat/private registry support (#21)

* Add support for private registry auth. Adapted documentation and tests.
* Clarified README.
This commit is contained in:
Manuel Vogel 2017-11-21 10:14:07 +01:00 committed by GitHub
parent 9e21a8f683
commit 76925f68f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 366 additions and 37 deletions

View file

@ -42,7 +42,7 @@ If you wish to work on the provider, you'll first need [Go](http://www.golang.or
To compile the provider, run `make build`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory.
```sh
$ make bin
$ make build
...
$ $GOPATH/bin/terraform-provider-$PROVIDER_NAME
...
@ -60,4 +60,25 @@ In order to run the full suite of Acceptance tests, run `make testacc`.
```sh
$ make testacc
# e.g. run a single acceptance test: e.g. 'TestAccDockerRegistryImage_private' in 'data_source_docker_registry_image_test.go'
go test -v -timeout 30s github.com/terraform-providers/terraform-provider-docker/docker -run ^TestAccDockerRegistryImage_private$
```
In order to extend the provider and test it with `terraform`, build the provider as mentioned above with
```sh
$ make build
```
Remove an explicit version of the provider you develop, because `terraform` will fetch
the locally built one in `$GOPATH/bin`
```hcl
provider "$PROVIDER_NAME" {
# version = "~> 0.1.2"
...
}
```
Don't forget to run `terraform init` each time you rebuild the provider. Check [here](https://www.youtube.com/watch?v=TMmovxyo5sY&t=30m14s) for a more detailed explanation.
You can check the latest released version of a provider at https://releases.hashicorp.com/terraform-provider-$PROVIDER_NAME/.

View file

@ -3,13 +3,14 @@ package docker
import (
"fmt"
"path/filepath"
"strings"
dc "github.com/fsouza/go-dockerclient"
)
// Config is the structure that stores the configuration to talk to a
// DockerConfig is the structure that stores the configuration to talk to a
// Docker API compatible host.
type Config struct {
type DockerConfig struct {
Host string
Ca string
Cert string
@ -18,7 +19,7 @@ type Config struct {
}
// NewClient() returns a new Docker client.
func (c *Config) NewClient() (*dc.Client, error) {
func (c *DockerConfig) NewClient() (*dc.Client, error) {
if c.Ca != "" || c.Cert != "" || c.Key != "" {
if c.Ca == "" || c.Cert == "" || c.Key == "" {
return nil, fmt.Errorf("ca_material, cert_material, and key_material must be specified")
@ -43,7 +44,22 @@ func (c *Config) NewClient() (*dc.Client, error) {
return dc.NewClient(c.Host)
}
// Data ia structure for holding data that we fetch from Docker.
// Data structure for holding data that we fetch from Docker.
type Data struct {
DockerImages map[string]*dc.APIImages
}
// ProviderConfig for the custom registry provider
type ProviderConfig struct {
DockerClient *dc.Client
AuthConfigs *dc.AuthConfigurations
}
// The registry address can be referenced in various places (registry auth, docker config file, image name)
// with or without the http(s):// prefix; this function is used to standardize the inputs
func normalizeRegistryAddress(address string) string {
if !strings.HasPrefix(address, "https://") && !strings.HasPrefix(address, "http://") {
return "https://" + address
}
return address
}

View file

@ -31,6 +31,7 @@ func dataSourceDockerRegistryImage() *schema.Resource {
func dataSourceDockerRegistryImageRead(d *schema.ResourceData, meta interface{}) error {
pullOpts := parseImageOptions(d.Get("name").(string))
authConfig := meta.(*ProviderConfig).AuthConfigs
// Use the official Docker Hub if a registry isn't specified
if pullOpts.Registry == "" {
@ -49,7 +50,15 @@ func dataSourceDockerRegistryImageRead(d *schema.ResourceData, meta interface{})
pullOpts.Tag = "latest"
}
digest, err := getImageDigest(pullOpts.Registry, pullOpts.Repository, pullOpts.Tag, "", "")
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)
if err != nil {
return fmt.Errorf("Got error when attempting to fetch image version from registry: %s", err)
@ -74,6 +83,9 @@ func getImageDigest(registry, image, tag, username, password string) (string, er
req.SetBasicAuth(username, password)
}
// Set this header so that we get the v2 manifest back from the registry.
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
resp, err := client.Do(req)
if err != nil {

View file

@ -1,6 +1,8 @@
package docker
import (
"fmt"
"os"
"regexp"
"testing"
@ -39,6 +41,23 @@ func TestAccDockerRegistryImage_private(t *testing.T) {
})
}
func TestAccDockerRegistryImage_auth(t *testing.T) {
registry := os.Getenv("DOCKER_REGISTRY_ADDRESS")
image := os.Getenv("DOCKER_PRIVATE_IMAGE")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(testAccDockerImageDataSourceAuthConfig, registry, image),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr("data.docker_registry_image.foobar", "sha256_digest", registryDigestRegexp),
),
},
},
})
}
const testAccDockerImageDataSourceConfig = `
data "docker_registry_image" "foo" {
name = "alpine:latest"
@ -50,3 +69,16 @@ data "docker_registry_image" "bar" {
name = "gcr.io:443/google_containers/pause:0.8.0"
}
`
const testAccDockerImageDataSourceAuthConfig = `
provider "docker" {
alias = "private"
registry_auth {
address = "%s"
}
}
data "docker_registry_image" "foobar" {
provider = "docker.private"
name = "%s"
}
`

View file

@ -2,7 +2,11 @@ package docker
import (
"fmt"
"os"
"os/user"
"strings"
dc "github.com/fsouza/go-dockerclient"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
@ -42,6 +46,44 @@ func Provider() terraform.ResourceProvider {
DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_PATH", ""),
Description: "Path to directory with Docker TLS config",
},
"registry_auth": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Address of the registry",
},
"username": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"registry_auth.config_file"},
DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_USER", ""),
Description: "Username for the registry",
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"registry_auth.config_file"},
DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_PASS", ""),
Description: "Password for the registry",
},
"config_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"registry_auth.username", "registry_auth.password"},
DefaultFunc: schema.EnvDefaultFunc("DOCKER_CONFIG", "~/.docker/config.json"),
Description: "Path to docker json file for registry auth",
},
},
},
},
},
ResourcesMap: map[string]*schema.Resource{
@ -60,7 +102,7 @@ func Provider() terraform.ResourceProvider {
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
config := DockerConfig{
Host: d.Get("host").(string),
Ca: d.Get("ca_material").(string),
Cert: d.Get("cert_material").(string),
@ -78,5 +120,77 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
return nil, fmt.Errorf("Error pinging Docker server: %s", err)
}
return client, nil
authConfigs := &dc.AuthConfigurations{}
if v, ok := d.GetOk("registry_auth"); ok {
authConfigs, err = providerSetToRegistryAuth(v.(*schema.Set))
if err != nil {
return nil, fmt.Errorf("Error loading registry auth config: %s", err)
}
}
providerConfig := ProviderConfig{
DockerClient: client,
AuthConfigs: authConfigs,
}
return &providerConfig, nil
}
// Take the given registry_auth schemas and return a map of registry auth configurations
func providerSetToRegistryAuth(authSet *schema.Set) (*dc.AuthConfigurations, error) {
authConfigs := dc.AuthConfigurations{
Configs: make(map[string]dc.AuthConfiguration),
}
for _, authInt := range authSet.List() {
auth := authInt.(map[string]interface{})
authConfig := dc.AuthConfiguration{}
authConfig.ServerAddress = normalizeRegistryAddress(auth["address"].(string))
// For each registry_auth block, generate an AuthConfiguration using either
// username/password or the given config file
if username, ok := auth["username"]; ok && username.(string) != "" {
authConfig.Username = auth["username"].(string)
authConfig.Password = auth["password"].(string)
} else if configFile, ok := auth["config_file"]; ok && configFile.(string) != "" {
filePath := configFile.(string)
if strings.HasPrefix(filePath, "~/") {
usr, err := user.Current()
if err != nil {
return nil, err
}
filePath = strings.Replace(filePath, "~", usr.HomeDir, 1)
}
r, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("Error opening docker registry config file: %v", err)
}
auths, err := dc.NewAuthConfigurations(r)
if err != nil {
return nil, fmt.Errorf("Error parsing docker registry config json: %v", err)
}
foundRegistry := false
for registry, authFileConfig := range auths.Configs {
if authConfig.ServerAddress == normalizeRegistryAddress(registry) {
authConfig.Username = authFileConfig.Username
authConfig.Password = authFileConfig.Password
foundRegistry = true
}
}
if !foundRegistry {
return nil, fmt.Errorf("Couldn't find registry config for '%s' in file: %s",
authConfig.ServerAddress, filePath)
}
}
authConfigs.Configs[authConfig.ServerAddress] = authConfig
}
return &authConfigs, nil
}

View file

@ -1,6 +1,7 @@
package docker
import (
"os"
"os/exec"
"testing"
@ -33,4 +34,17 @@ func testAccPreCheck(t *testing.T) {
if err := cmd.Run(); err != nil {
t.Fatalf("Docker must be available: %s", err)
}
if v := os.Getenv("DOCKER_REGISTRY_ADDRESS"); v == "" {
t.Fatalf("DOCKER_REGISTRY_ADDRESS must be set for acceptance tests")
}
if v := os.Getenv("DOCKER_REGISTRY_USER"); v == "" {
t.Fatalf("DOCKER_REGISTRY_USER must be set for acceptance tests")
}
if v := os.Getenv("DOCKER_REGISTRY_PASS"); v == "" {
t.Fatalf("DOCKER_REGISTRY_PASS must be set for acceptance tests")
}
if v := os.Getenv("DOCKER_PRIVATE_IMAGE"); v == "" {
t.Fatalf("DOCKER_PRIVATE_IMAGE must be set for acceptance tests")
}
}

View file

@ -18,7 +18,7 @@ var (
func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) error {
var err error
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
var data Data
if err := fetchLocalImages(&data, client); err != nil {
@ -247,7 +247,7 @@ func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) err
}
func resourceDockerContainerRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
apiContainer, err := fetchDockerContainer(d.Id(), client)
if err != nil {
@ -314,7 +314,7 @@ func resourceDockerContainerUpdate(d *schema.ResourceData, meta interface{}) err
}
func resourceDockerContainerDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
// Stop the container before removing if destroy_grace_seconds is defined
if d.Get("destroy_grace_seconds").(int) > 0 {

View file

@ -229,7 +229,7 @@ func TestAccDockerContainer_upload(t *testing.T) {
var c dc.Container
testCheck := func(*terraform.State) error {
client := testAccProvider.Meta().(*dc.Client)
client := testAccProvider.Meta().(*ProviderConfig).DockerClient
buf := new(bytes.Buffer)
opts := dc.DownloadFromContainerOptions{
@ -285,7 +285,7 @@ func testAccContainerRunning(n string, container *dc.Container) resource.TestChe
return fmt.Errorf("No ID is set")
}
client := testAccProvider.Meta().(*dc.Client)
client := testAccProvider.Meta().(*ProviderConfig).DockerClient
containers, err := client.ListContainers(dc.ListContainersOptions{})
if err != nil {
return err

View file

@ -9,8 +9,8 @@ import (
)
func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
apiImage, err := findImage(d, client)
client := meta.(*ProviderConfig).DockerClient
apiImage, err := findImage(d, client, meta.(*ProviderConfig).AuthConfigs)
if err != nil {
return fmt.Errorf("Unable to read Docker image into resource: %s", err)
}
@ -22,7 +22,7 @@ func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error {
}
func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
var data Data
if err := fetchLocalImages(&data, client); err != nil {
return fmt.Errorf("Error reading docker image list: %s", err)
@ -41,8 +41,8 @@ func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error {
// We need to re-read in case switching parameters affects
// the value of "latest" or others
client := meta.(*dc.Client)
apiImage, err := findImage(d, client)
client := meta.(*ProviderConfig).DockerClient
apiImage, err := findImage(d, client, meta.(*ProviderConfig).AuthConfigs)
if err != nil {
return fmt.Errorf("Unable to read Docker image into resource: %s", err)
}
@ -53,7 +53,7 @@ func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error {
}
func resourceDockerImageDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
err := removeImage(d, client)
if err != nil {
return fmt.Errorf("Unable to remove Docker image: %s", err)
@ -125,12 +125,24 @@ func fetchLocalImages(data *Data, client *dc.Client) error {
return nil
}
func pullImage(data *Data, client *dc.Client, image string) error {
func pullImage(data *Data, client *dc.Client, authConfig *dc.AuthConfigurations, image string) error {
// TODO: Test local registry handling. It should be working
// based on the code that was ported over
pullOpts := parseImageOptions(image)
// If a registry was specified in the image name, try to find auth for it
auth := dc.AuthConfiguration{}
if pullOpts.Registry != "" {
if authConfig, ok := authConfig.Configs[normalizeRegistryAddress(pullOpts.Registry)]; ok {
auth = authConfig
}
} else {
// Try to find an auth config for the public docker hub if a registry wasn't given
if authConfig, ok := authConfig.Configs["https://registry.hub.docker.com"]; ok {
auth = authConfig
}
}
if err := client.PullImage(pullOpts, auth); err != nil {
return fmt.Errorf("Error pulling image %s: %s\n", image, err)
@ -152,7 +164,7 @@ func parseImageOptions(image string) dc.PullImageOptions {
pullOpts.Tag = splitImageName[2]
pullOpts.Repository = pullOpts.Registry + "/" + strings.Join(splitPortRepo[1:], "/")
// It's either registry:port/username/repo, registry:port/repo,
// It's either registry:port/username/repo, registry:port/repo
// or repo:tag with default registry
case 2:
splitPortRepo := strings.Split(splitImageName[1], "/")
@ -169,15 +181,24 @@ func parseImageOptions(image string) dc.PullImageOptions {
pullOpts.Tag = "latest"
}
// Plain username/repo or repo
// Registry/username/repo or plain username/repo or repo
default:
pullOpts.Repository = image
splitRegistryRepo := strings.Split(image, "/")
switch len(splitRegistryRepo) {
// registry/username/repo
case 3:
pullOpts.Registry = splitRegistryRepo[0]
pullOpts.Repository = pullOpts.Registry + "/" + strings.Join(splitRegistryRepo[1:], "/")
// plain username/repo or repo
default:
pullOpts.Repository = image
}
}
return pullOpts
}
func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) {
func findImage(d *schema.ResourceData, client *dc.Client, authConfig *dc.AuthConfigurations) (*dc.APIImages, error) {
var data Data
if err := fetchLocalImages(&data, client); err != nil {
return nil, err
@ -188,7 +209,7 @@ func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error)
return nil, fmt.Errorf("Empty image name is not allowed")
}
if err := pullImage(&data, client, imageName); err != nil {
if err := pullImage(&data, client, authConfig, imageName); err != nil {
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
}

View file

@ -2,6 +2,7 @@ package docker
import (
"fmt"
"os"
"regexp"
"testing"
@ -54,7 +55,7 @@ func TestAccDockerImage_destroy(t *testing.T) {
continue
}
client := testAccProvider.Meta().(*dc.Client)
client := testAccProvider.Meta().(*ProviderConfig).DockerClient
_, err := client.InspectImage(rs.Primary.Attributes["latest"])
if err != nil {
return err
@ -105,13 +106,32 @@ func TestAccDockerImage_data_pull_trigger(t *testing.T) {
})
}
func TestAccDockerImage_data_private(t *testing.T) {
registry := os.Getenv("DOCKER_REGISTRY_ADDRESS")
image := os.Getenv("DOCKER_PRIVATE_IMAGE")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
PreventPostDestroyRefresh: true,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(testAccDockerImageFromDataPrivateConfig, registry, image),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr("docker_image.foo_private", "latest", contentDigestRegexp),
),
},
},
})
}
func testAccDockerImageDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "docker_image" {
continue
}
client := testAccProvider.Meta().(*dc.Client)
client := testAccProvider.Meta().(*ProviderConfig).DockerClient
_, err := client.InspectImage(rs.Primary.Attributes["latest"])
if err == nil {
return fmt.Errorf("Image still exists")
@ -160,3 +180,21 @@ resource "docker_image" "foobarbazoo" {
pull_trigger = "${data.docker_registry_image.foobarbazoo.sha256_digest}"
}
`
const testAccDockerImageFromDataPrivateConfig = `
provider "docker" {
alias = "private"
registry_auth {
address = "%s"
}
}
data "docker_registry_image" "foo_private" {
provider = "docker.private"
name = "%s"
}
resource "docker_image" "foo_private" {
provider = "docker.private"
name = "${data.docker_registry_image.foo_private.name}"
pull_triggers = ["${data.docker_registry_image.foo_private.sha256_digest}"]
}
`

View file

@ -8,7 +8,7 @@ import (
)
func resourceDockerNetworkCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
createOpts := dc.CreateNetworkOptions{
Name: d.Get("name").(string),
@ -63,7 +63,7 @@ func resourceDockerNetworkCreate(d *schema.ResourceData, meta interface{}) error
}
func resourceDockerNetworkRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
var err error
var retNetwork *dc.Network
@ -86,7 +86,7 @@ func resourceDockerNetworkRead(d *schema.ResourceData, meta interface{}) error {
}
func resourceDockerNetworkDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
if err := client.RemoveNetwork(d.Id()); err != nil {
if _, ok := err.(*dc.NoSuchNetwork); !ok {

View file

@ -37,7 +37,7 @@ func testAccNetwork(n string, network *dc.Network) resource.TestCheckFunc {
return fmt.Errorf("No ID is set")
}
client := testAccProvider.Meta().(*dc.Client)
client := testAccProvider.Meta().(*ProviderConfig).DockerClient
networks, err := client.ListNetworks()
if err != nil {
return err

View file

@ -40,7 +40,7 @@ func resourceDockerVolume() *schema.Resource {
}
func resourceDockerVolumeCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
createOpts := dc.CreateVolumeOptions{}
if v, ok := d.GetOk("name"); ok {
@ -71,7 +71,7 @@ func resourceDockerVolumeCreate(d *schema.ResourceData, meta interface{}) error
}
func resourceDockerVolumeRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
var err error
var retVolume *dc.Volume
@ -91,7 +91,7 @@ func resourceDockerVolumeRead(d *schema.ResourceData, meta interface{}) error {
}
func resourceDockerVolumeDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
client := meta.(*ProviderConfig).DockerClient
if err := client.RemoveVolume(d.Id()); err != nil && err != dc.ErrNoSuchVolume {
return fmt.Errorf("Error deleting volume %s: %s", d.Id(), err)

View file

@ -39,7 +39,7 @@ func checkDockerVolume(n string, volume *dc.Volume) resource.TestCheckFunc {
return fmt.Errorf("No ID is set")
}
client := testAccProvider.Meta().(*dc.Client)
client := testAccProvider.Meta().(*ProviderConfig).DockerClient
volumes, err := client.ListVolumes(dc.ListVolumesOptions{})
if err != nil {
return err

View file

@ -37,8 +37,50 @@ resource "docker_image" "ubuntu" {
## Registry Credentials
The initial (current) version of the Docker provider **doesn't** support registry authentication.
This limits any use cases to public images for now.
Registry credentials can be provided on a per-registry basis with the `registry_auth`
field, passing either a config file or the username/password directly.
``` hcl
provider "docker" {
host = "tcp://localhost:2376"
registry_auth {
address = "registry.hub.docker.com"
config_file = "~/.docker/config.json"
}
registry_auth {
address = "quay.io:8181"
username = "someuser"
password = "somepass"
}
}
data "docker_registry_image" "quay" {
name = "myorg/privateimage"
}
data "docker_registry_image" "quay" {
name = "quay.io:8181/myorg/privateimage"
}
```
**NOTES**
- The location of the config file is on the machine terraform runs on, nevertheless if the specified docker host is on another machine.
- When passing in a config file make sure every repo in the `auths` object has
an `auth` string. If not you'll get an `ErrCannotParseDockercfg` by the underlying `go-dockerclient`. On OSX the `auth` base64 string is stored in the `osxkeychain`, but reading from there is not yet supported. See [go-dockerclient#677](https://github.com/fsouza/go-dockerclient/issues/677) for details. In this case, either use `username` and `password` directly or add the string manually to the `config.json` by creating it via `echo -n "user:pass" | base64`.
`~/.docker/config.json`
```json
{
"auths": {
"repo.mycompany:8181": {
"auth": "dXNlcjpwYXNz="
}
},
...
}
```
## Argument Reference
@ -54,6 +96,25 @@ The following arguments are supported:
* `ca_material`, `cert_material`, `key_material`, - (Optional) Content of `ca.pem`, `cert.pem`, and `key.pem` files
for TLS authentication. Cannot be used together with `cert_path`.
* `registry_auth` - (Optional) A block specifying the credentials for a target
v2 Docker registry.
* `address` - (Required) The address of the registry.
* `username` - (Optional) The username to use for authenticating to the registry.
Cannot be used with the `config_file` option. If this is blank, the `DOCKER_REGISTRY_USER`
will also be checked.
* `password` - (Optional) The password to use for authenticating to the registry.
Cannot be used with the `config_file` option. If this is blank, the `DOCKER_REGISTRY_PASS`
will also be checked.
* `config_file` - (Optional) The path to a config file containing credentials for
authenticating to the registry. Cannot be used with the `username`/`password` options.
If this is blank, the `DOCKER_CONFIG` will also be checked.
~> **NOTE on Certificates and `docker-machine`:** As per [Docker Remote API
documentation](https://docs.docker.com/engine/reference/api/docker_remote_api/),
in any docker-machine environment, the Docker daemon uses an encrypted TCP