2021-03-18 03:30:54 -04:00
package provider
2016-07-26 11:18:38 -04:00
import (
2021-03-18 03:30:54 -04:00
"context"
2019-10-06 05:25:02 -04:00
"crypto/sha256"
2022-06-16 13:47:17 -04:00
b64 "encoding/base64"
2016-07-26 11:18:38 -04:00
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
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"
2016-07-26 11:18:38 -04:00
)
func dataSourceDockerRegistryImage ( ) * schema . Resource {
return & schema . Resource {
2021-05-21 08:30:56 -04:00
Description : "Reads the image metadata from a Docker Registry. Used in conjunction with the [docker_image](../resources/image.md) resource to keep an image up to date on the latest available version of the tag." ,
2021-03-18 03:30:54 -04:00
ReadContext : dataSourceDockerRegistryImageRead ,
2016-07-26 11:18:38 -04:00
Schema : map [ string ] * schema . Schema {
2019-03-01 16:02:17 -05:00
"name" : {
2021-05-21 08:30:56 -04:00
Type : schema . TypeString ,
Description : "The name of the Docker image, including any tags. e.g. `alpine:latest`" ,
Required : true ,
2016-07-26 11:18:38 -04:00
} ,
2019-03-01 16:02:17 -05:00
"sha256_digest" : {
2021-05-21 08:30:56 -04:00
Type : schema . TypeString ,
Description : "The content digest of the image, as stored in the registry." ,
Computed : true ,
2016-07-26 11:18:38 -04:00
} ,
2021-05-31 03:11:49 -04:00
"insecure_skip_verify" : {
Type : schema . TypeBool ,
Description : "If `true`, the verification of TLS certificates of the server/registry is disabled. Defaults to `false`" ,
Optional : true ,
Default : false ,
} ,
2016-07-26 11:18:38 -04:00
} ,
}
}
2021-03-18 03:30:54 -04:00
func dataSourceDockerRegistryImageRead ( ctx context . Context , d * schema . ResourceData , meta interface { } ) diag . Diagnostics {
2016-07-26 11:18:38 -04:00
pullOpts := parseImageOptions ( d . Get ( "name" ) . ( string ) )
2017-11-21 04:14:07 -05:00
2022-07-22 05:19:15 -04:00
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
2017-11-21 04:14:07 -05: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 := getImageDigest ( pullOpts . Registry , authConfig . ServerAddress , pullOpts . Repository , pullOpts . Tag , authConfig . Username , authConfig . Password , insecureSkipVerify , false )
2016-07-26 11:18:38 -04:00
if err != nil {
2022-07-22 05:19:15 -04:00
digest , err = getImageDigest ( pullOpts . Registry , authConfig . ServerAddress , pullOpts . Repository , pullOpts . Tag , authConfig . Username , authConfig . Password , insecureSkipVerify , true )
2018-02-09 14:11:30 -05:00
if err != nil {
2021-06-21 03:52:34 -04:00
return diag . Errorf ( "Got error when attempting to fetch image version %s:%s from registry: %s" , pullOpts . Repository , pullOpts . Tag , err )
2018-02-09 14:11:30 -05:00
}
2016-07-26 11:18:38 -04:00
}
d . SetId ( digest )
d . Set ( "sha256_digest" , digest )
return nil
}
2022-07-22 05:19:15 -04:00
func getImageDigest ( registry string , registryWithProtocol string , image , tag , username , password string , insecureSkipVerify , fallback bool ) ( string , error ) {
client := buildHttpClientForRegistry ( registryWithProtocol , insecureSkipVerify )
2016-07-26 11:18:38 -04:00
2022-08-29 09:53:40 -04:00
req , err := http . NewRequest ( "HEAD" , registryWithProtocol + "/v2/" + image + "/manifests/" + tag , nil )
2016-07-26 11:18:38 -04:00
if err != nil {
return "" , fmt . Errorf ( "Error creating registry request: %s" , err )
}
if username != "" {
2022-07-15 06:25:36 -04:00
if registry != "ghcr.io" && ! isECRRepositoryURL ( registry ) && registry != "gcr.io" {
2022-06-16 13:47:17 -04:00
req . SetBasicAuth ( username , password )
} else {
2022-07-15 06:25:36 -04:00
if isECRRepositoryURL ( registry ) {
password = normalizeECRPasswordForHTTPUsage ( password )
req . Header . Add ( "Authorization" , "Basic " + password )
} else {
req . Header . Add ( "Authorization" , "Bearer " + b64 . StdEncoding . EncodeToString ( [ ] byte ( password ) ) )
}
2022-06-16 13:47:17 -04:00
}
2016-07-26 11:18:38 -04:00
}
2022-07-22 05:19:15 -04:00
setupHTTPHeadersForRegistryRequests ( req , fallback )
2017-11-21 04:14:07 -05:00
2016-07-26 11:18:38 -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 :
2019-10-06 05:25:02 -04:00
return getDigestFromResponse ( resp )
2016-07-26 11:18:38 -04:00
// Either OAuth is required or the basic auth creds were invalid
case http . StatusUnauthorized :
2022-08-29 09:53:40 -04:00
if ! strings . HasPrefix ( resp . Header . Get ( "www-authenticate" ) , "Bearer" ) {
return "" , fmt . Errorf ( "Bad credentials: " + resp . Status )
}
2016-07-26 11:18:38 -04:00
2022-08-29 09:53:40 -04:00
token , err := getAuthToken ( resp . Header . Get ( "www-authenticate" ) , username , password , client )
if err != nil {
return "" , err
}
2016-07-26 11:18:38 -04:00
2022-08-29 09:53:40 -04:00
req . Header . Set ( "Authorization" , "Bearer " + token )
2016-07-26 11:18:38 -04:00
2022-08-29 09:53:40 -04:00
// Do a HEAD request to docker registry first (avoiding Docker Hub rate limiting)
digestResponse , err := doDigestRequest ( req , client )
if err != nil {
return "" , err
}
2016-07-26 11:18:38 -04:00
2022-08-29 09:53:40 -04:00
digest , err := getDigestFromResponse ( digestResponse )
if err == nil {
return digest , nil
}
2016-07-26 11:18:38 -04:00
2022-08-29 09:53:40 -04:00
// If previous HEAD request does not contain required info, do a GET request
req . Method = "GET"
digestResponse , err = doDigestRequest ( req , client )
2016-07-26 11:18:38 -04:00
2022-08-29 09:53:40 -04:00
if err != nil {
return "" , err
2016-07-26 11:18:38 -04:00
}
2022-08-29 09:53:40 -04:00
return getDigestFromResponse ( digestResponse )
2018-07-03 11:30:53 -04:00
2022-08-29 09:53:40 -04:00
// Some unexpected status was given, return an error
2016-07-26 11:18:38 -04:00
default :
return "" , fmt . Errorf ( "Got bad response from registry: " + resp . Status )
}
}
type TokenResponse struct {
Token string
}
// Parses key/value pairs from a WWW-Authenticate header
func parseAuthHeader ( header string ) map [ string ] string {
parts := strings . SplitN ( header , " " , 2 )
parts = strings . Split ( parts [ 1 ] , "," )
opts := make ( map [ string ] string )
for _ , part := range parts {
vals := strings . SplitN ( part , "=" , 2 )
key := vals [ 0 ]
val := strings . Trim ( vals [ 1 ] , "\", " )
opts [ key ] = val
}
return opts
}
2019-10-06 05:25:02 -04:00
func getDigestFromResponse ( response * http . Response ) ( string , error ) {
header := response . Header . Get ( "Docker-Content-Digest" )
if header == "" {
body , err := ioutil . ReadAll ( response . Body )
2022-08-29 09:53:40 -04:00
if err != nil || len ( body ) == 0 {
2019-10-06 05:25:02 -04:00
return "" , fmt . Errorf ( "Error reading registry response body: %s" , err )
}
return fmt . Sprintf ( "sha256:%x" , sha256 . Sum256 ( body ) ) , nil
}
return header , nil
}
2022-08-29 09:53:40 -04:00
func getAuthToken ( authHeader string , username string , password string , client * http . Client ) ( string , error ) {
auth := parseAuthHeader ( authHeader )
params := url . Values { }
params . Set ( "service" , auth [ "service" ] )
params . Set ( "scope" , auth [ "scope" ] )
tokenRequest , err := http . NewRequest ( "GET" , auth [ "realm" ] + "?" + params . Encode ( ) , nil )
if err != nil {
return "" , fmt . Errorf ( "Error creating registry request: %s" , err )
}
if username != "" {
tokenRequest . SetBasicAuth ( username , password )
}
tokenResponse , err := client . Do ( tokenRequest )
if err != nil {
return "" , fmt . Errorf ( "Error during registry request: %s" , err )
}
if tokenResponse . StatusCode != http . StatusOK {
return "" , fmt . Errorf ( "Got bad response from registry: " + tokenResponse . Status )
}
body , err := ioutil . ReadAll ( tokenResponse . Body )
if err != nil {
return "" , fmt . Errorf ( "Error reading response body: %s" , err )
}
token := & TokenResponse { }
err = json . Unmarshal ( body , token )
if err != nil {
return "" , fmt . Errorf ( "Error parsing OAuth token response: %s" , err )
}
return token . Token , nil
}
func doDigestRequest ( req * http . Request , client * http . Client ) ( * http . Response , error ) {
digestResponse , err := client . Do ( req )
if err != nil {
return nil , fmt . Errorf ( "Error during registry request: %s" , err )
}
if digestResponse . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "Got bad response from registry: " + digestResponse . Status )
}
return digestResponse , nil
}