Change for running as a non-root user (#276)

* Enable running container as mqm

* Fix merge problem

* Don't force root usage

* RHEL image runs as mqm instead of root

* Build on host with SELinux enabled

* Enable building on node in an OpenShift cluster

* Enable running container as mqm

* Fix merge problem

* Don't force root usage

* Merge lastest changes from master

* RHEL image runs as mqm instead of root

* Fix merge issues

* Test changes for non-root

* Make timeout properly, and more non-root test fixes

* Run tests with fewer/no capabilities

* Correct usage docs for non-root

* Add security docs

* Add temporary debug output

* Remove debug code

* Fixes for termination-log

* Allow init container to run as root

* Fixes for CentOS build

* Fixes for RHEL build

* Logging improvements

* Fix Dockerfile RHEL/CentOS build

* Fix bash error

* Make all builds specify UID

* Use redist client for Go SDK

* Inspect image before running tests

* New test for init container

* Log container runtime in runmqdevserver

* Add extra capabilities if using a RHEL image
This commit is contained in:
Arthur Barr
2019-02-25 15:44:14 +00:00
parent 2dbee560fe
commit cc0f072908
35 changed files with 871 additions and 504 deletions

View File

@@ -1,5 +1,5 @@
/*
© Copyright IBM Corporation 2017, 2018
© Copyright IBM Corporation 2017, 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -59,6 +59,18 @@ func imageNameDevJMS() string {
return image
}
func baseImage(t *testing.T, cli *client.Client) string {
rc, out := runContainerOneShot(t, cli, "bash", "-c", "cat /etc/*release | grep \"^ID=\"")
if rc != 0 {
t.Fatal("Couldn't determine base image")
}
s := strings.Split(out, "=")
if len(s) < 2 {
t.Fatal("Couldn't determine base image string")
}
return s[1]
}
// isWSL return whether we are running in the Windows Subsystem for Linux
func isWSL(t *testing.T) bool {
if runtime.GOOS == "linux" {
@@ -124,46 +136,31 @@ func getTempDir(t *testing.T, unixStylePath bool) string {
return "/tmp/"
}
// terminationLogUnixPath returns the name of the file to use for the termination log message, with a UNIX path
func terminationLogUnixPath(t *testing.T) string {
// Warning: this directory must be accessible to the Docker daemon,
// in order to enable the bind mount
return getTempDir(t, true) + t.Name() + "-termination-log"
}
// terminationLogOSPath returns the name of the file to use for the termination log message, with an OS specific path
func terminationLogOSPath(t *testing.T) string {
// Warning: this directory must be accessible to the Docker daemon,
// in order to enable the bind mount
return getTempDir(t, false) + t.Name() + "-termination-log"
}
// terminationBind returns a string to use to bind-mount a termination log file.
// This is done using a bind, because you can't copy files from /dev out of the container.
func terminationBind(t *testing.T) string {
n := terminationLogUnixPath(t)
// Remove it if it already exists
os.Remove(n)
// Create the empty file
f, err := os.OpenFile(n, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
t.Fatal(err)
}
f.Close()
return terminationLogOSPath(t) + ":/dev/termination-log"
}
// terminationMessage return the termination message, or an empty string if not set
func terminationMessage(t *testing.T) string {
b, err := ioutil.ReadFile(terminationLogUnixPath(t))
func terminationMessage(t *testing.T, cli *client.Client, ID string) string {
r, _, err := cli.CopyFromContainer(context.Background(), ID, "/run/termination-log")
if err != nil {
t.Log(err)
return ""
}
return string(b)
b, err := ioutil.ReadAll(r)
tr := tar.NewReader(bytes.NewReader(b))
_, err = tr.Next()
if err != nil {
t.Log(err)
return ""
}
// read the complete content of the file h.Name into the bs []byte
content, err := ioutil.ReadAll(tr)
if err != nil {
t.Log(err)
return ""
}
return string(content)
}
func expectTerminationMessage(t *testing.T) {
m := terminationMessage(t)
func expectTerminationMessage(t *testing.T, cli *client.Client, ID string) {
m := terminationMessage(t, cli, ID)
if m == "" {
t.Error("Expected termination message to be set")
}
@@ -195,11 +192,10 @@ func cleanContainer(t *testing.T, cli *client.Client, ID string) {
// Log the container output for any container we're about to delete
t.Logf("Console log from container %v:\n%v", ID, inspectTextLogs(t, cli, ID))
m := terminationMessage(t)
m := terminationMessage(t, cli, ID)
if m != "" {
t.Logf("Termination message: %v", m)
}
os.Remove(terminationLogUnixPath(t))
t.Logf("Removing container: %s", ID)
opts := types.ContainerRemoveOptions{
@@ -212,6 +208,22 @@ func cleanContainer(t *testing.T, cli *client.Client, ID string) {
}
}
// devImage returns true if the specified image is a developer image,
// determined by use of the MQ_ADMIN_PASSWORD or MQ_APP_PASSWORD
// environment variables
func devImage(t *testing.T, cli *client.Client, imageID string) bool {
i, _, err := cli.ImageInspectWithRaw(context.Background(), imageID)
if err != nil {
t.Fatal(err)
}
for _, e := range i.ContainerConfig.Env {
if strings.HasPrefix(e, "MQ_ADMIN_PASSWORD") || strings.HasPrefix(e, "MQ_APP_PASSWORD") {
return true
}
}
return false
}
// runContainerWithPorts creates and starts a container, exposing the specified ports on the host.
// If no image is specified in the container config, then the image name is retrieved from the TEST_IMAGE
// environment variable.
@@ -219,15 +231,35 @@ func runContainerWithPorts(t *testing.T, cli *client.Client, containerConfig *co
if containerConfig.Image == "" {
containerConfig.Image = imageName()
}
// Always run as the "mqm" user, unless the test has specified otherwise
if containerConfig.User == "" {
containerConfig.User = "mqm"
}
// if coverage
containerConfig.Env = append(containerConfig.Env, "COVERAGE_FILE="+t.Name()+".cov")
containerConfig.Env = append(containerConfig.Env, "EXIT_CODE_FILE="+getExitCodeFilename(t))
hostConfig := container.HostConfig{
Binds: []string{
coverageBind(t),
terminationBind(t),
// terminationBind(t),
},
PortBindings: nat.PortMap{},
CapDrop: []string{
"ALL",
},
}
if devImage(t, cli, containerConfig.Image) {
t.Logf("Detected MQ Advanced for Developers image — adding extra Linux capabilities to container")
hostConfig.CapAdd = []string{
"CHOWN",
"SETUID",
"SETGID",
"AUDIT_WRITE",
}
// Only needed for a RHEL-based image
if baseImage(t, cli) != "ubuntu" {
hostConfig.CapAdd = append(hostConfig.CapAdd, "DAC_OVERRIDE")
}
}
for _, p := range ports {
port := nat.Port(fmt.Sprintf("%v/tcp", p))
@@ -254,13 +286,49 @@ func runContainer(t *testing.T, cli *client.Client, containerConfig *container.C
return runContainerWithPorts(t, cli, containerConfig, nil)
}
// runContainerOneShot runs a container with a custom entrypoint, as the root
// user and with default capabilities
func runContainerOneShot(t *testing.T, cli *client.Client, command ...string) (int64, string) {
containerConfig := container.Config{
Entrypoint: command,
User: "root",
Image: imageName(),
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
return waitForContainer(t, cli, id, 10), inspectLogs(t, cli, id)
hostConfig := container.HostConfig{}
networkingConfig := network.NetworkingConfig{}
t.Logf("Running one shot container (%s)", containerConfig.Image)
ctr, err := cli.ContainerCreate(context.Background(), &containerConfig, &hostConfig, &networkingConfig, t.Name())
if err != nil {
t.Fatal(err)
}
startContainer(t, cli, ctr.ID)
defer cleanContainer(t, cli, ctr.ID)
return waitForContainer(t, cli, ctr.ID, 10*time.Second), inspectLogs(t, cli, ctr.ID)
}
// runContainerOneShot runs a container with a custom entrypoint, as the root
// user, with default capabilities, and a volume mounted
func runContainerOneShotWithVolume(t *testing.T, cli *client.Client, bind string, command ...string) (int64, string) {
containerConfig := container.Config{
Entrypoint: command,
User: "root",
Image: imageName(),
}
hostConfig := container.HostConfig{
Binds: []string{
bind,
},
}
networkingConfig := network.NetworkingConfig{}
t.Logf("Running one shot container with volume (%s): %v", containerConfig.Image, command)
ctr, err := cli.ContainerCreate(context.Background(), &containerConfig, &hostConfig, &networkingConfig, t.Name())
if err != nil {
t.Fatal(err)
}
t.Logf("One shot container ID: %v", ctr.ID)
startContainer(t, cli, ctr.ID)
defer cleanContainer(t, cli, ctr.ID)
return waitForContainer(t, cli, ctr.ID, 10*time.Second), inspectLogs(t, cli, ctr.ID)
}
func startContainer(t *testing.T, cli *client.Client, ID string) {
@@ -309,19 +377,19 @@ func getCoverageExitCode(t *testing.T, orig int64) int64 {
}
// waitForContainer waits until a container has exited
func waitForContainer(t *testing.T, cli *client.Client, ID string, timeout int64) int64 {
rc, err := cli.ContainerWait(context.Background(), ID)
func waitForContainer(t *testing.T, cli *client.Client, ID string, timeout time.Duration) int64 {
c, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
rc, err := cli.ContainerWait(c, ID)
if err != nil {
t.Fatal(err)
}
if coverage() {
// COVERAGE: When running coverage, the exit code is written to a file,
// to allow the coverage to be generated (which doesn't happen for non-zero
// exit codes)
rc = getCoverageExitCode(t, rc)
}
if err != nil {
t.Fatal(err)
}
return rc
}
@@ -395,7 +463,7 @@ func execContainer(t *testing.T, cli *client.Client, ID string, user string, cmd
}
func waitForReady(t *testing.T, cli *client.Client, ID string) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
for {