Merge pull request #5422 from localghost/upload_owner_fix

Fix owner of files uploaded to docker container run as non-root.
This commit is contained in:
Matthew Hooker 2017-10-13 14:56:16 -07:00 committed by GitHub
commit f617a678b1
5 changed files with 199 additions and 25 deletions

View File

@ -23,6 +23,7 @@ type Communicator struct {
ContainerDir string ContainerDir string
Version *version.Version Version *version.Version
Config *Config Config *Config
ContainerUser string
lock sync.Mutex lock sync.Mutex
} }
@ -154,6 +155,10 @@ func (c *Communicator) uploadFile(dst string, src io.Reader, fi *os.FileInfo) er
return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err) return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err)
} }
if err := c.fixDestinationOwner(dst); err != nil {
return err
}
return nil return nil
} }
@ -207,6 +212,10 @@ func (c *Communicator) UploadDir(dst string, src string, exclude []string) error
return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err) return fmt.Errorf("Failed to upload to '%s' in container: %s. %s.", dst, stderrOut, err)
} }
if err := c.fixDestinationOwner(dst); err != nil {
return err
}
return nil return nil
} }
@ -310,3 +319,25 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin io.Wri
// Set the exit status which triggers waiters // Set the exit status which triggers waiters
remote.SetExited(exitStatus) remote.SetExited(exitStatus)
} }
// TODO Workaround for #5307. Remove once #5409 is fixed.
func (c *Communicator) fixDestinationOwner(destination string) error {
if !c.Config.FixUploadOwner {
return nil
}
owner := c.ContainerUser
if owner == "" {
owner = "root"
}
chownArgs := []string{
"docker", "exec", "--user", "root", c.ContainerID, "/bin/sh", "-c",
fmt.Sprintf("chown -R %s %s", owner, destination),
}
if output, err := exec.Command(chownArgs[0], chownArgs[1:]...).CombinedOutput(); err != nil {
return fmt.Errorf("Failed to set owner of the uploaded file: %s, %s", err, output)
}
return nil
}

View File

@ -209,6 +209,82 @@ func TestLargeDownload(t *testing.T) {
} }
// TestFixUploadOwner verifies that owner of uploaded files is the user the container is running as.
func TestFixUploadOwner(t *testing.T) {
ui := packer.TestUi(t)
cache := &packer.FileCache{CacheDir: os.TempDir()}
tpl, err := template.Parse(strings.NewReader(testFixUploadOwnerTemplate))
if err != nil {
t.Fatalf("Unable to parse config: %s", err)
}
if os.Getenv("PACKER_ACC") == "" {
t.Skip("This test is only run with PACKER_ACC=1")
}
cmd := exec.Command("docker", "-v")
cmd.Run()
if !cmd.ProcessState.Success() {
t.Error("docker command not found; please make sure docker is installed")
}
// Setup the builder
builder := &Builder{}
warnings, err := builder.Prepare(tpl.Builders["docker"].Config)
if err != nil {
t.Fatalf("Error preparing configuration %s", err)
}
if len(warnings) > 0 {
t.Fatal("Encountered configuration warnings; aborting")
}
// Setup the provisioners
fileProvisioner := &file.Provisioner{}
err = fileProvisioner.Prepare(tpl.Provisioners[0].Config)
if err != nil {
t.Fatalf("Error preparing single file upload provisioner: %s", err)
}
dirProvisioner := &file.Provisioner{}
err = dirProvisioner.Prepare(tpl.Provisioners[1].Config)
if err != nil {
t.Fatalf("Error preparing directory upload provisioner: %s", err)
}
shellProvisioner := &shell.Provisioner{}
err = shellProvisioner.Prepare(tpl.Provisioners[2].Config)
if err != nil {
t.Fatalf("Error preparing shell provisioner: %s", err)
}
verifyProvisioner := &shell.Provisioner{}
err = verifyProvisioner.Prepare(tpl.Provisioners[3].Config)
if err != nil {
t.Fatalf("Error preparing verification provisioner: %s", err)
}
// Add hooks so the provisioners run during the build
hooks := map[string][]packer.Hook{}
hooks[packer.HookProvision] = []packer.Hook{
&packer.ProvisionHook{
Provisioners: []packer.Provisioner{
fileProvisioner,
dirProvisioner,
shellProvisioner,
verifyProvisioner,
},
ProvisionerTypes: []string{"", "", "", ""},
},
}
hook := &packer.DispatchHook{Mapping: hooks}
artifact, err := builder.Run(ui, hook, cache)
if err != nil {
t.Fatalf("Error running build %s", err)
}
defer artifact.Destroy()
}
const dockerBuilderConfig = ` const dockerBuilderConfig = `
{ {
"builders": [ "builders": [
@ -269,3 +345,40 @@ const dockerLargeBuilderConfig = `
] ]
} }
` `
const testFixUploadOwnerTemplate = `
{
"builders": [
{
"type": "docker",
"image": "ubuntu",
"discard": true,
"run_command": ["-d", "-i", "-t", "-u", "42", "{{.Image}}", "/bin/sh"]
}
],
"provisioners": [
{
"type": "file",
"source": "test-fixtures/onecakes/strawberry",
"destination": "/tmp/strawberry-cake"
},
{
"type": "file",
"source": "test-fixtures/manycakes",
"destination": "/tmp/"
},
{
"type": "shell",
"inline": "touch /tmp/testUploadOwner"
},
{
"type": "shell",
"inline": [
"[ $(stat -c %u /tmp/strawberry-cake) -eq 42 ] || (echo 'Invalid owner of /tmp/strawberry-cake' && exit 1)",
"[ $(stat -c %u /tmp/testUploadOwner) -eq 42 ] || (echo 'Invalid owner of /tmp/testUploadOwner' && exit 1)",
"find /tmp/manycakes | xargs -n1 -IFILE /bin/sh -c '[ $(stat -c %u FILE) -eq 42 ] || (echo \"Invalid owner of FILE\" && exit 1)'"
]
}
]
}
`

View File

@ -37,6 +37,7 @@ type Config struct {
Pull bool Pull bool
RunCommand []string `mapstructure:"run_command"` RunCommand []string `mapstructure:"run_command"`
Volumes map[string]string Volumes map[string]string
FixUploadOwner bool `mapstructure:"fix_upload_owner"`
// This is used to login to dockerhub to pull a private base container. For // This is used to login to dockerhub to pull a private base container. For
// pushing to dockerhub, see the docker post-processors // pushing to dockerhub, see the docker post-processors
@ -54,6 +55,8 @@ type Config struct {
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config) c := new(Config)
c.FixUploadOwner = true
var md mapstructure.Metadata var md mapstructure.Metadata
err := config.Decode(c, &config.DecodeOpts{ err := config.Decode(c, &config.DecodeOpts{
Metadata: &md, Metadata: &md,

View File

@ -1,7 +1,10 @@
package docker package docker
import ( import (
"fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"os/exec"
"strings"
) )
type StepConnectDocker struct{} type StepConnectDocker struct{}
@ -19,6 +22,12 @@ func (s *StepConnectDocker) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
containerUser, err := getContainerUser(containerId)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
// Create the communicator that talks to Docker via various // Create the communicator that talks to Docker via various
// os/exec tricks. // os/exec tricks.
comm := &Communicator{ comm := &Communicator{
@ -27,6 +36,7 @@ func (s *StepConnectDocker) Run(state multistep.StateBag) multistep.StepAction {
ContainerDir: config.ContainerDir, ContainerDir: config.ContainerDir,
Version: version, Version: version,
Config: config, Config: config,
ContainerUser: containerUser,
} }
state.Put("communicator", comm) state.Put("communicator", comm)
@ -34,3 +44,16 @@ func (s *StepConnectDocker) Run(state multistep.StateBag) multistep.StepAction {
} }
func (s *StepConnectDocker) Cleanup(state multistep.StateBag) {} func (s *StepConnectDocker) Cleanup(state multistep.StateBag) {}
func getContainerUser(containerId string) (string, error) {
inspectArgs := []string{"docker", "inspect", "--format", "{{.Config.User}}", containerId}
stdout, err := exec.Command(inspectArgs[0], inspectArgs[1:]...).Output()
if err != nil {
errStr := fmt.Sprintf("Failed to inspect the container: %s", err)
if ee, ok := err.(*exec.ExitError); ok {
errStr = fmt.Sprintf("%s, %s", errStr, ee.Stderr)
}
return "", fmt.Errorf(errStr)
}
return strings.TrimSpace(string(stdout)), nil
}

View File

@ -212,6 +212,10 @@ You must specify (only) one of `commit`, `discard`, or `export_path`.
temp directory from host server for work [file provisioner](/docs/provisioners/file.html). temp directory from host server for work [file provisioner](/docs/provisioners/file.html).
By default this is set to `/packer-files`. By default this is set to `/packer-files`.
- `fix_upload_owner` (boolean) - If true, files uploaded to the container will
be owned by the user the container is running as. If false, the owner will depend
on the version of docker installed in the system. Defaults to true.
## Using the Artifact: Export ## Using the Artifact: Export
Once the tar artifact has been generated, you will likely want to import, tag, Once the tar artifact has been generated, you will likely want to import, tag,