mirror of
https://github.com/kreuzwerker/terraform-provider-docker.git
synced 2025-12-18 23:06:10 -05:00
feat: Implement support for insecure registries (#414)
* feat: Add new tests for insecure registries. * chore: Refactor code into parseImageOptions and add tests. * feat: normalizeRegistryAddress supports http addresses. * feat: keys of authConfigs are now stored without protocol. * chore: Refactor of docker registry fallback in parseImageOptions. * refactor: Improve tests and implementation of parseImageOptions * feat: Implement support for http registries. * fix: authConfig unit tests now reflect newest structure. * fix: docker_image_registry data source can pull without authentication. * fix: Refactor setup of http headers for registry requests. * docs: Add note about http registries. * docs: Fix linting error in docs.
This commit is contained in:
parent
984edeed30
commit
cf3f8d6457
15 changed files with 243 additions and 113 deletions
|
|
@ -81,9 +81,10 @@ provider "docker" {
|
|||
|
||||
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.
|
||||
If you want to use an insecure http registry, please explicitly specify the `address` with the `http` protocol.
|
||||
|
||||
-> **Note**
|
||||
The location of the config file is on the machine terraform runs on, nevertheless if the specified docker host is on another machine.
|
||||
The config file is loaded from the machine `terraform` runs on. This also applies when the specified docker host is on another machine.
|
||||
|
||||
```terraform
|
||||
provider "docker" {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package provider
|
|||
import (
|
||||
b64 "encoding/base64"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -34,7 +35,7 @@ func normalizeECRPasswordForDockerCLIUsage(password string) string {
|
|||
if err != nil {
|
||||
log.Fatalf("Error creating registry request: %s", err)
|
||||
}
|
||||
password = string(decodedPassword)
|
||||
return string(decodedPassword)
|
||||
}
|
||||
|
||||
return password[4:]
|
||||
|
|
@ -48,3 +49,16 @@ func isECRRepositoryURL(url string) bool {
|
|||
var ecrRexp = regexp.MustCompile(`^.*?dkr\.ecr\..*?\.amazonaws\.com$`)
|
||||
return ecrRexp.MatchString(url)
|
||||
}
|
||||
|
||||
func setupHTTPHeadersForRegistryRequests(req *http.Request, fallback bool) {
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,7 +151,11 @@ type ProviderConfig struct {
|
|||
|
||||
// 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
|
||||
// To support insecure (http) registries, if the address explicitly states "http://" we do not change it.
|
||||
func normalizeRegistryAddress(address string) string {
|
||||
if strings.HasPrefix(address, "http://") {
|
||||
return address
|
||||
}
|
||||
if !strings.HasPrefix(address, "https://") && !strings.HasPrefix(address, "http://") {
|
||||
return "https://" + address
|
||||
}
|
||||
|
|
|
|||
32
internal/provider/config_test.go
Normal file
32
internal/provider/config_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeRegistryAddress(t *testing.T) {
|
||||
t.Run("Should return same address if http:// is used", func(t *testing.T) {
|
||||
address := "http://registry.com"
|
||||
expected := "http://registry.com"
|
||||
actual := normalizeRegistryAddress(address)
|
||||
if actual != expected {
|
||||
t.Fatalf("Expected %s, got %s", expected, actual)
|
||||
}
|
||||
})
|
||||
t.Run("Should return https address if no protocol is specified", func(t *testing.T) {
|
||||
address := "registry.com"
|
||||
expected := "https://registry.com"
|
||||
actual := normalizeRegistryAddress(address)
|
||||
if actual != expected {
|
||||
t.Fatalf("Expected %s, got %s", expected, actual)
|
||||
}
|
||||
})
|
||||
t.Run("Should return https address if https protocol is specified", func(t *testing.T) {
|
||||
address := "https://registry.com"
|
||||
expected := "https://registry.com"
|
||||
actual := normalizeRegistryAddress(address)
|
||||
if actual != expected {
|
||||
t.Fatalf("Expected %s, got %s", expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package provider
|
|||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
b64 "encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
|
@ -47,39 +46,21 @@ func dataSourceDockerRegistryImage() *schema.Resource {
|
|||
|
||||
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-1.docker.io"
|
||||
} else {
|
||||
// Otherwise, filter the registry name out of the repo name
|
||||
pullOpts.Repository = strings.Replace(pullOpts.Repository, pullOpts.Registry+"/", "", 1)
|
||||
}
|
||||
|
||||
if pullOpts.Registry == "registry-1.docker.io" {
|
||||
// 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
|
||||
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)
|
||||
digest, err := getImageDigest(pullOpts.Registry, pullOpts.Repository, pullOpts.Tag, username, password, insecureSkipVerify, false)
|
||||
digest, err := getImageDigest(pullOpts.Registry, authConfig.ServerAddress, pullOpts.Repository, pullOpts.Tag, authConfig.Username, authConfig.Password, insecureSkipVerify, false)
|
||||
if err != nil {
|
||||
digest, err = getImageDigest(pullOpts.Registry, pullOpts.Repository, pullOpts.Tag, username, password, insecureSkipVerify, true)
|
||||
digest, err = getImageDigest(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)
|
||||
}
|
||||
|
|
@ -91,12 +72,10 @@ func dataSourceDockerRegistryImageRead(ctx context.Context, d *schema.ResourceDa
|
|||
return nil
|
||||
}
|
||||
|
||||
func getImageDigest(registry, image, tag, username, password string, insecureSkipVerify, fallback bool) (string, error) {
|
||||
client := http.DefaultClient
|
||||
// DevSkim: ignore DS440000
|
||||
client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify}}
|
||||
func getImageDigest(registry string, registryWithProtocol string, image, tag, username, password string, insecureSkipVerify, fallback bool) (string, error) {
|
||||
client := buildHttpClientForRegistry(registryWithProtocol, insecureSkipVerify)
|
||||
|
||||
req, err := http.NewRequest("GET", "https://"+registry+"/v2/"+image+"/manifests/"+tag, nil)
|
||||
req, err := http.NewRequest("GET", registryWithProtocol+"/v2/"+image+"/manifests/"+tag, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error creating registry request: %s", err)
|
||||
}
|
||||
|
|
@ -114,16 +93,7 @@ func getImageDigest(registry, image, tag, username, password string, insecureSki
|
|||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
setupHTTPHeadersForRegistryRequests(req, fallback)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,27 @@ func TestAccDockerRegistryImage_auth(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAccDockerRegistryImage_httpAuth(t *testing.T) {
|
||||
registry := "http://127.0.0.1:15001"
|
||||
image := "127.0.0.1:15001/tftest-service:v1"
|
||||
ctx := context.Background()
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
ProviderFactories: providerFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: fmt.Sprintf(loadTestConfiguration(t, DATA_SOURCE, "docker_registry_image", "testAccDockerImageDataSourceAuthConfig"), registry, image),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestMatchResourceAttr("data.docker_registry_image.foobar", "sha256_digest", registryDigestRegexp),
|
||||
),
|
||||
},
|
||||
},
|
||||
CheckDestroy: func(state *terraform.State) error {
|
||||
return checkAndRemoveImages(ctx, state)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDigestFromResponse(t *testing.T) {
|
||||
headerContent := "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
|
||||
respWithHeaders := &http.Response{
|
||||
|
|
|
|||
|
|
@ -181,7 +181,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
|
||||
if v, ok := d.GetOk("registry_auth"); ok {
|
||||
authConfigs, err = providerSetToRegistryAuth(v.(*schema.Set))
|
||||
if err != nil {
|
||||
return nil, diag.Errorf("Error loading registry auth config: %s", err)
|
||||
|
|
@ -274,7 +274,7 @@ func providerSetToRegistryAuth(authList *schema.Set) (*AuthConfigs, error) {
|
|||
authConfig.Password = authFileConfig.Password
|
||||
}
|
||||
|
||||
authConfigs.Configs[authConfig.ServerAddress] = authConfig
|
||||
authConfigs.Configs[registryHostname] = authConfig
|
||||
}
|
||||
|
||||
return &authConfigs, nil
|
||||
|
|
|
|||
|
|
@ -197,17 +197,9 @@ func fetchLocalImages(ctx context.Context, data *Data, client *client.Client) er
|
|||
func pullImage(ctx context.Context, data *Data, client *client.Client, authConfig *AuthConfigs, image string) error {
|
||||
pullOpts := parseImageOptions(image)
|
||||
|
||||
// If a registry was specified in the image name, try to find auth for it
|
||||
auth := types.AuthConfig{}
|
||||
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-1.docker.io"]; ok {
|
||||
auth = authConfig
|
||||
}
|
||||
if authConfig, ok := authConfig.Configs[pullOpts.Registry]; ok {
|
||||
auth = authConfig
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(auth)
|
||||
|
|
@ -243,6 +235,9 @@ type internalPullImageOptions struct {
|
|||
Registry string
|
||||
}
|
||||
|
||||
// Parses an image name into a PullImageOptions struct.
|
||||
// If the name has no registry, the registry-1.docker.io is used
|
||||
// If the name has no tag, the tag "latest" is used
|
||||
func parseImageOptions(image string) internalPullImageOptions {
|
||||
pullOpts := internalPullImageOptions{}
|
||||
|
||||
|
|
@ -271,6 +266,18 @@ func parseImageOptions(image string) internalPullImageOptions {
|
|||
pullOpts.Tag = "latest"
|
||||
}
|
||||
|
||||
// Use the official Docker Hub if a registry isn't specified
|
||||
if pullOpts.Registry == "" {
|
||||
pullOpts.Registry = "registry-1.docker.io"
|
||||
// Docker prefixes 'library' to official images in the path; 'consul' becomes 'library/consul'
|
||||
if !strings.Contains(pullOpts.Repository, "/") {
|
||||
pullOpts.Repository = "library/" + pullOpts.Repository
|
||||
}
|
||||
} else {
|
||||
// Otherwise, filter the registry name out of the repo name
|
||||
pullOpts.Repository = strings.Replace(pullOpts.Repository, pullOpts.Registry+"/", "", 1)
|
||||
}
|
||||
|
||||
return pullOpts
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -381,3 +382,41 @@ func testAccImageCreated(resourceName string, image *types.ImageInspect) resourc
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseImageOptions(t *testing.T) {
|
||||
t.Run("Should parse image name with registry", func(t *testing.T) {
|
||||
expected := internalPullImageOptions{Registry: "registry.com", Repository: "image", Tag: "tag"}
|
||||
result := parseImageOptions("registry.com/image:tag")
|
||||
if !reflect.DeepEqual(expected, result) {
|
||||
t.Fatalf("Result %#v did not match expectation %#v", result, expected)
|
||||
}
|
||||
})
|
||||
t.Run("Should parse image name with registryPort", func(t *testing.T) {
|
||||
expected := internalPullImageOptions{Registry: "registry.com:8080", Repository: "image", Tag: "tag"}
|
||||
result := parseImageOptions("registry.com:8080/image:tag")
|
||||
if !reflect.DeepEqual(expected, result) {
|
||||
t.Fatalf("Result %#v did not match expectation %#v", result, expected)
|
||||
}
|
||||
})
|
||||
t.Run("Should parse image name with registry and proper repository", func(t *testing.T) {
|
||||
expected := internalPullImageOptions{Registry: "registry.com", Repository: "repo/image", Tag: "tag"}
|
||||
result := parseImageOptions("registry.com/repo/image:tag")
|
||||
if !reflect.DeepEqual(expected, result) {
|
||||
t.Fatalf("Result %#v did not match expectation %#v", result, expected)
|
||||
}
|
||||
})
|
||||
t.Run("Should parse image with no tag", func(t *testing.T) {
|
||||
expected := internalPullImageOptions{Registry: "registry.com", Repository: "repo/image", Tag: "latest"}
|
||||
result := parseImageOptions("registry.com/repo/image")
|
||||
if !reflect.DeepEqual(expected, result) {
|
||||
t.Fatalf("Result %#v did not match expectation %#v", result, expected)
|
||||
}
|
||||
})
|
||||
t.Run("Should parse image name without registry and default to docker registry", func(t *testing.T) {
|
||||
expected := internalPullImageOptions{Registry: "registry-1.docker.io", Repository: "library/image", Tag: "tag"}
|
||||
result := parseImageOptions("image:tag")
|
||||
if !reflect.DeepEqual(expected, result) {
|
||||
t.Fatalf("Result %#v did not match expectation %#v", result, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,13 +46,16 @@ func resourceDockerRegistryImageCreate(ctx context.Context, d *schema.ResourceDa
|
|||
}
|
||||
}
|
||||
|
||||
username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig)
|
||||
if err := pushDockerRegistryImage(ctx, client, pushOpts, username, password); err != nil {
|
||||
authConfig, err := getAuthConfigForRegistry(pushOpts.Registry, providerConfig)
|
||||
if err != nil {
|
||||
return diag.Errorf("resourceDockerRegistryImageCreate: Unable to get authConfig for registry: %s", err)
|
||||
}
|
||||
if err := pushDockerRegistryImage(ctx, client, pushOpts, authConfig.Username, authConfig.Password); err != nil {
|
||||
return diag.Errorf("Error pushing docker image: %s", err)
|
||||
}
|
||||
|
||||
insecureSkipVerify := d.Get("insecure_skip_verify").(bool)
|
||||
digest, err := getImageDigestWithFallback(pushOpts, username, password, insecureSkipVerify)
|
||||
digest, err := getImageDigestWithFallback(pushOpts, authConfig.ServerAddress, authConfig.Username, authConfig.Password, insecureSkipVerify)
|
||||
if err != nil {
|
||||
return diag.Errorf("Unable to create image, image not found: %s", err)
|
||||
}
|
||||
|
|
@ -65,10 +68,13 @@ func resourceDockerRegistryImageRead(ctx context.Context, d *schema.ResourceData
|
|||
providerConfig := meta.(*ProviderConfig)
|
||||
name := d.Get("name").(string)
|
||||
pushOpts := createPushImageOptions(name)
|
||||
username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig)
|
||||
authConfig, err := getAuthConfigForRegistry(pushOpts.Registry, providerConfig)
|
||||
if err != nil {
|
||||
return diag.Errorf("resourceDockerRegistryImageRead: Unable to get authConfig for registry: %s", err)
|
||||
}
|
||||
|
||||
insecureSkipVerify := d.Get("insecure_skip_verify").(bool)
|
||||
digest, err := getImageDigestWithFallback(pushOpts, username, password, insecureSkipVerify)
|
||||
digest, err := getImageDigestWithFallback(pushOpts, authConfig.ServerAddress, authConfig.Username, authConfig.Password, insecureSkipVerify)
|
||||
if err != nil {
|
||||
log.Printf("Got error getting registry image digest: %s", err)
|
||||
d.SetId("")
|
||||
|
|
@ -85,11 +91,15 @@ func resourceDockerRegistryImageDelete(ctx context.Context, d *schema.ResourceDa
|
|||
providerConfig := meta.(*ProviderConfig)
|
||||
name := d.Get("name").(string)
|
||||
pushOpts := createPushImageOptions(name)
|
||||
username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig)
|
||||
digest := d.Get("sha256_digest").(string)
|
||||
err := deleteDockerRegistryImage(pushOpts, digest, username, password, true, false)
|
||||
authConfig, err := getAuthConfigForRegistry(pushOpts.Registry, providerConfig)
|
||||
if err != nil {
|
||||
err = deleteDockerRegistryImage(pushOpts, pushOpts.Tag, username, password, true, true)
|
||||
return diag.Errorf("resourceDockerRegistryImageDelete: Unable to get authConfig for registry: %s", err)
|
||||
}
|
||||
|
||||
digest := d.Get("sha256_digest").(string)
|
||||
err = deleteDockerRegistryImage(pushOpts, authConfig.ServerAddress, digest, authConfig.Username, authConfig.Password, true, false)
|
||||
if err != nil {
|
||||
err = deleteDockerRegistryImage(pushOpts, authConfig.ServerAddress, pushOpts.Tag, authConfig.Username, authConfig.Password, true, true)
|
||||
if err != nil {
|
||||
return diag.Errorf("Got error deleting registry image: %s", err)
|
||||
}
|
||||
|
|
@ -446,26 +456,28 @@ func pushDockerRegistryImage(ctx context.Context, client *client.Client, pushOpt
|
|||
return nil
|
||||
}
|
||||
|
||||
func getDockerRegistryImageRegistryUserNameAndPassword(
|
||||
pushOpts internalPushImageOptions,
|
||||
providerConfig *ProviderConfig) (string, string) {
|
||||
registry := pushOpts.NormalizedRegistry
|
||||
username := ""
|
||||
password := ""
|
||||
if authConfig, ok := providerConfig.AuthConfigs.Configs[registry]; ok {
|
||||
username = authConfig.Username
|
||||
password = authConfig.Password
|
||||
func getAuthConfigForRegistry(
|
||||
registryWithoutProtocol string,
|
||||
providerConfig *ProviderConfig) (types.AuthConfig, error) {
|
||||
if authConfig, ok := providerConfig.AuthConfigs.Configs[registryWithoutProtocol]; ok {
|
||||
return authConfig, nil
|
||||
}
|
||||
return username, password
|
||||
return types.AuthConfig{}, fmt.Errorf("no auth config found for registry %s in auth configs: %#v", registryWithoutProtocol, providerConfig.AuthConfigs.Configs)
|
||||
}
|
||||
|
||||
func deleteDockerRegistryImage(pushOpts internalPushImageOptions, sha256Digest, username, password string, insecureSkipVerify, fallback bool) error {
|
||||
func buildHttpClientForRegistry(registryAddressWithProtocol string, insecureSkipVerify bool) *http.Client {
|
||||
client := http.DefaultClient
|
||||
|
||||
// DevSkim: ignore DS440000
|
||||
client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify}}
|
||||
if strings.HasPrefix(registryAddressWithProtocol, "https://") {
|
||||
client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify}}
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", pushOpts.NormalizedRegistry+"/v2/"+pushOpts.Repository+"/manifests/"+sha256Digest, nil)
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error deleting registry image: %s", err)
|
||||
}
|
||||
|
|
@ -474,16 +486,7 @@ func deleteDockerRegistryImage(pushOpts internalPushImageOptions, sha256Digest,
|
|||
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")
|
||||
}
|
||||
setupHTTPHeadersForRegistryRequests(req, fallback)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -553,10 +556,10 @@ func deleteDockerRegistryImage(pushOpts internalPushImageOptions, sha256Digest,
|
|||
}
|
||||
}
|
||||
|
||||
func getImageDigestWithFallback(opts internalPushImageOptions, username, password string, insecureSkipVerify bool) (string, error) {
|
||||
digest, err := getImageDigest(opts.Registry, opts.Repository, opts.Tag, username, password, insecureSkipVerify, false)
|
||||
func getImageDigestWithFallback(opts internalPushImageOptions, serverAddress string, username, password string, insecureSkipVerify bool) (string, error) {
|
||||
digest, err := getImageDigest(opts.Registry, serverAddress, opts.Repository, opts.Tag, username, password, insecureSkipVerify, false)
|
||||
if err != nil {
|
||||
digest, err = getImageDigest(opts.Registry, opts.Repository, opts.Tag, username, password, insecureSkipVerify, true)
|
||||
digest, err = getImageDigest(opts.Registry, serverAddress, opts.Repository, opts.Tag, username, password, insecureSkipVerify, true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to get digest: %s", err)
|
||||
}
|
||||
|
|
@ -566,11 +569,6 @@ func getImageDigestWithFallback(opts internalPushImageOptions, username, passwor
|
|||
|
||||
func createPushImageOptions(image string) internalPushImageOptions {
|
||||
pullOpts := parseImageOptions(image)
|
||||
if pullOpts.Registry == "" {
|
||||
pullOpts.Registry = "registry-1.docker.io"
|
||||
} else {
|
||||
pullOpts.Repository = strings.Replace(pullOpts.Repository, pullOpts.Registry+"/", "", 1)
|
||||
}
|
||||
pushOpts := internalPushImageOptions{
|
||||
Name: image,
|
||||
Registry: pullOpts.Registry,
|
||||
|
|
|
|||
|
|
@ -132,6 +132,24 @@ func TestAccDockerRegistryImageResource_build(t *testing.T) {
|
|||
CheckDestroy: testDockerRegistryImageNotInRegistry(pushOptions),
|
||||
})
|
||||
}
|
||||
func TestAccDockerRegistryImageResource_build_insecure_registry(t *testing.T) {
|
||||
pushOptions := createPushImageOptions("127.0.0.1:15001/tftest-dockerregistryimage:1.0")
|
||||
wd, _ := os.Getwd()
|
||||
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_context")), "\\", "\\\\")
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
ProviderFactories: providerFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_registry_image", "testBuildDockerRegistryImageNoKeepConfig"), "http://127.0.0.1:15001", pushOptions.Name, context),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttrSet("docker_registry_image.foo", "sha256_digest"),
|
||||
),
|
||||
},
|
||||
},
|
||||
CheckDestroy: testDockerRegistryImageNotInRegistry(pushOptions),
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccDockerRegistryImageResource_buildAndKeep(t *testing.T) {
|
||||
pushOptions := createPushImageOptions("127.0.0.1:15000/tftest-dockerregistryimage:1.0")
|
||||
|
|
@ -290,8 +308,8 @@ func TestAccDockerRegistryImageResource_pushMissingImage(t *testing.T) {
|
|||
func testDockerRegistryImageNotInRegistry(pushOpts internalPushImageOptions) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
providerConfig := testAccProvider.Meta().(*ProviderConfig)
|
||||
username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig)
|
||||
digest, _ := getImageDigestWithFallback(pushOpts, username, password, true)
|
||||
authConfig, _ := getAuthConfigForRegistry(pushOpts.Registry, providerConfig)
|
||||
digest, _ := getImageDigestWithFallback(pushOpts, normalizeRegistryAddress(pushOpts.Registry), authConfig.Username, authConfig.Password, true)
|
||||
if digest != "" {
|
||||
return fmt.Errorf("image found")
|
||||
}
|
||||
|
|
@ -301,12 +319,12 @@ func testDockerRegistryImageNotInRegistry(pushOpts internalPushImageOptions) res
|
|||
|
||||
func testDockerRegistryImageInRegistry(username, password string, pushOpts internalPushImageOptions, cleanup bool) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
digest, err := getImageDigestWithFallback(pushOpts, username, password, true)
|
||||
digest, err := getImageDigestWithFallback(pushOpts, normalizeRegistryAddress(pushOpts.Registry), username, password, true)
|
||||
if err != nil || len(digest) < 1 {
|
||||
return fmt.Errorf("image '%s' with credentials('%s' - '%s') not found: %w", pushOpts.Name, username, password, err)
|
||||
}
|
||||
if cleanup {
|
||||
err := deleteDockerRegistryImage(pushOpts, digest, username, password, true, false)
|
||||
err := deleteDockerRegistryImage(pushOpts, normalizeRegistryAddress(pushOpts.Registry), digest, username, password, true, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to remove test image '%s': %w", pushOpts.Name, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -620,7 +620,7 @@ func fromRegistryAuth(image string, authConfigs map[string]types.AuthConfig) typ
|
|||
// No auth given and image name has no slash like 'alpine:3.1'
|
||||
if lastBin != -1 {
|
||||
serverAddress := image[0:lastBin]
|
||||
if fromRegistryAuth, ok := authConfigs[normalizeRegistryAddress(serverAddress)]; ok {
|
||||
if fromRegistryAuth, ok := authConfigs[serverAddress]; ok {
|
||||
return fromRegistryAuth
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,40 +228,52 @@ func TestMigrateServiceLabelState_with_labels(t *testing.T) {
|
|||
|
||||
func TestDockerSecretFromRegistryAuth_basic(t *testing.T) {
|
||||
authConfigs := make(map[string]types.AuthConfig)
|
||||
authConfigs["https://repo.my-company.com:8787"] = types.AuthConfig{
|
||||
authConfigs["repo.my-company.com:8787"] = types.AuthConfig{
|
||||
Username: "myuser",
|
||||
Password: "mypass",
|
||||
Email: "",
|
||||
ServerAddress: "repo.my-company.com:8787",
|
||||
ServerAddress: "https://repo.my-company.com:8787",
|
||||
}
|
||||
|
||||
foundAuthConfig := fromRegistryAuth("repo.my-company.com:8787/my_image", authConfigs)
|
||||
checkAttribute(t, "Username", foundAuthConfig.Username, "myuser")
|
||||
checkAttribute(t, "Password", foundAuthConfig.Password, "mypass")
|
||||
checkAttribute(t, "Email", foundAuthConfig.Email, "")
|
||||
checkAttribute(t, "ServerAddress", foundAuthConfig.ServerAddress, "repo.my-company.com:8787")
|
||||
checkAttribute(t, "ServerAddress", foundAuthConfig.ServerAddress, "https://repo.my-company.com:8787")
|
||||
}
|
||||
|
||||
func TestDockerSecretFromRegistryAuth_multiple(t *testing.T) {
|
||||
authConfigs := make(map[string]types.AuthConfig)
|
||||
authConfigs["https://repo.my-company.com:8787"] = types.AuthConfig{
|
||||
authConfigs["repo.my-company.com:8787"] = types.AuthConfig{
|
||||
Username: "myuser",
|
||||
Password: "mypass",
|
||||
Email: "",
|
||||
ServerAddress: "repo.my-company.com:8787",
|
||||
ServerAddress: "https://repo.my-company.com:8787",
|
||||
}
|
||||
authConfigs["https://nexus.my-fancy-company.com"] = types.AuthConfig{
|
||||
authConfigs["nexus.my-fancy-company.com"] = types.AuthConfig{
|
||||
Username: "myuser33",
|
||||
Password: "mypass123",
|
||||
Email: "test@example.com",
|
||||
ServerAddress: "nexus.my-fancy-company.com",
|
||||
ServerAddress: "https://nexus.my-fancy-company.com",
|
||||
}
|
||||
authConfigs["http-nexus.my-fancy-company.com"] = types.AuthConfig{
|
||||
Username: "myuser33",
|
||||
Password: "mypass123",
|
||||
Email: "test@example.com",
|
||||
ServerAddress: "http://http-nexus.my-fancy-company.com",
|
||||
}
|
||||
|
||||
foundAuthConfig := fromRegistryAuth("nexus.my-fancy-company.com/the_image", authConfigs)
|
||||
checkAttribute(t, "Username", foundAuthConfig.Username, "myuser33")
|
||||
checkAttribute(t, "Password", foundAuthConfig.Password, "mypass123")
|
||||
checkAttribute(t, "Email", foundAuthConfig.Email, "test@example.com")
|
||||
checkAttribute(t, "ServerAddress", foundAuthConfig.ServerAddress, "nexus.my-fancy-company.com")
|
||||
checkAttribute(t, "ServerAddress", foundAuthConfig.ServerAddress, "https://nexus.my-fancy-company.com")
|
||||
|
||||
foundAuthConfig = fromRegistryAuth("http-nexus.my-fancy-company.com/the_image", authConfigs)
|
||||
checkAttribute(t, "Username", foundAuthConfig.Username, "myuser33")
|
||||
checkAttribute(t, "Password", foundAuthConfig.Password, "mypass123")
|
||||
checkAttribute(t, "Email", foundAuthConfig.Email, "test@example.com")
|
||||
checkAttribute(t, "ServerAddress", foundAuthConfig.ServerAddress, "http://http-nexus.my-fancy-company.com")
|
||||
|
||||
foundAuthConfig = fromRegistryAuth("alpine:3.1", authConfigs)
|
||||
checkAttribute(t, "Username", foundAuthConfig.Username, "")
|
||||
|
|
|
|||
|
|
@ -29,10 +29,19 @@ docker run -d -p 15000:5000 --rm --name private_registry \
|
|||
-e "REGISTRY_HTTP_TLS_KEY=/certs/registry_auth.key" \
|
||||
-e "REGISTRY_STORAGE_DELETE_ENABLED=true" \
|
||||
registry:2.7.0
|
||||
|
||||
docker run -d -p 15001:5000 --rm --name http_private_registry \
|
||||
-v "$(pwd)"/scripts/testing/auth:/auth \
|
||||
-e "REGISTRY_AUTH=htpasswd" \
|
||||
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
|
||||
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
|
||||
-e "REGISTRY_STORAGE_DELETE_ENABLED=true" \
|
||||
registry:2.7.0
|
||||
# wait a bit for travis...
|
||||
sleep 5
|
||||
# Login to private registry
|
||||
docker login -u testuser -p testpwd 127.0.0.1:15000
|
||||
docker login -u testuser -p testpwd 127.0.0.1:15001
|
||||
# Build private images
|
||||
for i in $(seq 1 3); do
|
||||
docker build -t tftest-service --build-arg MAIN_FILE_PATH=v${i}/main.go "$(pwd)"/scripts/testing -f "$(pwd)"/scripts/testing/Dockerfile
|
||||
|
|
@ -40,6 +49,10 @@ for i in $(seq 1 3); do
|
|||
docker push 127.0.0.1:15000/tftest-service:v${i}
|
||||
docker tag tftest-service 127.0.0.1:15000/tftest-service
|
||||
docker push 127.0.0.1:15000/tftest-service
|
||||
docker tag tftest-service 127.0.0.1:15001/tftest-service:v${i}
|
||||
docker push 127.0.0.1:15001/tftest-service:v${i}
|
||||
docker tag tftest-service 127.0.0.1:15001/tftest-service
|
||||
docker push 127.0.0.1:15001/tftest-service
|
||||
done
|
||||
# Remove images from host machine before starting the tests
|
||||
for i in $(docker images -aq 127.0.0.1:15000/tftest-service); do docker rmi -f "$i"; done
|
||||
|
|
|
|||
|
|
@ -36,9 +36,10 @@ The configuration would look as follows:
|
|||
|
||||
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.
|
||||
If you want to use an insecure http registry, please explicitly specify the `address` with the `http` protocol.
|
||||
|
||||
-> **Note**
|
||||
The location of the config file is on the machine terraform runs on, nevertheless if the specified docker host is on another machine.
|
||||
The config file is loaded from the machine `terraform` runs on. This also applies when the specified docker host is on another machine.
|
||||
|
||||
{{tffile "examples/provider/provider-credentials.tf"}}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue