Fixes for image pulling and local registry (#143)

Multiple fixes for handling private registries and login credential retrieval on OSX.

- Fixes check for image locally before pulling it: #24
- Fixes image prefix workaround: #120
- Fixes passing of the `--with-registry-auth` flag: #77 
- Fixes docker registry credentials in osxkeychain: #125
This commit is contained in:
Manuel Vogel 2019-05-26 11:42:53 +02:00 committed by GitHub
parent a8f3437b29
commit a6fdf4c2a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1510 additions and 2992 deletions

View file

@ -2,7 +2,6 @@ package docker
import (
"fmt"
"os"
"regexp"
"testing"
@ -42,8 +41,8 @@ func TestAccDockerRegistryImage_private(t *testing.T) {
}
func TestAccDockerRegistryImage_auth(t *testing.T) {
registry := os.Getenv("DOCKER_REGISTRY_ADDRESS")
image := os.Getenv("DOCKER_PRIVATE_IMAGE")
registry := "127.0.0.1:15000"
image := "127.0.0.1:15000/tftest-service:v1"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
@ -55,6 +54,7 @@ func TestAccDockerRegistryImage_auth(t *testing.T) {
),
},
},
CheckDestroy: checkAndRemoveImages,
})
}

View file

@ -8,10 +8,13 @@ import (
"errors"
"fmt"
"io"
"log"
"os"
"os/user"
"runtime"
"strings"
osx "github.com/docker/docker-credential-helpers/client"
"github.com/docker/docker/api/types"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
@ -134,7 +137,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
authConfigs := &AuthConfigs{}
if v, ok := d.GetOk("registry_auth"); ok {
if v, ok := d.GetOk("registry_auth"); ok { // TODO load them anyway
authConfigs, err = providerSetToRegistryAuth(v.(*schema.Set))
if err != nil {
@ -224,14 +227,17 @@ func providerSetToRegistryAuth(authSet *schema.Set) (*AuthConfigs, error) {
}
// newAuthConfigurations returns AuthConfigs from a JSON encoded string in the
// same format as the .dockercfg file.
// same format as the .dockercfg/ ~/.docker/config.json file.
func newAuthConfigurations(r io.Reader) (*AuthConfigs, error) {
var auth *AuthConfigs
log.Println("[DEBUG] Parsing Docker config file")
confs, err := parseDockerConfig(r)
if err != nil {
return nil, err
}
auth, err = authConfigs(confs)
log.Printf("[DEBUG] Found Docker configs '%v'", confs)
auth, err = convertDockerConfigToAuthConfigs(confs)
if err != nil {
return nil, err
}
@ -260,13 +266,18 @@ func parseDockerConfig(r io.Reader) (map[string]dockerConfig, error) {
return confs, nil
}
// authConfigs converts a dockerConfigs map to a AuthConfigs object.
func authConfigs(confs map[string]dockerConfig) (*AuthConfigs, error) {
// convertDockerConfigToAuthConfigs converts a dockerConfigs map to a AuthConfigs object.
func convertDockerConfigToAuthConfigs(confs map[string]dockerConfig) (*AuthConfigs, error) {
c := &AuthConfigs{
Configs: make(map[string]types.AuthConfig),
}
for reg, conf := range confs {
for registryAddress, conf := range confs {
if conf.Auth == "" {
authFromKeyChain, err := getCredentialsFromOSKeychain(registryAddress)
if err != nil {
return nil, err
}
c.Configs[registryAddress] = authFromKeyChain
continue
}
data, err := base64.StdEncoding.DecodeString(conf.Auth)
@ -277,13 +288,31 @@ func authConfigs(confs map[string]dockerConfig) (*AuthConfigs, error) {
if len(userpass) != 2 {
return nil, ErrCannotParseDockercfg
}
c.Configs[reg] = types.AuthConfig{
c.Configs[registryAddress] = types.AuthConfig{
Email: conf.Email,
Username: userpass[0],
Password: userpass[1],
ServerAddress: reg,
ServerAddress: registryAddress,
Auth: conf.Auth,
}
}
return c, nil
}
// getCredentialsFromOSKeychain get config from system specific keychains
func getCredentialsFromOSKeychain(registryAddress string) (types.AuthConfig, error) {
authConfig := types.AuthConfig{}
log.Printf("[DEBUG] Getting auth for registry '%s' on OS: '%s'", registryAddress, runtime.GOOS)
if runtime.GOOS == "darwin" {
p := osx.NewShellProgramFunc("docker-credential-osxkeychain")
credentials, err := osx.Get(p, registryAddress)
if err != nil {
return authConfig, err
}
authConfig.Username = credentials.Username
authConfig.Password = credentials.Secret
authConfig.ServerAddress = registryAddress
authConfig.Auth = base64.StdEncoding.EncodeToString([]byte(credentials.Username + ":" + credentials.Secret))
}
return authConfig, nil
}

View file

@ -1,7 +1,6 @@
package docker
import (
"os"
"os/exec"
"testing"
@ -43,19 +42,6 @@ func testAccPreCheck(t *testing.T) {
}
}
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")
}
err := testAccProvider.Configure(terraform.NewResourceConfig(nil))
if err != nil {
t.Fatal(err)

View file

@ -84,6 +84,7 @@ func resourceDockerContainer() *schema.Resource {
Type: schema.TypeString,
Required: true,
ForceNew: true,
// DiffSuppressFunc: suppressIfSHAwasAdded(), // TODO mvogel
},
"hostname": {

View file

@ -32,18 +32,11 @@ var (
func resourceDockerContainerCreate(d *schema.ResourceData, meta interface{}) error {
var err error
client := meta.(*ProviderConfig).DockerClient
var data Data
if err := fetchLocalImages(&data, client); err != nil {
return err
}
authConfigs := meta.(*ProviderConfig).AuthConfigs
image := d.Get("image").(string)
if _, ok := data.DockerImages[image]; !ok {
if _, ok := data.DockerImages[image+":latest"]; !ok {
return fmt.Errorf("Unable to find image %s", image)
}
image = image + ":latest"
_, err = findImage(image, client, authConfigs)
if err != nil {
return fmt.Errorf("Unable to create container with image %s: %s", image, err)
}
config := &container.Config{

View file

@ -4,6 +4,7 @@ import (
"archive/tar"
"bytes"
"fmt"
"os"
"reflect"
"strconv"
"strings"
@ -29,6 +30,28 @@ func TestMapTypeMapValsToStringSlice(t *testing.T) {
}
}
func TestAccDockerContainer_private_image(t *testing.T) {
registry := "127.0.0.1:15000"
image := "127.0.0.1:15000/tftest-service:v1"
wd, _ := os.Getwd()
dockerConfig := wd + "/../scripts/testing/dockerconfig.json"
var c types.ContainerJSON
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testAccDockerContainerPrivateImage, registry, dockerConfig, image),
Check: resource.ComposeTestCheckFunc(
testAccContainerRunning("docker_container.foo", &c),
),
},
},
CheckDestroy: checkAndRemoveImages,
})
}
func TestAccDockerContainer_basic(t *testing.T) {
var c types.ContainerJSON
resource.Test(t, resource.TestCase{
@ -1125,6 +1148,22 @@ func testValueHigherEqualThan(name, key string, value int) resource.TestCheckFun
}
}
const testAccDockerContainerPrivateImage = `
provider "docker" {
alias = "private"
registry_auth {
address = "%s"
config_file = "%s"
}
}
resource "docker_container" "foo" {
provider = "docker.private"
name = "tf-test"
image = "%s"
}
`
const testAccDockerContainerConfig = `
resource "docker_image" "foo" {
name = "nginx:latest"

View file

@ -17,7 +17,8 @@ import (
func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ProviderConfig).DockerClient
apiImage, err := findImage(d, client, meta.(*ProviderConfig).AuthConfigs)
imageName := d.Get("name").(string)
apiImage, err := findImage(imageName, client, meta.(*ProviderConfig).AuthConfigs)
if err != nil {
return fmt.Errorf("Unable to read Docker image into resource: %s", err)
}
@ -49,7 +50,8 @@ 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.(*ProviderConfig).DockerClient
apiImage, err := findImage(d, client, meta.(*ProviderConfig).AuthConfigs)
imageName := d.Get("name").(string)
apiImage, err := findImage(imageName, client, meta.(*ProviderConfig).AuthConfigs)
if err != nil {
return fmt.Errorf("Unable to read Docker image into resource: %s", err)
}
@ -170,7 +172,7 @@ func pullImage(data *Data, client *client.Client, authConfig *AuthConfigs, image
s := buf.String()
log.Printf("[DEBUG] pulled image %v: %v", image, s)
return fetchLocalImages(data, client)
return nil
}
type internalPullImageOptions struct {
@ -210,19 +212,15 @@ func parseImageOptions(image string) internalPullImageOptions {
return pullOpts
}
func findImage(d *schema.ResourceData, client *client.Client, authConfig *AuthConfigs) (*types.ImageSummary, error) {
var data Data
//if err := fetchLocalImages(&data, client); err != nil {
// return nil, err
//} Is done in pullImage
imageName := d.Get("name").(string)
func findImage(imageName string, client *client.Client, authConfig *AuthConfigs) (*types.ImageSummary, error) {
if imageName == "" {
return nil, fmt.Errorf("Empty image name is not allowed")
}
if err := pullImage(&data, client, authConfig, imageName); err != nil {
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
var data Data
// load local images into the data structure
if err := fetchLocalImages(&data, client); err != nil {
return nil, err
}
foundImage := searchLocalImages(data, imageName)
@ -230,5 +228,19 @@ func findImage(d *schema.ResourceData, client *client.Client, authConfig *AuthCo
return foundImage, nil
}
if err := pullImage(&data, client, authConfig, imageName); err != nil {
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
}
// update the data structure of the images
if err := fetchLocalImages(&data, client); err != nil {
return nil, err
}
foundImage = searchLocalImages(data, imageName)
if foundImage != nil {
return foundImage, nil
}
return nil, fmt.Errorf("Unable to find or pull image %s", imageName)
}

View file

@ -107,8 +107,8 @@ 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")
registry := "127.0.0.1:15000"
image := "127.0.0.1:15000/tftest-service:v1"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@ -122,6 +122,29 @@ func TestAccDockerImage_data_private(t *testing.T) {
),
},
},
CheckDestroy: checkAndRemoveImages,
})
}
func TestAccDockerImage_data_private_config_file(t *testing.T) {
registry := "127.0.0.1:15000"
image := "127.0.0.1:15000/tftest-service:v1"
wd, _ := os.Getwd()
dockerConfig := wd + "/../scripts/testing/dockerconfig.json"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
PreventPostDestroyRefresh: true,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testAccDockerImageFromDataPrivateConfigFile, registry, dockerConfig, image),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr("docker_image.foo_private", "latest", contentDigestRegexp),
),
},
},
CheckDestroy: checkAndRemoveImages,
})
}
@ -214,6 +237,20 @@ resource "docker_image" "foo_private" {
}
`
const testAccDockerImageFromDataPrivateConfigFile = `
provider "docker" {
alias = "private"
registry_auth {
address = "%s"
config_file = "%s"
}
}
resource "docker_image" "foo_private" {
provider = "docker.private"
name = "%s"
}
`
const testAddDockerImageWithSHA256RepoDigest = `
resource "docker_image" "foobar" {
name = "stocard/gotthard@sha256:ed752380c07940c651b46c97ca2101034b3be112f4d86198900aa6141f37fe7b"

View file

@ -1,6 +1,10 @@
package docker
import (
"fmt"
"log"
"strings"
"github.com/hashicorp/terraform/helper/schema"
)
@ -70,9 +74,10 @@ func resourceDockerService() *schema.Resource {
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"image": {
Type: schema.TypeString,
Description: "The image name to use for the containers of the service",
Required: true,
Type: schema.TypeString,
Description: "The image name to use for the containers of the service",
Required: true,
DiffSuppressFunc: suppressIfSHAwasAdded(),
},
"labels": {
Type: schema.TypeMap,
@ -899,3 +904,96 @@ func resourceDockerService() *schema.Resource {
},
}
}
func suppressIfSHAwasAdded() schema.SchemaDiffSuppressFunc {
return func(k, old, new string, d *schema.ResourceData) bool {
// the initial case when the service is created
if old == "" && new != "" {
return false
}
oldURL, oldImage, oldTag, oldDigest, oldErr := splitImageName(old)
if oldErr != nil {
log.Printf("[DEBUG] invalid old image name: %s\n", oldErr.Error())
return false
}
log.Printf("[DEBUG] old image parse: %s, %s, %s, %s\n", oldURL, oldImage, oldTag, oldDigest)
newURL, newImage, newTag, newDigest, newErr := splitImageName(new)
if newErr != nil {
log.Printf("[DEBUG] invalid new image name: %s\n", newErr.Error())
return false
}
log.Printf("[DEBUG] new image parse: %s, %s, %s, %s\n", newURL, newImage, newTag, newDigest)
if oldURL != newURL || oldImage != newImage {
return false
}
// special case with latest
if oldTag == "latest" && (newTag == "" || newTag == "latest") {
if oldDigest != "" && newDigest == "" {
return true
}
return false
}
// https://success.docker.com/article/images-tagging-vs-digests
// we always pull if the tag changes, also in the empty and 'latest' case
if (oldTag == "latest" || newTag == "") || (oldTag == "" && newTag == "latest") {
return false
}
if oldTag != newTag {
return false
}
// tags are the same and so should be its digests
if oldDigest == newDigest || (oldDigest == "" && newDigest != "") || (oldDigest != "" && newDigest == "") {
return true
}
// we only update if the digests are given and different
if oldDigest != newDigest {
return false
}
return true
}
}
// spitImageName splits an image with name 127.0.0.1:15000/tftest-service:v1@sha256:24..
// into its parts. Handles edge cases like no tag and no digest
func splitImageName(imageNameToSplit string) (url, image, tag, digest string, err error) {
urlToRestSplit := strings.Split(imageNameToSplit, "/")
if len(urlToRestSplit) != 2 {
return "", "", "", "", fmt.Errorf("image name is not valid: %s", imageNameToSplit)
}
url = urlToRestSplit[0]
imageNameToRestSplit := strings.Split(urlToRestSplit[1], ":")
// we only have an image name without tag and sha256
if len(imageNameToRestSplit) == 1 {
image = imageNameToRestSplit[0]
return url, image, "", "", nil
}
// has tag and sha256
if len(imageNameToRestSplit) == 3 {
image = imageNameToRestSplit[0]
tag = strings.Replace(imageNameToRestSplit[1], "@sha256", "", 1)
digest = imageNameToRestSplit[2]
return url, image, tag, digest, nil
}
// can be either with tag or sha256, which implies 'latest' tag
if len(imageNameToRestSplit) == 2 {
image = imageNameToRestSplit[0]
if strings.Contains(imageNameToRestSplit[1], "sha256") {
digest = imageNameToRestSplit[1]
return url, image, "", digest, nil
}
tag = strings.Replace(imageNameToRestSplit[1], "@sha256", "", 1)
return url, image, tag, "", nil
}
return "", "", "", "", fmt.Errorf("image name is not valid: %s", imageNameToSplit)
}

View file

@ -62,13 +62,15 @@ func resourceDockerServiceCreate(d *schema.ResourceData, meta interface{}) error
if v, ok := d.GetOk("auth"); ok {
auth = authToServiceAuth(v.(map[string]interface{}))
} else {
auth = fromRegistryAuth(d.Get("task_spec.0.container_spec.0.image").(string), meta.(*ProviderConfig).AuthConfigs.Configs)
authConfigs := meta.(*ProviderConfig).AuthConfigs.Configs
log.Printf("[DEBUG] Getting configs from '%v'", authConfigs)
auth = fromRegistryAuth(d.Get("task_spec.0.container_spec.0.image").(string), authConfigs)
}
encodedJSON, err := json.Marshal(auth)
if err != nil {
return fmt.Errorf("error creating auth config: %s", err)
}
serviceOptions.EncodedRegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
marshalledAuth, _ := json.Marshal(auth) // https://docs.docker.com/engine/api/v1.37/#section/Versioning
serviceOptions.EncodedRegistryAuth = base64.URLEncoding.EncodeToString(marshalledAuth)
serviceOptions.QueryRegistry = true
log.Printf("[DEBUG] Passing registry auth '%s'", serviceOptions.EncodedRegistryAuth)
service, err := client.ServiceCreate(context.Background(), serviceSpec, serviceOptions)
if err != nil {
@ -1284,15 +1286,15 @@ func authToServiceAuth(auth map[string]interface{}) types.AuthConfig {
}
// fromRegistryAuth extract the desired AuthConfiguration for the given image
func fromRegistryAuth(image string, configs map[string]types.AuthConfig) types.AuthConfig {
// Remove normalized prefixes to simlify substring
func fromRegistryAuth(image string, authConfigs map[string]types.AuthConfig) types.AuthConfig {
// Remove normalized prefixes to simplify substring
image = strings.Replace(strings.Replace(image, "http://", "", 1), "https://", "", 1)
// Get the registry with optional port
lastBin := strings.Index(image, "/")
// No auth given and image name has no slash like 'alpine:3.1'
if lastBin != -1 {
serverAddress := image[0:lastBin]
if fromRegistryAuth, ok := configs[normalizeRegistryAddress(serverAddress)]; ok {
if fromRegistryAuth, ok := authConfigs[normalizeRegistryAddress(serverAddress)]; ok {
return fromRegistryAuth
}
}

File diff suppressed because it is too large Load diff

1
go.mod
View file

@ -5,6 +5,7 @@ require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/docker/distribution v0.0.0-20180522175653-f0cc92778478 // indirect
github.com/docker/docker v0.0.0-20180530012807-65bd038fc5e4
github.com/docker/docker-credential-helpers v0.6.2
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.0.0-20171221200356-d59758554a3d
github.com/gogo/protobuf v0.0.0-20180121160031-26de2f9a7d3b // indirect

2
go.sum
View file

@ -62,6 +62,8 @@ github.com/docker/distribution v0.0.0-20180522175653-f0cc92778478 h1:yCzgNaIN+6i
github.com/docker/distribution v0.0.0-20180522175653-f0cc92778478/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v0.0.0-20180530012807-65bd038fc5e4 h1:l3X/U8oT8xvht8bB/kHUntALLPIYSzmUf4Wyr6UnGWU=
github.com/docker/docker v0.0.0-20180530012807-65bd038fc5e4/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.2 h1:CrW9H1VMf3a4GrtyAi7IUJjkJVpwBBpX0+mvkvYJaus=
github.com/docker/docker-credential-helpers v0.6.2/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.0.0-20171221200356-d59758554a3d h1:QEYTOUa4JROW5CSAHU8qc0rD8uhx1tsUkBsSDk8P5eM=

View file

@ -2,32 +2,63 @@
set -e
log() {
echo ""
echo "##################################"
echo "-------> $1"
echo "##################################"
echo "####################"
echo "## -> $1 "
echo "####################"
}
setup() {
export DOCKER_REGISTRY_ADDRESS="127.0.0.1:15000"
export DOCKER_REGISTRY_USER="testuser"
export DOCKER_REGISTRY_PASS="testpwd"
export DOCKER_PRIVATE_IMAGE="127.0.0.1:15000/tftest-service:v1"
sh "$(pwd)"/scripts/testing/setup_private_registry.sh
# Create self signed certs
mkdir -p "$(pwd)"/scripts/testing/certs
openssl req \
-newkey rsa:2048 \
-nodes \
-x509 \
-days 365 \
-subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=127.0.0.1" \
-keyout "$(pwd)"/scripts/testing/certs/registry_auth.key \
-out "$(pwd)"/scripts/testing/certs/registry_auth.crt
# Create auth
mkdir -p "$(pwd)"/scripts/testing/auth
# Start registry
docker run --rm --entrypoint htpasswd registry:2 -Bbn testuser testpwd > "$(pwd)"/scripts/testing/auth/htpasswd
docker run -d -p 15000:5000 --rm --name 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" \
-v "$(pwd)"/scripts/testing/certs:/certs \
-e "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry_auth.crt" \
-e "REGISTRY_HTTP_TLS_KEY=/certs/registry_auth.key" \
registry:2
# wait a bit for travis...
sleep 5
# Login to private registry
docker login -u testuser -p testpwd 127.0.0.1:15000
# Build private images
for i in $(seq 1 3); do
docker build -t tftest-service --build-arg JS_FILE_PATH=server_v${i}.js "$(pwd)"/scripts/testing -f "$(pwd)"/scripts/testing/Dockerfile
docker tag tftest-service 127.0.0.1:15000/tftest-service:v${i}
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
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
}
run() {
go clean -testcache
TF_ACC=1 go test ./docker -v -timeout 120m
# for a single test comment the previous line and uncomment the next line
#TF_LOG=INFO TF_ACC=1 go test -v github.com/terraform-providers/terraform-provider-docker/docker -run ^TestAccDockerContainer_port$ -timeout 360s
#TF_LOG=INFO TF_ACC=1 go test -v ./docker -run ^TestAccDockerImage_data_private_config_file$ -timeout 360s
# keep the return value for the scripts to fail and clean properly
return $?
}
cleanup() {
unset DOCKER_REGISTRY_ADDRESS DOCKER_REGISTRY_USER DOCKER_REGISTRY_PASS DOCKER_PRIVATE_IMAGE
echo "### unsetted env ###"
for p in $(docker container ls -f 'name=private_registry' -q); do docker stop $p; done
echo "### stopped private registry ###"

View file

@ -0,0 +1,7 @@
{
"auths": {
"127.0.0.1:15000": {
"auth": "dGVzdHVzZXI6dGVzdHB3ZA=="
}
}
}

View file

@ -1,37 +0,0 @@
#!/bin/bash
set -e
# Create private registry
## Create self signed certs
mkdir -p "$(pwd)"/scripts/testing/certs
openssl req \
-newkey rsa:2048 \
-nodes \
-x509 \
-days 365 \
-subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=127.0.0.1" \
-keyout "$(pwd)"/scripts/testing/certs/registry_auth.key \
-out "$(pwd)"/scripts/testing/certs/registry_auth.crt
## Create auth
mkdir -p "$(pwd)"/scripts/testing/auth
# Start registry
docker run --rm --entrypoint htpasswd registry:2 -Bbn testuser testpwd > "$(pwd)"/scripts/testing/auth/htpasswd
docker run -d -p 15000:5000 --rm --name 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" \
-v "$(pwd)"/scripts/testing/certs:/certs \
-e "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry_auth.crt" \
-e "REGISTRY_HTTP_TLS_KEY=/certs/registry_auth.key" \
registry:2
# wait a bit for travis...
sleep 5
# Login to private registry
docker login -u testuser -p testpwd 127.0.0.1:15000
# Build private images
for i in $(seq 1 3); do
docker build -t tftest-service --build-arg JS_FILE_PATH=server_v${i}.js "$(pwd)"/scripts/testing -f "$(pwd)"/scripts/testing/Dockerfile
docker tag tftest-service 127.0.0.1:15000/tftest-service:v${i}
docker push 127.0.0.1:15000/tftest-service:v${i}
done

View file

@ -0,0 +1,20 @@
Copyright (c) 2016 David Calavera
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,121 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/docker/docker-credential-helpers/credentials"
)
// isValidCredsMessage checks if 'msg' contains invalid credentials error message.
// It returns whether the logs are free of invalid credentials errors and the error if it isn't.
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername.
func isValidCredsMessage(msg string) error {
if credentials.IsCredentialsMissingServerURLMessage(msg) {
return credentials.NewErrCredentialsMissingServerURL()
}
if credentials.IsCredentialsMissingUsernameMessage(msg) {
return credentials.NewErrCredentialsMissingUsername()
}
return nil
}
// Store uses an external program to save credentials.
func Store(program ProgramFunc, creds *credentials.Credentials) error {
cmd := program("store")
buffer := new(bytes.Buffer)
if err := json.NewEncoder(buffer).Encode(creds); err != nil {
return err
}
cmd.Input(buffer)
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t)
}
return nil
}
// Get executes an external program to get the credentials from a native store.
func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error) {
cmd := program("get")
cmd.Input(strings.NewReader(serverURL))
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
if credentials.IsErrCredentialsNotFoundMessage(t) {
return nil, credentials.NewErrCredentialsNotFound()
}
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t)
}
resp := &credentials.Credentials{
ServerURL: serverURL,
}
if err := json.NewDecoder(bytes.NewReader(out)).Decode(resp); err != nil {
return nil, err
}
return resp, nil
}
// Erase executes a program to remove the server credentials from the native store.
func Erase(program ProgramFunc, serverURL string) error {
cmd := program("erase")
cmd.Input(strings.NewReader(serverURL))
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t)
}
return nil
}
// List executes a program to list server credentials in the native store.
func List(program ProgramFunc) (map[string]string, error) {
cmd := program("list")
cmd.Input(strings.NewReader("unused"))
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t)
}
var resp map[string]string
if err = json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil {
return nil, err
}
return resp, nil
}

View file

@ -0,0 +1,56 @@
package client
import (
"fmt"
"io"
"os"
"os/exec"
)
// Program is an interface to execute external programs.
type Program interface {
Output() ([]byte, error)
Input(in io.Reader)
}
// ProgramFunc is a type of function that initializes programs based on arguments.
type ProgramFunc func(args ...string) Program
// NewShellProgramFunc creates programs that are executed in a Shell.
func NewShellProgramFunc(name string) ProgramFunc {
return NewShellProgramFuncWithEnv(name, nil)
}
// NewShellProgramFuncWithEnv creates programs that are executed in a Shell with environment variables
func NewShellProgramFuncWithEnv(name string, env *map[string]string) ProgramFunc {
return func(args ...string) Program {
return &Shell{cmd: createProgramCmdRedirectErr(name, args, env)}
}
}
func createProgramCmdRedirectErr(commandName string, args []string, env *map[string]string) *exec.Cmd {
programCmd := exec.Command(commandName, args...)
programCmd.Env = os.Environ()
if env != nil {
for k, v := range *env {
programCmd.Env = append(programCmd.Env, fmt.Sprintf("%s=%s", k, v))
}
}
programCmd.Stderr = os.Stderr
return programCmd
}
// Shell invokes shell commands to talk with a remote credentials helper.
type Shell struct {
cmd *exec.Cmd
}
// Output returns responses from the remote credentials helper.
func (s *Shell) Output() ([]byte, error) {
return s.cmd.Output()
}
// Input sets the input to send to a remote credentials helper.
func (s *Shell) Input(in io.Reader) {
s.cmd.Stdin = in
}

View file

@ -0,0 +1,186 @@
package credentials
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"strings"
)
// Credentials holds the information shared between docker and the credentials store.
type Credentials struct {
ServerURL string
Username string
Secret string
}
// isValid checks the integrity of Credentials object such that no credentials lack
// a server URL or a username.
// It returns whether the credentials are valid and the error if it isn't.
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername
func (c *Credentials) isValid() (bool, error) {
if len(c.ServerURL) == 0 {
return false, NewErrCredentialsMissingServerURL()
}
if len(c.Username) == 0 {
return false, NewErrCredentialsMissingUsername()
}
return true, nil
}
// CredsLabel holds the way Docker credentials should be labeled as such in credentials stores that allow labelling.
// That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain,
// Windows credentials manager and Linux libsecret. Default value is "Docker Credentials"
var CredsLabel = "Docker Credentials"
// SetCredsLabel is a simple setter for CredsLabel
func SetCredsLabel(label string) {
CredsLabel = label
}
// Serve initializes the credentials helper and parses the action argument.
// This function is designed to be called from a command line interface.
// It uses os.Args[1] as the key for the action.
// It uses os.Stdin as input and os.Stdout as output.
// This function terminates the program with os.Exit(1) if there is an error.
func Serve(helper Helper) {
var err error
if len(os.Args) != 2 {
err = fmt.Errorf("Usage: %s <store|get|erase|list|version>", os.Args[0])
}
if err == nil {
err = HandleCommand(helper, os.Args[1], os.Stdin, os.Stdout)
}
if err != nil {
fmt.Fprintf(os.Stdout, "%v\n", err)
os.Exit(1)
}
}
// HandleCommand uses a helper and a key to run a credential action.
func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error {
switch key {
case "store":
return Store(helper, in)
case "get":
return Get(helper, in, out)
case "erase":
return Erase(helper, in)
case "list":
return List(helper, out)
case "version":
return PrintVersion(out)
}
return fmt.Errorf("Unknown credential action `%s`", key)
}
// Store uses a helper and an input reader to save credentials.
// The reader must contain the JSON serialization of a Credentials struct.
func Store(helper Helper, reader io.Reader) error {
scanner := bufio.NewScanner(reader)
buffer := new(bytes.Buffer)
for scanner.Scan() {
buffer.Write(scanner.Bytes())
}
if err := scanner.Err(); err != nil && err != io.EOF {
return err
}
var creds Credentials
if err := json.NewDecoder(buffer).Decode(&creds); err != nil {
return err
}
if ok, err := creds.isValid(); !ok {
return err
}
return helper.Add(&creds)
}
// Get retrieves the credentials for a given server url.
// The reader must contain the server URL to search.
// The writer is used to write the JSON serialization of the credentials.
func Get(helper Helper, reader io.Reader, writer io.Writer) error {
scanner := bufio.NewScanner(reader)
buffer := new(bytes.Buffer)
for scanner.Scan() {
buffer.Write(scanner.Bytes())
}
if err := scanner.Err(); err != nil && err != io.EOF {
return err
}
serverURL := strings.TrimSpace(buffer.String())
if len(serverURL) == 0 {
return NewErrCredentialsMissingServerURL()
}
username, secret, err := helper.Get(serverURL)
if err != nil {
return err
}
resp := Credentials{
ServerURL: serverURL,
Username: username,
Secret: secret,
}
buffer.Reset()
if err := json.NewEncoder(buffer).Encode(resp); err != nil {
return err
}
fmt.Fprint(writer, buffer.String())
return nil
}
// Erase removes credentials from the store.
// The reader must contain the server URL to remove.
func Erase(helper Helper, reader io.Reader) error {
scanner := bufio.NewScanner(reader)
buffer := new(bytes.Buffer)
for scanner.Scan() {
buffer.Write(scanner.Bytes())
}
if err := scanner.Err(); err != nil && err != io.EOF {
return err
}
serverURL := strings.TrimSpace(buffer.String())
if len(serverURL) == 0 {
return NewErrCredentialsMissingServerURL()
}
return helper.Delete(serverURL)
}
//List returns all the serverURLs of keys in
//the OS store as a list of strings
func List(helper Helper, writer io.Writer) error {
accts, err := helper.List()
if err != nil {
return err
}
return json.NewEncoder(writer).Encode(accts)
}
//PrintVersion outputs the current version.
func PrintVersion(writer io.Writer) error {
fmt.Fprintln(writer, Version)
return nil
}

View file

@ -0,0 +1,102 @@
package credentials
const (
// ErrCredentialsNotFound standardizes the not found error, so every helper returns
// the same message and docker can handle it properly.
errCredentialsNotFoundMessage = "credentials not found in native keychain"
// ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize
// invalid credentials or credentials management operations
errCredentialsMissingServerURLMessage = "no credentials server URL"
errCredentialsMissingUsernameMessage = "no credentials username"
)
// errCredentialsNotFound represents an error
// raised when credentials are not in the store.
type errCredentialsNotFound struct{}
// Error returns the standard error message
// for when the credentials are not in the store.
func (errCredentialsNotFound) Error() string {
return errCredentialsNotFoundMessage
}
// NewErrCredentialsNotFound creates a new error
// for when the credentials are not in the store.
func NewErrCredentialsNotFound() error {
return errCredentialsNotFound{}
}
// IsErrCredentialsNotFound returns true if the error
// was caused by not having a set of credentials in a store.
func IsErrCredentialsNotFound(err error) bool {
_, ok := err.(errCredentialsNotFound)
return ok
}
// IsErrCredentialsNotFoundMessage returns true if the error
// was caused by not having a set of credentials in a store.
//
// This function helps to check messages returned by an
// external program via its standard output.
func IsErrCredentialsNotFoundMessage(err string) bool {
return err == errCredentialsNotFoundMessage
}
// errCredentialsMissingServerURL represents an error raised
// when the credentials object has no server URL or when no
// server URL is provided to a credentials operation requiring
// one.
type errCredentialsMissingServerURL struct{}
func (errCredentialsMissingServerURL) Error() string {
return errCredentialsMissingServerURLMessage
}
// errCredentialsMissingUsername represents an error raised
// when the credentials object has no username or when no
// username is provided to a credentials operation requiring
// one.
type errCredentialsMissingUsername struct{}
func (errCredentialsMissingUsername) Error() string {
return errCredentialsMissingUsernameMessage
}
// NewErrCredentialsMissingServerURL creates a new error for
// errCredentialsMissingServerURL.
func NewErrCredentialsMissingServerURL() error {
return errCredentialsMissingServerURL{}
}
// NewErrCredentialsMissingUsername creates a new error for
// errCredentialsMissingUsername.
func NewErrCredentialsMissingUsername() error {
return errCredentialsMissingUsername{}
}
// IsCredentialsMissingServerURL returns true if the error
// was an errCredentialsMissingServerURL.
func IsCredentialsMissingServerURL(err error) bool {
_, ok := err.(errCredentialsMissingServerURL)
return ok
}
// IsCredentialsMissingServerURLMessage checks for an
// errCredentialsMissingServerURL in the error message.
func IsCredentialsMissingServerURLMessage(err string) bool {
return err == errCredentialsMissingServerURLMessage
}
// IsCredentialsMissingUsername returns true if the error
// was an errCredentialsMissingUsername.
func IsCredentialsMissingUsername(err error) bool {
_, ok := err.(errCredentialsMissingUsername)
return ok
}
// IsCredentialsMissingUsernameMessage checks for an
// errCredentialsMissingUsername in the error message.
func IsCredentialsMissingUsernameMessage(err string) bool {
return err == errCredentialsMissingUsernameMessage
}

View file

@ -0,0 +1,14 @@
package credentials
// Helper is the interface a credentials store helper must implement.
type Helper interface {
// Add appends credentials to the store.
Add(*Credentials) error
// Delete removes credentials from the store.
Delete(serverURL string) error
// Get retrieves credentials from the store.
// It returns username and secret as strings.
Get(serverURL string) (string, string, error)
// List returns the stored serverURLs and their associated usernames.
List() (map[string]string, error)
}

View file

@ -0,0 +1,4 @@
package credentials
// Version holds a string describing the current version
const Version = "0.6.2"

3
vendor/modules.txt vendored
View file

@ -69,6 +69,9 @@ github.com/docker/docker/api
github.com/docker/docker/api/types/events
github.com/docker/docker/api/types/image
github.com/docker/docker/api/types/time
# github.com/docker/docker-credential-helpers v0.6.2
github.com/docker/docker-credential-helpers/client
github.com/docker/docker-credential-helpers/credentials
# github.com/docker/go-connections v0.4.0
github.com/docker/go-connections/nat
github.com/docker/go-connections/sockets