feat: Migrate build block to docker_image (#501)

* feat: docker_image now has all build capabilities from docker_registry_image

* tests: Move all docker_registry_image build tests to docker_image.

* fix: Change build.context to optional.

* docs: Update docs.
This commit is contained in:
Martin 2023-01-05 13:27:40 +01:00 committed by GitHub
parent a5332be18d
commit 661c6628ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 623 additions and 313 deletions

View file

@ -104,17 +104,68 @@ resource "docker_image" "zoo" {
<a id="nestedblock--build"></a> <a id="nestedblock--build"></a>
### Nested Schema for `build` ### Nested Schema for `build`
Optional:
- `auth_config` (Block List) The configuration for the authentication (see [below for nested schema](#nestedblock--build--auth_config))
- `build_arg` (Map of String) Set build-time variables
- `build_args` (Map of String) Pairs for build-time variables in the form TODO
- `build_id` (String) BuildID is an optional identifier that can be passed together with the build request. The same identifier can be used to gracefully cancel the build with the cancel request.
- `cache_from` (List of String) Images to consider as cache sources
- `cgroup_parent` (String) Optional parent cgroup for the container
- `context` (String) Value to specify the build context. Currently, only a `PATH` context is supported. You can use the helper function '${path.cwd}/context-dir'. Please see https://docs.docker.com/build/building/context/ for more information about build contexts.
- `cpu_period` (Number) The length of a CPU period in microseconds
- `cpu_quota` (Number) Microseconds of CPU time that the container can get in a CPU period
- `cpu_set_cpus` (String) CPUs in which to allow execution (e.g., `0-3`, `0`, `1`)
- `cpu_set_mems` (String) MEMs in which to allow execution (`0-3`, `0`, `1`)
- `cpu_shares` (Number) CPU shares (relative weight)
- `dockerfile` (String) Name of the Dockerfile. Defaults to `Dockerfile`.
- `extra_hosts` (List of String) A list of hostnames/IP mappings to add to the containers /etc/hosts file. Specified in the form ["hostname:IP"]
- `force_remove` (Boolean) Always remove intermediate containers
- `isolation` (String) Isolation represents the isolation technology of a container. The supported values are
- `label` (Map of String) Set metadata for an image
- `labels` (Map of String) User-defined key/value metadata
- `memory` (Number) Set memory limit for build
- `memory_swap` (Number) Total memory (memory + swap), -1 to enable unlimited swap
- `network_mode` (String) Set the networking mode for the RUN instructions during build
- `no_cache` (Boolean) Do not use the cache when building the image
- `path` (String, Deprecated) Context path
- `platform` (String) Set platform if server is multi-platform capable
- `pull_parent` (Boolean) Attempt to pull the image even if an older image exists locally
- `remote_context` (String) A Git repository URI or HTTP/HTTPS context URI
- `remove` (Boolean) Remove intermediate containers after a successful build. Defaults to `true`.
- `security_opt` (List of String) The security options
- `session_id` (String) Set an ID for the build session
- `shm_size` (Number) Size of /dev/shm in bytes. The size must be greater than 0
- `squash` (Boolean) If true the new layers are squashed into a new image with a single new layer
- `suppress_output` (Boolean) Suppress the build output and print image ID on success
- `tag` (List of String) Name and optionally a tag in the 'name:tag' format
- `target` (String) Set the target build stage to build
- `ulimit` (Block List) Configuration for ulimits (see [below for nested schema](#nestedblock--build--ulimit))
- `version` (String) Version of the underlying builder to use
<a id="nestedblock--build--auth_config"></a>
### Nested Schema for `build.auth_config`
Required: Required:
- `path` (String) Context path - `host_name` (String) hostname of the registry
Optional: Optional:
- `build_arg` (Map of String) Set build-time variables - `auth` (String) the auth token
- `dockerfile` (String) Name of the Dockerfile. Defaults to `Dockerfile`. - `email` (String) the user emal
- `force_remove` (Boolean) Always remove intermediate containers - `identity_token` (String) the identity token
- `label` (Map of String) Set metadata for an image - `password` (String) the registry password
- `no_cache` (Boolean) Do not use cache when building the image - `registry_token` (String) the registry token
- `remove` (Boolean) Remove intermediate containers after a successful build. Defaults to `true`. - `server_address` (String) the server address
- `tag` (List of String) Name and optionally a tag in the 'name:tag' format - `user_name` (String) the registry user name
- `target` (String) Set the target build stage to build
<a id="nestedblock--build--ulimit"></a>
### Nested Schema for `build.ulimit`
Required:
- `hard` (Number) soft limit
- `name` (String) type of ulimit, e.g. `nofile`
- `soft` (Number) hard limit

View file

@ -33,10 +33,10 @@ resource "docker_registry_image" "helloworld" {
### Optional ### Optional
- `build` (Block List, Max: 1) Definition for building the image (see [below for nested schema](#nestedblock--build)) - `build` (Block List, Max: 1, Deprecated) Definition for building the image (see [below for nested schema](#nestedblock--build))
- `insecure_skip_verify` (Boolean) If `true`, the verification of TLS certificates of the server/registry is disabled. Defaults to `false` - `insecure_skip_verify` (Boolean) If `true`, the verification of TLS certificates of the server/registry is disabled. Defaults to `false`
- `keep_remotely` (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. Defaults to `false` - `keep_remotely` (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. Defaults to `false`
- `triggers` (Map of String) A map of arbitrary strings that, when changed, will force the `docker_registry_image` resource to be replaced. This can be used to rebuild an image when contents of source code folders change or to repush a local image - `triggers` (Map of String) A map of arbitrary strings that, when changed, will force the `docker_registry_image` resource to be replaced. This can be used to repush a local image
### Read-Only ### Read-Only

View file

@ -96,8 +96,9 @@ func resourceDockerImage() *schema.Resource {
"path": { "path": {
Type: schema.TypeString, Type: schema.TypeString,
Description: "Context path", Description: "Context path",
Required: true, Optional: true,
ForceNew: true, ForceNew: true,
Deprecated: "Use context instead. This attribute will be removed in the next major release.",
}, },
"dockerfile": { "dockerfile": {
Type: schema.TypeString, Type: schema.TypeString,
@ -114,27 +115,12 @@ func resourceDockerImage() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
}, },
}, },
"force_remove": {
Type: schema.TypeBool,
Description: "Always remove intermediate containers",
Optional: true,
},
"remove": { "remove": {
Type: schema.TypeBool, Type: schema.TypeBool,
Description: "Remove intermediate containers after a successful build. Defaults to `true`.", Description: "Remove intermediate containers after a successful build. Defaults to `true`.",
Default: true, Default: true,
Optional: true, Optional: true,
}, },
"no_cache": {
Type: schema.TypeBool,
Description: "Do not use cache when building the image",
Optional: true,
},
"target": {
Type: schema.TypeString,
Description: "Set the target build stage to build",
Optional: true,
},
"build_arg": { "build_arg": {
Type: schema.TypeMap, Type: schema.TypeMap,
Description: "Set build-time variables", Description: "Set build-time variables",
@ -152,6 +138,270 @@ func resourceDockerImage() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
}, },
}, },
"suppress_output": {
Type: schema.TypeBool,
Description: "Suppress the build output and print image ID on success",
Optional: true,
ForceNew: true,
},
"remote_context": {
Type: schema.TypeString,
Description: "A Git repository URI or HTTP/HTTPS context URI",
Optional: true,
ForceNew: true,
},
"no_cache": {
Type: schema.TypeBool,
Description: "Do not use the cache when building the image",
Optional: true,
ForceNew: true,
},
"force_remove": {
Type: schema.TypeBool,
Description: "Always remove intermediate containers",
Optional: true,
ForceNew: true,
},
"pull_parent": {
Type: schema.TypeBool,
Description: "Attempt to pull the image even if an older image exists locally",
Optional: true,
ForceNew: true,
},
"isolation": {
Type: schema.TypeString,
Description: "Isolation represents the isolation technology of a container. The supported values are ",
Optional: true,
ForceNew: true,
},
"cpu_set_cpus": {
Type: schema.TypeString,
Description: "CPUs in which to allow execution (e.g., `0-3`, `0`, `1`)",
Optional: true,
ForceNew: true,
},
"cpu_set_mems": {
Type: schema.TypeString,
Description: "MEMs in which to allow execution (`0-3`, `0`, `1`)",
Optional: true,
ForceNew: true,
},
"cpu_shares": {
Type: schema.TypeInt,
Description: "CPU shares (relative weight)",
Optional: true,
ForceNew: true,
},
"cpu_quota": {
Type: schema.TypeInt,
Description: "Microseconds of CPU time that the container can get in a CPU period",
Optional: true,
ForceNew: true,
},
"cpu_period": {
Type: schema.TypeInt,
Description: "The length of a CPU period in microseconds",
Optional: true,
ForceNew: true,
},
"memory": {
Type: schema.TypeInt,
Description: "Set memory limit for build",
Optional: true,
ForceNew: true,
},
"memory_swap": {
Type: schema.TypeInt,
Description: "Total memory (memory + swap), -1 to enable unlimited swap",
Optional: true,
ForceNew: true,
},
"cgroup_parent": {
Type: schema.TypeString,
Description: "Optional parent cgroup for the container",
Optional: true,
ForceNew: true,
},
"network_mode": {
Type: schema.TypeString,
Description: "Set the networking mode for the RUN instructions during build",
Optional: true,
ForceNew: true,
},
"shm_size": {
Type: schema.TypeInt,
Description: "Size of /dev/shm in bytes. The size must be greater than 0",
Optional: true,
ForceNew: true,
},
"ulimit": {
Type: schema.TypeList,
Description: "Configuration for ulimits",
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "type of ulimit, e.g. `nofile`",
Required: true,
ForceNew: true,
},
"hard": {
Type: schema.TypeInt,
Description: "soft limit",
Required: true,
ForceNew: true,
},
"soft": {
Type: schema.TypeInt,
Description: "hard limit",
Required: true,
ForceNew: true,
},
},
},
},
"build_args": {
Type: schema.TypeMap,
Description: "Pairs for build-time variables in the form TODO",
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
Description: "The argument",
},
},
"auth_config": {
Type: schema.TypeList,
Description: "The configuration for the authentication",
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"host_name": {
Type: schema.TypeString,
Description: "hostname of the registry",
Required: true,
},
"user_name": {
Type: schema.TypeString,
Description: "the registry user name",
Optional: true,
},
"password": {
Type: schema.TypeString,
Description: "the registry password",
Optional: true,
},
"auth": {
Type: schema.TypeString,
Description: "the auth token",
Optional: true,
},
"email": {
Type: schema.TypeString,
Description: "the user emal",
Optional: true,
},
"server_address": {
Type: schema.TypeString,
Description: "the server address",
Optional: true,
},
"identity_token": {
Type: schema.TypeString,
Description: "the identity token",
Optional: true,
},
"registry_token": {
Type: schema.TypeString,
Description: "the registry token",
Optional: true,
},
},
},
},
"context": {
Type: schema.TypeString,
Description: "Value to specify the build context. Currently, only a `PATH` context is supported. You can use the helper function '${path.cwd}/context-dir'. Please see https://docs.docker.com/build/building/context/ for more information about build contexts.",
Optional: true,
ForceNew: true,
},
"labels": {
Type: schema.TypeMap,
Description: "User-defined key/value metadata",
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
Description: "The key/value pair",
},
},
"squash": {
Type: schema.TypeBool,
Description: "If true the new layers are squashed into a new image with a single new layer",
Optional: true,
ForceNew: true,
},
"cache_from": {
Type: schema.TypeList,
Description: "Images to consider as cache sources",
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
Description: "The image",
},
},
"security_opt": {
Type: schema.TypeList,
Description: "The security options",
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
Description: "The option",
},
},
"extra_hosts": {
Type: schema.TypeList,
Description: "A list of hostnames/IP mappings to add to the containers /etc/hosts file. Specified in the form [\"hostname:IP\"]",
Optional: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
Description: "",
},
},
"target": {
Type: schema.TypeString,
Description: "Set the target build stage to build",
Optional: true,
ForceNew: true,
},
"session_id": {
Type: schema.TypeString,
Description: "Set an ID for the build session",
Optional: true,
ForceNew: true,
},
"platform": {
Type: schema.TypeString,
Description: "Set platform if server is multi-platform capable",
Optional: true,
ForceNew: true,
},
"version": {
Type: schema.TypeString,
Description: "Version of the underlying builder to use",
Optional: true,
ForceNew: true,
},
"build_id": {
Type: schema.TypeString,
Description: "BuildID is an optional identifier that can be passed together with the build request. The same identifier can be used to gracefully cancel the build with the cancel request.",
Optional: true,
ForceNew: true,
},
}, },
}, },
}, },

View file

@ -325,9 +325,8 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag
err error err error
) )
buildOptions := types.ImageBuildOptions{} log.Printf("[DEBUG] Building docker image")
buildOptions := createImageBuildOptions(rawBuild)
buildOptions.Dockerfile = rawBuild["dockerfile"].(string)
tags := []string{imageName} tags := []string{imageName}
for _, t := range rawBuild["tag"].([]interface{}) { for _, t := range rawBuild["tag"].([]interface{}) {
@ -335,29 +334,26 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag
} }
buildOptions.Tags = tags buildOptions.Tags = tags
buildOptions.ForceRemove = rawBuild["force_remove"].(bool) // Supporting both "path" and "context" for backwards compatibility
buildOptions.Remove = rawBuild["remove"].(bool) // TODO: remove "path" in the next major release
buildOptions.NoCache = rawBuild["no_cache"].(bool) pathAttribute := rawBuild["path"].(string)
buildOptions.Target = rawBuild["target"].(string) contextAttribute := rawBuild["context"].(string)
if len(pathAttribute) == 0 && len(contextAttribute) == 0 {
buildArgs := make(map[string]*string) return errors.New("one of path or context must be configured")
for k, v := range rawBuild["build_arg"].(map[string]interface{}) {
val := v.(string)
buildArgs[k] = &val
} }
buildOptions.BuildArgs = buildArgs
log.Printf("[DEBUG] Build Args: %v\n", buildArgs)
labels := make(map[string]string) buildContext := ""
for k, v := range rawBuild["label"].(map[string]interface{}) { if len(pathAttribute) > 0 {
labels[k] = v.(string) // add existingAttribute to provider create API call
buildContext = pathAttribute
} else {
// add newAttribute to provider create API call
buildContext = contextAttribute
} }
buildOptions.Labels = labels // End backwards compatibility, remove until here
log.Printf("[DEBUG] Labels: %v\n", labels)
enableBuildKitIfSupported(ctx, client, &buildOptions) enableBuildKitIfSupported(ctx, client, &buildOptions)
buildCtx, relDockerfile, err := prepareBuildContext(rawBuild["path"].(string), buildOptions.Dockerfile) buildCtx, relDockerfile, err := prepareBuildContext(buildContext, buildOptions.Dockerfile)
if err != nil { if err != nil {
return err return err
} }

View file

@ -13,12 +13,115 @@ import (
"testing" "testing"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-units"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
) )
var contentDigestRegexp = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`) var contentDigestRegexp = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`)
func TestAccDockerRegistryImageResource_mapping(t *testing.T) {
assert := func(condition bool, msg string) {
if !condition {
t.Errorf("assertion failed: wrong build parameter %s", msg)
}
}
dummyProvider := New("dev")()
dummyResource := dummyProvider.ResourcesMap["docker_image"]
dummyResource.CreateContext = func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if value, ok := d.GetOk("build"); ok {
for _, rawBuild := range value.(*schema.Set).List() {
build := rawBuild.(map[string]interface{})
// build := d.Get("build").([]interface{})[0].(map[string]interface{})
options := createImageBuildOptions(build)
assert(options.SuppressOutput == true, "SuppressOutput")
assert(options.RemoteContext == "fooRemoteContext", "RemoteContext")
assert(options.NoCache == true, "NoCache")
assert(options.Remove == true, "Remove")
assert(options.ForceRemove == true, "ForceRemove")
assert(options.PullParent == true, "PullParent")
assert(options.Isolation == container.Isolation("hyperv"), "Isolation")
assert(options.CPUSetCPUs == "fooCpuSetCpus", "CPUSetCPUs")
assert(options.CPUSetMems == "fooCpuSetMems", "CPUSetMems")
assert(options.CPUShares == int64(4), "CPUShares")
assert(options.CPUQuota == int64(5), "CPUQuota")
assert(options.CPUPeriod == int64(6), "CPUPeriod")
assert(options.Memory == int64(1), "Memory")
assert(options.MemorySwap == int64(2), "MemorySwap")
assert(options.CgroupParent == "fooCgroupParent", "CgroupParent")
assert(options.NetworkMode == "fooNetworkMode", "NetworkMode")
assert(options.ShmSize == int64(3), "ShmSize")
assert(options.Dockerfile == "fooDockerfile", "Dockerfile")
assert(len(options.Ulimits) == 1, "Ulimits")
assert(reflect.DeepEqual(*options.Ulimits[0], units.Ulimit{
Name: "foo",
Hard: int64(1),
Soft: int64(2),
}), "Ulimits")
assert(len(options.BuildArgs) == 1, "BuildArgs")
// DevSkim: ignore DS137138
assert(*options.BuildArgs["HTTP_PROXY"] == "http://10.20.30.2:1234", "BuildArgs")
assert(len(options.AuthConfigs) == 1, "AuthConfigs")
assert(reflect.DeepEqual(options.AuthConfigs["foo.host"], types.AuthConfig{
Username: "fooUserName",
Password: "fooPassword",
Auth: "fooAuth",
Email: "fooEmail",
ServerAddress: "fooServerAddress",
IdentityToken: "fooIdentityToken",
RegistryToken: "fooRegistryToken",
}), "AuthConfigs")
assert(reflect.DeepEqual(options.Labels, map[string]string{"foo": "bar"}), "Labels")
assert(options.Squash == true, "Squash")
assert(reflect.DeepEqual(options.CacheFrom, []string{"fooCacheFrom", "barCacheFrom"}), "CacheFrom")
assert(reflect.DeepEqual(options.SecurityOpt, []string{"fooSecurityOpt", "barSecurityOpt"}), "SecurityOpt")
assert(reflect.DeepEqual(options.ExtraHosts, []string{"fooExtraHost", "barExtraHost"}), "ExtraHosts")
assert(options.Target == "fooTarget", "Target")
assert(options.SessionID == "fooSessionId", "SessionID")
assert(options.Platform == "fooPlatform", "Platform")
assert(options.Version == types.BuilderVersion("1"), "Version")
assert(options.BuildID == "fooBuildId", "BuildID")
// output
d.SetId("foo")
}
}
return nil
}
dummyResource.UpdateContext = func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return nil
}
dummyResource.DeleteContext = func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return nil
}
dummyResource.ReadContext = func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
d.Set("id", "foo")
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: map[string]func() (*schema.Provider, error){
"docker": func() (*schema.Provider, error) {
return dummyProvider, nil
},
},
Steps: []resource.TestStep{
{
Config: loadTestConfiguration(t, RESOURCE, "docker_image", "testBuildDockerImageMappingConfig"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_image.foo", "id"),
),
},
},
})
}
func TestAccDockerImage_basic(t *testing.T) { func TestAccDockerImage_basic(t *testing.T) {
// run a Docker container which refers the Docker image to test "force_remove" option // run a Docker container which refers the Docker image to test "force_remove" option
containerName := "test-docker-image-force-remove" containerName := "test-docker-image-force-remove"
@ -396,6 +499,113 @@ func TestAccDockerImage_buildOutsideContext(t *testing.T) {
}) })
} }
func TestAccDockerImageResource_build(t *testing.T) {
name := "tftest-dockerregistryimage:1.0"
wd, _ := os.Getwd()
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_context")), "\\", "\\\\")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_image", "testBuildDockerImageNoKeepConfig"), name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_image.foo", "image_id"),
),
},
},
})
}
// Test for https://github.com/kreuzwerker/terraform-provider-docker/issues/249
func TestAccDockerImageResource_whitelistDockerignore(t *testing.T) {
name := "tftest-dockerregistryimage-whitelistdockerignore:1.0"
wd, _ := os.Getwd()
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_file_whitelist_dockerignore")), "\\", "\\\\")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_image", "testDockerImageFilePermissions"), name, context, "Dockerfile"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_image.file_permissions", "image_id"),
),
},
},
})
}
// Tests for issue https://github.com/kreuzwerker/terraform-provider-docker/issues/293
// First we check if we can build the docker_registry_image resource at all
// TODO in a second step we want to check whether the file has the correct permissions
func TestAccDockerImageResource_correctFilePermissions(t *testing.T) {
name := "tftest-dockerregistryimage-filepermissions:1.0"
wd, _ := os.Getwd()
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_file_permissions")), "\\", "\\\\")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_image", "testDockerImageFilePermissions"), name, context, "Dockerfile"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_image.file_permissions", "image_id"),
),
// TODO another check which starts the the newly built docker image and checks the file permissions to see if they are correct
},
},
})
}
func TestAccDockerImageResource_buildWithDockerignore(t *testing.T) {
name := "tftest-dockerregistryimage-ignore:1.0"
wd, _ := os.Getwd()
ctx := context.Background()
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_context_dockerignore")), "\\", "\\\\")
ignoredFile := context + "/to_be_ignored"
expectedSha := ""
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_image", "testBuildDockerImageNoKeepJustCache"), "one", name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_image.one", "image_id"),
resource.TestCheckResourceAttrWith("docker_image.one", "image_id", func(value string) error {
expectedSha = value
return nil
}),
),
},
{
PreConfig: func() {
// create a file that should be ignored
f, err := os.OpenFile(ignoredFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic("failed to create test file")
}
f.Close()
},
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_image", "testBuildDockerImageNoKeepJustCache"), "two", name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrWith("docker_image.two", "image_id", func(value string) error {
if value != expectedSha {
return fmt.Errorf("Image sha256_digest changed, expected %#v, got %#v", expectedSha, value)
}
return nil
}),
),
},
},
CheckDestroy: func(state *terraform.State) error {
return testAccDockerImageDestroy(ctx, state)
},
})
}
func testAccImageCreated(resourceName string, image *types.ImageInspect) resource.TestCheckFunc { func testAccImageCreated(resourceName string, image *types.ImageInspect) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
ctx := context.Background() ctx := context.Background()

View file

@ -42,6 +42,7 @@ func resourceDockerRegistryImage() *schema.Resource {
Description: "Definition for building the image", Description: "Definition for building the image",
Optional: true, Optional: true,
MaxItems: 1, MaxItems: 1,
Deprecated: "Use `docker_image` resource instead. This field will be removed in the next major release.",
Elem: &schema.Resource{ Elem: &schema.Resource{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"suppress_output": { "suppress_output": {
@ -333,7 +334,7 @@ func resourceDockerRegistryImage() *schema.Resource {
}, },
"triggers": { "triggers": {
Description: "A map of arbitrary strings that, when changed, will force the `docker_registry_image` resource to be replaced. This can be used to rebuild an image when contents of source code folders change or to repush a local image", Description: "A map of arbitrary strings that, when changed, will force the `docker_registry_image` resource to be replaced. This can be used to repush a local image",
Type: schema.TypeMap, Type: schema.TypeMap,
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,

View file

@ -1,137 +1,17 @@
package provider package provider
import ( import (
"context"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"regexp" "regexp"
"strings" "strings"
"testing" "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/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
) )
func TestAccDockerRegistryImageResource_mapping(t *testing.T) {
assert := func(condition bool, msg string) {
if !condition {
t.Errorf("assertion failed: wrong build parameter %s", msg)
}
}
dummyProvider := New("dev")()
dummyResource := dummyProvider.ResourcesMap["docker_registry_image"]
dummyResource.CreateContext = func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
build := d.Get("build").([]interface{})[0].(map[string]interface{})
options := createImageBuildOptions(build)
assert(options.SuppressOutput == true, "SuppressOutput")
assert(options.RemoteContext == "fooRemoteContext", "RemoteContext")
assert(options.NoCache == true, "NoCache")
assert(options.Remove == true, "Remove")
assert(options.ForceRemove == true, "ForceRemove")
assert(options.PullParent == true, "PullParent")
assert(options.Isolation == container.Isolation("hyperv"), "Isolation")
assert(options.CPUSetCPUs == "fooCpuSetCpus", "CPUSetCPUs")
assert(options.CPUSetMems == "fooCpuSetMems", "CPUSetMems")
assert(options.CPUShares == int64(4), "CPUShares")
assert(options.CPUQuota == int64(5), "CPUQuota")
assert(options.CPUPeriod == int64(6), "CPUPeriod")
assert(options.Memory == int64(1), "Memory")
assert(options.MemorySwap == int64(2), "MemorySwap")
assert(options.CgroupParent == "fooCgroupParent", "CgroupParent")
assert(options.NetworkMode == "fooNetworkMode", "NetworkMode")
assert(options.ShmSize == int64(3), "ShmSize")
assert(options.Dockerfile == "fooDockerfile", "Dockerfile")
assert(len(options.Ulimits) == 1, "Ulimits")
assert(reflect.DeepEqual(*options.Ulimits[0], units.Ulimit{
Name: "foo",
Hard: int64(1),
Soft: int64(2),
}), "Ulimits")
assert(len(options.BuildArgs) == 1, "BuildArgs")
// DevSkim: ignore DS137138
assert(*options.BuildArgs["HTTP_PROXY"] == "http://10.20.30.2:1234", "BuildArgs")
assert(len(options.AuthConfigs) == 1, "AuthConfigs")
assert(reflect.DeepEqual(options.AuthConfigs["foo.host"], types.AuthConfig{
Username: "fooUserName",
Password: "fooPassword",
Auth: "fooAuth",
Email: "fooEmail",
ServerAddress: "fooServerAddress",
IdentityToken: "fooIdentityToken",
RegistryToken: "fooRegistryToken",
}), "AuthConfigs")
assert(reflect.DeepEqual(options.Labels, map[string]string{"foo": "bar"}), "Labels")
assert(options.Squash == true, "Squash")
assert(reflect.DeepEqual(options.CacheFrom, []string{"fooCacheFrom", "barCacheFrom"}), "CacheFrom")
assert(reflect.DeepEqual(options.SecurityOpt, []string{"fooSecurityOpt", "barSecurityOpt"}), "SecurityOpt")
assert(reflect.DeepEqual(options.ExtraHosts, []string{"fooExtraHost", "barExtraHost"}), "ExtraHosts")
assert(options.Target == "fooTarget", "Target")
assert(options.SessionID == "fooSessionId", "SessionID")
assert(options.Platform == "fooPlatform", "Platform")
assert(options.Version == types.BuilderVersion("1"), "Version")
assert(options.BuildID == "fooBuildId", "BuildID")
// output
d.SetId("foo")
d.Set("sha256_digest", "bar")
return nil
}
dummyResource.UpdateContext = func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return nil
}
dummyResource.DeleteContext = func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return nil
}
dummyResource.ReadContext = func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
d.Set("sha256_digest", "bar")
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: map[string]func() (*schema.Provider, error){
"docker": func() (*schema.Provider, error) {
return dummyProvider, nil
},
},
Steps: []resource.TestStep{
{
Config: loadTestConfiguration(t, RESOURCE, "docker_registry_image", "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")
wd, _ := os.Getwd()
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_context")), "\\", "\\\\")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_registry_image", "testBuildDockerRegistryImageNoKeepConfig"), pushOptions.Registry, pushOptions.Name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_registry_image.foo", "sha256_digest"),
),
},
},
CheckDestroy: testDockerRegistryImageNotInRegistry(pushOptions),
})
}
func TestAccDockerRegistryImageResource_build_insecure_registry(t *testing.T) { func TestAccDockerRegistryImageResource_build_insecure_registry(t *testing.T) {
pushOptions := createPushImageOptions("127.0.0.1:15001/tftest-dockerregistryimage:1.0") pushOptions := createPushImageOptions("127.0.0.1:15001/tftest-dockerregistryimage:1.0")
wd, _ := os.Getwd() wd, _ := os.Getwd()
@ -173,125 +53,6 @@ func TestAccDockerRegistryImageResource_buildAndKeep(t *testing.T) {
}) })
} }
func TestAccDockerRegistryImageResource_buildWithDockerignore(t *testing.T) {
pushOptions := createPushImageOptions("127.0.0.1:15000/tftest-dockerregistryimage-ignore:1.0")
wd, _ := os.Getwd()
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_context_dockerignore")), "\\", "\\\\")
ignoredFile := context + "/to_be_ignored"
expectedSha := ""
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_registry_image", "testBuildDockerRegistryImageNoKeepJustCache"), pushOptions.Registry, "one", pushOptions.Name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_registry_image.one", "sha256_digest"),
resource.TestCheckResourceAttrWith("docker_registry_image.one", "sha256_digest", func(value string) error {
expectedSha = value
return nil
}),
),
},
{
PreConfig: func() {
// create a file that should be ignored
f, err := os.OpenFile(ignoredFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic("failed to create test file")
}
f.Close()
},
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_registry_image", "testBuildDockerRegistryImageNoKeepJustCache"), pushOptions.Registry, "two", pushOptions.Name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrWith("docker_registry_image.two", "sha256_digest", func(value string) error {
if value != expectedSha {
return fmt.Errorf("Image sha256_digest changed, expected %#v, got %#v", expectedSha, value)
}
return nil
}),
),
},
},
CheckDestroy: resource.ComposeTestCheckFunc(
testDockerRegistryImageNotInRegistry(pushOptions),
func(*terraform.State) error {
err := os.Remove(ignoredFile)
if err != nil {
return fmt.Errorf("failed to remove ignored file: %w", err)
}
return nil
},
),
})
}
// Tests for issue https://github.com/kreuzwerker/terraform-provider-docker/issues/293
// First we check if we can build the docker_registry_image resource at all
// TODO in a second step we want to check whether the file has the correct permissions
func TestAccDockerRegistryImageResource_correctFilePermissions(t *testing.T) {
pushOptions := createPushImageOptions("127.0.0.1:15000/tftest-dockerregistryimage-filepermissions:1.0")
wd, _ := os.Getwd()
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_file_permissions")), "\\", "\\\\")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_registry_image", "testDockerRegistryImageFilePermissions"), pushOptions.Registry, pushOptions.Name, context, "Dockerfile"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_registry_image.file_permissions", "sha256_digest"),
),
// TODO another check which starts the the newly built docker image and checks the file permissions to see if they are correct
},
},
})
}
// Test for https://github.com/kreuzwerker/terraform-provider-docker/issues/249
func TestAccDockerRegistryImageResource_whitelistDockerignore(t *testing.T) {
pushOptions := createPushImageOptions("127.0.0.1:15000/tftest-dockerregistryimage-whitelistdockerignore:1.0")
wd, _ := os.Getwd()
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_file_whitelist_dockerignore")), "\\", "\\\\")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_registry_image", "testDockerRegistryImageFilePermissions"), pushOptions.Registry, pushOptions.Name, context, "Dockerfile"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_registry_image.file_permissions", "sha256_digest"),
),
},
},
})
}
// Test for https://github.com/kreuzwerker/terraform-provider-docker/issues/249
func TestAccDockerRegistryImageResource_DockerfileOutsideContext(t *testing.T) {
pushOptions := createPushImageOptions("127.0.0.1:15000/tftest-dockerregistryimage-dockerfileoutsidecontext:1.0")
wd, _ := os.Getwd()
dfPath := filepath.Join(wd, "..", "Dockerfile")
if err := ioutil.WriteFile(dfPath, []byte(testDockerFileExample), 0o644); err != nil {
t.Fatalf("failed to create a Dockerfile %s for test: %+v", dfPath, err)
}
defer os.Remove(dfPath)
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_file_permissions")), "\\", "\\\\")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_registry_image", "testDockerRegistryImageDockerfileOutsideContext"), pushOptions.Registry, pushOptions.Name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_registry_image.outside_context", "sha256_digest"),
),
},
},
})
}
func TestAccDockerRegistryImageResource_pushMissingImage(t *testing.T) { func TestAccDockerRegistryImageResource_pushMissingImage(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },

View file

@ -0,0 +1,14 @@
provider "docker" {
alias = "private"
}
resource "docker_image" "foo" {
provider = "docker.private"
name = "%s"
build {
context = "%s"
remove = true
force_remove = true
no_cache = true
}
}

View file

@ -1,6 +1,5 @@
resource "docker_registry_image" "foo" { resource "docker_image" "foo" {
name = "localhost:15000/foo:1.0" name = "localhost:15000/foo:1.0"
insecure_skip_verify = true
build { build {
suppress_output = true suppress_output = true
remote_context = "fooRemoteContext" remote_context = "fooRemoteContext"

View file

@ -0,0 +1,14 @@
provider "docker" {
alias = "private"
}
resource "docker_image" "foo" {
provider = "docker.private"
name = "%s"
build {
context = "%s"
remove = true
force_remove = true
no_cache = true
}
}

View file

@ -0,0 +1,14 @@
provider "docker" {
alias = "private"
}
resource "docker_image" "%s" {
provider = "docker.private"
name = "%s"
build {
context = "%s"
remove = false
force_remove = false
no_cache = false
}
}

View file

@ -1,14 +1,10 @@
provider "docker" { provider "docker" {
alias = "private" alias = "private"
registry_auth {
address = "%s"
}
} }
resource "docker_registry_image" "file_permissions" { resource "docker_image" "file_permissions" {
provider = "docker.private" provider = "docker.private"
name = "%s" name = "%s"
insecure_skip_verify = true
build { build {
context = "%s" context = "%s"

View file

@ -4,16 +4,18 @@ provider "docker" {
address = "%s" address = "%s"
} }
} }
resource "docker_registry_image" "foo" {
provider = "docker.private"
name = "%s"
insecure_skip_verify = true
keep_remotely = true
resource "docker_image" "foo_image" {
provider = "docker.private"
name = "%s"
build { build {
context = "%s" context = "%s"
remove = true
force_remove = true
no_cache = true
} }
} }
resource "docker_registry_image" "foo" {
provider = "docker.private"
name = docker_image.foo_image.name
insecure_skip_verify = true
keep_remotely = true
}

View file

@ -4,15 +4,17 @@ provider "docker" {
address = "%s" address = "%s"
} }
} }
resource "docker_registry_image" "foo" {
provider = "docker.private"
name = "%s"
insecure_skip_verify = true
resource "docker_image" "foo_image" {
provider = "docker.private"
name = "%s"
build { build {
context = "%s" context = "%s"
remove = true
force_remove = true
no_cache = true
} }
} }
resource "docker_registry_image" "foo" {
provider = "docker.private"
name = docker_image.foo_image.name
insecure_skip_verify = true
}