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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user