provider/docker: Added docker_registry_image data source (#7000)
This commit is contained in:
parent
5050d4555c
commit
09bba0424c
|
@ -0,0 +1,166 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
)
|
||||
|
||||
func dataSourceDockerRegistryImage() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Read: dataSourceDockerRegistryImageRead,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"name": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"sha256_digest": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func dataSourceDockerRegistryImageRead(d *schema.ResourceData, meta interface{}) error {
|
||||
pullOpts := parseImageOptions(d.Get("name").(string))
|
||||
|
||||
// Use the official Docker Hub if a registry isn't specified
|
||||
if pullOpts.Registry == "" {
|
||||
pullOpts.Registry = "registry.hub.docker.com"
|
||||
} else {
|
||||
// Otherwise, filter the registry name out of the repo name
|
||||
pullOpts.Repository = strings.Replace(pullOpts.Repository, pullOpts.Registry+"/", "", 1)
|
||||
}
|
||||
|
||||
// Docker prefixes 'library' to official images in the path; 'consul' becomes 'library/consul'
|
||||
if !strings.Contains(pullOpts.Repository, "/") {
|
||||
pullOpts.Repository = "library/" + pullOpts.Repository
|
||||
}
|
||||
|
||||
if pullOpts.Tag == "" {
|
||||
pullOpts.Tag = "latest"
|
||||
}
|
||||
|
||||
digest, err := getImageDigest(pullOpts.Registry, pullOpts.Repository, pullOpts.Tag, "", "")
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Got error when attempting to fetch image version from registry: %s", err)
|
||||
}
|
||||
|
||||
d.SetId(digest)
|
||||
d.Set("sha256_digest", digest)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getImageDigest(registry, image, tag, username, password string) (string, error) {
|
||||
client := http.DefaultClient
|
||||
|
||||
req, err := http.NewRequest("GET", "https://"+registry+"/v2/"+image+"/manifests/"+tag, nil)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error creating registry request: %s", err)
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
req.SetBasicAuth(username, password)
|
||||
}
|
||||
|
||||
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:
|
||||
return resp.Header.Get("Docker-Content-Digest"), nil
|
||||
|
||||
// Either OAuth is required or the basic auth creds were invalid
|
||||
case http.StatusUnauthorized:
|
||||
if strings.HasPrefix(resp.Header.Get("www-authenticate"), "Bearer") {
|
||||
auth := parseAuthHeader(resp.Header.Get("www-authenticate"))
|
||||
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)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+token.Token)
|
||||
digestResponse, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error during registry request: %s", err)
|
||||
}
|
||||
|
||||
if digestResponse.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("Got bad response from registry: " + digestResponse.Status)
|
||||
}
|
||||
|
||||
return digestResponse.Header.Get("Docker-Content-Digest"), nil
|
||||
} else {
|
||||
return "", fmt.Errorf("Bad credentials: " + resp.Status)
|
||||
}
|
||||
|
||||
// Some unexpected status was given, return an error
|
||||
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
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
)
|
||||
|
||||
var registryDigestRegexp = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`)
|
||||
|
||||
func TestAccDockerRegistryImage_basic(t *testing.T) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccDockerImageDataSourceConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestMatchResourceAttr("data.docker_registry_image.foo", "sha256_digest", registryDigestRegexp),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccDockerRegistryImage_private(t *testing.T) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccDockerImageDataSourcePrivateConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestMatchResourceAttr("data.docker_registry_image.bar", "sha256_digest", registryDigestRegexp),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const testAccDockerImageDataSourceConfig = `
|
||||
data "docker_registry_image" "foo" {
|
||||
name = "alpine:latest"
|
||||
}
|
||||
`
|
||||
|
||||
const testAccDockerImageDataSourcePrivateConfig = `
|
||||
data "docker_registry_image" "bar" {
|
||||
name = "gcr.io:443/google_containers/pause:0.8.0"
|
||||
}
|
||||
`
|
|
@ -32,6 +32,10 @@ func Provider() terraform.ResourceProvider {
|
|||
"docker_volume": resourceDockerVolume(),
|
||||
},
|
||||
|
||||
DataSourcesMap: map[string]*schema.Resource{
|
||||
"docker_registry_image": dataSourceDockerRegistryImage(),
|
||||
},
|
||||
|
||||
ConfigureFunc: providerConfigure,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,6 @@ func resourceDockerImage() *schema.Resource {
|
|||
Required: true,
|
||||
},
|
||||
|
||||
"keep_updated": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"latest": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
|
@ -31,6 +26,12 @@ func resourceDockerImage() *schema.Resource {
|
|||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"pull_trigger": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,25 @@ func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error {
|
|||
}
|
||||
|
||||
func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
|
||||
client := meta.(*dc.Client)
|
||||
var data Data
|
||||
if err := fetchLocalImages(&data, client); err != nil {
|
||||
return fmt.Errorf("Error reading docker image list: %s", err)
|
||||
}
|
||||
foundImage := searchLocalImages(data, d.Get("name").(string))
|
||||
|
||||
if foundImage != nil {
|
||||
d.Set("latest", foundImage.ID)
|
||||
} else {
|
||||
d.SetId("")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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.(*dc.Client)
|
||||
apiImage, err := findImage(d, client)
|
||||
if err != nil {
|
||||
|
@ -33,13 +52,6 @@ func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
// We need to re-read in case switching parameters affects
|
||||
// the value of "latest" or others
|
||||
|
||||
return resourceDockerImageRead(d, meta)
|
||||
}
|
||||
|
||||
func resourceDockerImageDelete(d *schema.ResourceData, meta interface{}) error {
|
||||
client := meta.(*dc.Client)
|
||||
err := removeImage(d, client)
|
||||
|
@ -117,6 +129,17 @@ func pullImage(data *Data, client *dc.Client, image string) error {
|
|||
// TODO: Test local registry handling. It should be working
|
||||
// based on the code that was ported over
|
||||
|
||||
pullOpts := parseImageOptions(image)
|
||||
auth := dc.AuthConfiguration{}
|
||||
|
||||
if err := client.PullImage(pullOpts, auth); err != nil {
|
||||
return fmt.Errorf("Error pulling image %s: %s\n", image, err)
|
||||
}
|
||||
|
||||
return fetchLocalImages(data, client)
|
||||
}
|
||||
|
||||
func parseImageOptions(image string) dc.PullImageOptions {
|
||||
pullOpts := dc.PullImageOptions{}
|
||||
|
||||
splitImageName := strings.Split(image, ":")
|
||||
|
@ -151,32 +174,7 @@ func pullImage(data *Data, client *dc.Client, image string) error {
|
|||
pullOpts.Repository = image
|
||||
}
|
||||
|
||||
if err := client.PullImage(pullOpts, dc.AuthConfiguration{}); err != nil {
|
||||
return fmt.Errorf("Error pulling image %s: %s\n", image, err)
|
||||
}
|
||||
|
||||
return fetchLocalImages(data, client)
|
||||
}
|
||||
|
||||
func getImageTag(image string) string {
|
||||
splitImageName := strings.Split(image, ":")
|
||||
switch {
|
||||
|
||||
// It's in registry:port/repo:tag format
|
||||
case len(splitImageName) == 3:
|
||||
return splitImageName[2]
|
||||
|
||||
// It's either registry:port/repo or repo:tag with default registry
|
||||
case len(splitImageName) == 2:
|
||||
splitPortRepo := strings.Split(splitImageName[1], "/")
|
||||
if len(splitPortRepo) == 2 {
|
||||
return ""
|
||||
} else {
|
||||
return splitImageName[1]
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return pullOpts
|
||||
}
|
||||
|
||||
func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) {
|
||||
|
@ -192,7 +190,7 @@ func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error)
|
|||
|
||||
foundImage := searchLocalImages(data, imageName)
|
||||
|
||||
if d.Get("keep_updated").(bool) || foundImage == nil {
|
||||
if foundImage == nil {
|
||||
if err := pullImage(&data, client, imageName); err != nil {
|
||||
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
|
||||
}
|
||||
|
|
|
@ -73,6 +73,22 @@ func TestAccDockerImage_destroy(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAccDockerImage_data(t *testing.T) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
PreventPostDestroyRefresh: true,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccDockerImageFromDataConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestMatchResourceAttr("docker_image.foobarbaz", "latest", contentDigestRegexp),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccDockerImageDestroy(s *terraform.State) error {
|
||||
for _, rs := range s.RootModule().Resources {
|
||||
if rs.Type != "docker_image" {
|
||||
|
@ -93,14 +109,12 @@ func testAccDockerImageDestroy(s *terraform.State) error {
|
|||
const testAccDockerImageConfig = `
|
||||
resource "docker_image" "foo" {
|
||||
name = "alpine:3.1"
|
||||
keep_updated = false
|
||||
}
|
||||
`
|
||||
|
||||
const testAddDockerPrivateImageConfig = `
|
||||
resource "docker_image" "foobar" {
|
||||
name = "gcr.io:443/google_containers/pause:0.8.0"
|
||||
keep_updated = true
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -110,3 +124,13 @@ resource "docker_image" "foobarzoo" {
|
|||
keep_locally = true
|
||||
}
|
||||
`
|
||||
|
||||
const testAccDockerImageFromDataConfig = `
|
||||
data "docker_registry_image" "foobarbaz" {
|
||||
name = "alpine:3.1"
|
||||
}
|
||||
resource "docker_image" "foobarbaz" {
|
||||
name = "${data.docker_registry_image.foobarbaz.name}"
|
||||
pull_trigger = "${data.docker_registry_image.foobarbaz.sha256_digest}"
|
||||
}
|
||||
`
|
||||
|
|
Loading…
Reference in New Issue