2015-02-17 11:28:33 -05:00
|
|
|
package docker
|
|
|
|
|
|
|
|
|
|
import (
|
2018-07-03 11:30:53 -04:00
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
2015-03-28 22:06:48 -04:00
|
|
|
"fmt"
|
2018-07-03 11:30:53 -04:00
|
|
|
"io"
|
2019-05-26 05:42:53 -04:00
|
|
|
"log"
|
2017-11-21 04:14:07 -05:00
|
|
|
"os"
|
|
|
|
|
"os/user"
|
2019-05-26 05:42:53 -04:00
|
|
|
"runtime"
|
2017-11-21 04:14:07 -05:00
|
|
|
"strings"
|
2015-03-28 22:06:48 -04:00
|
|
|
|
2019-07-16 13:41:06 -04:00
|
|
|
credhelper "github.com/docker/docker-credential-helpers/client"
|
2018-07-03 11:30:53 -04:00
|
|
|
"github.com/docker/docker/api/types"
|
2015-02-17 11:28:33 -05:00
|
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
|
|
|
)
|
|
|
|
|
|
2018-05-16 12:00:04 -04:00
|
|
|
// Provider creates the Docker provider
|
2015-02-17 11:28:33 -05:00
|
|
|
func Provider() terraform.ResourceProvider {
|
|
|
|
|
return &schema.Provider{
|
|
|
|
|
Schema: map[string]*schema.Schema{
|
2019-03-01 16:02:17 -05:00
|
|
|
"host": {
|
2015-02-17 11:28:33 -05:00
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Required: true,
|
2016-02-10 14:21:17 -05:00
|
|
|
DefaultFunc: schema.EnvDefaultFunc("DOCKER_HOST", "unix:///var/run/docker.sock"),
|
2015-03-27 18:18:52 -04:00
|
|
|
Description: "The Docker daemon address",
|
|
|
|
|
},
|
|
|
|
|
|
2019-03-01 16:02:17 -05:00
|
|
|
"ca_material": {
|
2016-12-17 07:41:08 -05:00
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("DOCKER_CA_MATERIAL", ""),
|
|
|
|
|
Description: "PEM-encoded content of Docker host CA certificate",
|
2016-11-22 07:18:09 -05:00
|
|
|
},
|
2019-03-01 16:02:17 -05:00
|
|
|
"cert_material": {
|
2016-12-17 07:41:08 -05:00
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_MATERIAL", ""),
|
|
|
|
|
Description: "PEM-encoded content of Docker client certificate",
|
2016-11-22 07:18:09 -05:00
|
|
|
},
|
2019-03-01 16:02:17 -05:00
|
|
|
"key_material": {
|
2016-12-17 07:41:08 -05:00
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("DOCKER_KEY_MATERIAL", ""),
|
|
|
|
|
Description: "PEM-encoded content of Docker client private key",
|
2016-11-22 07:18:09 -05:00
|
|
|
},
|
|
|
|
|
|
2019-03-01 16:02:17 -05:00
|
|
|
"cert_path": {
|
2015-03-27 18:18:52 -04:00
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
2015-04-09 12:49:03 -04:00
|
|
|
DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_PATH", ""),
|
2015-03-27 18:18:52 -04:00
|
|
|
Description: "Path to directory with Docker TLS config",
|
2015-02-17 11:28:33 -05:00
|
|
|
},
|
2017-11-21 04:14:07 -05:00
|
|
|
|
2019-03-01 16:02:17 -05:00
|
|
|
"registry_auth": {
|
2017-11-21 04:14:07 -05:00
|
|
|
Type: schema.TypeSet,
|
|
|
|
|
Optional: true,
|
|
|
|
|
Elem: &schema.Resource{
|
|
|
|
|
Schema: map[string]*schema.Schema{
|
2019-03-01 16:02:17 -05:00
|
|
|
"address": {
|
2017-11-21 04:14:07 -05:00
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Required: true,
|
|
|
|
|
Description: "Address of the registry",
|
|
|
|
|
},
|
|
|
|
|
|
2019-03-01 16:02:17 -05:00
|
|
|
"username": {
|
2017-11-21 04:14:07 -05:00
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
|
|
|
|
ConflictsWith: []string{"registry_auth.config_file"},
|
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_USER", ""),
|
|
|
|
|
Description: "Username for the registry",
|
|
|
|
|
},
|
|
|
|
|
|
2019-03-01 16:02:17 -05:00
|
|
|
"password": {
|
2017-11-21 04:14:07 -05:00
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
2018-05-16 12:00:04 -04:00
|
|
|
Sensitive: true,
|
2017-11-21 04:14:07 -05:00
|
|
|
ConflictsWith: []string{"registry_auth.config_file"},
|
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_PASS", ""),
|
|
|
|
|
Description: "Password for the registry",
|
|
|
|
|
},
|
|
|
|
|
|
2019-03-01 16:02:17 -05:00
|
|
|
"config_file": {
|
2017-11-21 04:14:07 -05:00
|
|
|
Type: schema.TypeString,
|
|
|
|
|
Optional: true,
|
|
|
|
|
ConflictsWith: []string{"registry_auth.username", "registry_auth.password"},
|
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("DOCKER_CONFIG", "~/.docker/config.json"),
|
|
|
|
|
Description: "Path to docker json file for registry auth",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2015-02-17 11:28:33 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
ResourcesMap: map[string]*schema.Resource{
|
|
|
|
|
"docker_container": resourceDockerContainer(),
|
|
|
|
|
"docker_image": resourceDockerImage(),
|
2016-01-02 06:20:55 -05:00
|
|
|
"docker_network": resourceDockerNetwork(),
|
2016-01-15 12:33:17 -05:00
|
|
|
"docker_volume": resourceDockerVolume(),
|
2018-05-16 12:00:04 -04:00
|
|
|
"docker_config": resourceDockerConfig(),
|
|
|
|
|
"docker_secret": resourceDockerSecret(),
|
|
|
|
|
"docker_service": resourceDockerService(),
|
2015-02-17 11:28:33 -05:00
|
|
|
},
|
|
|
|
|
|
2016-07-26 11:18:38 -04:00
|
|
|
DataSourcesMap: map[string]*schema.Resource{
|
|
|
|
|
"docker_registry_image": dataSourceDockerRegistryImage(),
|
2019-05-26 15:20:01 -04:00
|
|
|
"docker_network": dataSourceDockerNetwork(),
|
2016-07-26 11:18:38 -04:00
|
|
|
},
|
|
|
|
|
|
2015-02-17 11:28:33 -05:00
|
|
|
ConfigureFunc: providerConfigure,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
|
2018-07-03 11:30:53 -04:00
|
|
|
config := Config{
|
2015-03-27 18:18:52 -04:00
|
|
|
Host: d.Get("host").(string),
|
2016-11-22 07:18:09 -05:00
|
|
|
Ca: d.Get("ca_material").(string),
|
|
|
|
|
Cert: d.Get("cert_material").(string),
|
|
|
|
|
Key: d.Get("key_material").(string),
|
2015-03-27 18:18:52 -04:00
|
|
|
CertPath: d.Get("cert_path").(string),
|
2015-02-17 11:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2015-03-28 22:06:48 -04:00
|
|
|
client, err := config.NewClient()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("Error initializing Docker client: %s", err)
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-03 11:30:53 -04:00
|
|
|
ctx := context.Background()
|
|
|
|
|
_, err = client.Ping(ctx)
|
2015-03-28 22:06:48 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("Error pinging Docker server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-03 11:30:53 -04:00
|
|
|
authConfigs := &AuthConfigs{}
|
2017-11-21 04:14:07 -05:00
|
|
|
|
2019-05-26 05:42:53 -04:00
|
|
|
if v, ok := d.GetOk("registry_auth"); ok { // TODO load them anyway
|
2017-11-21 04:14:07 -05:00
|
|
|
authConfigs, err = providerSetToRegistryAuth(v.(*schema.Set))
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("Error loading registry auth config: %s", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
providerConfig := ProviderConfig{
|
|
|
|
|
DockerClient: client,
|
|
|
|
|
AuthConfigs: authConfigs,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &providerConfig, nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-03 11:30:53 -04:00
|
|
|
// ErrCannotParseDockercfg is the error returned by NewAuthConfigurations when the dockercfg cannot be parsed.
|
|
|
|
|
var ErrCannotParseDockercfg = errors.New("Failed to read authentication from dockercfg")
|
|
|
|
|
|
|
|
|
|
// AuthConfigs represents authentication options to use for the
|
|
|
|
|
// PushImage method accommodating the new X-Registry-Config header
|
|
|
|
|
type AuthConfigs struct {
|
|
|
|
|
Configs map[string]types.AuthConfig `json:"configs"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dockerConfig represents a registry authentation configuration from the
|
|
|
|
|
// .dockercfg file.
|
|
|
|
|
type dockerConfig struct {
|
|
|
|
|
Auth string `json:"auth"`
|
|
|
|
|
Email string `json:"email"`
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-21 04:14:07 -05:00
|
|
|
// Take the given registry_auth schemas and return a map of registry auth configurations
|
2018-07-03 11:30:53 -04:00
|
|
|
func providerSetToRegistryAuth(authSet *schema.Set) (*AuthConfigs, error) {
|
|
|
|
|
authConfigs := AuthConfigs{
|
|
|
|
|
Configs: make(map[string]types.AuthConfig),
|
2017-11-21 04:14:07 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, authInt := range authSet.List() {
|
|
|
|
|
auth := authInt.(map[string]interface{})
|
2018-07-03 11:30:53 -04:00
|
|
|
authConfig := types.AuthConfig{}
|
2017-11-21 04:14:07 -05:00
|
|
|
authConfig.ServerAddress = normalizeRegistryAddress(auth["address"].(string))
|
|
|
|
|
|
|
|
|
|
// For each registry_auth block, generate an AuthConfiguration using either
|
|
|
|
|
// username/password or the given config file
|
|
|
|
|
if username, ok := auth["username"]; ok && username.(string) != "" {
|
|
|
|
|
authConfig.Username = auth["username"].(string)
|
|
|
|
|
authConfig.Password = auth["password"].(string)
|
|
|
|
|
} else if configFile, ok := auth["config_file"]; ok && configFile.(string) != "" {
|
|
|
|
|
filePath := configFile.(string)
|
|
|
|
|
if strings.HasPrefix(filePath, "~/") {
|
|
|
|
|
usr, err := user.Current()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
filePath = strings.Replace(filePath, "~", usr.HomeDir, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r, err := os.Open(filePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("Error opening docker registry config file: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-03 11:30:53 -04:00
|
|
|
auths, err := newAuthConfigurations(r)
|
2017-11-21 04:14:07 -05:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("Error parsing docker registry config json: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foundRegistry := false
|
|
|
|
|
for registry, authFileConfig := range auths.Configs {
|
|
|
|
|
if authConfig.ServerAddress == normalizeRegistryAddress(registry) {
|
|
|
|
|
authConfig.Username = authFileConfig.Username
|
|
|
|
|
authConfig.Password = authFileConfig.Password
|
|
|
|
|
foundRegistry = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !foundRegistry {
|
|
|
|
|
return nil, fmt.Errorf("Couldn't find registry config for '%s' in file: %s",
|
|
|
|
|
authConfig.ServerAddress, filePath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authConfigs.Configs[authConfig.ServerAddress] = authConfig
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &authConfigs, nil
|
2015-02-17 11:28:33 -05:00
|
|
|
}
|
2018-07-03 11:30:53 -04:00
|
|
|
|
|
|
|
|
// newAuthConfigurations returns AuthConfigs from a JSON encoded string in the
|
2019-05-26 05:42:53 -04:00
|
|
|
// same format as the .dockercfg/ ~/.docker/config.json file.
|
2018-07-03 11:30:53 -04:00
|
|
|
func newAuthConfigurations(r io.Reader) (*AuthConfigs, error) {
|
|
|
|
|
var auth *AuthConfigs
|
2019-05-26 05:42:53 -04:00
|
|
|
log.Println("[DEBUG] Parsing Docker config file")
|
2019-07-16 13:41:06 -04:00
|
|
|
confs, credsStore, err := parseDockerConfig(r)
|
2018-07-03 11:30:53 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2019-05-26 05:42:53 -04:00
|
|
|
|
|
|
|
|
log.Printf("[DEBUG] Found Docker configs '%v'", confs)
|
2019-07-16 13:41:06 -04:00
|
|
|
auth, err = convertDockerConfigToAuthConfigs(confs, credsStore)
|
2018-07-03 11:30:53 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return auth, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseDockerConfig parses the docker config file for auths
|
2019-07-16 13:41:06 -04:00
|
|
|
func parseDockerConfig(r io.Reader) (map[string]dockerConfig, string, error) {
|
2018-07-03 11:30:53 -04:00
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
|
buf.ReadFrom(r)
|
|
|
|
|
byteData := buf.Bytes()
|
|
|
|
|
|
|
|
|
|
confsWrapper := struct {
|
2019-07-16 13:41:06 -04:00
|
|
|
Auths map[string]dockerConfig `json:"auths"`
|
|
|
|
|
CredsStore string `json:"credsStore,omitempty"`
|
2018-07-03 11:30:53 -04:00
|
|
|
}{}
|
|
|
|
|
if err := json.Unmarshal(byteData, &confsWrapper); err == nil {
|
|
|
|
|
if len(confsWrapper.Auths) > 0 {
|
2019-07-16 13:41:06 -04:00
|
|
|
return confsWrapper.Auths, confsWrapper.CredsStore, nil
|
2018-07-03 11:30:53 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var confs map[string]dockerConfig
|
|
|
|
|
if err := json.Unmarshal(byteData, &confs); err != nil {
|
2019-07-16 13:41:06 -04:00
|
|
|
return nil, "", err
|
2018-07-03 11:30:53 -04:00
|
|
|
}
|
2019-07-16 13:41:06 -04:00
|
|
|
return confs, "", nil
|
2018-07-03 11:30:53 -04:00
|
|
|
}
|
|
|
|
|
|
2019-05-26 05:42:53 -04:00
|
|
|
// convertDockerConfigToAuthConfigs converts a dockerConfigs map to a AuthConfigs object.
|
2019-07-16 13:41:06 -04:00
|
|
|
func convertDockerConfigToAuthConfigs(confs map[string]dockerConfig, credsStore string) (*AuthConfigs, error) {
|
2018-07-03 11:30:53 -04:00
|
|
|
c := &AuthConfigs{
|
|
|
|
|
Configs: make(map[string]types.AuthConfig),
|
|
|
|
|
}
|
2019-05-26 05:42:53 -04:00
|
|
|
for registryAddress, conf := range confs {
|
2018-07-03 11:30:53 -04:00
|
|
|
if conf.Auth == "" {
|
2019-07-16 13:41:06 -04:00
|
|
|
authFromKeyChain, err := getCredentialsFromOSKeychain(registryAddress, credsStore)
|
2019-05-26 05:42:53 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
c.Configs[registryAddress] = authFromKeyChain
|
2018-07-03 11:30:53 -04:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
data, err := base64.StdEncoding.DecodeString(conf.Auth)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
userpass := strings.SplitN(string(data), ":", 2)
|
|
|
|
|
if len(userpass) != 2 {
|
|
|
|
|
return nil, ErrCannotParseDockercfg
|
|
|
|
|
}
|
2019-05-26 05:42:53 -04:00
|
|
|
c.Configs[registryAddress] = types.AuthConfig{
|
2018-07-03 11:30:53 -04:00
|
|
|
Email: conf.Email,
|
|
|
|
|
Username: userpass[0],
|
|
|
|
|
Password: userpass[1],
|
2019-05-26 05:42:53 -04:00
|
|
|
ServerAddress: registryAddress,
|
2018-07-03 11:30:53 -04:00
|
|
|
Auth: conf.Auth,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return c, nil
|
|
|
|
|
}
|
2019-05-26 05:42:53 -04:00
|
|
|
|
|
|
|
|
// getCredentialsFromOSKeychain get config from system specific keychains
|
2019-07-16 13:41:06 -04:00
|
|
|
func getCredentialsFromOSKeychain(registryAddress string, credsStore string) (types.AuthConfig, error) {
|
2019-05-26 05:42:53 -04:00
|
|
|
authConfig := types.AuthConfig{}
|
2019-07-16 13:41:06 -04:00
|
|
|
log.Printf("[DEBUG] Getting auth for registry '%s' from credential store: '%s'", registryAddress, credsStore)
|
|
|
|
|
if credsStore == "" {
|
|
|
|
|
return authConfig, errors.New("No credential store configured")
|
|
|
|
|
}
|
|
|
|
|
executable := "docker-credential-" + credsStore
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
executable = executable + ".exe"
|
|
|
|
|
}
|
|
|
|
|
p := credhelper.NewShellProgramFunc(executable)
|
|
|
|
|
credentials, err := credhelper.Get(p, registryAddress)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return authConfig, err
|
2019-05-26 05:42:53 -04:00
|
|
|
}
|
2019-07-16 13:41:06 -04:00
|
|
|
authConfig.Username = credentials.Username
|
|
|
|
|
authConfig.Password = credentials.Secret
|
|
|
|
|
authConfig.ServerAddress = registryAddress
|
|
|
|
|
authConfig.Auth = base64.StdEncoding.EncodeToString([]byte(credentials.Username + ":" + credentials.Secret))
|
2019-05-26 05:42:53 -04:00
|
|
|
return authConfig, nil
|
|
|
|
|
}
|