provider/docker: Added docker_registry_image data source (#7000)

This commit is contained in:
kyhavlov 2016-07-26 11:18:38 -04:00 committed by Radek Simko
parent 5050d4555c
commit 09bba0424c
6 changed files with 286 additions and 41 deletions

View File

@ -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
}

View File

@ -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"
}
`

View File

@ -32,6 +32,10 @@ func Provider() terraform.ResourceProvider {
"docker_volume": resourceDockerVolume(),
},
DataSourcesMap: map[string]*schema.Resource{
"docker_registry_image": dataSourceDockerRegistryImage(),
},
ConfigureFunc: providerConfigure,
}
}

View File

@ -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,
},
},
}
}

View File

@ -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)
}

View File

@ -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}"
}
`