2021-03-18 03:30:54 -04:00
package provider
2020-03-24 10:34:14 -04:00
import (
"bufio"
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
2025-10-01 11:32:01 -04:00
"github.com/docker/docker/api/types/build"
2020-03-24 10:34:14 -04:00
"github.com/docker/docker/api/types/container"
2025-04-17 13:22:08 -04:00
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
2020-03-24 10:34:14 -04:00
"github.com/docker/docker/client"
"github.com/docker/go-units"
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"
2020-03-24 10:34:14 -04:00
)
2025-04-18 11:16:05 -04:00
func buildAuthConfigFromResource ( v interface { } ) registry . AuthConfig {
auth := v . ( [ ] interface { } ) [ 0 ] . ( map [ string ] interface { } )
return registry . AuthConfig {
ServerAddress : normalizeRegistryAddress ( auth [ "address" ] . ( string ) ) ,
Username : auth [ "username" ] . ( string ) ,
Password : auth [ "password" ] . ( string ) ,
}
}
2021-04-19 09:33:13 -04:00
func resourceDockerRegistryImageCreate ( ctx context . Context , d * schema . ResourceData , meta interface { } ) diag . Diagnostics {
client := meta . ( * ProviderConfig ) . DockerClient
providerConfig := meta . ( * ProviderConfig )
name := d . Get ( "name" ) . ( string )
log . Printf ( "[DEBUG] Creating docker image %s" , name )
pushOpts := createPushImageOptions ( name )
2025-04-18 11:16:05 -04:00
var authConfig registry . AuthConfig
if v , ok := d . GetOk ( "auth_config" ) ; ok {
log . Printf ( "[INFO] Using auth config from resource: %s" , v )
authConfig = buildAuthConfigFromResource ( v )
} else {
var err error
authConfig , err = getAuthConfigForRegistry ( pushOpts . Registry , providerConfig )
2025-06-04 13:51:09 -04:00
log . Printf ( "[INFO] Using auth config from provider: %#v" , authConfig )
2025-04-18 11:16:05 -04:00
if err != nil {
return diag . Errorf ( "resourceDockerRegistryImageCreate: Unable to get authConfig for registry: %s" , err )
}
2022-07-22 05:19:15 -04:00
}
2025-04-18 11:16:05 -04:00
2022-07-22 05:19:15 -04:00
if err := pushDockerRegistryImage ( ctx , client , pushOpts , authConfig . Username , authConfig . Password ) ; err != nil {
2021-04-19 09:33:13 -04:00
return diag . Errorf ( "Error pushing docker image: %s" , err )
}
2021-05-31 03:11:49 -04:00
insecureSkipVerify := d . Get ( "insecure_skip_verify" ) . ( bool )
2022-07-22 05:19:15 -04:00
digest , err := getImageDigestWithFallback ( pushOpts , authConfig . ServerAddress , authConfig . Username , authConfig . Password , insecureSkipVerify )
2021-04-19 09:33:13 -04:00
if err != nil {
2025-04-18 11:16:05 -04:00
return diag . Errorf ( "Got error getting registry image digest inside resourceDockerRegistryImageCreate: %s" , err )
2021-04-19 09:33:13 -04:00
}
d . SetId ( digest )
d . Set ( "sha256_digest" , digest )
return nil
}
func resourceDockerRegistryImageRead ( ctx context . Context , d * schema . ResourceData , meta interface { } ) diag . Diagnostics {
providerConfig := meta . ( * ProviderConfig )
name := d . Get ( "name" ) . ( string )
pushOpts := createPushImageOptions ( name )
2025-04-18 11:16:05 -04:00
var authConfig registry . AuthConfig
if v , ok := d . GetOk ( "auth_config" ) ; ok {
authConfig = buildAuthConfigFromResource ( v )
} else {
var err error
authConfig , err = getAuthConfigForRegistry ( pushOpts . Registry , providerConfig )
if err != nil {
return diag . Errorf ( "resourceDockerRegistryImageRead: Unable to get authConfig for registry: %s" , err )
}
2022-07-22 05:19:15 -04:00
}
2021-05-31 03:11:49 -04:00
insecureSkipVerify := d . Get ( "insecure_skip_verify" ) . ( bool )
2022-07-22 05:19:15 -04:00
digest , err := getImageDigestWithFallback ( pushOpts , authConfig . ServerAddress , authConfig . Username , authConfig . Password , insecureSkipVerify )
2021-04-19 09:33:13 -04:00
if err != nil {
log . Printf ( "Got error getting registry image digest: %s" , err )
d . SetId ( "" )
return nil
}
d . Set ( "sha256_digest" , digest )
return nil
}
func resourceDockerRegistryImageDelete ( ctx context . Context , d * schema . ResourceData , meta interface { } ) diag . Diagnostics {
if d . Get ( "keep_remotely" ) . ( bool ) {
return nil
}
providerConfig := meta . ( * ProviderConfig )
name := d . Get ( "name" ) . ( string )
pushOpts := createPushImageOptions ( name )
2025-04-24 12:29:42 -04:00
var authConfig registry . AuthConfig
if v , ok := d . GetOk ( "auth_config" ) ; ok {
authConfig = buildAuthConfigFromResource ( v )
} else {
var err error
authConfig , err = getAuthConfigForRegistry ( pushOpts . Registry , providerConfig )
if err != nil {
return diag . Errorf ( "resourceDockerRegistryImageDelete: Unable to get authConfig for registry: %s" , err )
}
2022-07-22 05:19:15 -04:00
}
2021-04-19 09:33:13 -04:00
digest := d . Get ( "sha256_digest" ) . ( string )
2025-04-24 12:29:42 -04:00
err := deleteDockerRegistryImage ( pushOpts , authConfig . ServerAddress , digest , authConfig . Username , authConfig . Password , true , false )
2021-04-19 09:33:13 -04:00
if err != nil {
2022-07-22 05:19:15 -04:00
err = deleteDockerRegistryImage ( pushOpts , authConfig . ServerAddress , pushOpts . Tag , authConfig . Username , authConfig . Password , true , true )
2021-04-19 09:33:13 -04:00
if err != nil {
2022-06-10 14:06:31 -04:00
return diag . Errorf ( "Got error deleting registry image: %s" , err )
2021-04-19 09:33:13 -04:00
}
}
return nil
}
func resourceDockerRegistryImageUpdate ( ctx context . Context , d * schema . ResourceData , meta interface { } ) diag . Diagnostics {
return resourceDockerRegistryImageRead ( ctx , d , meta )
}
// Helpers
2020-03-24 10:34:14 -04:00
type internalPushImageOptions struct {
Name string
FqName string
Registry string
NormalizedRegistry string
Repository string
Tag string
}
2025-10-01 11:32:01 -04:00
func createImageBuildOptions ( buildOptions map [ string ] interface { } ) build . ImageBuildOptions {
2020-03-24 10:34:14 -04:00
mapOfInterfacesToMapOfStrings := func ( mapOfInterfaces map [ string ] interface { } ) map [ string ] string {
mapOfStrings := make ( map [ string ] string , len ( mapOfInterfaces ) )
for k , v := range mapOfInterfaces {
mapOfStrings [ k ] = fmt . Sprintf ( "%v" , v )
}
return mapOfStrings
}
mapToBuildArgs := func ( buildArgsOptions map [ string ] interface { } ) map [ string ] * string {
buildArgs := make ( map [ string ] * string , len ( buildArgsOptions ) )
for k , v := range buildArgsOptions {
value := v . ( string )
buildArgs [ k ] = & value
}
return buildArgs
}
readULimits := func ( options [ ] interface { } ) [ ] * units . Ulimit {
ulimits := make ( [ ] * units . Ulimit , len ( options ) )
for i , v := range options {
ulimitOption := v . ( map [ string ] interface { } )
ulimit := units . Ulimit {
Name : ulimitOption [ "name" ] . ( string ) ,
Hard : int64 ( ulimitOption [ "hard" ] . ( int ) ) ,
Soft : int64 ( ulimitOption [ "soft" ] . ( int ) ) ,
}
ulimits [ i ] = & ulimit
}
return ulimits
}
2025-04-17 13:22:08 -04:00
readAuthConfigs := func ( options [ ] interface { } ) map [ string ] registry . AuthConfig {
authConfigs := make ( map [ string ] registry . AuthConfig , len ( options ) )
2020-03-24 10:34:14 -04:00
for _ , v := range options {
authOptions := v . ( map [ string ] interface { } )
2025-04-17 13:22:08 -04:00
auth := registry . AuthConfig {
2020-03-24 10:34:14 -04:00
Username : authOptions [ "user_name" ] . ( string ) ,
Password : authOptions [ "password" ] . ( string ) ,
Auth : authOptions [ "auth" ] . ( string ) ,
Email : authOptions [ "email" ] . ( string ) ,
ServerAddress : authOptions [ "server_address" ] . ( string ) ,
IdentityToken : authOptions [ "identity_token" ] . ( string ) ,
RegistryToken : authOptions [ "registry_token" ] . ( string ) ,
}
authConfigs [ authOptions [ "host_name" ] . ( string ) ] = auth
}
return authConfigs
}
2025-10-01 11:32:01 -04:00
buildImageOptions := build . ImageBuildOptions { }
2020-03-24 10:34:14 -04:00
buildImageOptions . SuppressOutput = buildOptions [ "suppress_output" ] . ( bool )
buildImageOptions . RemoteContext = buildOptions [ "remote_context" ] . ( string )
buildImageOptions . NoCache = buildOptions [ "no_cache" ] . ( bool )
buildImageOptions . Remove = buildOptions [ "remove" ] . ( bool )
buildImageOptions . ForceRemove = buildOptions [ "force_remove" ] . ( bool )
buildImageOptions . PullParent = buildOptions [ "pull_parent" ] . ( bool )
buildImageOptions . Isolation = container . Isolation ( buildOptions [ "isolation" ] . ( string ) )
buildImageOptions . CPUSetCPUs = buildOptions [ "cpu_set_cpus" ] . ( string )
buildImageOptions . CPUSetMems = buildOptions [ "cpu_set_mems" ] . ( string )
buildImageOptions . CPUShares = int64 ( buildOptions [ "cpu_shares" ] . ( int ) )
buildImageOptions . CPUQuota = int64 ( buildOptions [ "cpu_quota" ] . ( int ) )
buildImageOptions . CPUPeriod = int64 ( buildOptions [ "cpu_period" ] . ( int ) )
buildImageOptions . Memory = int64 ( buildOptions [ "memory" ] . ( int ) )
buildImageOptions . MemorySwap = int64 ( buildOptions [ "memory_swap" ] . ( int ) )
buildImageOptions . CgroupParent = buildOptions [ "cgroup_parent" ] . ( string )
buildImageOptions . NetworkMode = buildOptions [ "network_mode" ] . ( string )
buildImageOptions . ShmSize = int64 ( buildOptions [ "shm_size" ] . ( int ) )
buildImageOptions . Dockerfile = buildOptions [ "dockerfile" ] . ( string )
buildImageOptions . Ulimits = readULimits ( buildOptions [ "ulimit" ] . ( [ ] interface { } ) )
buildImageOptions . BuildArgs = mapToBuildArgs ( buildOptions [ "build_args" ] . ( map [ string ] interface { } ) )
buildImageOptions . AuthConfigs = readAuthConfigs ( buildOptions [ "auth_config" ] . ( [ ] interface { } ) )
buildImageOptions . Labels = mapOfInterfacesToMapOfStrings ( buildOptions [ "labels" ] . ( map [ string ] interface { } ) )
buildImageOptions . Squash = buildOptions [ "squash" ] . ( bool )
buildImageOptions . CacheFrom = interfaceArrayToStringArray ( buildOptions [ "cache_from" ] . ( [ ] interface { } ) )
buildImageOptions . SecurityOpt = interfaceArrayToStringArray ( buildOptions [ "security_opt" ] . ( [ ] interface { } ) )
buildImageOptions . ExtraHosts = interfaceArrayToStringArray ( buildOptions [ "extra_hosts" ] . ( [ ] interface { } ) )
buildImageOptions . Target = buildOptions [ "target" ] . ( string )
buildImageOptions . SessionID = buildOptions [ "session_id" ] . ( string )
buildImageOptions . Platform = buildOptions [ "platform" ] . ( string )
2025-10-01 11:32:01 -04:00
buildImageOptions . Version = build . BuilderVersion ( buildOptions [ "version" ] . ( string ) )
2020-03-24 10:34:14 -04:00
buildImageOptions . BuildID = buildOptions [ "build_id" ] . ( string )
// outputs
return buildImageOptions
}
2021-03-18 03:30:54 -04:00
func pushDockerRegistryImage ( ctx context . Context , client * client . Client , pushOpts internalPushImageOptions , username string , password string ) error {
2025-04-17 13:22:08 -04:00
pushOptions := image . PushOptions { }
2020-03-24 10:34:14 -04:00
if username != "" {
2025-04-17 13:22:08 -04:00
auth := registry . AuthConfig { Username : username , Password : password }
2020-03-24 10:34:14 -04:00
authBytes , err := json . Marshal ( auth )
if err != nil {
return fmt . Errorf ( "Error creating push options: %s" , err )
}
authBase64 := base64 . URLEncoding . EncodeToString ( authBytes )
pushOptions . RegistryAuth = authBase64
}
2025-06-04 13:51:09 -04:00
log . Printf ( "[DEBUG] Pushing image %s with options %#v" , pushOpts . FqName , pushOptions )
2021-03-18 03:30:54 -04:00
out , err := client . ImagePush ( ctx , pushOpts . FqName , pushOptions )
2020-03-24 10:34:14 -04:00
if err != nil {
return err
}
2025-04-15 12:54:25 -04:00
defer out . Close ( ) //nolint:errcheck
2020-03-24 10:34:14 -04:00
type ErrorMessage struct {
Error string
}
var errorMessage ErrorMessage
buffIOReader := bufio . NewReader ( out )
for {
streamBytes , err := buffIOReader . ReadBytes ( '\n' )
if err == io . EOF {
break
}
2020-12-20 05:04:51 -05:00
if err := json . Unmarshal ( streamBytes , & errorMessage ) ; err != nil {
return err
}
2020-03-24 10:34:14 -04:00
if errorMessage . Error != "" {
2025-06-04 13:51:09 -04:00
additionalMessage := createAdditionalErrorMessage ( pushOpts . FqName )
return fmt . Errorf ( "Error pushing image. %s. Full error: %s" , additionalMessage , errorMessage . Error )
2020-03-24 10:34:14 -04:00
}
}
log . Printf ( "[DEBUG] Pushed image: %s" , pushOpts . FqName )
return nil
}
2025-06-04 13:51:09 -04:00
func createAdditionalErrorMessage ( imageFqName string ) string {
message := ""
if strings . HasPrefix ( imageFqName , "public.ecr.aws/" ) {
message = "You are trying to push to a public ECR repository. One error cause might be that the image name does not have the correct format and registry alias: public.ecr.aws/<registry_alias>/<image>"
}
return message
}
2022-07-22 05:19:15 -04:00
func getAuthConfigForRegistry (
registryWithoutProtocol string ,
2025-04-17 13:22:08 -04:00
providerConfig * ProviderConfig ) ( registry . AuthConfig , error ) {
2022-07-22 05:19:15 -04:00
if authConfig , ok := providerConfig . AuthConfigs . Configs [ registryWithoutProtocol ] ; ok {
return authConfig , nil
}
2025-04-17 13:22:08 -04:00
return registry . AuthConfig { } , fmt . Errorf ( "no auth config found for registry %s in auth configs: %#v" , registryWithoutProtocol , providerConfig . AuthConfigs . Configs )
2020-03-24 10:34:14 -04:00
}
2022-07-22 05:19:15 -04:00
func buildHttpClientForRegistry ( registryAddressWithProtocol string , insecureSkipVerify bool ) * http . Client {
2020-03-24 10:34:14 -04:00
client := http . DefaultClient
2022-07-22 05:19:15 -04:00
if strings . HasPrefix ( registryAddressWithProtocol , "https://" ) {
2023-03-17 11:15:41 -04:00
client . Transport = & http . Transport { TLSClientConfig : & tls . Config { InsecureSkipVerify : insecureSkipVerify } , Proxy : http . ProxyFromEnvironment }
} else {
client . Transport = & http . Transport { Proxy : http . ProxyFromEnvironment }
2022-07-22 05:19:15 -04:00
}
2023-03-17 11:15:41 -04:00
2022-07-22 05:19:15 -04:00
return client
}
func deleteDockerRegistryImage ( pushOpts internalPushImageOptions , registryWithProtocol string , sha256Digest , username , password string , insecureSkipVerify , fallback bool ) error {
client := buildHttpClientForRegistry ( registryWithProtocol , insecureSkipVerify )
2020-03-24 10:34:14 -04:00
2025-04-29 00:19:37 -04:00
req , err := setupHTTPRequestForRegistry ( "DELETE" , pushOpts . Registry , registryWithProtocol , pushOpts . Repository , sha256Digest , username , password , fallback )
2020-03-24 10:34:14 -04:00
if err != nil {
2025-04-29 00:19:37 -04:00
return err
2020-03-24 10:34:14 -04:00
}
resp , err := client . Do ( req )
if err != nil {
return fmt . Errorf ( "Error during registry request: %s" , err )
}
switch resp . StatusCode {
// Basic auth was valid or not needed
case http . StatusOK , http . StatusAccepted , http . StatusNotFound :
return nil
// Either OAuth is required or the basic auth creds were invalid
case http . StatusUnauthorized :
2025-04-29 00:19:37 -04:00
auth , err := parseAuthHeader ( resp . Header . Get ( "www-authenticate" ) )
if err != nil {
return fmt . Errorf ( "bad credentials: %s" , resp . Status )
2020-03-24 10:34:14 -04:00
}
2025-04-29 10:21:08 -04:00
token , err := getAuthToken ( auth , username , password , "repository:" + pushOpts . Repository + ":*" , client )
2022-09-09 07:59:18 -04:00
if err != nil {
return err
}
2020-03-24 10:34:14 -04:00
2022-09-09 07:59:18 -04:00
req . Header . Set ( "Authorization" , "Bearer " + token )
oauthResp , err := client . Do ( req )
if err != nil {
return err
}
switch oauthResp . StatusCode {
case http . StatusOK , http . StatusAccepted , http . StatusNotFound :
return nil
default :
2025-10-01 03:03:26 -04:00
return fmt . Errorf ( "got bad response from registry: %s" , resp . Status )
2022-09-09 07:59:18 -04:00
}
2020-03-24 10:34:14 -04:00
// Some unexpected status was given, return an error
default :
2025-10-01 03:03:26 -04:00
return fmt . Errorf ( "got bad response from registry: %s" , resp . Status )
2020-03-24 10:34:14 -04:00
}
}
2022-07-22 05:19:15 -04:00
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 )
2020-03-24 10:34:14 -04:00
if err != nil {
2022-07-22 05:19:15 -04:00
digest , err = getImageDigest ( opts . Registry , serverAddress , opts . Repository , opts . Tag , username , password , insecureSkipVerify , true )
2020-03-24 10:34:14 -04:00
if err != nil {
2021-05-21 08:30:56 -04:00
return "" , fmt . Errorf ( "unable to get digest: %s" , err )
2020-03-24 10:34:14 -04:00
}
}
return digest , nil
}
func createPushImageOptions ( image string ) internalPushImageOptions {
pullOpts := parseImageOptions ( image )
pushOpts := internalPushImageOptions {
Name : image ,
Registry : pullOpts . Registry ,
NormalizedRegistry : normalizeRegistryAddress ( pullOpts . Registry ) ,
Repository : pullOpts . Repository ,
Tag : pullOpts . Tag ,
FqName : fmt . Sprintf ( "%s/%s:%s" , pullOpts . Registry , pullOpts . Repository , pullOpts . Tag ) ,
}
return pushOpts
}