From e8d26aa79e12c1559ffa7b0eb1e733118798bc3a Mon Sep 17 00:00:00 2001 From: Arthur Barr Date: Mon, 2 Nov 2020 12:03:12 +0000 Subject: [PATCH] Don't use setuid on chkmq* Also add new tests for chkmqhealthy and privileges --- Dockerfile-server | 2 +- Makefile | 10 +++-- cmd/chkmqhealthy/main.go | 4 +- test/docker/docker_api_test.go | 43 +++++++++++++++++++++- test/docker/docker_api_test_util.go | 57 ++++++++++++++++++----------- 5 files changed, 86 insertions(+), 30 deletions(-) diff --git a/Dockerfile-server b/Dockerfile-server index dd67702..39cebad 100644 --- a/Dockerfile-server +++ b/Dockerfile-server @@ -98,7 +98,7 @@ COPY web /etc/mqm/web COPY etc/mqm/*.tpl /etc/mqm/ RUN chmod ug+x /usr/local/bin/runmqserver \ && chown 1001:root /usr/local/bin/*mq* \ - && chmod ug+xs /usr/local/bin/chkmq* \ + && chmod ug+x /usr/local/bin/chkmq* \ && chown -R 1001:root /etc/mqm/* \ && install --directory --mode 2775 --owner 1001 --group root /run/runmqserver \ && touch /run/termination-log \ diff --git a/Makefile b/Makefile index 8a3c42f..94eacce 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,9 @@ MQ_ARCHIVE_DEV ?= $(MQ_VERSION)-IBM-MQ-Advanced-for-Developers-Non-Install-$(MQ_ # MQ_SDK_ARCHIVE specifies the archive to use for building the golang programs. Defaults vary on developer or advanced. MQ_SDK_ARCHIVE ?= $(MQ_ARCHIVE_DEV_$(MQ_VERSION)) # Options to `go test` for the Docker tests -TEST_OPTS_DOCKER ?= +TEST_OPTS_DOCKER ?= +# Timeout for the Docker tests +TEST_TIMEOUT_DOCKER ?= 30m # MQ_IMAGE_ADVANCEDSERVER is the name of the built MQ Advanced image MQ_IMAGE_ADVANCEDSERVER ?=ibm-mqadvanced-server # MQ_IMAGE_DEVSERVER is the name of the built MQ Advanced for Developers image @@ -78,7 +80,7 @@ MQ_ARCHIVE_DEV_TYPE=Linux BUILD_SERVER_CONTAINER=build-server # NUM_CPU is the number of CPUs available to Docker. Used to control how many # test run in parallel -NUM_CPU = $(or $(shell docker info --format "{{ .NCPU }}"),2) +NUM_CPU ?= $(or $(shell docker info --format "{{ .NCPU }}"),2) # BASE_IMAGE_TAG is a normalized version of BASE_IMAGE, suitable for use in a Docker tag BASE_IMAGE_TAG=$(lastword $(subst /, ,$(subst :,-,$(BASE_IMAGE)))) #BASE_IMAGE_TAG=$(subst /,-,$(subst :,-,$(BASE_IMAGE))) @@ -241,7 +243,7 @@ test-unit: test-advancedserver: test/docker/vendor $(info $(SPACER)$(shell printf $(TITLE)"Test $(MQ_IMAGE_ADVANCEDSERVER):$(MQ_TAG_CACHED_$(ARCH)) on $(shell docker --version)"$(END))) docker inspect $(MQ_IMAGE_ADVANCEDSERVER):$(MQ_TAG_CACHED_$(ARCH)) - cd test/docker && TEST_IMAGE=$(MQ_IMAGE_ADVANCEDSERVER):$(MQ_TAG_CACHED_$(ARCH)) EXPECTED_LICENSE=Production go test -parallel $(NUM_CPU) $(TEST_OPTS_DOCKER) + cd test/docker && TEST_IMAGE=$(MQ_IMAGE_ADVANCEDSERVER):$(MQ_TAG_CACHED_$(ARCH)) EXPECTED_LICENSE=Production go test -parallel $(NUM_CPU) -timeout $(TEST_TIMEOUT_DOCKER) $(TEST_OPTS_DOCKER) .PHONY: build-devjmstest build-devjmstest: @@ -252,7 +254,7 @@ build-devjmstest: test-devserver: test/docker/vendor $(info $(SPACER)$(shell printf $(TITLE)"Test $(MQ_IMAGE_DEVSERVER):$(MQ_TAG_CACHED_$(ARCH)) on $(shell docker --version)"$(END))) docker inspect $(MQ_IMAGE_DEVSERVER):$(MQ_TAG_CACHED_$(ARCH)) - cd test/docker && TEST_IMAGE=$(MQ_IMAGE_DEVSERVER):$(MQ_TAG_CACHED_$(ARCH)) EXPECTED_LICENSE=Developer DEV_JMS_IMAGE=$(DEV_JMS_IMAGE) IBMJRE=true go test -parallel $(NUM_CPU) -tags mqdev $(TEST_OPTS_DOCKER) + cd test/docker && TEST_IMAGE=$(MQ_IMAGE_DEVSERVER):$(MQ_TAG_CACHED_$(ARCH)) EXPECTED_LICENSE=Developer DEV_JMS_IMAGE=$(DEV_JMS_IMAGE) IBMJRE=true go test -parallel $(NUM_CPU) -timeout $(TEST_TIMEOUT_DOCKER) -tags mqdev $(TEST_OPTS_DOCKER) .PHONY: coverage coverage: diff --git a/cmd/chkmqhealthy/main.go b/cmd/chkmqhealthy/main.go index 623a554..7132bda 100644 --- a/cmd/chkmqhealthy/main.go +++ b/cmd/chkmqhealthy/main.go @@ -1,5 +1,5 @@ /* -© Copyright IBM Corporation 2017, 2019 +© Copyright IBM Corporation 2017, 2020 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -36,11 +36,11 @@ func queueManagerHealthy() (bool, error) { cmd := exec.Command("dspmq", "-n", "-m", name) // Run the command and wait for completion out, err := cmd.CombinedOutput() + fmt.Printf("%s", out) if err != nil { fmt.Println(err) return false, err } - fmt.Printf("%s", out) if !strings.Contains(string(out), "(RUNNING)") && !strings.Contains(string(out), "(RUNNING AS STANDBY)") && !strings.Contains(string(out), "(STARTING)") { return false, nil } diff --git a/test/docker/docker_api_test.go b/test/docker/docker_api_test.go index 6ff3d16..3c395fd 100644 --- a/test/docker/docker_api_test.go +++ b/test/docker/docker_api_test.go @@ -1,5 +1,5 @@ /* -© Copyright IBM Corporation 2017, 2019 +© Copyright IBM Corporation 2017, 2020 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1434,3 +1434,44 @@ func TestTraceStrmqm(t *testing.T) { t.Fatalf("No trace files found in trace directory /var/mqm/trace. RC=%d.", rc) } } + +// utilTestHealthCheck is used by TestHealthCheck* to run a container with +// privileges enabled or disabled. Otherwise the same as the golden path tests. +func utilTestHealthCheck(t *testing.T, nonewpriv bool) { + t.Parallel() + cli, err := client.NewEnvClient() + if err != nil { + t.Fatal(err) + } + containerConfig := container.Config{ + Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"}, + } + hostConfig := getDefaultHostConfig(t, cli) + hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("no-new-privileges:%v", nonewpriv)) + id := runContainerWithHostConfig(t, cli, &containerConfig, hostConfig) + defer cleanContainer(t, cli, id) + waitForReady(t, cli, id) + rc, out := execContainer(t, cli, id, "", []string{"chkmqhealthy"}) + t.Log(out) + if rc != 0 { + t.Errorf("Expected chkmqhealthy to return with exit code 0; got \"%v\"", rc) + t.Logf("Output from chkmqhealthy:\n%v", out) + } + // Stop the container cleanly + stopContainer(t, cli, id) +} + +// TestHealthCheckWithNoNewPrivileges tests golden path start/stop plus +// chkmqhealthy, when running in a container where no new privileges are +// allowed (i.e. setuid is disabled) +func TestHealthCheckWithNoNewPrivileges(t *testing.T) { + utilTestHealthCheck(t, true) +} + +// TestHealthCheckWithNoNewPrivileges tests golden path start/stop plus +// chkmqhealthy when running in a container where new privileges are +// allowed (i.e. setuid is allowed) +// See https://github.com/ibm-messaging/mq-container/issues/428 +func TestHealthCheckWithNewPrivileges(t *testing.T) { + utilTestHealthCheck(t, false) +} diff --git a/test/docker/docker_api_test_util.go b/test/docker/docker_api_test_util.go index c2c589c..597d155 100644 --- a/test/docker/docker_api_test_util.go +++ b/test/docker/docker_api_test_util.go @@ -267,20 +267,8 @@ func generateRandomUID() string { return fmt.Sprint(rand.Intn(max-min) + min) } -// 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. -func runContainerWithPorts(t *testing.T, cli *client.Client, containerConfig *container.Config, ports []int) string { - if containerConfig.Image == "" { - containerConfig.Image = imageName() - } - // Always run as a random user, unless the test has specified otherwise - if containerConfig.User == "" { - containerConfig.User = generateRandomUID() - } - // if coverage - containerConfig.Env = append(containerConfig.Env, "COVERAGE_FILE="+t.Name()+".cov") - containerConfig.Env = append(containerConfig.Env, "EXIT_CODE_FILE="+getExitCodeFilename(t)) +// getDefaultHostConfig creates a HostConfig and populates it with the defaults used in testing +func getDefaultHostConfig(t *testing.T, cli *client.Client) *container.HostConfig { hostConfig := container.HostConfig{ Binds: []string{ coverageBind(t), @@ -299,6 +287,37 @@ func runContainerWithPorts(t *testing.T, cli *client.Client, containerConfig *co } else { t.Logf("Detected MQ Advanced image - dropping all capabilities") } + return &hostConfig +} + +// runContainerWithHostConfig creates and starts a container, using the supplied HostConfig. +// Note that a default HostConfig can be created using getDefaultHostConfig. +func runContainerWithHostConfig(t *testing.T, cli *client.Client, containerConfig *container.Config, hostConfig *container.HostConfig) string { + if containerConfig.Image == "" { + containerConfig.Image = imageName() + } + // Always run as a random user, unless the test has specified otherwise + if containerConfig.User == "" { + containerConfig.User = generateRandomUID() + } + // if coverage + containerConfig.Env = append(containerConfig.Env, "COVERAGE_FILE="+t.Name()+".cov") + containerConfig.Env = append(containerConfig.Env, "EXIT_CODE_FILE="+getExitCodeFilename(t)) + networkingConfig := network.NetworkingConfig{} + t.Logf("Running 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) + return ctr.ID +} + +// 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. +func runContainerWithPorts(t *testing.T, cli *client.Client, containerConfig *container.Config, ports []int) string { + hostConfig := getDefaultHostConfig(t, cli) for _, p := range ports { port := nat.Port(fmt.Sprintf("%v/tcp", p)) hostConfig.PortBindings[port] = []nat.PortBinding{ @@ -307,14 +326,7 @@ func runContainerWithPorts(t *testing.T, cli *client.Client, containerConfig *co }, } } - networkingConfig := network.NetworkingConfig{} - t.Logf("Running 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) - return ctr.ID + return runContainerWithHostConfig(t, cli, containerConfig, hostConfig) } // runContainer creates and starts a container. If no image is specified in @@ -526,6 +538,7 @@ func waitForContainer(t *testing.T, cli *client.Client, ID string, timeout time. // execContainer runs a command in a running container, and returns the exit code and output func execContainer(t *testing.T, cli *client.Client, ID string, user string, cmd []string) (int, string) { + t.Logf("Running command: %v", cmd) config := types.ExecConfig{ User: user, Privileged: false,