diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ba6ff9f4d8ad5811357fe13c25aacb42dd1e4801..c111767e8c4a814e969fe4eb9c207752be1aafba 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -137,6 +137,40 @@ test_with_artifact_dockerfile: - cat /folder/testfile - test $(cat /folder/testfile) == "testcontent" +# Build image with a symlink +build_symlinks_dockerfile: + <<: *build_test_image + variables: + TO: ${CI_REGISTRY_IMAGE}/test/symlinks:test_${CI_COMMIT_REF_NAME} + CONTEXT_DIR: test/symlinks + DOCKER_DEBUG: "true" + +test_symlinks_dockerfile: + <<: *verify_test_image + image: ${CI_REGISTRY_IMAGE}/test/symlinks:test_${CI_COMMIT_REF_NAME} + script: + - grep -qi alpine /etc/os-release + - test $(cat /test/fromsymlink) == "symlink" + - test -h /test/fromsymlink + +# Build image with a .dockerignore file +build_dockerignore_dockerfile: + <<: *build_test_image + variables: + TO: ${CI_REGISTRY_IMAGE}/test/dockerignore:test_${CI_COMMIT_REF_NAME} + CONTEXT_DIR: test/with_dockerignore + DOCKER_DEBUG: "true" + +test_dockerignore_dockerfile: + <<: *verify_test_image + image: ${CI_REGISTRY_IMAGE}/test/dockerignore:test_${CI_COMMIT_REF_NAME} + script: + - grep -qi alpine /etc/os-release + - test -e /test/notignored + - test $(cat /test/notignored) == "file" + # Verify the file in the .dockerignore file is not present + - test ! -e /test/ignored + # If all tests pass and we are running on master, retag the image to latest. This image will be used for user docker builds from now on. build_stable_image: stage: promote_tested_image_to_latest diff --git a/docker.go b/docker.go index bc1619b555e77724a68f1d0d4f2608bbf0177464..c4ccb4c70dffd8c30b26e53529c21d98ce23b8f3 100644 --- a/docker.go +++ b/docker.go @@ -1,155 +1,69 @@ package main import ( - "archive/tar" - "bytes" - "context" - "encoding/base64" - "encoding/json" "fmt" - "io" "io/ioutil" "os" "path" - "path/filepath" "strings" + docker "github.com/fsouza/go-dockerclient" log "github.com/sirupsen/logrus" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/jsonmessage" ) const ( flagFile = "./runonce" dockerDebugEnvName = "DOCKER_DEBUG" + dockerSocket = "unix:///var/run/docker.sock" ) -func buildImage(options map[string]*string, loginInfo map[string]types.AuthConfig, buildArgs map[string]*string) error { - cli, err := client.NewEnvClient() - if err != nil { - log.Printf("Unable to create docker client: %v", err) - return err - } - defer cli.Close() +func buildImage(client *docker.Client, options map[string]*string, loginInfo docker.AuthConfigurations, buildArgs []docker.BuildArg) error { noCache := options["NO_CACHE"] != nil && *options["NO_CACHE"] == "" - log.Printf("Preparing local tar file of current dir '%s' for Docker daemon ...", *options["CONTEXT_DIR"]) - buildOptions := types.ImageBuildOptions{ - ForceRemove: true, - PullParent: true, - Dockerfile: *options["DOCKER_FILE"], - NoCache: noCache, - BuildArgs: buildArgs, - Tags: []string{*options["TO"]}, - AuthConfigs: loginInfo, - } - resp, err := cli.ImageBuild(context.Background(), makeTarReader(*options["CONTEXT_DIR"]), buildOptions) - if err != nil { - return err + buildOptions := docker.BuildImageOptions{ + Name: *options["TO"], + Dockerfile: *options["DOCKER_FILE"], + // Always pull image even if locally accessible. This is absolutely necessary to protect + // private source images from being reused if present already. + Pull: true, + NoCache: noCache, + BuildArgs: buildArgs, + AuthConfigs: loginInfo, + OutputStream: os.Stdout, + ContextDir: *options["CONTEXT_DIR"], } - defer resp.Body.Close() - err = jsonmessage.DisplayJSONMessagesStream(resp.Body, os.Stdout, 0, true, nil) - if err != nil { + // resp, err := cli.ImageBuild(context.Background(), makeTarReader(*options["CONTEXT_DIR"]), buildOptions) + if err := client.BuildImage(buildOptions); err != nil { return err } - return nil } -func pushImage(options map[string]*string) error { - cli, err := client.NewEnvClient() +func pushImage(client *docker.Client, options map[string]*string, loginInfo docker.AuthConfigurations) error { - if err != nil { - log.Printf("Unable to create docker client: %v", err) - return err - } - defer cli.Close() + // Obtain registry name to pushImage + destinationImageFields := strings.Split(*options["TO"], "/") + registry := destinationImageFields[0] - // Prepare login to push the image - authObj := map[string]string{ - "username": *options["DOCKER_LOGIN_USERNAME"], - "password": *options["DOCKER_LOGIN_PASSWORD"], - } - json, err := json.Marshal(authObj) - if err != nil { - log.Fatal(err) - } - encodedAuth := base64.StdEncoding.EncodeToString(json) - - pushOptions := types.ImagePushOptions{ - RegistryAuth: encodedAuth, + pushOptions := docker.PushImageOptions{ + Name: *options["TO"], + Registry: registry, + OutputStream: os.Stdout, } - resp, err := cli.ImagePush(context.Background(), *options["TO"], pushOptions) - if err != nil { - return err + var authInfo docker.AuthConfiguration + if val, ok := loginInfo.Configs[registry]; ok { + authInfo = val + } else { + authInfo = docker.AuthConfiguration{} } - defer resp.Close() + client.PushImage(pushOptions, authInfo) - err = jsonmessage.DisplayJSONMessagesStream(resp, os.Stdout, 0, true, nil) - if err != nil { - return err - } return nil } -// makeTarReader - Making memory tar for image build context -// Heavily based on https://github.com/da4nik/ssci/blob/42e0b9c6d93bda94284a2d73343d22dbc9a7e75a/ci/build.go#L48 -func makeTarReader(workdir string) *bytes.Reader { - buffer := new(bytes.Buffer) - - tw := tar.NewWriter(buffer) - defer tw.Close() - - log.Debug("Preparing to make Tar contents for Docker build") - filepath.Walk(workdir, func(file string, fi os.FileInfo, err error) error { - // return on any error - if err != nil { - return err - } - - // create a new dir/file header - header, err := tar.FileInfoHeader(fi, fi.Name()) - if err != nil { - return err - } - - // update the name to correctly reflect the desired destination when untaring - header.Name = strings.TrimPrefix(strings.Replace(file, workdir, "", -1), string(filepath.Separator)) - - // write the header - if err = tw.WriteHeader(header); err != nil { - return err - } - - // return on directories since there will be no content to tar - if fi.Mode().IsDir() { - return nil - } - - // open files for taring - log.Debugf("Adding file %s to Tar contents", file) - f, err := os.Open(file) - defer f.Close() - if err != nil { - return err - } - - // copy file data into tar writer - if _, err := io.Copy(tw, f); err != nil { - return err - } - - return nil - }) - - return bytes.NewReader(buffer.Bytes()) -} - // Helper function to convert a string into a pointer of a string func getPointer(s string) *string { return &s @@ -196,17 +110,20 @@ func parseEnvVariables(defaults map[string]*string) map[string]*string { } // Obtain a types.AuthConfig object that can be use to build using a private image -func loginWithRegistry(username, password, server *string) map[string]types.AuthConfig { +func loginWithRegistry(username, password, server *string) docker.AuthConfigurations { // If no username and password, ignore auth - if username == nil || password == nil || server == nil { - return nil + authConfigs := docker.AuthConfigurations{ + Configs: make(map[string]docker.AuthConfiguration), } - loginInfo := make(map[string]types.AuthConfig) - loginInfo[*server] = types.AuthConfig{ - Username: *username, - Password: *password, + + // If no username and password, ignore auth + if !(username == nil || password == nil || server == nil) { + authConfigs.Configs[*server] = docker.AuthConfiguration{ + Username: *username, + Password: *password, + } } - return loginInfo + return authConfigs } //Prints useful information for the build @@ -255,9 +172,9 @@ func overrideFrom(dockerfile, newFrom *string) error { } // Obtain build args from environment variables starting with 'BUILD_ARG' -func parseBuildArgs() (map[string]*string, error) { +func parseBuildArgs() ([]docker.BuildArg, error) { - buildArgs := make(map[string]*string) + buildArgs := make([]docker.BuildArg, 0) // os.Environ returns a copy of strings representing the environment, // in the form "key=value". for _, env := range os.Environ() { @@ -266,7 +183,10 @@ func parseBuildArgs() (map[string]*string, error) { if len(comps) != 3 { return buildArgs, fmt.Errorf("Build arg '%s' is malformed. The value should be a key value pair", env) } - buildArgs[comps[1]] = &comps[2] + buildArgs = append(buildArgs, docker.BuildArg{ + Name: comps[1], + Value: comps[2], + }) } } return buildArgs, nil @@ -313,8 +233,16 @@ func main() { log.Fatal(err) } + // Allow users to override the docker host via DOCKER_HOST environment variable + // This would allow to work with DinD in a service container for instance + dockerHost := dockerSocket + if env, present := os.LookupEnv("DOCKER_HOST"); present { + dockerHost = env + } + client, err := docker.NewClient(dockerHost) + // Proceed to build the image - if err := buildImage(options, loginInfo, buildArgs); err != nil { + if err := buildImage(client, options, loginInfo, buildArgs); err != nil { log.Fatal(err) } log.Println("Build successfully completed!") @@ -322,7 +250,7 @@ func main() { // If all good building the image, push it to the registry if options["TO"] != nil { log.Printf("Pushing the built image to '%s'", *options["TO"]) - if err := pushImage(options); err != nil { + if err := pushImage(client, options, loginInfo); err != nil { log.Fatal(err) } log.Printf("Image successfully pushed to '%s'", *options["TO"]) diff --git a/test/symlinks/Dockerfile b/test/symlinks/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a3d2aa8eae6b22d321ae7b0ecbc4ffab10e129dd --- /dev/null +++ b/test/symlinks/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:latest + +COPY . /test diff --git a/test/symlinks/dest/testsymlink b/test/symlinks/dest/testsymlink new file mode 100644 index 0000000000000000000000000000000000000000..d933faa92d675020afeefd69de4580a0badead21 --- /dev/null +++ b/test/symlinks/dest/testsymlink @@ -0,0 +1 @@ +symlink diff --git a/test/symlinks/fromsymlink b/test/symlinks/fromsymlink new file mode 120000 index 0000000000000000000000000000000000000000..f6ea8d4570048353523ff7dfe80af5d9b351fbde --- /dev/null +++ b/test/symlinks/fromsymlink @@ -0,0 +1 @@ +dest/testsymlink \ No newline at end of file diff --git a/test/with_dockerignore/.dockerignore b/test/with_dockerignore/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..8485e986e458a566e6f6160f71d704edc10c57fc --- /dev/null +++ b/test/with_dockerignore/.dockerignore @@ -0,0 +1 @@ +ignore \ No newline at end of file diff --git a/test/with_dockerignore/Dockerfile b/test/with_dockerignore/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a3d2aa8eae6b22d321ae7b0ecbc4ffab10e129dd --- /dev/null +++ b/test/with_dockerignore/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:latest + +COPY . /test diff --git a/test/with_dockerignore/ignore b/test/with_dockerignore/ignore new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/with_dockerignore/notignored b/test/with_dockerignore/notignored new file mode 100644 index 0000000000000000000000000000000000000000..1a010b1c0f081b2e8901d55307a15c29ff30af0e --- /dev/null +++ b/test/with_dockerignore/notignored @@ -0,0 +1 @@ +file \ No newline at end of file