Skip to content

Commit

Permalink
new provider: dockerregistry
Browse files Browse the repository at this point in the history
(Initial draft. This commit does not include doc updates, and all of the
code is in a single file instead of being split into provider/resource
files as is the terraform standard.)

Unlike the 'docker' provider, which talks to your local Docker daemon to
read and write your local image stores, the 'dockerregistry' provider
talks to a Docker Registry V1 server and confirms that the named
repository/tag exists.

It is a read-only provider: it doesn't push to the registry; it just
helps you assert that the docker tag push via your build process
actually exists before creating other configuration that uses it.
Ideally this would use the `data` feature from hashicorp#4169 instead.
  • Loading branch information
glasser committed Apr 7, 2016
1 parent 2170588 commit eb32f48
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 0 deletions.
12 changes: 12 additions & 0 deletions builtin/bins/provider-dockerregistry/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package main

import (
"github.com/hashicorp/terraform/builtin/providers/dockerregistry"
"github.com/hashicorp/terraform/plugin"
)

func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: dockerregistry.Provider,
})
}
117 changes: 117 additions & 0 deletions builtin/providers/dockerregistry/dockerregistry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package dockerregistry

import (
"fmt"
"net/http"

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"github.com/meteor/docker-registry-client/registry"
)

func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"username": {
Type: schema.TypeString,
Optional: true,
Description: "Username to log in to Docker registry",
DefaultFunc: schema.EnvDefaultFunc("DOCKERREGISTRY_USERNAME", ""),
},
"password": {
Type: schema.TypeString,
Optional: true,
Description: "Password to log in to Docker registry",
DefaultFunc: schema.EnvDefaultFunc("DOCKERREGISTRY_PASSWORD", ""),
},
"registry": {
Type: schema.TypeString,
Optional: true,
Default: "https://registry-1.docker.io",
Description: "URL to Docker V2 registry",
},
},
ResourcesMap: map[string]*schema.Resource{
"dockerregistry_image": {
Schema: map[string]*schema.Schema{
"repository": {
Type: schema.TypeString,
Required: true,
ForceNew: true, // No Update command; we mutate Id
Description: "Name of the repository in the registry; eg, `mycompany/myproject` or `library/alpine`",
},
"tag": {
Type: schema.TypeString,
Required: true,
ForceNew: true, // No Update command; we mutate Id
Description: "Tag to search for",
},
},

Create: func(d *schema.ResourceData, meta interface{}) error {
id, err := ensureImageExists(d, meta)
if err != nil {
return err
}
d.SetId(id)
return nil
},
Read: func(d *schema.ResourceData, meta interface{}) error {
// Don't refresh. If we've changed the resource from something broken
// to something good, we don't want to error just because the state
// file contains the broken resource! It would be nice to be able to
// only refresh if the resource hasn't changed, but that's not how the
// API works.
return nil
},
Delete: func(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
},
},
},
ConfigureFunc: providerConfigure,
}
}

func providerConfigure(d *schema.ResourceData) (interface{}, error) {
registryURL := d.Get("registry").(string)
reg := &registry.Registry{
URL: registryURL,
Client: &http.Client{
Transport: registry.WrapTransport(http.DefaultTransport, registryURL,
d.Get("username").(string), d.Get("password").(string)),
},
Logf: registry.Quiet,
}
return reg, nil
}

func ensureImageExists(d *schema.ResourceData, meta interface{}) (string, error) {
reg := meta.(*registry.Registry)
repository := d.Get("repository").(string)
tag := d.Get("tag").(string)

serverTags, err := reg.Tags(repository)
if err != nil {
return "", fmt.Errorf("Error looking up tags for %s: %s", repository, err)
}

if !stringInSlice(tag, serverTags) {
return "", fmt.Errorf("Docker image %s:%s not found in registry", repository, tag)
}

return fmt.Sprintf("%s:%s", repository, tag), nil
}

// stringInSlice returns true if the string is an element of the slice.
//
// (It's great that Go makes it hard to ignore that this operation is O(n)!)
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
142 changes: 142 additions & 0 deletions builtin/providers/dockerregistry/dockerregistry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package dockerregistry

// Note: to run the tests that actually talk to the registry, run:
//
// $ TF_ACC=1 go test -v github.com/meteor/amsterdam/cmds/terraform-provider-dockerregistry
//
// (Username and password are not necessary as it only reads a public
// repository. No tests currently test auth.)

import (
"fmt"
"strings"
"testing"

"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)

var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider

func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"dockerregistry": testAccProvider,
}
}

func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}

func TestAccDockerRegistry_good(t *testing.T) {
resource.Test(t, resource.TestCase{
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccDockerRegistryConfigGood,
Check: resource.TestCheckResourceAttr("dockerregistry_image.good", "id", "library/alpine:3.1"),
},
},
})
}

func TestAccDockerRegistry_missing_tag(t *testing.T) {
expectT := &expectOneErrorTestT{
ErrorPredicate: func(args ...interface{}) bool {
return len(args) == 1 && strings.Contains(args[0].(string),
"Docker image library/alpine:3.1-does-not-exist not found in registry")
},
WrappedT: t,
}
resource.Test(expectT, resource.TestCase{
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccDockerRegistryConfigMissingTag,
},
},
})
if !expectT.GotError {
t.Error("Did not get expected error")
}
}

func TestAccDockerRegistry_missing_repo(t *testing.T) {
expectT := &expectOneErrorTestT{
ErrorPredicate: func(args ...interface{}) bool {
if len(args) != 1 {
return false
}
e := args[0].(string)
// This is not the best error, but it's what the registry gives us.
return strings.Contains(e, "Error looking up tags for library/alpine-does-not-exist") &&
strings.Contains(e, "UNAUTHORIZED")
},
WrappedT: t,
}
resource.Test(expectT, resource.TestCase{
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccDockerRegistryConfigMissingRepo,
},
},
})
if !expectT.GotError {
t.Error("Did not get expected error")
}
}

const testAccDockerRegistryConfigGood = `
resource "dockerregistry_image" "good" {
repository = "library/alpine"
tag = "3.1"
}
`

const testAccDockerRegistryConfigMissingTag = `
resource "dockerregistry_image" "bad" {
repository = "library/alpine"
tag = "3.1-does-not-exist"
}
`

const testAccDockerRegistryConfigMissingRepo = `
resource "dockerregistry_image" "bad" {
repository = "library/alpine-does-not-exist"
tag = "3.1"
}
`

// This implements the terraform TestT interface and expects to have Error
// called on it exactly once. Skip is passed through.
type expectOneErrorTestT struct {
ErrorPredicate func(args ...interface{}) bool
WrappedT *testing.T
GotError bool
}

func (t *expectOneErrorTestT) Fatal(args ...interface{}) {
t.WrappedT.Fatal(args...)
}

func (t *expectOneErrorTestT) Skip(args ...interface{}) {
t.WrappedT.Skip(args...)
}

func (t *expectOneErrorTestT) Error(args ...interface{}) {
if t.GotError {
t.WrappedT.Error("Got unexpected additional error:", fmt.Sprintln(args...))
return
}
if !t.ErrorPredicate(args...) {
t.WrappedT.Error("Got non-matching error:", fmt.Sprintln(args...))
return
}
t.GotError = true
}

0 comments on commit eb32f48

Please sign in to comment.