From 44156f217dbeed0f47237cd8021e34df0aba0e4b Mon Sep 17 00:00:00 2001 From: Martin Wentzel Date: Mon, 11 Jul 2022 17:04:19 +0200 Subject: [PATCH 1/6] fix: Enable authentication to multiple registries again. --- internal/provider/provider.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 2808f9c99..bf0d91c52 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -84,7 +84,6 @@ func New(version string) func() *schema.Provider { "registry_auth": { Type: schema.TypeList, - MaxItems: 1, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ From 98e7df0ae36ff1ded4b86dea363f303a675b7bd1 Mon Sep 17 00:00:00 2001 From: Martin Wentzel Date: Mon, 11 Jul 2022 19:02:25 +0200 Subject: [PATCH 2/6] fix: Tests run when conflictsWith is disabled. --- internal/provider/provider.go | 69 +++++++++++++++++------------------ 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index bf0d91c52..c6850d521 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -83,7 +83,7 @@ func New(version string) func() *schema.Provider { }, "registry_auth": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -94,35 +94,35 @@ func New(version string) func() *schema.Provider { }, "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, + // ConflictsWith: []string{"registry_auth.config_file", "registry_auth.config_file_content"}, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_USER", ""), + Description: "Username for the registry", }, "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, + // ConflictsWith: []string{"registry_auth.config_file", "registry_auth.config_file_content"}, + DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_PASS", ""), + Description: "Password for the registry", }, "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, + // 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", }, "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, + // ConflictsWith: []string{"registry_auth.username", "registry_auth.password", "registry_auth.config_file"}, + Description: "Plain content of the docker json file for registry auth", }, }, }, @@ -184,8 +184,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) } @@ -207,30 +206,30 @@ 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) - authConfig.Username = auth["username"].(string) - authConfig.Password = auth["password"].(string) + authConfig.Username = username + authConfig.Password = auth.(map[string]interface{})["password"].(string) // 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 { @@ -244,8 +243,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 From d4df643195ff7ab145b51ccd424ef2b03f44f17c Mon Sep 17 00:00:00 2001 From: Martin Wentzel Date: Fri, 15 Jul 2022 12:38:57 +0200 Subject: [PATCH 3/6] chore: Improve docs for multiple registry auth. --- docs/index.md | 13 +++++++---- internal/provider/provider.go | 44 +++++++++++++++++------------------ templates/index.md.tmpl | 3 +++ 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/docs/index.md b/docs/index.md index 5b5c3c7a7..643821d69 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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: @@ -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 @@ -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 \ No newline at end of file +- `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. \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index ecdcd8866..4f1ee6ffb 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -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() { @@ -88,41 +89,38 @@ func New(version string) func() *schema.Provider { 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"}, + Type: schema.TypeString, + Optional: true, DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_USER", ""), - Description: "Username for the registry", + 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"}, + Type: schema.TypeString, + Optional: true, + Sensitive: true, DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_PASS", ""), - Description: "Password for the registry", + 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"}, + Type: schema.TypeString, + Optional: true, DefaultFunc: schema.EnvDefaultFunc("DOCKER_CONFIG", "~/.docker/config.json"), - Description: "Path to docker json file for registry auth", + 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.", }, }, }, @@ -258,15 +256,15 @@ func providerSetToRegistryAuth(authList *schema.Set) (*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 diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl index c2ee4f1c4..9ae66dfda 100644 --- a/templates/index.md.tmpl +++ b/templates/index.md.tmpl @@ -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: From 7579408f0b75cefcfeca3c45b195f8824a087fb2 Mon Sep 17 00:00:00 2001 From: Martin Wentzel Date: Fri, 15 Jul 2022 12:39:10 +0200 Subject: [PATCH 4/6] tests: Add multiple registry auth test. --- internal/provider/provider_test.go | 19 ++++++++++++++++++- ...stAccDockerProviderMultipleRegistryAuth.tf | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 testdata/resources/provider/testAccDockerProviderMultipleRegistryAuth.tf diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 3624a1231..c6aa89cbd 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -2,6 +2,7 @@ package provider import ( "context" + "fmt" "os/exec" "regexp" "testing" @@ -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"), + ), }, }, }) diff --git a/testdata/resources/provider/testAccDockerProviderMultipleRegistryAuth.tf b/testdata/resources/provider/testAccDockerProviderMultipleRegistryAuth.tf new file mode 100644 index 000000000..369b26e64 --- /dev/null +++ b/testdata/resources/provider/testAccDockerProviderMultipleRegistryAuth.tf @@ -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 +} \ No newline at end of file From dd7b2b55d6b410b3646955d901bfb6259b1c2fdb Mon Sep 17 00:00:00 2001 From: Martin Wentzel Date: Fri, 15 Jul 2022 12:54:41 +0200 Subject: [PATCH 5/6] fix: Correct index of auth structure. --- internal/provider/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5ad1d4579..5c3ec1188 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -219,7 +219,7 @@ func providerSetToRegistryAuth(authList *schema.Set) (*AuthConfigs, error) { // username/password or the given config file 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) } From 9c795e5cbee46dfddce95f5435fdcb7aa03cc82f Mon Sep 17 00:00:00 2001 From: Martin Wentzel Date: Fri, 15 Jul 2022 13:05:03 +0200 Subject: [PATCH 6/6] chore: Add newest docs [ci skip] --- docs/resources/container.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/resources/container.md b/docs/resources/container.md index 8f42db5bf..7b5fc1344 100644 --- a/docs/resources/container.md +++ b/docs/resources/container.md @@ -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))