Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 151 additions & 3 deletions dockerclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dockerclient

import (
"archive/tar"
"bufio"
"bytes"
"context"
"crypto/rand"
Expand Down Expand Up @@ -793,7 +794,146 @@ func (e *ClientExecutor) Copy(excludes []string, copies ...imagebuilder.Copy) er

// CopyContainer copies the provided content into a destination container.
func (e *ClientExecutor) CopyContainer(container *docker.Container, excludes []string, copies ...imagebuilder.Copy) error {
chownUid, chownGid := -1, -1
chown := func(h *tar.Header, r io.Reader) (data []byte, update bool, skip bool, err error) {
if chownUid != -1 {
h.Uid = chownUid
}
if chownGid != -1 {
h.Gid = chownGid
}
return nil, false, false, nil
}
readFile := func(path string) ([]byte, error) {
var buffer, contents bytes.Buffer
if err := e.Client.DownloadFromContainer(e.Container.ID, docker.DownloadFromContainerOptions{
OutputStream: &buffer,
Path: path,
Context: context.TODO(),
}); err != nil {
return nil, err
}
tr := tar.NewReader(&buffer)
hdr, err := tr.Next()
if err != nil {
return nil, err
}
if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA {
return nil, fmt.Errorf("expected %q to be a regular file, but it was of type %q", path, string(hdr.Typeflag))
}
if filepath.FromSlash(hdr.Name) != filepath.Base(path) {
return nil, fmt.Errorf("error reading contents of %q: got %q instead", path, hdr.Name)
}
n, err := io.Copy(&contents, tr)
if err != nil {
return nil, fmt.Errorf("error reading contents of %q: %v", path, err)
}
if n != hdr.Size {
return nil, fmt.Errorf("size mismatch reading contents of %q: %v", path, err)
}
hdr, err = tr.Next()
if err != nil && !errorIsEOF(err) {
return nil, fmt.Errorf("error reading archive of %q: %v", path, err)
}
if err == nil {
return nil, fmt.Errorf("got unexpected extra content while reading archive of %q: %v", path, err)
}
return contents.Bytes(), nil
}
parse := func(file []byte, matchField int, key string, numFields, readField int) (string, error) {
var value *string
scanner := bufio.NewScanner(bytes.NewReader(file))
for scanner.Scan() {
line := scanner.Text()
fields := strings.SplitN(line, ":", numFields)
if len(fields) != numFields {
return "", fmt.Errorf("error parsing line %q: incorrect number of fields", line)
}
if fields[matchField] != key {
continue
}
v := fields[readField]
value = &v
}
if err := scanner.Err(); err != nil {
return "", fmt.Errorf("error scanning file: %v", err)
}
if value == nil {
return "", os.ErrNotExist
}
return *value, nil
}
for _, c := range copies {
chownUid, chownGid = -1, -1
if c.Chown != "" {
spec := strings.SplitN(c.Chown, ":", 2)
if len(spec) == 2 {
parsedUid, err := strconv.ParseUint(spec[0], 10, 32)
if err != nil {
// maybe it's a user name? look up the UID
passwdFile, err := readFile("/etc/passwd")
if err != nil {
return err
}
uid, err := parse(passwdFile, 0, spec[0], 7, 2)
if err != nil {
return fmt.Errorf("error reading UID value from passwd file for --chown=%s: %v", spec[0], err)
}
parsedUid, err = strconv.ParseUint(uid, 10, 32)
if err != nil {
return fmt.Errorf("error parsing UID value %q from passwd file for --chown=%s", uid, c.Chown)
}
}
parsedGid, err := strconv.ParseUint(spec[1], 10, 32)
if err != nil {
// maybe it's a group name? look up the GID
groupFile, err := readFile("/etc/group")
if err != nil {
return err
}
gid, err := parse(groupFile, 0, spec[1], 4, 2)
if err != nil {
return err
}
parsedGid, err = strconv.ParseUint(gid, 10, 32)
if err != nil {
return fmt.Errorf("error parsing GID value %q from group file for --chown=%s", gid, c.Chown)
}
}
chownUid = int(parsedUid)
chownGid = int(parsedGid)
} else {
var parsedUid, parsedGid uint64
if id, err := strconv.ParseUint(spec[0], 10, 32); err == nil {
// it's an ID. use it as both the UID and the GID
parsedUid = id
parsedGid = id
} else {
// it's a user name, we'll need to look up their UID and primary GID
passwdFile, err := readFile("/etc/passwd")
if err != nil {
return err
}
// read the UID and primary GID
uid, err := parse(passwdFile, 0, spec[0], 7, 2)
if err != nil {
return fmt.Errorf("error reading UID value from /etc/passwd for --chown=%s", c.Chown)
}
gid, err := parse(passwdFile, 0, spec[0], 7, 3)
if err != nil {
return fmt.Errorf("error reading GID value from /etc/passwd for --chown=%s", c.Chown)
}
if parsedUid, err = strconv.ParseUint(uid, 10, 32); err != nil {
return fmt.Errorf("error parsing UID value %q from /etc/passwd for --chown=%s", uid, c.Chown)
}
if parsedGid, err = strconv.ParseUint(gid, 10, 32); err != nil {
return fmt.Errorf("error parsing GID value %q from /etc/passwd for --chown=%s", gid, c.Chown)
}
}
chownUid = int(parsedUid)
chownGid = int(parsedGid)
}
}
// TODO: reuse source
for _, src := range c.Src {
klog.V(4).Infof("Archiving %s download=%t fromFS=%t from=%s", src, c.Download, c.FromFS, c.From)
Expand All @@ -808,8 +948,16 @@ func (e *ClientExecutor) CopyContainer(container *docker.Container, excludes []s
if err != nil {
return err
}

klog.V(5).Infof("Uploading to %s at %s", container.ID, c.Dest)
asOwner := ""
if c.Chown != "" {
filtered, err := transformArchive(r, false, chown)
if err != nil {
return err
}
r = filtered
asOwner = fmt.Sprintf(" as %d:%d", chownUid, chownGid)
}
klog.V(5).Infof("Uploading to %s%s at %s", container.ID, asOwner, c.Dest)
if klog.V(6) {
logArchiveOutput(r, "Archive file for %s")
}
Expand Down Expand Up @@ -1083,7 +1231,7 @@ func snapshotPath(path, containerID, tempDir string, client *docker.Client) (str
}
return len(h.Name) > 0
})
if err == nil || err == io.EOF {
if err == nil || errorIsEOF(err) {
tw.Flush()
w.Close()
klog.V(5).Infof("Snapshot rewritten from %s", path)
Expand Down
11 changes: 11 additions & 0 deletions dockerclient/client_112.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// +build !go1.13

package dockerclient

import (
"io"
)

func errorIsEOF(err error) bool {
return err == io.EOF
}
12 changes: 12 additions & 0 deletions dockerclient/client_113.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// +build go1.13

package dockerclient

import (
"errors"
"io"
)

func errorIsEOF(err error) bool {
return errors.Is(err, io.EOF)
}
4 changes: 4 additions & 0 deletions dockerclient/conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ func TestConformanceInternal(t *testing.T) {
Name: "copy to dir",
ContextDir: "testdata/copy",
},
{
Name: "copy chown",
ContextDir: "testdata/copychown",
},
{
Name: "copy dir",
ContextDir: "testdata/copydir",
Expand Down
11 changes: 11 additions & 0 deletions dockerclient/testdata/copychown/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM centos:7
COPY --chown=1:2 script /usr/bin/script.12
COPY --chown=1:adm script /usr/bin/script.1-adm
COPY --chown=1 script /usr/bin/script.1
COPY --chown=lp:adm script /usr/bin/script.lp-adm
COPY --chown=2:mail script /usr/bin/script.2-mail
COPY --chown=2 script /usr/bin/script.2
COPY --chown=bin script /usr/bin/script.bin
COPY --chown=lp script /usr/bin/script.lp
COPY --chown=3 script script2 /usr/local/bin/
RUN ls -al /usr/bin/script
2 changes: 2 additions & 0 deletions dockerclient/testdata/copychown/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exit 0
2 changes: 2 additions & 0 deletions dockerclient/testdata/copychown/script2
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exit 1