forked from hashicorp/terraform
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(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
Showing
3 changed files
with
271 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 := ®istry.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
142
builtin/providers/dockerregistry/dockerregistry_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |