diff --git a/internal/provider/resource_docker_image_funcs.go b/internal/provider/resource_docker_image_funcs.go index 92149bc4..e369efaf 100644 --- a/internal/provider/resource_docker_image_funcs.go +++ b/internal/provider/resource_docker_image_funcs.go @@ -9,6 +9,7 @@ import ( "io" "log" "net" + "os" "path/filepath" "strings" @@ -22,6 +23,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/mitchellh/go-homedir" "github.com/moby/buildkit/session" + "github.com/pkg/errors" ) const minBuildkitDockerVersion = "1.39" @@ -293,6 +295,10 @@ func findImage(ctx context.Context, imageName string, client *client.Client, aut } func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imageName string, client *client.Client) error { + var ( + err error + ) + buildOptions := types.ImageBuildOptions{} buildOptions.Dockerfile = rawBuild["dockerfile"].(string) @@ -323,11 +329,32 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag buildOptions.Labels = labels log.Printf("[DEBUG] Labels: %v\n", labels) + enableBuildKitIfSupported(ctx, client, &buildOptions) + + buildCtx, relDockerfile, err := prepareBuildContext(rawBuild["path"].(string), buildOptions.Dockerfile) + if err != nil { + return err + } + buildOptions.Dockerfile = relDockerfile + + var response types.ImageBuildResponse + response, err = client.ImageBuild(ctx, buildCtx, buildOptions) + if err != nil { + return err + } + defer response.Body.Close() + + buildResult, err := decodeBuildMessages(response) + if err != nil { + return fmt.Errorf("%s\n\n%s", err, buildResult) + } + return nil +} + +func enableBuildKitIfSupported(ctx context.Context, client *client.Client, buildOptions *types.ImageBuildOptions) { dockerClientVersion := client.ClientVersion() log.Printf("[DEBUG] DockerClientVersion: %v, minBuildKitDockerVersion: %v\n", dockerClientVersion, minBuildkitDockerVersion) - if versions.GreaterThanOrEqualTo(dockerClientVersion, minBuildkitDockerVersion) { - // docker client supports BuildKit log.Printf("[DEBUG] Enabling BuildKit") s, _ := session.NewSession(ctx, "docker-provider", "") dialSession := func(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error) { @@ -341,28 +368,50 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag } else { buildOptions.Version = types.BuilderV1 } - contextDir := rawBuild["path"].(string) - excludes, err := build.ReadDockerignore(contextDir) - if err != nil { - return err - } - excludes = build.TrimBuildFilesFromExcludes(excludes, buildOptions.Dockerfile, false) - - var response types.ImageBuildResponse - response, err = client.ImageBuild(ctx, getBuildContext(contextDir, excludes), buildOptions) - if err != nil { - return err - } - defer response.Body.Close() - - buildResult, err := decodeBuildMessages(response) - if err != nil { - return fmt.Errorf("%s\n\n%s", err, buildResult) - } - return nil } -func getBuildContext(filePath string, excludes []string) io.Reader { +func prepareBuildContext(specifiedContext string, specifiedDockerfile string) (io.ReadCloser, string, error) { + var ( + dockerfileCtx io.ReadCloser + contextDir string + relDockerfile string + err error + ) + contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, specifiedDockerfile) + log.Printf("[DEBUG] contextDir %s", contextDir) + log.Printf("[DEBUG] relDockerfile %s", relDockerfile) + if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { + // Dockerfile is outside of build-context; read the Dockerfile and pass it as dockerfileCtx + log.Printf("[DEBUG] Dockerfile is outside of build-context") + dockerfileCtx, err = os.Open(specifiedDockerfile) + if err != nil { + return nil, "", errors.Errorf("unable to open Dockerfile: %v", err) + } + defer dockerfileCtx.Close() + } + excludes, err := build.ReadDockerignore(contextDir) + if err != nil { + return nil, "", err + } + + specifiedDockerfile = archive.CanonicalTarNameForPath(specifiedDockerfile) + excludes = build.TrimBuildFilesFromExcludes(excludes, specifiedDockerfile, false) + log.Printf("[DEBUG] Excludes: %v", excludes) + buildCtx := getBuildContext(contextDir, excludes) + + // replace Dockerfile if it was added from stdin or a file outside the build-context, and there is archive context + if dockerfileCtx != nil && buildCtx != nil { + log.Printf("[DEBUG] Adding dockerfile to build context") + buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx) + if err != nil { + return nil, "", err + } + return buildCtx, relDockerfile, nil + } + return buildCtx, specifiedDockerfile, nil +} + +func getBuildContext(filePath string, excludes []string) io.ReadCloser { filePath, _ = homedir.Expand(filePath) //TarWithOptions works only with absolute paths in Windows. filePath, err := filepath.Abs(filePath) diff --git a/internal/provider/resource_docker_image_test.go b/internal/provider/resource_docker_image_test.go index 7f705dad..84b05288 100644 --- a/internal/provider/resource_docker_image_test.go +++ b/internal/provider/resource_docker_image_test.go @@ -324,6 +324,32 @@ RUN echo ${test_arg} > test_arg.txt RUN apt-get update -qq ` +// Test for implementation of https://github.com/kreuzwerker/terraform-provider-docker/issues/401 +func TestAccDockerImage_buildOutsideContext(t *testing.T) { + ctx := context.Background() + 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) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: func(state *terraform.State) error { + return testAccDockerImageDestroy(ctx, state) + }, + Steps: []resource.TestStep{ + { + Config: loadTestConfiguration(t, RESOURCE, "docker_image", "testDockerImageDockerfileOutsideContext"), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("docker_image.outside_context", "name", regexp.MustCompile(`\Aoutside-context:latest\z`)), + ), + }, + }, + }) +} + 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_funcs.go b/internal/provider/resource_docker_registry_image_funcs.go index 829fbb3d..a3afa49b 100644 --- a/internal/provider/resource_docker_registry_image_funcs.go +++ b/internal/provider/resource_docker_registry_image_funcs.go @@ -248,13 +248,15 @@ func buildDockerRegistryImage(ctx context.Context, client *client.Client, buildO buildContext = buildContext[:lastIndex] } - excludes, err := build.ReadDockerignore(buildContext) + enableBuildKitIfSupported(ctx, client, &imageBuildOptions) + + buildCtx, relDockerfile, err := prepareBuildContext(buildContext, imageBuildOptions.Dockerfile) if err != nil { - return fmt.Errorf("unable to read dockerignore: %v", err) + return err } - excludes = build.TrimBuildFilesFromExcludes(excludes, imageBuildOptions.Dockerfile, false) - log.Printf("[DEBUG] Excludes: %v", excludes) - buildResponse, err := client.ImageBuild(ctx, getBuildContext(buildContext, excludes), imageBuildOptions) + imageBuildOptions.Dockerfile = relDockerfile + + buildResponse, err := client.ImageBuild(ctx, buildCtx, imageBuildOptions) if err != nil { return fmt.Errorf("unable to build image for docker_registry_image: %v", err) } diff --git a/internal/provider/resource_docker_registry_image_test.go b/internal/provider/resource_docker_registry_image_test.go index 520afa7d..2deeb3e2 100644 --- a/internal/provider/resource_docker_registry_image_test.go +++ b/internal/provider/resource_docker_registry_image_test.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "io/ioutil" "os" "path/filepath" "reflect" @@ -249,6 +250,30 @@ func TestAccDockerRegistryImageResource_whitelistDockerignore(t *testing.T) { }) } +// 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/testDockerImageDockerfileOutsideContext.tf b/testdata/resources/docker_image/testDockerImageDockerfileOutsideContext.tf new file mode 100644 index 00000000..129e6336 --- /dev/null +++ b/testdata/resources/docker_image/testDockerImageDockerfileOutsideContext.tf @@ -0,0 +1,9 @@ +resource "docker_image" "outside_context" { + name = "outside-context:latest" + + build { + path = "." + dockerfile = "../Dockerfile" + } +} + diff --git a/testdata/resources/docker_registry_image/testDockerRegistryImageDockerfileOutsideContext.tf b/testdata/resources/docker_registry_image/testDockerRegistryImageDockerfileOutsideContext.tf new file mode 100644 index 00000000..e2aec9e0 --- /dev/null +++ b/testdata/resources/docker_registry_image/testDockerRegistryImageDockerfileOutsideContext.tf @@ -0,0 +1,17 @@ +provider "docker" { + alias = "private" + registry_auth { + address = "%s" + } +} + +resource "docker_registry_image" "outside_context" { + provider = "docker.private" + name = "%s" + insecure_skip_verify = true + + build { + context = "%s" + dockerfile = "../Dockerfile" + } +}