Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Enable authentication to multiple registries again. #400

Merged
merged 8 commits into from
Jul 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ When passing in a config file either the corresponding `auth` string of the repo
[credential helpers](https://github.com/docker/docker-credential-helpers#available-programs) are
used to retrieve the authentication credentials.

-> **Note**
`config_file` has predence over all other options. You can theoretically specify values for every attribute but the credentials obtained through the `config_file` will override the manually set `username`/`password`

You can still use the environment variables `DOCKER_REGISTRY_USER` and `DOCKER_REGISTRY_PASS`.

An example content of the file `~/.docker/config.json` on macOS may look like follows:
Expand Down Expand Up @@ -165,7 +168,7 @@ provider "docker" {
- `cert_path` (String) Path to directory with Docker TLS config
- `host` (String) The Docker daemon address
- `key_material` (String) PEM-encoded content of Docker client private key
- `registry_auth` (Block List, Max: 1) (see [below for nested schema](#nestedblock--registry_auth))
- `registry_auth` (Block Set) (see [below for nested schema](#nestedblock--registry_auth))
- `ssh_opts` (List of String) Additional SSH option flags to be appended when using `ssh://` protocol

<a id="nestedblock--registry_auth"></a>
Expand All @@ -177,7 +180,7 @@ Required:

Optional:

- `config_file` (String) Path to docker json file for registry auth
- `config_file_content` (String) Plain content of the docker json file for registry auth
- `password` (String, Sensitive) Password for the registry
- `username` (String) Username for the registry
- `config_file` (String) Path to docker json file for registry auth. Defaults to `~/.docker/config.json`. If `DOCKER_CONFIG` is set, the value of `DOCKER_CONFIG` is used as the path. `config_file` has predencen over all other options.
- `config_file_content` (String) Plain content of the docker json file for registry auth. `config_file_content` has precedence over username/password.
- `password` (String, Sensitive) Password for the registry. Defaults to `DOCKER_REGISTRY_PASS` env variable if set.
- `username` (String) Username for the registry. Defaults to `DOCKER_REGISTRY_USER` env variable if set.
1 change: 1 addition & 0 deletions docs/resources/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ resource "docker_image" "ubuntu" {
- `domainname` (String) Domain name of the container.
- `entrypoint` (List of String) The command to use as the Entrypoint for the container. The Entrypoint allows you to configure a container to run as an executable. For example, to run `/usr/bin/myprogram` when starting a container, set the entrypoint to be `"/usr/bin/myprogra"]`.
- `env` (Set of String) Environment variables to set in the form of `KEY=VALUE`, e.g. `DEBUG=0`
- `gpus` (String) GPU devices to add to the container. Currently, only the value `all` is supported. Passing any other value will result in unexpected behavior.
- `group_add` (Set of String) Additional groups for the container user
- `healthcheck` (Block List, Max: 1) A test to perform to check that the container is healthy (see [below for nested schema](#nestedblock--healthcheck))
- `host` (Block Set) Additional hosts to add to the container. (see [below for nested schema](#nestedblock--host))
Expand Down
80 changes: 38 additions & 42 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

func init() {
Expand Down Expand Up @@ -83,47 +84,43 @@ func New(version string) func() *schema.Provider {
},

"registry_auth": {
Type: schema.TypeList,
MaxItems: 1,
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"address": {
Type: schema.TypeString,
Required: true,
Description: "Address of the registry",
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
Description: "Address of the registry",
},

"username": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"registry_auth.config_file", "registry_auth.config_file_content"},
DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_USER", ""),
Description: "Username for the registry",
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_USER", ""),
Description: "Username for the registry. Defaults to `DOCKER_REGISTRY_USER` env variable if set.",
},

"password": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
ConflictsWith: []string{"registry_auth.config_file", "registry_auth.config_file_content"},
DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_PASS", ""),
Description: "Password for the registry",
Type: schema.TypeString,
Optional: true,
Sensitive: true,
DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_PASS", ""),
Description: "Password for the registry. Defaults to `DOCKER_REGISTRY_PASS` env variable if set.",
},

"config_file": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"registry_auth.username", "registry_auth.password", "registry_auth.config_file_content"},
DefaultFunc: schema.EnvDefaultFunc("DOCKER_CONFIG", "~/.docker/config.json"),
Description: "Path to docker json file for registry auth",
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("DOCKER_CONFIG", "~/.docker/config.json"),
Description: "Path to docker json file for registry auth. Defaults to `~/.docker/config.json`. If `DOCKER_CONFIG` is set, the value of `DOCKER_CONFIG` is used as the path. `config_file` has predencen over all other options.",
},

"config_file_content": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"registry_auth.username", "registry_auth.password", "registry_auth.config_file"},
Description: "Plain content of the docker json file for registry auth",
Type: schema.TypeString,
Optional: true,
Description: "Plain content of the docker json file for registry auth. `config_file_content` has precedence over username/password.",
},
},
},
Expand Down Expand Up @@ -185,8 +182,7 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema
authConfigs := &AuthConfigs{}

if v, ok := d.GetOk("registry_auth"); ok { // TODO load them anyway
authConfigs, err = providerSetToRegistryAuth(v.([]interface{}))

authConfigs, err = providerSetToRegistryAuth(v.(*schema.Set))
if err != nil {
return nil, diag.Errorf("Error loading registry auth config: %s", err)
}
Expand All @@ -208,34 +204,34 @@ type AuthConfigs struct {
}

// Take the given registry_auth schemas and return a map of registry auth configurations
func providerSetToRegistryAuth(authList []interface{}) (*AuthConfigs, error) {
func providerSetToRegistryAuth(authList *schema.Set) (*AuthConfigs, error) {
authConfigs := AuthConfigs{
Configs: make(map[string]types.AuthConfig),
}

for _, authInt := range authList {
auth := authInt.(map[string]interface{})
for _, auth := range authList.List() {
authConfig := types.AuthConfig{}
authConfig.ServerAddress = normalizeRegistryAddress(auth["address"].(string))
address := auth.(map[string]interface{})["address"].(string)
authConfig.ServerAddress = normalizeRegistryAddress(address)
registryHostname := convertToHostname(authConfig.ServerAddress)

// 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) != "" {
if username, ok := auth.(map[string]interface{})["username"].(string); ok && username != "" {
log.Println("[DEBUG] Using username for registry auths:", username)
password := auth["password"].(string)
password := auth.(map[string]interface{})["password"].(string)
if isECRRepositoryURL(registryHostname) {
password = normalizeECRPasswordForDockerCLIUsage(password)
}
authConfig.Username = auth["username"].(string)
authConfig.Username = username
authConfig.Password = password

// Note: check for config_file_content first because config_file has a default which would be used
// nevertheless config_file_content is set or not. The default has to be kept to check for the
// environment variable and to be backwards compatible
} else if configFileContent, ok := auth["config_file_content"]; ok && configFileContent.(string) != "" {
log.Println("[DEBUG] Parsing file content for registry auths:", configFileContent.(string))
r := strings.NewReader(configFileContent.(string))
} else if configFileContent, ok := auth.(map[string]interface{})["config_file_content"].(string); ok && configFileContent != "" {
log.Println("[DEBUG] Parsing file content for registry auths:", configFileContent)
r := strings.NewReader(configFileContent)

c, err := loadConfigFile(r)
if err != nil {
Expand All @@ -249,8 +245,8 @@ func providerSetToRegistryAuth(authList []interface{}) (*AuthConfigs, error) {
authConfig.Password = authFileConfig.Password

// As last step we check if a config file path is given
} else if configFile, ok := auth["config_file"]; ok && configFile.(string) != "" {
filePath := configFile.(string)
} else if configFile, ok := auth.(map[string]interface{})["config_file"].(string); ok && configFile != "" {
filePath := configFile
log.Println("[DEBUG] Parsing file for registry auths:", filePath)

// We manually expand the path and do not use the 'pathexpand' interpolation function
Expand All @@ -264,15 +260,15 @@ func providerSetToRegistryAuth(authList []interface{}) (*AuthConfigs, error) {
}
r, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("Could not open config file from filePath: %s. Error: %v", filePath, err)
return nil, fmt.Errorf("could not open config file from filePath: %s. Error: %v", filePath, err)
}
c, err := loadConfigFile(r)
if err != nil {
return nil, fmt.Errorf("Could not read and load config file: %v", err)
return nil, fmt.Errorf("could not read and load config file: %v", err)
}
authFileConfig, err := c.GetAuthConfig(registryHostname)
if err != nil {
return nil, fmt.Errorf("Could not get auth config (the credentialhelper did not work or was not found): %v", err)
return nil, fmt.Errorf("could not get auth config (the credentialhelper did not work or was not found): %v", err)
}
authConfig.Username = authFileConfig.Username
authConfig.Password = authFileConfig.Password
Expand Down
19 changes: 18 additions & 1 deletion internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package provider

import (
"context"
"fmt"
"os/exec"
"regexp"
"testing"
Expand Down Expand Up @@ -51,7 +52,23 @@ func TestAccDockerProvider_WithIncompleteRegistryAuth(t *testing.T) {
Steps: []resource.TestStep{
{
Config: testAccDockerProviderWithIncompleteAuthConfig,
ExpectError: regexp.MustCompile(`401 Unauthorized`),
ExpectError: regexp.MustCompile(`expected "registry_auth.0.address" to not be an empty string, got `),
},
},
})
}

func TestAccDockerProvider_WithMultipleRegistryAuth(t *testing.T) {
pushOptions := createPushImageOptions("127.0.0.1:15000/tftest-dockerregistryimage-testtest:1.0")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "provider", "testAccDockerProviderMultipleRegistryAuth"), pushOptions.Registry),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("data.docker_registry_image.foobar", "sha256_digest"),
),
},
},
})
Expand Down
3 changes: 3 additions & 0 deletions templates/index.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ When passing in a config file either the corresponding `auth` string of the repo
[credential helpers](https://github.com/docker/docker-credential-helpers#available-programs) are
used to retrieve the authentication credentials.

-> **Note**
`config_file` has predence over all other options. You can theoretically specify values for every attribute but the credentials obtained through the `config_file` will override the manually set `username`/`password`

You can still use the environment variables `DOCKER_REGISTRY_USER` and `DOCKER_REGISTRY_PASS`.

An example content of the file `~/.docker/config.json` on macOS may look like follows:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
provider "docker" {
alias = "private"
registry_auth {
address = "%s"
}
registry_auth {
address = "public.ecr.aws"
username = "test"
password = "user"
}
}
data "docker_registry_image" "foobar" {
provider = "docker.private"
name = "127.0.0.1:15000/tftest-service:v1"
insecure_skip_verify = true
}