fix: Enables having a Dockerfile outside the context (#402)

* tests: Add failing test for Dockerfile outside context.

* fix: Enable support for Dockerfiles outside context.

Also extract some code to functions.

* fix: Adding docker outside context test and implementation for docker_registry_image.
This commit is contained in:
Martin 2022-07-13 09:01:52 +02:00 committed by GitHub
parent 6ac477b3c1
commit 78c42d7657
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 155 additions and 27 deletions

View file

@ -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)
}
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 err
return nil, "", err
}
excludes = build.TrimBuildFilesFromExcludes(excludes, buildOptions.Dockerfile, false)
var response types.ImageBuildResponse
response, err = client.ImageBuild(ctx, getBuildContext(contextDir, excludes), buildOptions)
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 err
return nil, "", err
}
defer response.Body.Close()
buildResult, err := decodeBuildMessages(response)
if err != nil {
return fmt.Errorf("%s\n\n%s", err, buildResult)
return buildCtx, relDockerfile, nil
}
return nil
return buildCtx, specifiedDockerfile, nil
}
func getBuildContext(filePath string, excludes []string) io.Reader {
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)

View file

@ -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()

View file

@ -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)
}

View file

@ -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) },

View file

@ -0,0 +1,9 @@
resource "docker_image" "outside_context" {
name = "outside-context:latest"
build {
path = "."
dockerfile = "../Dockerfile"
}
}

View file

@ -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"
}
}