From 661c6628ff4df1f838d125f267bd1e60a186402f Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 5 Jan 2023 13:27:40 +0100 Subject: [PATCH] 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. --- docs/resources/image.md | 69 ++++- docs/resources/registry_image.md | 4 +- internal/provider/resource_docker_image.go | 284 ++++++++++++++++-- .../provider/resource_docker_image_funcs.go | 38 ++- .../provider/resource_docker_image_test.go | 210 +++++++++++++ .../resource_docker_registry_image.go | 3 +- .../resource_docker_registry_image_test.go | 239 --------------- .../testBuildDockerImageKeepConfig.tf | 14 + .../testBuildDockerImageMappingConfig.tf} | 3 +- .../testBuildDockerImageNoKeepConfig.tf | 14 + .../testBuildDockerImageNoKeepJustCache.tf | 14 + .../testDockerImageFilePermissions.tf} | 6 +- .../testBuildDockerRegistryImageKeepConfig.tf | 20 +- ...estBuildDockerRegistryImageNoKeepConfig.tf | 18 +- 14 files changed, 623 insertions(+), 313 deletions(-) create mode 100644 testdata/resources/docker_image/testBuildDockerImageKeepConfig.tf rename testdata/resources/{docker_registry_image/testBuildDockerRegistryImageMappingConfig.tf => docker_image/testBuildDockerImageMappingConfig.tf} (95%) create mode 100644 testdata/resources/docker_image/testBuildDockerImageNoKeepConfig.tf create mode 100644 testdata/resources/docker_image/testBuildDockerImageNoKeepJustCache.tf rename testdata/resources/{docker_registry_image/testDockerRegistryImageFilePermissions.tf => docker_image/testDockerImageFilePermissions.tf} (59%) diff --git a/docs/resources/image.md b/docs/resources/image.md index 666c5552..477e1d7c 100644 --- a/docs/resources/image.md +++ b/docs/resources/image.md @@ -104,17 +104,68 @@ resource "docker_image" "zoo" { ### 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 container’s /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 + + +### Nested Schema for `build.auth_config` + Required: -- `path` (String) Context path +- `host_name` (String) hostname of the registry Optional: -- `build_arg` (Map of String) Set build-time variables -- `dockerfile` (String) Name of the Dockerfile. Defaults to `Dockerfile`. -- `force_remove` (Boolean) Always remove intermediate containers -- `label` (Map of String) Set metadata for an image -- `no_cache` (Boolean) Do not use cache when building the image -- `remove` (Boolean) Remove intermediate containers after a successful build. Defaults to `true`. -- `tag` (List of String) Name and optionally a tag in the 'name:tag' format -- `target` (String) Set the target build stage to build +- `auth` (String) the auth token +- `email` (String) the user emal +- `identity_token` (String) the identity token +- `password` (String) the registry password +- `registry_token` (String) the registry token +- `server_address` (String) the server address +- `user_name` (String) the registry user name + + + +### Nested Schema for `build.ulimit` + +Required: + +- `hard` (Number) soft limit +- `name` (String) type of ulimit, e.g. `nofile` +- `soft` (Number) hard limit diff --git a/docs/resources/registry_image.md b/docs/resources/registry_image.md index 1394deb4..bebf9bc5 100644 --- a/docs/resources/registry_image.md +++ b/docs/resources/registry_image.md @@ -33,10 +33,10 @@ resource "docker_registry_image" "helloworld" { ### 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` - `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 diff --git a/internal/provider/resource_docker_image.go b/internal/provider/resource_docker_image.go index 7f572ae7..768d9565 100644 --- a/internal/provider/resource_docker_image.go +++ b/internal/provider/resource_docker_image.go @@ -96,8 +96,9 @@ func resourceDockerImage() *schema.Resource { "path": { Type: schema.TypeString, Description: "Context path", - Required: true, + Optional: true, ForceNew: true, + Deprecated: "Use context instead. This attribute will be removed in the next major release.", }, "dockerfile": { Type: schema.TypeString, @@ -114,27 +115,12 @@ func resourceDockerImage() *schema.Resource { Type: schema.TypeString, }, }, - "force_remove": { - Type: schema.TypeBool, - Description: "Always remove intermediate containers", - Optional: true, - }, "remove": { 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, 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": { Type: schema.TypeMap, Description: "Set build-time variables", @@ -152,6 +138,270 @@ func resourceDockerImage() *schema.Resource { 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 container’s /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, + }, }, }, }, diff --git a/internal/provider/resource_docker_image_funcs.go b/internal/provider/resource_docker_image_funcs.go index e6adf67f..8c6d0e0b 100644 --- a/internal/provider/resource_docker_image_funcs.go +++ b/internal/provider/resource_docker_image_funcs.go @@ -325,9 +325,8 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag err error ) - buildOptions := types.ImageBuildOptions{} - - buildOptions.Dockerfile = rawBuild["dockerfile"].(string) + log.Printf("[DEBUG] Building docker image") + buildOptions := createImageBuildOptions(rawBuild) tags := []string{imageName} for _, t := range rawBuild["tag"].([]interface{}) { @@ -335,29 +334,26 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag } buildOptions.Tags = tags - buildOptions.ForceRemove = rawBuild["force_remove"].(bool) - buildOptions.Remove = rawBuild["remove"].(bool) - buildOptions.NoCache = rawBuild["no_cache"].(bool) - buildOptions.Target = rawBuild["target"].(string) - - buildArgs := make(map[string]*string) - for k, v := range rawBuild["build_arg"].(map[string]interface{}) { - val := v.(string) - buildArgs[k] = &val + // Supporting both "path" and "context" for backwards compatibility + // TODO: remove "path" in the next major release + pathAttribute := rawBuild["path"].(string) + contextAttribute := rawBuild["context"].(string) + if len(pathAttribute) == 0 && len(contextAttribute) == 0 { + return errors.New("one of path or context must be configured") } - buildOptions.BuildArgs = buildArgs - log.Printf("[DEBUG] Build Args: %v\n", buildArgs) - labels := make(map[string]string) - for k, v := range rawBuild["label"].(map[string]interface{}) { - labels[k] = v.(string) + buildContext := "" + if len(pathAttribute) > 0 { + // add existingAttribute to provider create API call + buildContext = pathAttribute + } else { + // add newAttribute to provider create API call + buildContext = contextAttribute } - buildOptions.Labels = labels - log.Printf("[DEBUG] Labels: %v\n", labels) - + // End backwards compatibility, remove until here enableBuildKitIfSupported(ctx, client, &buildOptions) - buildCtx, relDockerfile, err := prepareBuildContext(rawBuild["path"].(string), buildOptions.Dockerfile) + buildCtx, relDockerfile, err := prepareBuildContext(buildContext, buildOptions.Dockerfile) if err != nil { return err } diff --git a/internal/provider/resource_docker_image_test.go b/internal/provider/resource_docker_image_test.go index 57be2157..cde3ec7d 100644 --- a/internal/provider/resource_docker_image_test.go +++ b/internal/provider/resource_docker_image_test.go @@ -13,12 +13,115 @@ import ( "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/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) 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) { // run a Docker container which refers the Docker image to test "force_remove" option 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 { return func(s *terraform.State) error { ctx := context.Background() diff --git a/internal/provider/resource_docker_registry_image.go b/internal/provider/resource_docker_registry_image.go index 1fc40348..66a68c58 100644 --- a/internal/provider/resource_docker_registry_image.go +++ b/internal/provider/resource_docker_registry_image.go @@ -42,6 +42,7 @@ func resourceDockerRegistryImage() *schema.Resource { Description: "Definition for building the image", Optional: true, MaxItems: 1, + Deprecated: "Use `docker_image` resource instead. This field will be removed in the next major release.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "suppress_output": { @@ -333,7 +334,7 @@ func resourceDockerRegistryImage() *schema.Resource { }, "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, Optional: true, ForceNew: true, diff --git a/internal/provider/resource_docker_registry_image_test.go b/internal/provider/resource_docker_registry_image_test.go index 975df5c5..575a83c4 100644 --- a/internal/provider/resource_docker_registry_image_test.go +++ b/internal/provider/resource_docker_registry_image_test.go @@ -1,137 +1,17 @@ package provider import ( - "context" "fmt" - "io/ioutil" "os" "path/filepath" - "reflect" "regexp" "strings" "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/schema" "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) { pushOptions := createPushImageOptions("127.0.0.1:15001/tftest-dockerregistryimage:1.0") 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) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, diff --git a/testdata/resources/docker_image/testBuildDockerImageKeepConfig.tf b/testdata/resources/docker_image/testBuildDockerImageKeepConfig.tf new file mode 100644 index 00000000..d68c7a15 --- /dev/null +++ b/testdata/resources/docker_image/testBuildDockerImageKeepConfig.tf @@ -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 + } +} diff --git a/testdata/resources/docker_registry_image/testBuildDockerRegistryImageMappingConfig.tf b/testdata/resources/docker_image/testBuildDockerImageMappingConfig.tf similarity index 95% rename from testdata/resources/docker_registry_image/testBuildDockerRegistryImageMappingConfig.tf rename to testdata/resources/docker_image/testBuildDockerImageMappingConfig.tf index 0ca9a067..53787491 100644 --- a/testdata/resources/docker_registry_image/testBuildDockerRegistryImageMappingConfig.tf +++ b/testdata/resources/docker_image/testBuildDockerImageMappingConfig.tf @@ -1,6 +1,5 @@ -resource "docker_registry_image" "foo" { +resource "docker_image" "foo" { name = "localhost:15000/foo:1.0" - insecure_skip_verify = true build { suppress_output = true remote_context = "fooRemoteContext" diff --git a/testdata/resources/docker_image/testBuildDockerImageNoKeepConfig.tf b/testdata/resources/docker_image/testBuildDockerImageNoKeepConfig.tf new file mode 100644 index 00000000..d68c7a15 --- /dev/null +++ b/testdata/resources/docker_image/testBuildDockerImageNoKeepConfig.tf @@ -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 + } +} diff --git a/testdata/resources/docker_image/testBuildDockerImageNoKeepJustCache.tf b/testdata/resources/docker_image/testBuildDockerImageNoKeepJustCache.tf new file mode 100644 index 00000000..25d1bfcb --- /dev/null +++ b/testdata/resources/docker_image/testBuildDockerImageNoKeepJustCache.tf @@ -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 + } +} diff --git a/testdata/resources/docker_registry_image/testDockerRegistryImageFilePermissions.tf b/testdata/resources/docker_image/testDockerImageFilePermissions.tf similarity index 59% rename from testdata/resources/docker_registry_image/testDockerRegistryImageFilePermissions.tf rename to testdata/resources/docker_image/testDockerImageFilePermissions.tf index d1d35565..e97d29a0 100644 --- a/testdata/resources/docker_registry_image/testDockerRegistryImageFilePermissions.tf +++ b/testdata/resources/docker_image/testDockerImageFilePermissions.tf @@ -1,14 +1,10 @@ provider "docker" { alias = "private" - registry_auth { - address = "%s" - } } -resource "docker_registry_image" "file_permissions" { +resource "docker_image" "file_permissions" { provider = "docker.private" name = "%s" - insecure_skip_verify = true build { context = "%s" diff --git a/testdata/resources/docker_registry_image/testBuildDockerRegistryImageKeepConfig.tf b/testdata/resources/docker_registry_image/testBuildDockerRegistryImageKeepConfig.tf index 0553e1c6..310d49c6 100644 --- a/testdata/resources/docker_registry_image/testBuildDockerRegistryImageKeepConfig.tf +++ b/testdata/resources/docker_registry_image/testBuildDockerRegistryImageKeepConfig.tf @@ -4,16 +4,18 @@ provider "docker" { 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 { - context = "%s" - remove = true - force_remove = true - no_cache = true + context = "%s" } } + +resource "docker_registry_image" "foo" { + provider = "docker.private" + name = docker_image.foo_image.name + insecure_skip_verify = true + keep_remotely = true +} diff --git a/testdata/resources/docker_registry_image/testBuildDockerRegistryImageNoKeepConfig.tf b/testdata/resources/docker_registry_image/testBuildDockerRegistryImageNoKeepConfig.tf index 9c37bd0f..63108b94 100644 --- a/testdata/resources/docker_registry_image/testBuildDockerRegistryImageNoKeepConfig.tf +++ b/testdata/resources/docker_registry_image/testBuildDockerRegistryImageNoKeepConfig.tf @@ -4,15 +4,17 @@ provider "docker" { 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 { - context = "%s" - remove = true - force_remove = true - no_cache = true + context = "%s" } } + +resource "docker_registry_image" "foo" { + provider = "docker.private" + name = docker_image.foo_image.name + insecure_skip_verify = true +}