added docker_registry_image

This commit is contained in:
edgar poce 2020-03-24 11:34:14 -03:00
parent 5d0f0fb806
commit 6a3c615a30
11 changed files with 1125 additions and 8 deletions

1
.gitignore vendored
View file

@ -23,6 +23,7 @@ website/node_modules
*.iml *.iml
*.test *.test
*.iml *.iml
.vscode
website/vendor website/vendor

View file

@ -107,6 +107,7 @@ func Provider() terraform.ResourceProvider {
ResourcesMap: map[string]*schema.Resource{ ResourcesMap: map[string]*schema.Resource{
"docker_container": resourceDockerContainer(), "docker_container": resourceDockerContainer(),
"docker_image": resourceDockerImage(), "docker_image": resourceDockerImage(),
"docker_registry_image": resourceDockerRegistryImage(),
"docker_network": resourceDockerNetwork(), "docker_network": resourceDockerNetwork(),
"docker_volume": resourceDockerVolume(), "docker_volume": resourceDockerVolume(),
"docker_config": resourceDockerConfig(), "docker_config": resourceDockerConfig(),

View file

@ -0,0 +1,285 @@
package docker
import (
"os"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)
func resourceDockerRegistryImage() *schema.Resource {
return &schema.Resource{
Create: resourceDockerRegistryImageCreate,
Read: resourceDockerRegistryImageRead,
Delete: resourceDockerRegistryImageDelete,
Update: resourceDockerRegistryImageUpdate,
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"keep_remotely": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"build": &schema.Schema{
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"suppress_output": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"remote_context": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"no_cache": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"remove": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"force_remove": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"pull_parent": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"isolation": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"cpu_set_cpus": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"cpu_set_mems": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"cpu_shares": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"cpu_quota": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"cpu_period": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"memory": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"memory_swap": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"cgroup_parent": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"network_mode": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"shm_size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"dockerfile": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "Dockerfile",
ForceNew: true,
},
"ulimit": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"hard": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},
"soft": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},
},
},
},
"build_args": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"auth_config": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"host_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"user_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"auth": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"email": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"server_address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"identity_token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"registry_token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
},
"context": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
StateFunc: func(val interface{}) string {
// the context hash is stored to identify changes in the context files
dockerContextTarPath, _ := buildDockerImageContextTar(val.(string))
defer os.Remove(dockerContextTarPath)
contextTarHash, _ := getDockerImageContextTarHash(dockerContextTarPath)
return val.(string) + ":" + contextTarHash
},
},
"labels": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"squash": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"cache_from": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"security_opt": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"extra_hosts": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"target": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"session_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"platform": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"version": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"build_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
// "output": &schema.Schema{
// Type: schema.TypeString,
// Optional: true,
// },
},
},
},
"sha256_digest": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

View file

@ -0,0 +1,488 @@
package docker
import (
"archive/tar"
"bufio"
"context"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/term"
"github.com/docker/go-units"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)
type internalPushImageOptions struct {
Name string
FqName string
Registry string
NormalizedRegistry string
Repository string
Tag string
}
func createImageBuildOptions(buildOptions map[string]interface{}) types.ImageBuildOptions {
mapOfInterfacesToMapOfStrings := func(mapOfInterfaces map[string]interface{}) map[string]string {
mapOfStrings := make(map[string]string, len(mapOfInterfaces))
for k, v := range mapOfInterfaces {
mapOfStrings[k] = fmt.Sprintf("%v", v)
}
return mapOfStrings
}
interfaceArrayToStringArray := func(interfaceArray []interface{}) []string {
stringArray := make([]string, len(interfaceArray))
for i, v := range interfaceArray {
stringArray[i] = fmt.Sprintf("%v", v)
}
return stringArray
}
mapToBuildArgs := func(buildArgsOptions map[string]interface{}) map[string]*string {
buildArgs := make(map[string]*string, len(buildArgsOptions))
for k, v := range buildArgsOptions {
value := v.(string)
buildArgs[k] = &value
}
return buildArgs
}
readULimits := func(options []interface{}) []*units.Ulimit {
ulimits := make([]*units.Ulimit, len(options))
for i, v := range options {
ulimitOption := v.(map[string]interface{})
ulimit := units.Ulimit{
Name: ulimitOption["name"].(string),
Hard: int64(ulimitOption["hard"].(int)),
Soft: int64(ulimitOption["soft"].(int)),
}
ulimits[i] = &ulimit
}
return ulimits
}
readAuthConfigs := func(options []interface{}) map[string]types.AuthConfig {
authConfigs := make(map[string]types.AuthConfig, len(options))
for _, v := range options {
authOptions := v.(map[string]interface{})
auth := types.AuthConfig{
Username: authOptions["user_name"].(string),
Password: authOptions["password"].(string),
Auth: authOptions["auth"].(string),
Email: authOptions["email"].(string),
ServerAddress: authOptions["server_address"].(string),
IdentityToken: authOptions["identity_token"].(string),
RegistryToken: authOptions["registry_token"].(string),
}
authConfigs[authOptions["host_name"].(string)] = auth
}
return authConfigs
}
buildImageOptions := types.ImageBuildOptions{}
buildImageOptions.SuppressOutput = buildOptions["suppress_output"].(bool)
buildImageOptions.RemoteContext = buildOptions["remote_context"].(string)
buildImageOptions.NoCache = buildOptions["no_cache"].(bool)
buildImageOptions.Remove = buildOptions["remove"].(bool)
buildImageOptions.ForceRemove = buildOptions["force_remove"].(bool)
buildImageOptions.PullParent = buildOptions["pull_parent"].(bool)
buildImageOptions.Isolation = container.Isolation(buildOptions["isolation"].(string))
buildImageOptions.CPUSetCPUs = buildOptions["cpu_set_cpus"].(string)
buildImageOptions.CPUSetMems = buildOptions["cpu_set_mems"].(string)
buildImageOptions.CPUShares = int64(buildOptions["cpu_shares"].(int))
buildImageOptions.CPUQuota = int64(buildOptions["cpu_quota"].(int))
buildImageOptions.CPUPeriod = int64(buildOptions["cpu_period"].(int))
buildImageOptions.Memory = int64(buildOptions["memory"].(int))
buildImageOptions.MemorySwap = int64(buildOptions["memory_swap"].(int))
buildImageOptions.CgroupParent = buildOptions["cgroup_parent"].(string)
buildImageOptions.NetworkMode = buildOptions["network_mode"].(string)
buildImageOptions.ShmSize = int64(buildOptions["shm_size"].(int))
buildImageOptions.Dockerfile = buildOptions["dockerfile"].(string)
buildImageOptions.Ulimits = readULimits(buildOptions["ulimit"].([]interface{}))
buildImageOptions.BuildArgs = mapToBuildArgs(buildOptions["build_args"].(map[string]interface{}))
buildImageOptions.AuthConfigs = readAuthConfigs(buildOptions["auth_config"].([]interface{}))
buildImageOptions.Labels = mapOfInterfacesToMapOfStrings(buildOptions["labels"].(map[string]interface{}))
buildImageOptions.Squash = buildOptions["squash"].(bool)
buildImageOptions.CacheFrom = interfaceArrayToStringArray(buildOptions["cache_from"].([]interface{}))
buildImageOptions.SecurityOpt = interfaceArrayToStringArray(buildOptions["security_opt"].([]interface{}))
buildImageOptions.ExtraHosts = interfaceArrayToStringArray(buildOptions["extra_hosts"].([]interface{}))
buildImageOptions.Target = buildOptions["target"].(string)
buildImageOptions.SessionID = buildOptions["session_id"].(string)
buildImageOptions.Platform = buildOptions["platform"].(string)
buildImageOptions.Version = types.BuilderVersion(buildOptions["version"].(string))
buildImageOptions.BuildID = buildOptions["build_id"].(string)
// outputs
return buildImageOptions
}
func buildDockerImage(client *client.Client, buildOptions map[string]interface{}, fqName string) error {
log.Printf("[DEBUG] Building docker image")
imageBuildOptions := createImageBuildOptions(buildOptions)
imageBuildOptions.Tags = []string{fqName}
// the tar hash is passed only after the initial creation
buildContext := buildOptions["context"].(string)
if lastIndex := strings.LastIndexByte(buildContext, ':'); lastIndex > -1 {
buildContext = buildContext[:lastIndex]
}
dockerContextTarPath, err := buildDockerImageContextTar(buildContext)
defer os.Remove(dockerContextTarPath)
dockerBuildContext, err := os.Open(dockerContextTarPath)
defer dockerBuildContext.Close()
buildResponse, err := client.ImageBuild(context.Background(), dockerBuildContext, imageBuildOptions)
if err != nil {
return err
}
defer buildResponse.Body.Close()
termFd, isTerm := term.GetFdInfo(os.Stderr)
err = jsonmessage.DisplayJSONMessagesStream(buildResponse.Body, os.Stderr, termFd, isTerm, nil)
if err != nil {
return err
}
return nil
}
func buildDockerImageContextTar(buildContext string) (string, error) {
// Create our Temp File: This will create a filename like /tmp/terraform-provider-docker-123456.tar
tmpFile, err := ioutil.TempFile(os.TempDir(), "terraform-provider-docker-*.tar")
if err != nil {
return "", fmt.Errorf("Cannot create temporary file - %v", err.Error())
}
defer tmpFile.Close()
if _, err = os.Stat(buildContext); err != nil {
return "", fmt.Errorf("Unable to read build context - %v", err.Error())
}
tw := tar.NewWriter(tmpFile)
defer tw.Close()
err = filepath.Walk(buildContext, func(file string, info os.FileInfo, err error) error {
// return on any error
if err != nil {
return err
}
// create a new dir/file header
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
// update the name to correctly reflect the desired destination when untaring
header.Name = strings.TrimPrefix(strings.Replace(file, buildContext, "", -1), string(filepath.Separator))
// write the header
if err := tw.WriteHeader(header); err != nil {
return err
}
// return on non-regular files (thanks to [kumo](https://medium.com/@komuw/just-like-you-did-fbdd7df829d3) for this suggested update)
if !info.Mode().IsRegular() {
return nil
}
// open files for taring
f, err := os.Open(file)
if err != nil {
return err
}
// copy file data into tar writer
if _, err := io.Copy(tw, f); err != nil {
return err
}
// manually close here after each file operation; defering would cause each file close
// to wait until all operations have completed.
f.Close()
return nil
})
return tmpFile.Name(), nil
}
func getDockerImageContextTarHash(dockerContextTarPath string) (string, error) {
hasher := sha256.New()
s, err := ioutil.ReadFile(dockerContextTarPath)
if err != nil {
return "", err
}
hasher.Write(s)
contextHash := hex.EncodeToString(hasher.Sum(nil))
return contextHash, nil
}
func pushDockerRegistryImage(client *client.Client, pushOpts internalPushImageOptions, username string, password string) error {
pushOptions := types.ImagePushOptions{}
if username != "" {
auth := types.AuthConfig{Username: username, Password: password}
authBytes, err := json.Marshal(auth)
if err != nil {
return fmt.Errorf("Error creating push options: %s", err)
}
authBase64 := base64.URLEncoding.EncodeToString(authBytes)
pushOptions.RegistryAuth = authBase64
}
out, err := client.ImagePush(context.Background(), pushOpts.FqName, pushOptions)
if err != nil {
return err
}
defer out.Close()
type ErrorMessage struct {
Error string
}
var errorMessage ErrorMessage
buffIOReader := bufio.NewReader(out)
for {
streamBytes, err := buffIOReader.ReadBytes('\n')
if err == io.EOF {
break
}
json.Unmarshal(streamBytes, &errorMessage)
if errorMessage.Error != "" {
return fmt.Errorf("Error pushing image: %s", errorMessage.Error)
}
}
log.Printf("[DEBUG] Pushed image: %s", pushOpts.FqName)
return nil
}
func getDockerRegistryImageRegistryUserNameAndPassword(
pushOpts internalPushImageOptions,
providerConfig *ProviderConfig) (string, string) {
registry := pushOpts.NormalizedRegistry
username := ""
password := ""
if authConfig, ok := providerConfig.AuthConfigs.Configs[registry]; ok {
username = authConfig.Username
password = authConfig.Password
}
return username, password
}
func deleteDockerRegistryImage(pushOpts internalPushImageOptions, sha256Digest, username, password string, fallback bool) error {
client := http.DefaultClient
// Allow insecure registries only for ACC tests
// cuz we don't have a valid certs for this case
if env, okEnv := os.LookupEnv("TF_ACC"); okEnv {
if i, errConv := strconv.Atoi(env); errConv == nil && i >= 1 {
cfg := &tls.Config{
InsecureSkipVerify: true,
}
client.Transport = &http.Transport{
TLSClientConfig: cfg,
}
}
}
req, err := http.NewRequest("DELETE", pushOpts.NormalizedRegistry+"/v2/"+pushOpts.Repository+"/manifests/"+sha256Digest, nil)
if err != nil {
return fmt.Errorf("Error deleting registry image: %s", err)
}
if username != "" {
req.SetBasicAuth(username, password)
}
// Set this header so that we get the v2 manifest back from the registry.
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
if fallback {
// Fallback to this header if the registry does not support the v2 manifest like gcr.io
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v1+prettyjws")
}
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, http.StatusAccepted, http.StatusNotFound:
return 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)
oauthResp, err := client.Do(req)
switch oauthResp.StatusCode {
case http.StatusOK, http.StatusAccepted, http.StatusNotFound:
return nil
default:
return fmt.Errorf("Got bad response from registry: " + resp.Status)
}
}
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)
}
}
func getImageDigestWithFallback(opts internalPushImageOptions, username, password string) (string, error) {
digest, err := getImageDigest(opts.Registry, opts.Repository, opts.Tag, username, password, false)
if err != nil {
digest, err = getImageDigest(opts.Registry, opts.Repository, opts.Tag, username, password, true)
if err != nil {
return "", fmt.Errorf("Unable to get digest: %s", err)
}
}
return digest, nil
}
func createPushImageOptions(image string) internalPushImageOptions {
pullOpts := parseImageOptions(image)
if pullOpts.Registry == "" {
pullOpts.Registry = "registry.hub.docker.com"
} else {
pullOpts.Repository = strings.Replace(pullOpts.Repository, pullOpts.Registry+"/", "", 1)
}
pushOpts := internalPushImageOptions{
Name: image,
Registry: pullOpts.Registry,
NormalizedRegistry: normalizeRegistryAddress(pullOpts.Registry),
Repository: pullOpts.Repository,
Tag: pullOpts.Tag,
FqName: fmt.Sprintf("%s/%s:%s", pullOpts.Registry, pullOpts.Repository, pullOpts.Tag),
}
return pushOpts
}
func resourceDockerRegistryImageCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ProviderConfig).DockerClient
providerConfig := meta.(*ProviderConfig)
name := d.Get("name").(string)
log.Printf("[DEBUG] Creating docker image %s", name)
pushOpts := createPushImageOptions(name)
if buildOptions, ok := d.GetOk("build"); ok {
buildOptionsMap := buildOptions.([]interface{})[0].(map[string]interface{})
err := buildDockerImage(client, buildOptionsMap, pushOpts.FqName)
if err != nil {
return fmt.Errorf("Error building docker image: %s", err)
}
}
username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig)
if err := pushDockerRegistryImage(client, pushOpts, username, password); err != nil {
return fmt.Errorf("Error pushing docker image: %s", err)
}
digest, err := getImageDigestWithFallback(pushOpts, username, password)
if err != nil {
return fmt.Errorf("Unable to create image, image not found: %s", err)
}
d.SetId(digest)
d.Set("sha256_digest", digest)
return nil
}
func resourceDockerRegistryImageRead(d *schema.ResourceData, meta interface{}) error {
providerConfig := meta.(*ProviderConfig)
name := d.Get("name").(string)
pushOpts := createPushImageOptions(name)
username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig)
digest, err := getImageDigestWithFallback(pushOpts, username, password)
if err != nil {
log.Printf("Got error getting registry image digest: %s", err)
d.SetId("")
return nil
}
d.Set("sha256_digest", digest)
return nil
}
func resourceDockerRegistryImageDelete(d *schema.ResourceData, meta interface{}) error {
if d.Get("keep_remotely").(bool) {
return nil
}
providerConfig := meta.(*ProviderConfig)
name := d.Get("name").(string)
pushOpts := createPushImageOptions(name)
username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig)
digest := d.Get("sha256_digest").(string)
err := deleteDockerRegistryImage(pushOpts, digest, username, password, false)
if err != nil {
err = deleteDockerRegistryImage(pushOpts, pushOpts.Tag, username, password, true)
if err != nil {
return fmt.Errorf("Got error getting registry image digest: %s", err)
}
}
return nil
}
func resourceDockerRegistryImageUpdate(d *schema.ResourceData, meta interface{}) error {
return resourceDockerRegistryImageRead(d, meta)
}

View file

@ -0,0 +1,289 @@
package docker
import (
"fmt"
"regexp"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-units"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"gotest.tools/assert"
"gotest.tools/assert/cmp"
)
func TestAccDockerRegistryImageResource_mapping(t *testing.T) {
dummyProvider := Provider().(*schema.Provider)
dummyResource := dummyProvider.ResourcesMap["docker_registry_image"]
dummyResource.Create = func(d *schema.ResourceData, meta interface{}) error {
build := d.Get("build").([]interface{})[0].(map[string]interface{})
options := createImageBuildOptions(build)
assert.Check(t, cmp.Equal(options.SuppressOutput, true))
assert.Check(t, cmp.Equal(options.RemoteContext, "fooRemoteContext"))
assert.Check(t, cmp.Equal(options.NoCache, true))
assert.Check(t, cmp.Equal(options.Remove, true))
assert.Check(t, cmp.Equal(options.ForceRemove, true))
assert.Check(t, cmp.Equal(options.PullParent, true))
assert.Check(t, cmp.Equal(options.Isolation, container.Isolation("hyperv")))
assert.Check(t, cmp.Equal(options.CPUSetCPUs, "fooCpuSetCpus"))
assert.Check(t, cmp.Equal(options.CPUSetMems, "fooCpuSetMems"))
assert.Check(t, cmp.Equal(options.CPUShares, int64(4)))
assert.Check(t, cmp.Equal(options.CPUQuota, int64(5)))
assert.Check(t, cmp.Equal(options.CPUPeriod, int64(6)))
assert.Check(t, cmp.Equal(options.Memory, int64(1)))
assert.Check(t, cmp.Equal(options.MemorySwap, int64(2)))
assert.Check(t, cmp.Equal(options.CgroupParent, "fooCgroupParent"))
assert.Check(t, cmp.Equal(options.NetworkMode, "fooNetworkMode"))
assert.Check(t, cmp.Equal(options.ShmSize, int64(3)))
assert.Check(t, cmp.Equal(options.Dockerfile, "fooDockerfile"))
assert.Check(t, cmp.Equal(len(options.Ulimits), 1))
assert.Check(t, cmp.DeepEqual(*options.Ulimits[0], units.Ulimit{
Name: "foo",
Hard: int64(1),
Soft: int64(2),
}))
assert.Check(t, cmp.Equal(len(options.BuildArgs), 1))
assert.Check(t, cmp.Equal(*options.BuildArgs["HTTP_PROXY"], "http://10.20.30.2:1234"))
assert.Check(t, cmp.Equal(len(options.AuthConfigs), 1))
assert.Check(t, cmp.DeepEqual(options.AuthConfigs["foo.host"], types.AuthConfig{
Username: "fooUserName",
Password: "fooPassword",
Auth: "fooAuth",
Email: "fooEmail",
ServerAddress: "fooServerAddress",
IdentityToken: "fooIdentityToken",
RegistryToken: "fooRegistryToken",
}))
assert.Check(t, cmp.DeepEqual(options.Labels, map[string]string{"foo": "bar"}))
assert.Check(t, cmp.Equal(options.Squash, true))
assert.Check(t, cmp.DeepEqual(options.CacheFrom, []string{"fooCacheFrom", "barCacheFrom"}))
assert.Check(t, cmp.DeepEqual(options.SecurityOpt, []string{"fooSecurityOpt", "barSecurityOpt"}))
assert.Check(t, cmp.DeepEqual(options.ExtraHosts, []string{"fooExtraHost", "barExtraHost"}))
assert.Check(t, cmp.Equal(options.Target, "fooTarget"))
assert.Check(t, cmp.Equal(options.SessionID, "fooSessionId"))
assert.Check(t, cmp.Equal(options.Platform, "fooPlatform"))
assert.Check(t, cmp.Equal(options.Version, types.BuilderVersion("1")))
assert.Check(t, cmp.Equal(options.BuildID, "fooBuildId"))
// output
d.SetId("foo")
d.Set("sha256_digest", "bar")
return nil
}
dummyResource.Update = func(d *schema.ResourceData, meta interface{}) error {
return nil
}
dummyResource.Delete = func(d *schema.ResourceData, meta interface{}) error {
return nil
}
dummyResource.Read = func(d *schema.ResourceData, meta interface{}) error {
d.Set("sha256_digest", "bar")
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: map[string]terraform.ResourceProvider{"docker": dummyProvider},
Steps: []resource.TestStep{
{
Config: testBuildDockerRegistryImageMappingConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_registry_image.foo", "sha256_digest"),
),
},
},
})
}
func TestAccDockerRegistryImageResource_build(t *testing.T) {
pushOptions := createPushImageOptions("127.0.0.1:15000/tftest-dockerregistryimage:1.0")
context := "../scripts/testing/docker_registry_image_context"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testDockerRegistryImageNotInRegistry(pushOptions),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testBuildDockerRegistryImageNoKeepConfig, pushOptions.Registry, pushOptions.Name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_registry_image.foo", "sha256_digest"),
),
},
},
})
}
func TestAccDockerRegistryImageResource_buildAndKeep(t *testing.T) {
pushOptions := createPushImageOptions("127.0.0.1:15000/tftest-dockerregistryimage:1.0")
context := "../scripts/testing/docker_registry_image_context"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testDockerRegistryImageInRegistry(pushOptions, true),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testBuildDockerRegistryImageKeepConfig, pushOptions.Registry, pushOptions.Name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_registry_image.foo", "sha256_digest"),
),
},
},
})
}
func TestAccDockerRegistryImageResource_pushMissingImage(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testDockerRegistryImagePushMissingConfig,
ExpectError: regexp.MustCompile("An image does not exist locally"),
},
},
})
}
func testDockerRegistryImageNotInRegistry(pushOpts internalPushImageOptions) resource.TestCheckFunc {
return func(s *terraform.State) error {
providerConfig := testAccProvider.Meta().(*ProviderConfig)
username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig)
digest, _ := getImageDigestWithFallback(pushOpts, username, password)
if digest != "" {
return fmt.Errorf("image found")
}
return nil
}
}
func testDockerRegistryImageInRegistry(pushOpts internalPushImageOptions, cleanup bool) resource.TestCheckFunc {
return func(s *terraform.State) error {
providerConfig := testAccProvider.Meta().(*ProviderConfig)
username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig)
digest, err := getImageDigestWithFallback(pushOpts, username, password)
if err != nil || len(digest) < 1 {
return fmt.Errorf("image not found")
}
if cleanup {
err := deleteDockerRegistryImage(pushOpts, digest, username, password, false)
if err != nil {
return fmt.Errorf("Unable to remove test image. %s", err)
}
}
return nil
}
}
const testBuildDockerRegistryImageMappingConfig = `
resource "docker_registry_image" "foo" {
name = "localhost:15000/foo:1.0"
build {
suppress_output = true
remote_context = "fooRemoteContext"
no_cache = true
remove = true
force_remove = true
pull_parent = true
isolation = "hyperv"
cpu_set_cpus = "fooCpuSetCpus"
cpu_set_mems = "fooCpuSetMems"
cpu_shares = 4
cpu_quota = 5
cpu_period = 6
memory = 1
memory_swap = 2
cgroup_parent = "fooCgroupParent"
network_mode = "fooNetworkMode"
shm_size = 3
dockerfile = "fooDockerfile"
ulimit {
name = "foo"
hard = 1
soft = 2
}
auth_config {
host_name = "foo.host"
user_name = "fooUserName"
password = "fooPassword"
auth = "fooAuth"
email = "fooEmail"
server_address = "fooServerAddress"
identity_token = "fooIdentityToken"
registry_token = "fooRegistryToken"
}
build_args = {
"HTTP_PROXY" = "http://10.20.30.2:1234"
}
context = "context"
labels = {
foo = "bar"
}
squash = true
cache_from = ["fooCacheFrom", "barCacheFrom"]
security_opt = ["fooSecurityOpt", "barSecurityOpt"]
extra_hosts = ["fooExtraHost", "barExtraHost"]
target = "fooTarget"
session_id = "fooSessionId"
platform = "fooPlatform"
version = "1"
build_id = "fooBuildId"
}
}
`
const testBuildDockerRegistryImageNoKeepConfig = `
provider "docker" {
alias = "private"
registry_auth {
address = "%s"
}
}
resource "docker_registry_image" "foo" {
provider = "docker.private"
name = "%s"
build {
context = "%s"
remove = true
force_remove = true
no_cache = true
}
}
`
const testBuildDockerRegistryImageKeepConfig = `
provider "docker" {
alias = "private"
registry_auth {
address = "%s"
}
}
resource "docker_registry_image" "foo" {
provider = "docker.private"
name = "%s"
keep_remotely = true
build {
context = "%s"
remove = true
force_remove = true
no_cache = true
}
}
`
const testDockerRegistryImagePushMissingConfig = `
provider "docker" {
alias = "private"
registry_auth {
address = "127.0.0.1:15000"
}
}
resource "docker_registry_image" "foo" {
provider = "docker.private"
name = "127.0.0.1:15000/nonexistent:1.0"
}
`

2
go.mod
View file

@ -17,7 +17,7 @@ require (
github.com/opencontainers/image-spec v0.0.0-20171125024018-577479e4dc27 // indirect github.com/opencontainers/image-spec v0.0.0-20171125024018-577479e4dc27 // indirect
github.com/pkg/errors v0.8.1 // indirect github.com/pkg/errors v0.8.1 // indirect
github.com/sirupsen/logrus v1.4.2 // indirect github.com/sirupsen/logrus v1.4.2 // indirect
gotest.tools v2.2.0+incompatible // indirect gotest.tools v2.2.0+incompatible
) )
go 1.13 go 1.13

View file

@ -26,6 +26,7 @@ docker run -d -p 15000:5000 --rm --name private_registry \
-v "$(pwd)"/scripts/testing/certs:/certs \ -v "$(pwd)"/scripts/testing/certs:/certs \
-e "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry_auth.crt" \ -e "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry_auth.crt" \
-e "REGISTRY_HTTP_TLS_KEY=/certs/registry_auth.key" \ -e "REGISTRY_HTTP_TLS_KEY=/certs/registry_auth.key" \
-e "REGISTRY_STORAGE_DELETE_ENABLED=true" \
registry:2 registry:2
# wait a bit for travis... # wait a bit for travis...
sleep 5 sleep 5

View file

@ -0,0 +1,2 @@
FROM scratch
COPY empty /empty

View file

@ -30,6 +30,10 @@
<a href="/docs/providers/docker/r/image.html">docker_image</a> <a href="/docs/providers/docker/r/image.html">docker_image</a>
</li> </li>
<li<%= sidebar_current("docs-docker-resource-registry-image") %>>
<a href="/docs/providers/docker/r/registry_image.html">docker_registry_image</a>
</li>
<li<%= sidebar_current("docs-docker-resource-network") %>> <li<%= sidebar_current("docs-docker-resource-network") %>>
<a href="/docs/providers/docker/r/network.html">docker_network</a> <a href="/docs/providers/docker/r/network.html">docker_network</a>
</li> </li>

View file

@ -0,0 +1,46 @@
---
layout: "docker"
page_title: "Docker: docker_registry_image"
sidebar_current: "docs-docker-resource-registry-image"
description: |-
Manages the lifecycle of docker image/tag in a registry.
---
# docker\_registry\_image
Provides an image/tag in a Docker registry.
## Example Usage
```hcl
resource "docker_registry_image" "helloworld" {
name = "helloworld:1.0"
build {
context = "pathToContextFolder"
}
}
```
## Argument Reference
* `name` - (Required, string) The name of the Docker image.
* `keep_remotely` - (Optional, boolean) If true, then the Docker image won't be
deleted on destroy operation. If this is false, it will delete the image from
the docker registry on destroy operation.
* `build` - (Optional, Customize docker build arguments) See [Build](#build-1) below for details.
<a id="build-1"></a>
#### Build Block
## Attributes Reference
The following attributes are exported in addition to the above configuration:
* `sha256_digest` (string) - The sha256 digest of the image.