2021-03-18 03:30:54 -04:00
package provider
import (
"context"
"fmt"
"io"
"log"
"os"
"os/user"
2022-08-10 09:32:34 -04:00
"runtime"
2021-03-18 03:30:54 -04:00
"strings"
"github.com/docker/cli/cli/config/configfile"
2025-04-17 13:22:08 -04:00
"github.com/docker/docker/api/types/registry"
2021-03-18 03:30:54 -04:00
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
2022-07-15 07:05:26 -04:00
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
2021-03-18 03:30:54 -04:00
)
func init ( ) {
// Set descriptions to support markdown syntax, this will be used in document generation
// and the language server.
schema . DescriptionKind = schema . StringMarkdown
// Customize the content of descriptions when output. For example you can add defaults on
// to the exported descriptions if present.
// schema.SchemaDescriptionBuilder = func(s *schema.Schema) string {
// desc := s.Description
// if s.Default != nil {
// desc += fmt.Sprintf(" Defaults to `%v`.", s.Default)
// }
// return strings.TrimSpace(desc)
// }
}
// New creates the Docker provider
func New ( version string ) func ( ) * schema . Provider {
return func ( ) * schema . Provider {
p := & schema . Provider {
Schema : map [ string ] * schema . Schema {
"host" : {
2022-08-10 09:32:34 -04:00
Type : schema . TypeString ,
Required : true ,
DefaultFunc : func ( ) ( interface { } , error ) {
if v := os . Getenv ( "DOCKER_HOST" ) ; v != "" {
return v , nil
}
if runtime . GOOS == "windows" {
return "npipe:////./pipe/docker_engine" , nil
}
return "unix:///var/run/docker.sock" , nil
} ,
2021-03-18 03:30:54 -04:00
Description : "The Docker daemon address" ,
} ,
2022-01-18 00:37:50 -05:00
"ssh_opts" : {
Type : schema . TypeList ,
Optional : true ,
Elem : & schema . Schema { Type : schema . TypeString } ,
DefaultFunc : func ( ) ( interface { } , error ) {
if v := os . Getenv ( "DOCKER_SSH_OPTS" ) ; v != "" {
return strings . Fields ( v ) , nil
}
2021-03-18 03:30:54 -04:00
2022-01-18 00:37:50 -05:00
return nil , nil
} ,
2022-01-23 06:18:07 -05:00
Description : "Additional SSH option flags to be appended when using `ssh://` protocol" ,
2022-01-18 00:37:50 -05:00
} ,
2021-03-18 03:30:54 -04:00
"ca_material" : {
Type : schema . TypeString ,
Optional : true ,
DefaultFunc : schema . EnvDefaultFunc ( "DOCKER_CA_MATERIAL" , "" ) ,
Description : "PEM-encoded content of Docker host CA certificate" ,
} ,
"cert_material" : {
Type : schema . TypeString ,
Optional : true ,
DefaultFunc : schema . EnvDefaultFunc ( "DOCKER_CERT_MATERIAL" , "" ) ,
Description : "PEM-encoded content of Docker client certificate" ,
} ,
"key_material" : {
Type : schema . TypeString ,
Optional : true ,
DefaultFunc : schema . EnvDefaultFunc ( "DOCKER_KEY_MATERIAL" , "" ) ,
Description : "PEM-encoded content of Docker client private key" ,
} ,
"cert_path" : {
Type : schema . TypeString ,
Optional : true ,
DefaultFunc : schema . EnvDefaultFunc ( "DOCKER_CERT_PATH" , "" ) ,
Description : "Path to directory with Docker TLS config" ,
} ,
"registry_auth" : {
2022-07-15 07:05:26 -04:00
Type : schema . TypeSet ,
2021-03-18 03:30:54 -04:00
Optional : true ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
"address" : {
2022-07-15 07:05:26 -04:00
Type : schema . TypeString ,
Required : true ,
ValidateFunc : validation . StringIsNotEmpty ,
Description : "Address of the registry" ,
2021-03-18 03:30:54 -04:00
} ,
"username" : {
2022-07-15 07:05:26 -04:00
Type : schema . TypeString ,
Optional : true ,
DefaultFunc : schema . EnvDefaultFunc ( "DOCKER_REGISTRY_USER" , "" ) ,
Description : "Username for the registry. Defaults to `DOCKER_REGISTRY_USER` env variable if set." ,
2021-03-18 03:30:54 -04:00
} ,
"password" : {
2022-07-15 07:05:26 -04:00
Type : schema . TypeString ,
Optional : true ,
Sensitive : true ,
DefaultFunc : schema . EnvDefaultFunc ( "DOCKER_REGISTRY_PASS" , "" ) ,
Description : "Password for the registry. Defaults to `DOCKER_REGISTRY_PASS` env variable if set." ,
2021-03-18 03:30:54 -04:00
} ,
"config_file" : {
2022-07-15 07:05:26 -04:00
Type : schema . TypeString ,
Optional : true ,
DefaultFunc : schema . EnvDefaultFunc ( "DOCKER_CONFIG" , "~/.docker/config.json" ) ,
Description : "Path to docker json file for registry auth. Defaults to `~/.docker/config.json`. If `DOCKER_CONFIG` is set, the value of `DOCKER_CONFIG` is used as the path. `config_file` has predencen over all other options." ,
2021-03-18 03:30:54 -04:00
} ,
"config_file_content" : {
2022-07-15 07:05:26 -04:00
Type : schema . TypeString ,
Optional : true ,
Description : "Plain content of the docker json file for registry auth. `config_file_content` has precedence over username/password." ,
2021-03-18 03:30:54 -04:00
} ,
2022-12-22 10:55:26 -05:00
"auth_disabled" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "Setting this to `true` will tell the provider that this registry does not need authentication. Due to the docker internals, the provider will use dummy credentials (see https://github.com/kreuzwerker/terraform-provider-docker/issues/470 for more information). Defaults to `false`." ,
} ,
2021-03-18 03:30:54 -04:00
} ,
} ,
} ,
} ,
ResourcesMap : map [ string ] * schema . Resource {
"docker_container" : resourceDockerContainer ( ) ,
"docker_image" : resourceDockerImage ( ) ,
"docker_registry_image" : resourceDockerRegistryImage ( ) ,
"docker_network" : resourceDockerNetwork ( ) ,
"docker_volume" : resourceDockerVolume ( ) ,
"docker_config" : resourceDockerConfig ( ) ,
"docker_secret" : resourceDockerSecret ( ) ,
"docker_service" : resourceDockerService ( ) ,
"docker_plugin" : resourceDockerPlugin ( ) ,
2022-07-28 08:03:16 -04:00
"docker_tag" : resourceDockerTag ( ) ,
2021-03-18 03:30:54 -04:00
} ,
DataSourcesMap : map [ string ] * schema . Resource {
"docker_registry_image" : dataSourceDockerRegistryImage ( ) ,
"docker_network" : dataSourceDockerNetwork ( ) ,
"docker_plugin" : dataSourceDockerPlugin ( ) ,
2021-06-21 03:24:02 -04:00
"docker_image" : dataSourceDockerImage ( ) ,
2022-10-17 07:50:45 -04:00
"docker_logs" : dataSourceDockerLogs ( ) ,
2021-03-18 03:30:54 -04:00
} ,
}
p . ConfigureContextFunc = configure ( version , p )
return p
}
}
func configure ( version string , p * schema . Provider ) func ( context . Context , * schema . ResourceData ) ( interface { } , diag . Diagnostics ) {
return func ( ctx context . Context , d * schema . ResourceData ) ( interface { } , diag . Diagnostics ) {
2022-01-18 00:37:50 -05:00
SSHOptsI := d . Get ( "ssh_opts" ) . ( [ ] interface { } )
SSHOpts := make ( [ ] string , len ( SSHOptsI ) )
for i , s := range SSHOptsI {
SSHOpts [ i ] = s . ( string )
}
2021-03-18 03:30:54 -04:00
config := Config {
Host : d . Get ( "host" ) . ( string ) ,
2022-01-18 00:37:50 -05:00
SSHOpts : SSHOpts ,
2021-03-18 03:30:54 -04:00
Ca : d . Get ( "ca_material" ) . ( string ) ,
Cert : d . Get ( "cert_material" ) . ( string ) ,
Key : d . Get ( "key_material" ) . ( string ) ,
CertPath : d . Get ( "cert_path" ) . ( string ) ,
}
client , err := config . NewClient ( )
if err != nil {
return nil , diag . Errorf ( "Error initializing Docker client: %s" , err )
}
_ , err = client . Ping ( ctx )
if err != nil {
return nil , diag . Errorf ( "Error pinging Docker server: %s" , err )
}
authConfigs := & AuthConfigs { }
2022-07-22 05:19:15 -04:00
if v , ok := d . GetOk ( "registry_auth" ) ; ok {
2022-07-15 07:05:26 -04:00
authConfigs , err = providerSetToRegistryAuth ( v . ( * schema . Set ) )
2021-03-18 03:30:54 -04:00
if err != nil {
return nil , diag . Errorf ( "Error loading registry auth config: %s" , err )
}
}
providerConfig := ProviderConfig {
DockerClient : client ,
AuthConfigs : authConfigs ,
}
return & providerConfig , nil
}
}
// AuthConfigs represents authentication options to use for the
// PushImage method accommodating the new X-Registry-Config header
type AuthConfigs struct {
2025-04-17 13:22:08 -04:00
Configs map [ string ] registry . AuthConfig ` json:"configs" `
2021-03-18 03:30:54 -04:00
}
// Take the given registry_auth schemas and return a map of registry auth configurations
2022-07-15 07:05:26 -04:00
func providerSetToRegistryAuth ( authList * schema . Set ) ( * AuthConfigs , error ) {
2021-03-18 03:30:54 -04:00
authConfigs := AuthConfigs {
2025-04-17 13:22:08 -04:00
Configs : make ( map [ string ] registry . AuthConfig ) ,
2021-03-18 03:30:54 -04:00
}
2022-07-15 07:05:26 -04:00
for _ , auth := range authList . List ( ) {
2025-04-17 13:22:08 -04:00
authConfig := registry . AuthConfig { }
2022-07-15 07:05:26 -04:00
address := auth . ( map [ string ] interface { } ) [ "address" ] . ( string )
authConfig . ServerAddress = normalizeRegistryAddress ( address )
2021-03-18 03:30:54 -04:00
registryHostname := convertToHostname ( authConfig . ServerAddress )
2022-12-22 10:55:26 -05:00
username , ok := auth . ( map [ string ] interface { } ) [ "username" ] . ( string )
password := auth . ( map [ string ] interface { } ) [ "password" ] . ( string )
// If auth is disabled, set the auth config to any user/password combination
// See https://github.com/kreuzwerker/terraform-provider-docker/issues/470 for more information
if auth . ( map [ string ] interface { } ) [ "auth_disabled" ] . ( bool ) {
log . Printf ( "[DEBUG] Auth disabled for registry %s" , registryHostname )
username = "username"
password = "password"
}
2021-03-18 03:30:54 -04:00
// For each registry_auth block, generate an AuthConfiguration using either
// username/password or the given config file
2022-12-22 10:55:26 -05:00
if ok && username != "" {
2021-03-18 03:30:54 -04:00
log . Println ( "[DEBUG] Using username for registry auths:" , username )
2022-12-22 10:55:26 -05:00
2022-07-15 06:25:36 -04:00
if isECRRepositoryURL ( registryHostname ) {
password = normalizeECRPasswordForDockerCLIUsage ( password )
}
2022-07-15 07:05:26 -04:00
authConfig . Username = username
2022-07-15 06:25:36 -04:00
authConfig . Password = password
2021-03-18 03:30:54 -04:00
// 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
2022-07-15 07:05:26 -04:00
} 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 )
2021-03-18 03:30:54 -04:00
c , err := loadConfigFile ( r )
if err != nil {
return nil , fmt . Errorf ( "Error parsing docker registry config json: %v" , err )
}
authFileConfig , err := c . GetAuthConfig ( registryHostname )
if err != nil {
2021-05-21 08:30:56 -04:00
return nil , fmt . Errorf ( "couldn't find registry config for '%s' in file content" , registryHostname )
2021-03-18 03:30:54 -04:00
}
authConfig . Username = authFileConfig . Username
authConfig . Password = authFileConfig . Password
// As last step we check if a config file path is given
2022-07-15 07:05:26 -04:00
} else if configFile , ok := auth . ( map [ string ] interface { } ) [ "config_file" ] . ( string ) ; ok && configFile != "" {
filePath := configFile
2021-03-18 03:30:54 -04:00
log . Println ( "[DEBUG] Parsing file for registry auths:" , filePath )
// We manually expand the path and do not use the 'pathexpand' interpolation function
// because in the default of this varable we refer to '~/.docker/config.json'
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 {
2022-07-15 07:05:26 -04:00
return nil , fmt . Errorf ( "could not open config file from filePath: %s. Error: %v" , filePath , err )
2021-03-18 03:30:54 -04:00
}
c , err := loadConfigFile ( r )
if err != nil {
2022-07-15 07:05:26 -04:00
return nil , fmt . Errorf ( "could not read and load config file: %v" , err )
2021-03-18 03:30:54 -04:00
}
authFileConfig , err := c . GetAuthConfig ( registryHostname )
if err != nil {
2022-07-15 07:05:26 -04:00
return nil , fmt . Errorf ( "could not get auth config (the credentialhelper did not work or was not found): %v" , err )
2021-03-18 03:30:54 -04:00
}
authConfig . Username = authFileConfig . Username
authConfig . Password = authFileConfig . Password
}
2022-07-22 05:19:15 -04:00
authConfigs . Configs [ registryHostname ] = authConfig
2021-03-18 03:30:54 -04:00
}
return & authConfigs , nil
}
func loadConfigFile ( configData io . Reader ) ( * configfile . ConfigFile , error ) {
configFile := configfile . New ( "" )
if err := configFile . LoadFromReader ( configData ) ; err != nil {
2025-04-17 13:22:08 -04:00
return nil , err
2021-03-18 03:30:54 -04:00
}
return configFile , nil
}
// ConvertToHostname converts a registry url which has http|https prepended
// to just an hostname.
// Copied from github.com/docker/docker/registry.ConvertToHostname to reduce dependencies.
func convertToHostname ( url string ) string {
stripped := url
2021-05-21 08:30:56 -04:00
// DevSkim: ignore DS137138
2021-03-18 03:30:54 -04:00
if strings . HasPrefix ( url , "http://" ) {
2021-05-21 08:30:56 -04:00
// DevSkim: ignore DS137138
2021-03-18 03:30:54 -04:00
stripped = strings . TrimPrefix ( url , "http://" )
} else if strings . HasPrefix ( url , "https://" ) {
stripped = strings . TrimPrefix ( url , "https://" )
}
nameParts := strings . SplitN ( stripped , "/" , 2 )
return nameParts [ 0 ]
}