Compare commits

..

118 Commits

Author SHA1 Message Date
Arthur Barr
6ef3bfa28d Merge pull request #27 from arthurbarr/master
Change mq_log to mq_containerlog
2018-03-19 10:14:09 +00:00
Arthur Barr
401306b3ab Merge branch 'master' of https://github.com/ibm-messaging/mq-container 2018-03-19 09:53:29 +00:00
Arthur Barr
e809d0959b Changed mq_log to mq_containerlog 2018-03-19 09:53:02 +00:00
Arthur Barr
ef4232dee5 Merge pull request #26 from arthurbarr/master
Log missing termination errors
2018-03-14 14:50:12 +00:00
Arthur Barr
37b650c367 Merge branch 'master' of https://github.com/ibm-messaging/mq-container 2018-03-14 14:22:17 +00:00
Arthur Barr
4fc5460ac8 Log termination errors 2018-03-14 14:20:32 +00:00
Arthur Barr
01331b0203 Merge pull request #25 from arthurbarr/master
Latest updates
2018-03-12 16:42:37 +00:00
Arthur Barr
b240a84ce0 Fix occasional timing error in tests 2018-03-12 16:28:40 +00:00
Arthur Barr
c75aed0851 Fix typo in README 2018-03-12 16:28:12 +00:00
Arthur Barr
d495b3640e Add extra fields to runmqserver JSON log 2018-03-12 16:27:57 +00:00
Arthur Barr
98c594c91e Fix log message in Makefile 2018-03-12 11:19:55 +00:00
Arthur Barr
6b4720a07c Merge branch 'dev' 2018-03-12 11:16:00 +00:00
Arthur Barr
10e448056b Add web server to dev image 2018-03-12 11:14:59 +00:00
Arthur Barr
1b350549cc Merge pull request #24 from arthurbarr/master
Logging updates
2018-03-06 14:55:11 +00:00
Arthur Barr
104098c7b4 Merge branch 'master' of https://github.com/ibm-messaging/mq-container 2018-03-06 13:48:19 +00:00
Arthur Barr
a14da7aaa8 Change LOG_FORMAT to basic 2018-03-06 13:47:52 +00:00
Arthur Barr
ab60a8975f Remove logrus (MIT license) from notices file 2018-03-05 14:55:48 +00:00
Arthur Barr
9c072060df Remove alpha docs 2018-03-05 14:48:43 +00:00
Arthur Barr
6f8b47a670 Merge pull request #23 from arthurbarr/master
Latest updates
2018-03-05 14:38:20 +00:00
Arthur Barr
8710721fe3 Prevent pruning of test deps 2018-03-05 13:20:12 +00:00
Arthur Barr
37e07fc0b0 Merge conflict 2018-03-05 10:59:41 +00:00
Arthur Barr
97f5f44b92 Clarify creation of downloads dir 2018-03-05 10:57:22 +00:00
Arthur Barr
e07110108f Mirror MQ system logs 2018-03-05 10:57:05 +00:00
Arthur Barr
1e0ba3d897 Use mutex for logger 2018-03-05 10:15:09 +00:00
Arthur Barr
782fe367a5 Latest security fixes 2018-03-05 10:14:26 +00:00
Rob Parker
e1829f3f47 do make deps in travis and apply security fixes 2018-02-23 12:58:01 +00:00
Arthur Barr
91bf65ab57 Remove logrus to allow for easier customization 2018-02-22 17:29:06 +00:00
Arthur Barr
b497a04dcb Update to use LOG_FORMAT environment variable 2018-02-22 13:21:16 +00:00
Arthur Barr
c9cc1741c7 Write termination message 2018-02-22 11:31:42 +00:00
Arthur Barr
d70bbe4dfa Log output from chk commands 2018-02-22 11:22:30 +00:00
Arthur Barr
cb475e8dde Run chk commands as mqm 2018-02-21 12:55:58 +00:00
Arthur Barr
c30a8e4223 Avoid hard-coded mqm uid/gid 2018-02-21 10:19:29 +00:00
Arthur Barr
02cce7ab96 Tidy up debug logging 2018-02-21 09:58:49 +00:00
Arthur Barr
8c92e02a23 Additional logging tests 2018-02-21 09:58:26 +00:00
Arthur Barr
0845025cf8 Latest security fixes 2018-02-21 09:58:00 +00:00
Arthur Barr
3600841e2b Clean up commented code 2018-02-20 14:44:24 +00:00
Arthur Barr
05d1c6f0de Move qmgr code to its own file 2018-02-20 14:41:35 +00:00
Arthur Barr
ace6e6364c Use context and waitgroup for mirroring 2018-02-20 14:35:58 +00:00
Arthur Barr
b23ec084fa Rename logging to mirror 2018-02-19 15:03:39 +00:00
Arthur Barr
e65d679a21 Put variable data in /run instead of /tmp 2018-02-19 10:34:53 +00:00
Arthur Barr
b62a883d4c Improvements to log mirroring 2018-02-15 14:59:14 +00:00
Arthur Barr
9b8e32cbf0 Update CHANGELOG 2018-02-14 14:21:08 +00:00
Arthur Barr
4cdf03a77c Mirror error logs by default 2018-02-14 14:16:00 +00:00
Arthur Barr
2312842a9f Add initial internals doc 2018-02-14 11:29:21 +00:00
Arthur Barr
911f9db2fd Update readiness check 2018-02-14 11:18:20 +00:00
Arthur Barr
54a5052631 Add DEV.LISTENER.TCP to dev config 2018-02-14 11:14:32 +00:00
Arthur Barr
1d223f3157 Require Docker V17.06.1 or greater to build 2018-02-14 11:07:59 +00:00
Arthur Barr
cf687196b7 Ignore .vscode 2018-02-14 11:06:56 +00:00
Arthur Barr
47c865d497 Simplify text log output 2018-02-13 12:49:49 +00:00
Arthur Barr
40c7ab927e Fix typo for missing 'make' command 2018-02-12 16:44:39 +00:00
Arthur Barr
3f4dd2dd3a Limit apt to main/updates/security only 2018-02-07 16:28:16 +00:00
Arthur Barr
b24e375912 Fix copyright dates 2018-02-06 12:02:02 +00:00
Arthur Barr
48185668af Only mirror messages, not whole error log 2018-02-06 12:00:05 +00:00
Arthur Barr
cbfa24a267 Use TEST_OPTS_DOCKER for dev server tests 2018-02-06 11:58:35 +00:00
Arthur Barr
0459880a65 Initial developer config 2018-02-06 11:57:56 +00:00
Arthur Barr
741aa18da9 Upgrade to dep 0.4.1 2018-02-06 11:44:34 +00:00
Arthur Barr
eab783f3c5 Security fixes 2018-02-06 11:26:42 +00:00
Arthur Barr
a03bad9ee7 Improve build docs 2018-01-30 08:56:08 +00:00
Arthur Barr
81bba0db32 Add mirrorFunc to handle mirrored messages 2018-01-29 17:05:19 +00:00
Arthur Barr
753612530f MQ error logs in JSON by default 2018-01-29 12:34:54 +00:00
Arthur Barr
f565e9fe66 Change Bluemix to IBM Cloud 2018-01-29 11:09:17 +00:00
Arthur Barr
ce40e207a4 Merge branch 'master' of https://github.com/ibm-messaging/mq-container 2018-01-29 11:06:11 +00:00
Arthur Barr
ed1154c881 Alpha logging support (JSON+mirroring) 2018-01-29 11:04:01 +00:00
Arthur Barr
d1852d9af4 Allow unit tests on non-Linux OS 2018-01-29 09:35:02 +00:00
Rob Parker
0eacad5848 Fix docker version check to do both major and minor checks at same time 2018-01-12 15:37:09 +00:00
Arthur Barr
bf4cdfc906 Merge pull request #21 from arthurbarr/master
Helm charts V1.1, plus Makefile and test improvements
2018-01-02 14:14:51 +00:00
Arthur Barr
238529918c Switch from Moby to Docker import 2018-01-02 13:52:20 +00:00
Arthur Barr
20b54f35ce Merge branch 'master' of https://github.com/ibm-messaging/mq-container 2018-01-02 11:10:31 +00:00
Arthur Barr
bf31513b99 Update charts to V1.1.0 2018-01-02 11:09:40 +00:00
Arthur Barr
451ea00aed Update README with new defaults 2017-12-19 16:28:21 +00:00
Arthur Barr
095009dba0 New MQ logo 2017-12-19 15:01:06 +00:00
Arthur Barr
fe23ec9aaa Add metadata for ICP UI 2017-12-19 14:59:56 +00:00
Arthur Barr
45f53082aa Add nodeAffinity for mixed clusters 2017-12-19 11:22:57 +00:00
Arthur Barr
f6c583df0b Move chart tests to sub-directory 2017-12-19 10:59:42 +00:00
Arthur Barr
ee94b1702a Add a default nameOverride 2017-12-19 10:47:08 +00:00
Arthur Barr
64867944a3 Add selector for dev chart 2017-12-19 10:44:01 +00:00
Arthur Barr
b938a5eb95 Updates for coverage 2017-12-18 15:38:05 +00:00
Arthur Barr
b6b72952e4 Add TestMQSC 2017-12-18 12:00:19 +00:00
Arthur Barr
aeefd0a9fc Improve version checking in charts 2017-12-13 14:13:59 +00:00
Arthur Barr
a2326dc0f6 Add ability to use different base image 2017-12-11 09:12:39 +00:00
Arthur Barr
93389309f4 Merge pull request #20 from arthurbarr/master
Latest build and test changes
2017-12-06 11:23:28 +00:00
Arthur Barr
d6182bf2fc Fix build problems 2017-12-06 10:19:47 +00:00
Arthur Barr
499c6d4b18 Correct architecture test 2017-12-05 17:54:32 +00:00
Arthur Barr
18635b704c Additional fix for Travis 2017-12-05 16:08:35 +00:00
Arthur Barr
878442905d Fix Travis download of dep 2017-12-05 15:39:32 +00:00
Arthur Barr
b66a799d7f Fix Makefile error checking 2017-12-05 15:27:07 +00:00
Arthur Barr
584a4f2eb4 Fixes for running on older Docker versions 2017-12-05 13:22:36 +00:00
Arthur Barr
c297e1bf5d Improve docs 2017-12-05 11:49:44 +00:00
Arthur Barr
abbc8ce852 Remove unnecessary TTY usage 2017-12-05 10:57:08 +00:00
Arthur Barr
176543e100 Run Docker tests in parallel 2017-12-05 10:27:36 +00:00
Arthur Barr
f5515d72a3 Test for security vulnerabilities 2017-12-05 10:17:31 +00:00
Arthur Barr
70f1a43fd8 More license tweaks 2017-11-30 15:20:00 +00:00
Arthur Barr
81b5db4969 Add unit test for license resolution 2017-11-29 17:25:35 +00:00
Arthur Barr
2f3ca70d7b Document running a single test 2017-11-29 16:08:12 +00:00
Arthur Barr
a80b839c14 Add TestZombies and improve exec output handling 2017-11-29 15:50:10 +00:00
Arthur Barr
5b0259ec6e Add test comments 2017-11-29 12:36:48 +00:00
Arthur Barr
0ad595dc99 Improve dependency handling 2017-11-29 12:31:04 +00:00
Arthur Barr
0823fd1cea Add TestVolumeUnmount 2017-11-29 12:29:31 +00:00
Arthur Barr
f3a1f94445 Merge pull request #19 from arthurbarr/master
Miscellaneous improvements
2017-11-28 15:58:45 +00:00
Arthur Barr
5780ec2a26 Easier multi-arch or multi-version builds 2017-11-28 14:52:41 +00:00
Arthur Barr
7ea6b4c610 Detect arch for image tag 2017-11-28 12:08:19 +00:00
Arthur Barr
4aa8918745 MQ Advanced for Developers V9.0.4 2017-11-28 12:05:47 +00:00
Arthur Barr
4200d9df1b Enable coverage for failure cases 2017-11-22 17:20:57 +00:00
Arthur Barr
f492171276 Test extra error paths 2017-11-22 16:14:34 +00:00
Arthur Barr
d674334574 Improvements to coverage reporting 2017-11-22 16:14:16 +00:00
Arthur Barr
fef9ab4f06 Fix runmqserver error handling 2017-11-22 16:13:30 +00:00
Arthur Barr
46110436b8 Change command exit status handling 2017-11-21 15:53:14 +00:00
Arthur Barr
5e787ba4cf Clean up build network on failure 2017-11-21 15:51:12 +00:00
Arthur Barr
099397442b Restructure packages for reuse 2017-11-21 14:32:09 +00:00
Arthur Barr
3848c39147 Add build section to docs 2017-11-21 10:48:19 +00:00
Arthur Barr
3bbdeec3f0 Merge pull request #18 from parrobe/master
Fix README install file name and add MQ package install hook
2017-11-16 12:49:16 +00:00
Rob Parker
faf6c4bc08 Correct indentation of new build-arg 2017-11-16 11:22:31 +00:00
Rob Parker
15acd8ccdc Correct installer package file name 2017-11-15 13:09:23 +00:00
Rob Parker
fceac2d4b8 Add hook into Makefile to allow MQ_PACKAGES to be specified 2017-11-15 13:08:07 +00:00
Arthur Barr
3da06b27a5 Merge pull request #16 from arthurbarr/master
Updates to code coverage and Explorer
2017-11-13 14:57:19 +00:00
Arthur Barr
f65b06be3c Merge branch 'master' of https://github.com/ibm-messaging/mq-container 2017-11-13 13:54:05 +00:00
Arthur Barr
35043ee488 Update instructions for Explorer 2017-11-13 13:52:55 +00:00
Arthur Barr
4874483f9c Get code coverage working 2017-11-08 16:46:14 +00:00
415 changed files with 75841 additions and 7005 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.DS_Store
.vscode
test/docker/coverage
test/docker/vendor
test/kubernetes/vendor

View File

@@ -13,17 +13,13 @@ before_install:
- sudo apt-get update
- sudo apt-get -y install docker-ce
- curl https://glide.sh/get | sh
- curl -LO https://github.com/golang/dep/releases/download/v0.3.0/dep-linux-amd64.zip
- unzip dep-linux-amd64.zip
- sudo mv dep /usr/local/bin
- rm dep-linux-amd64.zip
- sudo curl -Lo /usr/local/bin/dep https://github.com/golang/dep/releases/download/v0.4.1/dep-linux-amd64
- sudo chmod +x /usr/local/bin/dep
install:
- echo nothing
before_script:
- make deps
script:
- make deps
- make build-devserver
- make test-devserver

View File

@@ -1,5 +1,13 @@
# Change log
## master
* Container's stdout can now be set to JSON format (set LOG_FORMAT=json)
* MQ error logs (in JSON or plain text) are now mirrored on stdout for the container.
* `chkmqready` now waits until MQSC scripts in `/etc/mqm` have been applied
* `chkmqready` and `chkmqhealthy` now run as the "mqm" user
* Added ability to optionally use an alternative base image
* Various build and test improvements
## 9.0.4 (2017-11-06)
* Updated to MQ version 9.0.4.0
* Updated to Go version 9

View File

@@ -1,4 +1,4 @@
# © Copyright IBM Corporation 2015, 2017
# © Copyright IBM Corporation 2015, 2018
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,31 +12,36 @@
# See the License for the specific language governing permissions and
# limitations under the License.
ARG BASE_IMAGE=ubuntu:16.04
###############################################################################
# Build stage to build Go code
###############################################################################
FROM golang:1.9 as builder
WORKDIR /go/src/github.com/ibm-messaging/mq-container/
COPY cmd/ ./cmd
COPY pkg/ ./pkg
COPY internal/ ./internal
COPY vendor/ ./vendor
RUN go build ./cmd/runmqserver/
RUN go build ./cmd/chkmqready/
RUN go build ./cmd/chkmqhealthy/
# Run all unit tests
RUN go test -v ./cmd/runmqserver/
RUN go test -v ./cmd/chkmqready/
RUN go test -v ./cmd/chkmqhealthy/
RUN go test -v ./internal/...
# Build stage to run Go unit tests
FROM golang:1.9 as tester
COPY pkg/ ./pkg
RUN cd pkg/name && go test
RUN cd pkg/linux/capabilities && go test
###############################################################################
# Main build stage, to build MQ image
FROM ubuntu:16.04
###############################################################################
FROM $BASE_IMAGE
# The URL to download the MQ installer from in tar.gz format
# This assumes an archive containing the MQ Debian (.deb) install packages
ARG MQ_URL
# The MQ packages to install
ARG MQ_PACKAGES="ibmmq-server ibmmq-java ibmmq-jre ibmmq-gskit ibmmq-msg-.* ibmmq-samples ibmmq-ams"
# The MQ packages to install - see install-mq.sh for default value
ARG MQ_PACKAGES
COPY install-mq.sh /usr/local/bin/
@@ -45,16 +50,21 @@ RUN chmod u+x /usr/local/bin/install-mq.sh \
&& sleep 1 \
&& install-mq.sh
# Create a directory for runtime data from runmqserver
RUN mkdir -p /run/runmqserver \
&& chown mqm:mqm /run/runmqserver
COPY --from=builder /go/src/github.com/ibm-messaging/mq-container/runmqserver /usr/local/bin/
COPY --from=builder /go/src/github.com/ibm-messaging/mq-container/chkmq* /usr/local/bin/
COPY NOTICES.txt /opt/mqm/licenses/notices-container.txt
RUN chmod +x /usr/local/bin/runmqserver \
&& chmod +x /usr/local/bin/chkmq*
RUN chmod ug+x /usr/local/bin/runmqserver \
&& chown mqm:mqm /usr/local/bin/*mq* \
&& chmod ug+xs /usr/local/bin/chkmq*
# Always use port 1414
EXPOSE 1414
ENV LANG=en_US.UTF-8 AMQ_DIAGNOSTIC_MSG_SEVERITY=1
ENV LANG=en_US.UTF-8 AMQ_DIAGNOSTIC_MSG_SEVERITY=1 AMQ_ADDITIONAL_JSON_LOG=1 LOG_FORMAT=basic
ENTRYPOINT ["runmqserver"]

View File

@@ -12,11 +12,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
FROM mqadvanced:latest-x86_64
ARG BASE_IMAGE
# Build stage to build Go code
FROM golang:1.9 as builder
WORKDIR /go/src/github.com/ibm-messaging/mq-container/
COPY cmd/ ./cmd
COPY internal/ ./internal
COPY vendor/ ./vendor
RUN go test -c -covermode=count -coverpkg $(go list ./cmd/runmqserver ./internal/... | paste -s -d, -) ./cmd/runmqserver
FROM $BASE_IMAGE
# Copy in the version of the code instrumented for code coverage
COPY build/runmqserver.test /usr/local/bin/runmqserver.test
COPY --from=builder /go/src/github.com/ibm-messaging/mq-container/runmqserver.test /usr/local/bin/
RUN chmod +x /usr/local/bin/runmqserver.test \
&& mkdir -p /var/coverage/
ENTRYPOINT ["runmqserver.test", "-test", "-test.coverprofile", "/var/coverage/coverage.cov"]
ENTRYPOINT ["runmqserver.test", "-test", "-test.coverprofile", "/var/coverage/container.cov"]

205
Makefile
View File

@@ -1,4 +1,4 @@
# © Copyright IBM Corporation 2017
# © Copyright IBM Corporation 2017, 2018
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,22 +12,75 @@
# See the License for the specific language governing permissions and
# limitations under the License.
BUILD_SERVER_CONTAINER=build-server
# Set architecture for Go code. Don't set GOOS globally, so that tests can be run locally
export GOARCH ?= amd64
DOCKER_TAG_ARCH ?= x86_64
# By default, all Docker client commands are run inside a Docker container.
# This means that newer features of the client can be used, even with an older daemon.
DOCKER ?= docker run --tty --interactive --rm --volume /var/run/docker.sock:/var/run/docker.sock --volume "$(CURDIR)":/var/src --workdir /var/src docker:stable docker
DOCKER_TAG ?= latest-$(DOCKER_TAG_ARCH)
DOCKER_REPO_DEVSERVER ?= mq-devserver
DOCKER_REPO_ADVANCEDSERVER ?= mq-advancedserver
DOCKER_FULL_DEVSERVER = $(DOCKER_REPO_DEVSERVER):$(DOCKER_TAG)
DOCKER_FULL_ADVANCEDSERVER = $(DOCKER_REPO_ADVANCEDSERVER):$(DOCKER_TAG)
###############################################################################
# Conditional variables - you can override the values of these variables from
# the command line
###############################################################################
# BASE_IMAGE is the base image to use for MQ, for example "ubuntu" or "rhel"
BASE_IMAGE ?= ubuntu:16.04
# MQ_VERSION is the fully qualified MQ version number to build
MQ_VERSION ?= 9.0.4.0
# MQ_ARCHIVE is the name of the file, under the downloads directory, from which MQ Advanced can
# be installed. The default value is derived from MQ_VERSION, BASE_IMAGE and architecture
# Does not apply to MQ Advanced for Developers.
MQ_ARCHIVE ?= IBM_MQ_$(MQ_VERSION)_$(MQ_ARCHIVE_TYPE)_$(MQ_ARCHIVE_ARCH).tar.gz
# MQ_ARCHIVE_DEV is the name of the file, under the downloads directory, from which MQ Advanced
# for Developers can be installed
MQ_ARCHIVE_DEV ?= $(MQ_ARCHIVE_DEV_$(MQ_VERSION))
# Options to `go test` for the Docker tests
TEST_OPTS_DOCKER ?=
# Options to `go test` for the Kubernetes tests
TEST_OPTS_KUBERNETES ?=
# MQ_IMAGE_ADVANCEDSERVER is the name and tag of the built MQ Advanced image
MQ_IMAGE_ADVANCEDSERVER ?=mqadvanced-server:$(MQ_VERSION)-$(ARCH)-$(BASE_IMAGE_TAG)
# MQ_IMAGE_DEVSERVER is the name and tag of the built MQ Advanced for Developers image
MQ_IMAGE_DEVSERVER ?=mqadvanced-server-dev:$(MQ_VERSION)-$(ARCH)-$(BASE_IMAGE_TAG)
# DOCKER is the Docker command to run
DOCKER ?= docker
# MQ_PACKAGES specifies the MQ packages (.deb or .rpm) to install. Defaults vary on base image.
MQ_PACKAGES ?=
###############################################################################
# Other variables
###############################################################################
# ARCH is the platform architecture (e.g. x86_64, ppc64le or s390x)
ARCH = $(shell uname -m)
# BUILD_SERVER_CONTAINER is the name of the web server container used at build time
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=$(shell docker info --format "{{ .NCPU }}")
# BASE_IMAGE_TAG is a normalized version of BASE_IMAGE, suitable for use in a Docker tag
BASE_IMAGE_TAG=$(subst /,-,$(subst :,-,$(BASE_IMAGE)))
MQ_IMAGE_DEVSERVER_BASE=mqadvanced-server-dev-base:$(MQ_VERSION)-$(ARCH)-$(BASE_IMAGE_TAG)
# Try to figure out which archive to use from the BASE_IMAGE
ifeq "$(findstring ubuntu,$(BASE_IMAGE))" "ubuntu"
MQ_ARCHIVE_TYPE=UBUNTU
else
MQ_ARCHIVE_TYPE=LINUX
endif
# Try to figure out which archive to use from the architecture
ifeq "$(ARCH)" "x86_64"
MQ_ARCHIVE_ARCH=X86-64
else ifeq "$(ARCH)" "ppc64le"
MQ_ARCHIVE_ARCH=LE_POWER
else ifeq "$(ARCH)" "s390x"
MQ_ARCHIVE_ARCH=SYSTEM_Z
endif
# Archive names for IBM MQ Advanced for Developers for Ubuntu
MQ_ARCHIVE_DEV_9.0.3.0=mqadv_dev903_ubuntu_x86-64.tar.gz
MQ_ARCHIVE_DEV_9.0.4.0=mqadv_dev904_ubuntu_x86-64.tar.gz
###############################################################################
# Build targets
###############################################################################
.PHONY: vars
vars:
#ifeq "$(findstring ubuntu,$(BASE_IMAGE))","ubuntu"
@echo $(MQ_ARCHIVE_ARCH)
@echo $(MQ_ARCHIVE_TYPE)
@echo $(MQ_ARCHIVE)
.PHONY: default
default: build-devserver test
@@ -49,47 +102,80 @@ clean:
rm -rf ./build
rm -rf ./deps
downloads/mqadv_dev903_ubuntu_x86-64.tar.gz:
downloads/$(MQ_ARCHIVE_DEV):
$(info $(SPACER)$(shell printf $(TITLE)"Downloading IBM MQ Advanced for Developers"$(END)))
mkdir -p downloads
cd downloads; curl -LO https://public.dhe.ibm.com/ibmdl/export/pub/software/websphere/messaging/mqadv/mqadv_dev903_ubuntu_x86-64.tar.gz
cd downloads; curl -LO https://public.dhe.ibm.com/ibmdl/export/pub/software/websphere/messaging/mqadv/$(MQ_ARCHIVE_DEV)
.PHONY: downloads
downloads: downloads/mqadv_dev903_ubuntu_x86-64.tar.gz
downloads: downloads/$(MQ_ARCHIVE_DEV)
.PHONY: deps
deps:
glide install --strip-vendor
# Vendor Go dependencies for the Docker tests
test/docker/vendor:
cd test/docker && dep ensure -vendor-only
# Vendor Go dependencies for the Kubernetes tests
test/kubernetes/vendor:
cd test/docker && dep ensure -vendor-only
cd test/kubernetes && dep ensure -vendor-only
.PHONY: build-cov
build-cov:
mkdir -p build
cd build; go test -c -covermode=count ../cmd/runmqserver
# Shortcut to just run the unit tests
.PHONY: test-unit
test-unit:
docker build --target builder --file Dockerfile-server .
.PHONY: test-advancedserver
test-advancedserver:
cd pkg/name && go test
cd test/docker && TEST_IMAGE=$(DOCKER_FULL_ADVANCEDSERVER) go test $(TEST_OPTS_DOCKER)
test-advancedserver: test/docker/vendor
$(info $(SPACER)$(shell printf $(TITLE)"Test $(MQ_IMAGE_ADVANCEDSERVER) on Docker"$(END)))
cd test/docker && TEST_IMAGE=$(MQ_IMAGE_ADVANCEDSERVER) go test -parallel $(NUM_CPU) $(TEST_OPTS_DOCKER)
.PHONY: test-devserver
test-devserver:
$(info $(SPACER)$(shell printf $(TITLE)"Test $(DOCKER_FULL_DEVSERVER)"$(END)))
cd pkg/name && go test
cd test/docker && TEST_IMAGE=$(DOCKER_FULL_DEVSERVER) go test
test-devserver: test/docker/vendor
$(info $(SPACER)$(shell printf $(TITLE)"Test $(MQ_IMAGE_DEVSERVER) on Docker"$(END)))
cd test/docker && TEST_IMAGE=$(MQ_IMAGE_DEVSERVER) go test -parallel $(NUM_CPU) -tags mqdev $(TEST_OPTS_DOCKER)
.PHONY: test-advancedserver-cover
test-advancedserver-cover: test/docker/vendor
$(info $(SPACER)$(shell printf $(TITLE)"Test $(MQ_IMAGE_ADVANCEDSERVER) on Docker with code coverage"$(END)))
rm -f ./coverage/unit*.cov
# Run unit tests with coverage, for each package under 'internal'
go list -f '{{.Name}}' ./internal/... | xargs -I {} go test -cover -covermode count -coverprofile ./coverage/unit-{}.cov ./internal/{}
# ls -1 ./cmd | xargs -I {} go test -cover -covermode count -coverprofile ./coverage/unit-{}.cov ./cmd/{}/...
echo 'mode: count' > ./coverage/unit.cov
tail -q -n +2 ./coverage/unit-*.cov >> ./coverage/unit.cov
go tool cover -html=./coverage/unit.cov -o ./coverage/unit.html
rm -f ./test/docker/coverage/*.cov
rm -f ./coverage/docker.*
mkdir -p ./test/docker/coverage/
cd test/docker && TEST_IMAGE=$(MQ_IMAGE_ADVANCEDSERVER)-cover TEST_COVER=true go test $(TEST_OPTS_DOCKER)
echo 'mode: count' > ./coverage/docker.cov
tail -q -n +2 ./test/docker/coverage/*.cov >> ./coverage/docker.cov
go tool cover -html=./coverage/docker.cov -o ./coverage/docker.html
echo 'mode: count' > ./coverage/combined.cov
tail -q -n +2 ./coverage/unit.cov ./coverage/docker.cov >> ./coverage/combined.cov
go tool cover -html=./coverage/combined.cov -o ./coverage/combined.html
.PHONY: test-kubernetes-devserver
test-kubernetes-devserver:
$(call test-kubernetes,$(DOCKER_REPO_DEVSERVER),$(DOCKER_TAG),"../../charts/ibm-mqadvanced-server-dev")
test-kubernetes-devserver: test/kubernetes/vendor
$(call test-kubernetes,$(MQ_IMAGE_DEVSERVER),"../../charts/ibm-mqadvanced-server-dev")
.PHONY: test-kubernetes-advancedserver
test-kubernetes-advancedserver:
$(call test-kubernetes,$(DOCKER_REPO_ADVANCEDSERVER),$(DOCKER_TAG),"../../charts/ibm-mqadvanced-server-prod")
test-kubernetes-advancedserver: test/kubernetes/vendor
$(call test-kubernetes,$(MQ_IMAGE_ADVANCEDSERVER),"../../charts/ibm-mqadvanced-server-prod")
define test-kubernetes
$(info $(SPACER)$(shell printf $(TITLE)"Test $1:$2 on Kubernetes"$(END)))
cd test/kubernetes && TEST_REPO=$1 TEST_TAG=$2 TEST_CHART=$3 go test $(TEST_OPTS_KUBERNETES)
$(info $(SPACER)$(shell printf $(TITLE)"Test $1 on Kubernetes"$(END)))
cd test/kubernetes && TEST_IMAGE=$1 TEST_CHART=$2 go test $(TEST_OPTS_KUBERNETES)
endef
define docker-build-mq
@@ -104,59 +190,48 @@ define docker-build-mq
--volume "$(realpath ./downloads/)":/usr/share/nginx/html:ro \
--detach \
nginx:alpine
# Build the new image
# Build the new image (use --pull to make sure we have the latest base image)
$(DOCKER) build \
--pull \
--tag $1 \
--file $2 \
--network build \
--build-arg MQ_URL=http://build:80/$3 \
--build-arg BASE_IMAGE=$(BASE_IMAGE) \
--label IBM_PRODUCT_ID=$4 \
--label IBM_PRODUCT_NAME=$5 \
--label IBM_PRODUCT_VERSION=$6 \
.
# Stop the web server (will also remove the container)
$(DOCKER) kill $(BUILD_SERVER_CONTAINER)
# Delete the temporary network
$(DOCKER) network rm build
--build-arg MQ_PACKAGES="$(MQ_PACKAGES)" \
. ; $(DOCKER) kill $(BUILD_SERVER_CONTAINER) && $(DOCKER) network rm build
endef
# .PHONY: build-advancedserver-903
# build-advancedserver-903: build downloads/CNJR7ML.tar.gz
# $(info $(SPACER)$(shell printf $(TITLE)"Build $(DOCKER_FULL_ADVANCEDSERVER)"$(END)))
# $(call docker-build-mq,$(DOCKER_FULL_ADVANCEDSERVER),Dockerfile-server,CNJR7ML.tar.gz,"4486e8c4cc9146fd9b3ce1f14a2dfc5b","IBM MQ Advanced","9.0.3")
# $(DOCKER) tag $(DOCKER_FULL_ADVANCEDSERVER) $(DOCKER_REPO_ADVANCEDSERVER):9.0.3-$(DOCKER_TAG_ARCH)
.PHONY: build-advancedserver-904
build-advancedserver-904: downloads/CNLE4ML.tar.gz
$(info $(SPACER)$(shell printf $(TITLE)"Build $(DOCKER_FULL_ADVANCEDSERVER)"$(END)))
$(call docker-build-mq,$(DOCKER_FULL_ADVANCEDSERVER),Dockerfile-server,CNLE4ML.tar.gz,"4486e8c4cc9146fd9b3ce1f14a2dfc5b","IBM MQ Advanced","9.0.4")
$(DOCKER) tag $(DOCKER_FULL_ADVANCEDSERVER) $(DOCKER_REPO_ADVANCEDSERVER):9.0.4-$(DOCKER_TAG_ARCH)
DOCKER_SERVER_VERSION=$(shell docker version --format "{{ .Server.Version }}")
DOCKER_CLIENT_VERSION=$(shell docker version --format "{{ .Client.Version }}")
.PHONY: docker-version
docker-version:
@test "$(word 1,$(subst ., ,$(DOCKER_CLIENT_VERSION)))" -ge "17" || ("$(word 1,$(subst ., ,$(DOCKER_CLIENT_VERSION)))" -eq "17" && "$(word 2,$(subst ., ,$(DOCKER_CLIENT_VERSION)))" -ge "05") || (echo "Error: Docker client 17.05 or greater is required" && exit 1)
@test "$(word 1,$(subst ., ,$(DOCKER_SERVER_VERSION)))" -ge "17" || ("$(word 1,$(subst ., ,$(DOCKER_SERVER_VERSION)))" -eq "17" && "$(word 2,$(subst ., ,$(DOCKER_CLIENT_VERSION)))" -ge "05") || (echo "Error: Docker server 17.05 or greater is required" && exit 1)
.PHONY: build-advancedserver
build-advancedserver: build-advancedserver-904
build-advancedserver: downloads/$(MQ_ARCHIVE) docker-version
$(info $(SPACER)$(shell printf $(TITLE)"Build $(MQ_IMAGE_ADVANCEDSERVER)"$(END)))
$(call docker-build-mq,$(MQ_IMAGE_ADVANCEDSERVER),Dockerfile-server,$(MQ_ARCHIVE),"4486e8c4cc9146fd9b3ce1f14a2dfc5b","IBM MQ Advanced",$(MQ_VERSION))
.PHONY: build-devserver
build-devserver: downloads/mqadv_dev903_ubuntu_x86-64.tar.gz
$(info $(shell printf $(TITLE)"Build $(DOCKER_FULL_DEVSERVER)"$(END)))
$(call docker-build-mq,$(DOCKER_FULL_DEVSERVER),Dockerfile-server,mqadv_dev903_ubuntu_x86-64.tar.gz,"98102d16795c4263ad9ca075190a2d4d","IBM MQ Advanced for Developers (Non-Warranted)","9.0.3")
$(DOCKER) tag $(DOCKER_FULL_DEVSERVER) $(DOCKER_REPO_DEVSERVER):9.0.3-$(DOCKER_TAG_ARCH)
# .PHONY: build-server
# build-server: build downloads/CNJR7ML.tar.gz
# $(call docker-build-mq,mq-server:latest-$(DOCKER_TAG_ARCH),Dockerfile-server,"79afd716d55b4f149a87bec52c9dc1aa","IBM MQ","9.0.3")
# $(DOCKER) tag mq-server:latest-$(DOCKER_TAG_ARCH) mq-server:9.0.3-$(DOCKER_TAG_ARCH)
# Target-specific variable to add web server into devserver image
build-devserver: MQ_PACKAGES=ibmmq-server ibmmq-java ibmmq-jre ibmmq-gskit ibmmq-msg-.* ibmmq-samples ibmmq-ams ibmmq-web
build-devserver: downloads/$(MQ_ARCHIVE_DEV) docker-version
@test "$(shell uname -m)" = "x86_64" || (echo "Error: MQ Advanced for Developers is only available for x86_64 architecture" && exit 1)
$(info $(shell printf $(TITLE)"Build $(MQ_IMAGE_DEVSERVER_BASE)"$(END)))
$(call docker-build-mq,$(MQ_IMAGE_DEVSERVER_BASE),Dockerfile-server,$(MQ_ARCHIVE_DEV),"98102d16795c4263ad9ca075190a2d4d","IBM MQ Advanced for Developers (Non-Warranted)",$(MQ_VERSION))
docker build --tag $(MQ_IMAGE_DEVSERVER) --file incubating/mqadvanced-server-dev/Dockerfile .
.PHONY: build-advancedserver-cover
build-advancedserver-cover: build-advanced-server build-cov
$(DOCKER) build -t mq-advancedserver:cover -f Dockerfile-server.cover .
# .PHONY: build-web
# build-web: build downloads/CNJR7ML.tar.gz
# $(call docker-build-mq,mq-web:latest-$(DOCKER_TAG_ARCH),Dockerfile-mq-web)
build-advancedserver-cover: docker-version
$(DOCKER) build --build-arg BASE_IMAGE=$(MQ_IMAGE_ADVANCEDSERVER) -t $(MQ_IMAGE_ADVANCEDSERVER)-cover -f Dockerfile-server.cover .
.PHONY: build-explorer
build-explorer: downloads/mqadv_dev903_ubuntu_x86-64.tar.gz
$(call docker-build-mq,mq-explorer:latest-$(DOCKER_TAG_ARCH),incubating/mq-explorer/Dockerfile-mq-explorer,mqadv_dev903_ubuntu_x86-64.tar.gz,"98102d16795c4263ad9ca075190a2d4d","IBM MQ Advanced for Developers (Non-Warranted)","9.0.3")
build-explorer: downloads/$(MQ_ARCHIVE_DEV)
$(call docker-build-mq,mq-explorer:latest-$(ARCH),incubating/mq-explorer/Dockerfile-mq-explorer,$(MQ_ARCHIVE_DEV),"98102d16795c4263ad9ca075190a2d4d","IBM MQ Advanced for Developers (Non-Warranted)",$(MQ_VERSION))
include formatting.mk

View File

@@ -1,100 +1,66 @@
===============================================================================
THE FOLLOWING SECTIONS IDENTIFY VARIOUS COMPONENTS CONTAINED IN THE PROGRAM, AND SPECIFY CERTAIN NOTICES AND OTHER INFORMATION REGARDING THOSE COMPONENTS THAT IBM IS REQUIRED TO PROVIDE TO YOU.
NOTWITHSTANDING ANY PROVISION CONTAINED IN ANY OF THE NOTICES AND OTHER INFORMATION SET FORTH BELOW, YOUR USE OF THESE PROGRAM COMPONENTS REMAINS SUBJECT TO THE TERMS AND CONDITIONS SET FORTH IN:
(i) THE PROGRAM'S LICENSE INFORMATION DOCUMENT; AND
(ii) THE IBM LICENSE AGREEMENT SPECIFIED IN THAT LICENSE INFORMATION DOCUMENT.
===============================================================================
==============================================================================
A. SUMMARY
===============================================================================
Section B. below contains provisions relating to certain other components of the Program, as follows:-
B.1 MIT license
Affected Components:
* github.com/hpcloud/tail
Copyright 2015 Hewlett Packard Enterprise Development LP
Copyright (c) 2014 ActiveState
B.2 BSD 3-Clause license
Affected Components:
* golang.org/x/crypto
Copyright (c) 2009 The Go Authors. All rights reserved.
* golang.org/x/sys
Copyright (c) 2009 The Go Authors. All rights reserved.
* gopkg.in/fsnotify.v1
Copyright (c) 2012 The Go Authors. All rights reserved.
Copyright (c) 2012 fsnotify Authors. All rights reserved.
* gopkg.in/tomb.v1
Copyright (c) 2010-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>. All rights reserved.
===============================================================================
END OF A. SUMMARY
===============================================================================
==========================================================================
B. LICENSE FILES AND OTHER INFORMATION
==========================================================================
==========================================================================
B.1 MIT license
==========================================================================
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
==========================================================================
END OF B.1 MIT license
==========================================================================
==========================================================================
B.2 BSD 3-Clause license
==========================================================================
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
==========================================================================
END OF B.2 BSD 3-Clause license
==========================================================================
==========================================================================
END OF B. LICENSE FILES AND OTHER INFORMATION
==========================================================================
===========================================================
===============================================================================
THE FOLLOWING SECTIONS IDENTIFY VARIOUS COMPONENTS CONTAINED IN THE PROGRAM, AND SPECIFY CERTAIN NOTICES AND OTHER INFORMATION REGARDING THOSE COMPONENTS THAT IBM IS REQUIRED TO PROVIDE TO YOU.
NOTWITHSTANDING ANY PROVISION CONTAINED IN ANY OF THE NOTICES AND OTHER INFORMATION SET FORTH BELOW, YOUR USE OF THESE PROGRAM COMPONENTS REMAINS SUBJECT TO THE TERMS AND CONDITIONS SET FORTH IN:
(i) THE PROGRAM'S LICENSE INFORMATION DOCUMENT; AND
(ii) THE IBM LICENSE AGREEMENT SPECIFIED IN THAT LICENSE INFORMATION DOCUMENT.
===============================================================================
==============================================================================
A. SUMMARY
===============================================================================
Section B. below contains provisions relating to certain other components of the Program, as follows:-
B.1 BSD 3-Clause license
Affected Components:
* golang.org/x/crypto
Copyright (c) 2009 The Go Authors. All rights reserved.
* golang.org/x/sys
Copyright (c) 2009 The Go Authors. All rights reserved.
===============================================================================
END OF A. SUMMARY
===============================================================================
==========================================================================
B. LICENSE FILES AND OTHER INFORMATION
==========================================================================
==========================================================================
B.1 BSD 3-Clause license
==========================================================================
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
==========================================================================
END OF B.1 BSD 3-Clause license
==========================================================================
==========================================================================
END OF B. LICENSE FILES AND OTHER INFORMATION
==========================================================================
===========================================================
END OF NOTICES AND INFORMATION

View File

@@ -2,21 +2,13 @@
# Overview
Run [IBM® MQ](http://www-03.ibm.com/software/products/en/ibm-mq) in a container. The supplied [Helm](https://helm.sh/) chart can be used to run the container on a [Kubernetes](https://kubernetes.io) cluster, such as [IBM Cloud Private](https://www.ibm.com/cloud-computing/products/ibm-cloud-private/) or the [IBM Bluemix Container Service](https://www.ibm.com/cloud-computing/bluemix/containers).
Run [IBM® MQ](http://www-03.ibm.com/software/products/en/ibm-mq) in a container. The supplied [Helm](https://helm.sh/) charts can be used to run the container on a [Kubernetes](https://kubernetes.io) cluster, such as [IBM Cloud Private](https://www.ibm.com/cloud-computing/products/ibm-cloud-private/) or the [IBM Cloud Container Service](https://www.ibm.com/cloud/container-service).
# Current status
MQ Advanced for Developers image - [![Build Status](https://travis-ci.org/ibm-messaging/mq-container.svg?branch=master)](https://travis-ci.org/ibm-messaging/mq-container)
# Build
After extracting the code from this repository, you can build an image with the latest version of MQ using the following command:
## Building MQ Advanced
You can build an image for MQ Advanced, follow these steps:
1. Clone this repository into the correct location in your [`GOPATH`](https://github.com/golang/go/wiki/GOPATH)
2. Create a directory called `downloads` under the cloned directory tree
3. Download the MQ Advanced for Ubuntu (debs) installer package file `CNJR7ML.tar.gz` into the `downloads` directory
4. Run `make build-advancedserver`
After extracting the code from this repository, you can build an image by following the instructions [here](docs/building.md)
# Usage
In order to use the image, it is necessary to accept the terms of the IBM MQ license. This is achieved by specifying the environment variable `LICENSE` equal to `accept` when running the image. You can also view the license terms by setting this variable to `view`. Failure to set the variable will result in the termination of the container with a usage statement. You can view the license in a different language by also setting the `LANG` environment variable.
@@ -27,6 +19,7 @@ In order to use the image, it is necessary to accept the terms of the IBM MQ lic
* **LICENSE** - Set this to `accept` to agree to the MQ Advanced for Developers license. If you wish to see the license you can set this to `view`.
* **LANG** - Set this to the language you would like the license to be printed in.
* **MQ_QMGR_NAME** - Set this to the name you want your Queue Manager to be created with.
* **LOG_FORMAT** - Set this to change the format of the logs which are printed on the container's stdout. Set to "json" to use JSON format (JSON object per line); set to "basic" to use a simple human-readable format. Defaults to "basic".
# Issues and contributions
@@ -47,4 +40,4 @@ Note: The IBM MQ Advanced for Developers license does not permit further distrib
# Copyright
© Copyright IBM Corporation 2015, 2017
© Copyright IBM Corporation 2015, 2018

View File

@@ -15,6 +15,6 @@
apiVersion: v1
description: IBM MQ queue manager
name: ibm-mqadvanced-server-dev
version: 1.0.2
icon: https://developer.ibm.com/messaging/wp-content/uploads/sites/18/2017/07/IBM-MQ-Square-200.png
version: 1.1.0
icon: https://developer.ibm.com/messaging/wp-content/uploads/sites/18/2017/12/ibm_mq_200.png
tillerVersion: ">=2.4.0"

View File

@@ -1,5 +1,3 @@
![IBM MQ logo](https://developer.ibm.com/messaging/wp-content/uploads/sites/18/2017/07/IBM-MQ-Square-200.png)
# IBM MQ
IBM® MQ is messaging middleware that simplifies and accelerates the integration of diverse applications and business data across multiple platforms. It uses message queues to facilitate the exchanges of information and offers a single messaging solution for cloud, mobile, Internet of Things (IoT) and on-premises environments.
@@ -12,7 +10,7 @@ This chart deploys a single IBM MQ Advanced for Developers server (queue manager
- Kubernetes 1.6 or greater, with beta APIs enabled
- If persistence is enabled (see [configuration](#configuration)), then you either need to create a PersistentVolume, or specify a Storage Class if classes are defined in your cluster.
¸
## Installing the Chart
To install the chart with the release name `foo`:
@@ -63,7 +61,7 @@ The following table lists the configurable parameters of the `ibm-mqadvanced-ser
| `queueManager.name` | MQ Queue Manager name | Helm release name |
| `queueManager.dev.adminPassword` | Developer defaults - administrator password | Random generated string. See the notes that appear when you install for how to retrieve this. |
| `queueManager.dev.appPassword` | Developer defaults - app password | `nil` (no password required to connect an MQ client) |
| `nameOverride` | Set to partially override the resource names used in this chart | `nil` |
| `nameOverride` | Set to partially override the resource names used in this chart | `ibm-mq` |
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`.

View File

@@ -12,7 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
{{- if .Capabilities.APIVersions.Has "apps/v1beta2" }}
apiVersion: apps/v1beta2
{{- else }}
apiVersion: apps/v1beta1
{{- end }}
kind: StatefulSet
metadata:
name: {{ template "fullname" . }}
@@ -22,9 +26,14 @@ metadata:
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
spec:
selector:
matchLabels:
app: {{ template "fullname" . }}
serviceName: {{ .Values.service.name }}
replicas: 1
{{- if and (ge (.Capabilities.KubeVersion.Major | int) 1) (ge (.Capabilities.KubeVersion.Minor | int) 7) }}
{{- if and (.Capabilities.KubeVersion.Major | hasPrefix "1") (.Capabilities.KubeVersion.Minor | hasPrefix "7") }}
# Set updateStrategy to "RollingUpdate", if we're on Kubernetes 1.7.
# It's already the default for apps/v1beta2 (Kubernetes 1.8 onwards)
updateStrategy:
type: RollingUpdate
{{- end }}
@@ -37,6 +46,19 @@ spec:
heritage: "{{ .Release.Service }}"
QM_IDENTIFIER: "{{ .Release.Name }}"
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: beta.kubernetes.io/arch
operator: In
values:
- amd64
- key: beta.kubernetes.io/os
operator: In
values:
- linux
{{- if .Values.image.pullSecret }}
imagePullSecrets:
- name: {{ .Values.image.pullSecret }}

View File

@@ -0,0 +1,176 @@
license:
__metadata:
label: "License"
type: "string"
required: true
options:
- label: "Accepted"
value: "accept"
- label: "Not accepted"
value: "not accepted"
image:
__metadata:
label: "Image"
repository:
__metadata:
label: "Image repository"
description: "Docker image location"
type: "string"
immutable: false
required: true
tag:
__metadata:
label: "Image tag"
description: "Docker image tag"
type: "string"
immutable: false
required: true
pullPolicy:
__metadata:
name: "pullPolicy"
label: "Image pull policy"
description: "Always, Never, or IfNotPresent. Defaults to Always"
type: "string"
immutable: false
required: true
options:
- label: "Always"
value: "Always"
- label: "Never"
value: "Never"
- label: "IfNotPresent"
value: "IfNotPresent"
pullSecret:
__metadata:
label: "Image pull secret"
description: "Secret to use when pulling the image. Set this when using an image from a private registry"
type: "string"
required: false
## global persistence parameters
persistence:
__metadata:
label: "Persistence"
enabled:
__metadata:
label: "Enable persistence"
description: "Whether or not to store MQ messages and configuration on a Persistent Volume"
type: "boolean"
required: true
useDynamicProvisioning:
__metadata:
label: "Use dynamic provisioning"
description: "Whether or not to use Storage Classes to provision a Persisent Volume automatically"
type: "boolean"
required: true
dataPVC:
__metadata:
label: "Data PVC"
name:
__metadata:
label: "Name"
description: "Name of Persistent Volume Claim, used for MQ objects and messages"
type: "string"
required: true
storageClassName:
__metadata:
label: "Storage Class name"
description: "Storage class of Persistent Volume Claim, used for MQ objects and messages"
type: "string"
required: false
size:
__metadata:
label: "Size"
description: "Size of Persistent Volume Claim, used for MQ objects and messages"
type: "string"
required: true
service:
__metadata:
label: "Service"
name:
__metadata:
label: "Service name"
description: "Service name"
type: "string"
required: true
type:
__metadata:
label: "Service type"
description: "Type of service"
type: "string"
required: true
options:
- label: "ClusterIP"
value: "ClusterIP"
- label: "NodePort"
value: "NodePort"
- label: "LoadBalancer"
value: "LoadBalancer"
- label: "ExternalName"
value: "ExternalName"
resources:
__metadata:
label: "Resources"
requests:
cpu:
__metadata:
label: "CPU request"
description: "The requested CPU"
type: "string"
required: true
memory:
__metadata:
label: "Memory request"
description: "The requested memory"
type: "string"
required: true
limits:
cpu:
__metadata:
label: "CPU limit"
description: "The CPU limit"
type: "string"
required: true
memory:
__metadata:
label: "Memory limit"
description: "The memory limit"
type: "string"
required: true
queueManager:
__metadata:
label: "Queue manager"
name:
__metadata:
label: "Queue manager name"
description: "MQ queue manager name, which defaults to the Helm release name"
type: "string"
required: false
dev:
__metadata:
label: "Default Developer Configuration"
description: "Configure some queue manager basics, suitable for developer use"
adminPassword:
__metadata:
label: "Admin password"
description: "Password for 'admin' user"
type: "password"
required: false
appPassword:
__metadata:
label: "App password"
description: "Password for 'app' user to use for messaging"
type: "password"
required: false
nameOverride:
__metadata:
label: "Name override"
description: "This can be set to partially override the name of the resources created by this chart"
type: "string"
required: false

View File

@@ -65,4 +65,4 @@ queueManager:
appPassword:
# nameOverride can be set to partially override the name of the resources created by this chart
nameOverride:
nameOverride: ibm-mq

View File

@@ -15,6 +15,6 @@
apiVersion: v1
description: IBM MQ queue manager
name: ibm-mqadvanced-server-prod
version: 1.0.2
icon: https://developer.ibm.com/messaging/wp-content/uploads/sites/18/2017/07/IBM-MQ-Square-200.png
version: 1.1.0
icon: https://developer.ibm.com/messaging/wp-content/uploads/sites/18/2017/12/ibm_mq_200.png
tillerVersion: ">=2.4.0"

View File

@@ -1,5 +1,3 @@
![IBM MQ logo](https://developer.ibm.com/messaging/wp-content/uploads/sites/18/2017/07/IBM-MQ-Square-200.png)
# IBM MQ
IBM® MQ is messaging middleware that simplifies and accelerates the integration of diverse applications and business data across multiple platforms. It uses message queues to facilitate the exchanges of information and offers a single messaging solution for cloud, mobile, Internet of Things (IoT) and on-premises environments.
@@ -45,8 +43,8 @@ The following table lists the configurable parameters of the `ibm-mqadvanced-ser
| Parameter | Description | Default |
| ------------------------------- | --------------------------------------------------------------- | ------------------------------------------ |
| `license` | Set to `accept` to accept the terms of the IBM license | `"not accepted"` |
| `image.repository` | Image full name including repository | `nil` |
| `image.tag` | Image tag | `nil` |
| `image.repository` | Image full name including repository | `MQ image in your registry` |
| `image.tag` | Image tag | `Tag of MQ image in your registry` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `image.pullSecret` | Image pull secret, if you are using a private Docker registry | `nil` |
| `persistence.enabled` | Use persistent volumes for all defined volumes | `true` |
@@ -61,7 +59,7 @@ The following table lists the configurable parameters of the `ibm-mqadvanced-ser
| `resources.requests.cpu` | Kubernetes CPU request for the Queue Manager container | `1` |
| `resources.requests.memory` | Kubernetes memory request for the Queue Manager container | `1Gi` |
| `queueManager.name` | MQ Queue Manager name | Helm release name |
| `nameOverride` | Set to partially override the resource names used in this chart | `nil` |
| `nameOverride` | Set to partially override the resource names used in this chart | `ibm-mq` |
| `livenessProbe.initialDelaySeconds` | The initial delay before starting the liveness probe. Useful for slower systems that take longer to start the Queue Manager. | 60 |
| `livenessProbe.periodSeconds` | How often to run the probe | 10 |
| `livenessProbe.timeoutSeconds` | Number of seconds after which the probe times out | 5 |

View File

@@ -12,7 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
{{- if .Capabilities.APIVersions.Has "apps/v1beta2" }}
apiVersion: apps/v1beta2
{{- else }}
apiVersion: apps/v1beta1
{{- end }}
kind: StatefulSet
metadata:
name: {{ template "fullname" . }}
@@ -22,9 +26,14 @@ metadata:
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
spec:
selector:
matchLabels:
app: {{ template "fullname" . }}
serviceName: {{ .Values.service.name }}
replicas: 1
{{- if and (ge (.Capabilities.KubeVersion.Major | int) 1) (ge (.Capabilities.KubeVersion.Minor | int) 7) }}
{{- if and (.Capabilities.KubeVersion.Major | hasPrefix "1") (.Capabilities.KubeVersion.Minor | hasPrefix "7") }}
# Set updateStrategy to "RollingUpdate", if we're on Kubernetes 1.7.
# It's already the default for apps/v1beta2 (Kubernetes 1.8 onwards)
updateStrategy:
type: RollingUpdate
{{- end }}
@@ -37,6 +46,19 @@ spec:
heritage: "{{ .Release.Service }}"
QM_IDENTIFIER: "{{ .Release.Name }}"
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: beta.kubernetes.io/arch
operator: In
values:
- amd64
- key: beta.kubernetes.io/os
operator: In
values:
- linux
{{- if .Values.image.pullSecret }}
imagePullSecrets:
- name: {{ .Values.image.pullSecret }}

View File

@@ -0,0 +1,219 @@
license:
__metadata:
label: "License"
type: "string"
required: true
options:
- label: "Accepted"
value: "accept"
- label: "Not accepted"
value: "not accepted"
image:
__metadata:
label: "Image"
repository:
__metadata:
label: "Image repository"
description: "Docker image location"
type: "string"
immutable: false
required: true
tag:
__metadata:
label: "Image tag"
description: "Docker image tag"
type: "string"
immutable: false
required: true
pullPolicy:
__metadata:
name: "pullPolicy"
label: "Image pull policy"
description: "Always, Never, or IfNotPresent. Defaults to Always"
type: "string"
immutable: false
required: true
options:
- label: "Always"
value: "Always"
- label: "Never"
value: "Never"
- label: "IfNotPresent"
value: "IfNotPresent"
pullSecret:
__metadata:
label: "Image pull secret"
description: "Secret to use when pulling the image. Set this when using an image from a private registry"
type: "string"
required: false
## global persistence parameters
persistence:
__metadata:
label: "Persistence"
enabled:
__metadata:
label: "Enable persistence"
description: "Whether or not to store MQ messages and configuration on a Persistent Volume"
type: "boolean"
required: true
useDynamicProvisioning:
__metadata:
label: "Use dynamic provisioning"
description: "Whether or not to use Storage Classes to provision a Persisent Volume automatically"
type: "boolean"
required: true
dataPVC:
__metadata:
label: "Data PVC"
name:
__metadata:
label: "Name"
description: "Name of Persistent Volume Claim, used for MQ objects and messages"
type: "string"
required: true
storageClassName:
__metadata:
label: "Storage Class name"
description: "Storage class of Persistent Volume Claim, used for MQ objects and messages"
type: "string"
required: false
size:
__metadata:
label: "Size"
description: "Size of Persistent Volume Claim, used for MQ objects and messages"
type: "string"
required: true
service:
__metadata:
label: "Service"
name:
__metadata:
label: "Service name"
description: "Service name"
type: "string"
required: true
type:
__metadata:
label: "Service type"
description: "Type of service"
type: "string"
required: true
options:
- label: "ClusterIP"
value: "ClusterIP"
- label: "NodePort"
value: "NodePort"
- label: "LoadBalancer"
value: "LoadBalancer"
- label: "ExternalName"
value: "ExternalName"
resources:
__metadata:
label: "Resources"
requests:
cpu:
__metadata:
label: "CPU request"
description: "The requested CPU"
type: "string"
required: true
memory:
__metadata:
label: "Memory request"
description: "The requested memory"
type: "string"
required: true
limits:
cpu:
__metadata:
label: "CPU limit"
description: "The CPU limit"
type: "string"
required: true
memory:
__metadata:
label: "Memory limit"
description: "The memory limit"
type: "string"
required: true
queueManager:
__metadata:
label: "Queue manager"
name:
__metadata:
label: "Queue manager name"
description: "MQ queue manager name, which defaults to the Helm release name"
type: "string"
required: false
nameOverride:
__metadata:
label: "Name override"
description: "This can be set to partially override the name of the resources created by this chart"
type: "string"
required: false
# livenessProbe section specifies setting for the MQ liveness probe, which checks for a running Queue Manager
livenessProbe:
__metadata:
label: "Liveness probe"
# initialDelaySeconds should be raised if your system cannot start the Queue Manager in 60 seconds
initialDelaySeconds:
__metadata:
label: "Initial delay (seconds)"
description: "How long to wait before starting the probe. Raise this delay if your system cannot start the Queue Manager in the default time period"
type: "number"
required: false
periodSeconds:
__metadata:
label: "Period (seconds)"
description: "How often to perform the probe"
type: "number"
required: false
timeoutSeconds:
__metadata:
label: "Timeout (seconds)"
description: "How long before a probe times out"
type: "number"
required: false
failureThreshold:
__metadata:
label: "Failure threshold"
description: "Number of times the probe can fail before taking action"
type: "number"
required: false
# readinessProbe section specifies setting for the MQ readiness probe, which checks when the MQ listener is running
readinessProbe:
__metadata:
label: "Readiness probe"
initialDelaySeconds:
__metadata:
label: "Initial delay (seconds)"
description: "How long to wait before starting the probe"
type: "number"
required: false
periodSeconds:
__metadata:
label: "Period (seconds)"
description: "How often to perform the probe"
type: "number"
required: false
timeoutSeconds:
__metadata:
label: "Timeout (seconds)"
description: "How long before a probe times out"
type: "number"
required: false
failureThreshold:
__metadata:
label: "Failure threshold"
description: "Number of times the probe can fail before taking action"
type: "number"
required: false

View File

@@ -59,7 +59,7 @@ queueManager:
name:
# nameOverride can be set to partially override the name of the resources created by this chart
nameOverride:
nameOverride: ibm-mq
# livenessProbe section specifies setting for the MQ liveness probe, which checks for a running Queue Manager
livenessProbe:

View File

@@ -18,11 +18,12 @@ limitations under the License.
package main
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/ibm-messaging/mq-container/pkg/name"
"github.com/ibm-messaging/mq-container/internal/name"
)
func queueManagerHealthy() (bool, error) {
@@ -35,8 +36,10 @@ func queueManagerHealthy() (bool, error) {
// Run the command and wait for completion
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Println(err)
return false, err
}
fmt.Println(out)
if !strings.Contains(string(out), "(RUNNING)") {
return false, nil
}

View File

@@ -1,5 +1,5 @@
/*
© Copyright IBM Corporation 2017
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -18,13 +18,23 @@ limitations under the License.
package main
import (
"fmt"
"net"
"os"
"github.com/ibm-messaging/mq-container/internal/ready"
)
func main() {
// Check if runmqserver has indicated that it's finished configuration
r, err := ready.Check()
if !r || err != nil {
os.Exit(1)
}
// Check if the queue manager has a running listener
conn, err := net.Dial("tcp", "127.0.0.1:1414")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
conn.Close()

145
cmd/runmqdevserver/main.go Normal file
View File

@@ -0,0 +1,145 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"syscall"
"github.com/ibm-messaging/mq-container/internal/command"
"github.com/ibm-messaging/mq-container/internal/logger"
"github.com/ibm-messaging/mq-container/internal/name"
)
var log *logger.Logger
func setPassword(user string, password string) error {
cmd := exec.Command("chpasswd")
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
fmt.Fprintf(stdin, "%s:%s", user, password)
stdin.Close()
_, _, err = command.RunCmd(cmd)
if err != nil {
return err
}
log.Printf("Set password for \"%v\" user", user)
return nil
}
func getLogFormat() string {
return os.Getenv("LOG_FORMAT")
}
func getDebug() bool {
debug := os.Getenv("DEBUG")
if debug == "true" || debug == "1" {
return true
}
return false
}
func configureLogger() error {
var err error
f := getLogFormat()
d := getDebug()
n, err := name.GetQueueManagerName()
if err != nil {
return err
}
switch f {
case "json":
log, err = logger.NewLogger(os.Stderr, d, true, n)
if err != nil {
return err
}
case "basic":
log, err = logger.NewLogger(os.Stderr, d, false, n)
if err != nil {
return err
}
default:
log, err = logger.NewLogger(os.Stdout, d, false, n)
return fmt.Errorf("invalid value for LOG_FORMAT: %v", f)
}
return nil
}
func logTerminationf(format string, args ...interface{}) {
logTermination(fmt.Sprintf(format, args))
}
// TODO: Duplicated code
func logTermination(args ...interface{}) {
msg := fmt.Sprint(args)
// Write the message to the termination log. This is the default place
// that Kubernetes will look for termination information.
log.Debugf("Writing termination message: %v", msg)
err := ioutil.WriteFile("/dev/termination-log", []byte(msg), 0660)
if err != nil {
log.Debug(err)
}
log.Error(msg)
}
func doMain() error {
err := configureLogger()
if err != nil {
logTermination(err)
return err
}
adminPassword, set := os.LookupEnv("MQ_ADMIN_PASSWORD")
if set {
err = setPassword("admin", adminPassword)
if err != nil {
logTerminationf("Error setting admin password: %v", err)
return err
}
}
appPassword, set := os.LookupEnv("MQ_APP_PASSWORD")
if set {
err = setPassword("app", appPassword)
if err != nil {
logTerminationf("Error setting app password: %v", err)
return err
}
}
err = updateMQSC(set)
if err != nil {
logTerminationf("Error updating MQSC: %v", err)
return err
}
return nil
}
var osExit = os.Exit
func main() {
err := doMain()
if err != nil {
osExit(1)
} else {
// Replace this process with runmqserver
syscall.Exec("/usr/local/bin/runmqserver", []string{"runmqserver"}, os.Environ())
}
}

View File

@@ -0,0 +1,57 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"html/template"
"os"
)
func updateMQSC(appPasswordRequired bool) error {
var checkClient string
if appPasswordRequired {
checkClient = "REQUIRED"
} else {
checkClient = "ASQMGR"
}
const mqsc string = "/etc/mqm/dev.mqsc"
if os.Getenv("MQ_DEV") == "true" {
const mqscTemplate string = mqsc + ".tpl"
// Re-configure channel if app password not set
t, err := template.ParseFiles(mqscTemplate)
if err != nil {
log.Error(err)
return err
}
f, err := os.OpenFile(mqsc, os.O_CREATE|os.O_WRONLY, 0660)
defer f.Close()
err = t.Execute(f, map[string]string{"ChckClnt": checkClient})
if err != nil {
log.Error(err)
return err
}
// TODO: Lookup value for MQM user here?
err = os.Chown(mqsc, 999, 999)
if err != nil {
log.Error(err)
return err
}
// os.Remove(mqscTemplate)
} else {
os.Remove(mqsc)
}
return nil
}

View File

@@ -1,5 +1,5 @@
/*
© Copyright IBM Corporation 2017
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,15 +16,29 @@ limitations under the License.
package main
import (
"log"
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
"syscall"
)
const mqmUID uint32 = 999
const mqmGID uint32 = 999
func lookupMQM() (int, int, error) {
mqm, err := user.Lookup("mqm")
if err != nil {
return -1, -1, err
}
mqmUID, err := strconv.Atoi(mqm.Uid)
if err != nil {
return -1, -1, err
}
mqmGID, err := strconv.Atoi(mqm.Gid)
if err != nil {
return -1, -1, err
}
return mqmUID, mqmGID, nil
}
func createVolume(path string) error {
dataPath := filepath.Join(path, "data")
@@ -46,8 +60,13 @@ func createVolume(path string) error {
sys := fi.Sys()
if sys != nil && runtime.GOOS == "linux" {
stat := sys.(*syscall.Stat_t)
if stat.Uid != mqmUID || stat.Gid != mqmGID {
err = os.Chown(dataPath, int(mqmUID), int(mqmGID))
mqmUID, mqmGID, err := lookupMQM()
if err != nil {
return err
}
log.Debugf("mqm user is %v (%v)", mqmUID, mqmGID)
if int(stat.Uid) != mqmUID || int(stat.Gid) != mqmGID {
err = os.Chown(dataPath, mqmUID, mqmGID)
if err != nil {
log.Printf("Error: Unable to change ownership of %v", dataPath)
return err

View File

@@ -0,0 +1,92 @@
/*
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// resolveLicenseFile returns the file name of the MQ license file, taking into
// account the language set by the LANG environment variable
func resolveLicenseFile() string {
lang, ok := os.LookupEnv("LANG")
if !ok {
return "English.txt"
}
switch {
case strings.HasPrefix(lang, "zh_TW"):
return "Chinese_TW.txt"
case strings.HasPrefix(lang, "zh"):
return "Chinese.txt"
// Differentiate Czech (cs) and Kashubian (csb)
case strings.HasPrefix(lang, "cs") && !strings.HasPrefix(lang, "csb"):
return "Czech.txt"
case strings.HasPrefix(lang, "fr"):
return "French.txt"
case strings.HasPrefix(lang, "de"):
return "German.txt"
case strings.HasPrefix(lang, "el"):
return "Greek.txt"
case strings.HasPrefix(lang, "id"):
return "Indonesian.txt"
case strings.HasPrefix(lang, "it"):
return "Italian.txt"
case strings.HasPrefix(lang, "ja"):
return "Japanese.txt"
// Differentiate Korean (ko) from Konkani (kok)
case strings.HasPrefix(lang, "ko") && !strings.HasPrefix(lang, "kok"):
return "Korean.txt"
case strings.HasPrefix(lang, "lt"):
return "Lithuanian.txt"
case strings.HasPrefix(lang, "pl"):
return "Polish.txt"
case strings.HasPrefix(lang, "pt"):
return "Portugese.txt"
case strings.HasPrefix(lang, "ru"):
return "Russian.txt"
case strings.HasPrefix(lang, "sl"):
return "Slovenian.txt"
case strings.HasPrefix(lang, "es"):
return "Spanish.txt"
case strings.HasPrefix(lang, "tr"):
return "Turkish.txt"
}
return "English.txt"
}
func checkLicense() (bool, error) {
lic, ok := os.LookupEnv("LICENSE")
switch {
case ok && lic == "accept":
return true, nil
case ok && lic == "view":
file := filepath.Join("/opt/mqm/licenses", resolveLicenseFile())
buf, err := ioutil.ReadFile(file)
if err != nil {
log.Println(err)
return false, err
}
log.Println(string(buf))
return false, nil
}
log.Println("Error: Set environment variable LICENSE=accept to indicate acceptance of license terms and conditions.")
log.Println("License agreements and information can be viewed by setting the environment variable LICENSE=view. You can also set the LANG environment variable to view the license in a different language.")
return false, errors.New("Set environment variable LICENSE=accept to indicate acceptance of license terms and conditions")
}

View File

@@ -0,0 +1,282 @@
/*
© Copyright IBM Corporation 2017
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE_2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"testing"
)
var licenseTests = []struct {
in string
out string
}{
{"en_US.UTF_8", "English.txt"},
{"en_US.ISO-8859-15", "English.txt"},
{"es_GB", "Spanish.txt"},
{"el_ES.UTF_8", "Greek.txt"},
// Cover a wide variety of valid values
{"af", "English.txt"},
{"af_ZA", "English.txt"},
{"ar", "English.txt"},
{"ar_AE", "English.txt"},
{"ar_BH", "English.txt"},
{"ar_DZ", "English.txt"},
{"ar_EG", "English.txt"},
{"ar_IQ", "English.txt"},
{"ar_JO", "English.txt"},
{"ar_KW", "English.txt"},
{"ar_LB", "English.txt"},
{"ar_LY", "English.txt"},
{"ar_MA", "English.txt"},
{"ar_OM", "English.txt"},
{"ar_QA", "English.txt"},
{"ar_SA", "English.txt"},
{"ar_SY", "English.txt"},
{"ar_TN", "English.txt"},
{"ar_YE", "English.txt"},
{"az", "English.txt"},
{"az_AZ", "English.txt"},
{"az_AZ", "English.txt"},
{"be", "English.txt"},
{"be_BY", "English.txt"},
{"bg", "English.txt"},
{"bg_BG", "English.txt"},
{"bs_BA", "English.txt"},
{"ca", "English.txt"},
{"ca_ES", "English.txt"},
{"cs", "Czech.txt"},
{"cs_CZ", "Czech.txt"},
{"csb_PL", "English.txt"},
{"cy", "English.txt"},
{"cy_GB", "English.txt"},
{"da", "English.txt"},
{"da_DK", "English.txt"},
{"de", "German.txt"},
{"de_AT", "German.txt"},
{"de_CH", "German.txt"},
{"de_DE", "German.txt"},
{"de_LI", "German.txt"},
{"de_LU", "German.txt"},
{"dv", "English.txt"},
{"dv_MV", "English.txt"},
{"el", "Greek.txt"},
{"el_GR", "Greek.txt"},
{"en", "English.txt"},
{"en_AU", "English.txt"},
{"en_BZ", "English.txt"},
{"en_CA", "English.txt"},
{"en_CB", "English.txt"},
{"en_GB", "English.txt"},
{"en_IE", "English.txt"},
{"en_JM", "English.txt"},
{"en_NZ", "English.txt"},
{"en_PH", "English.txt"},
{"en_TT", "English.txt"},
{"en_US", "English.txt"},
{"en_ZA", "English.txt"},
{"en_ZW", "English.txt"},
{"eo", "English.txt"},
{"es", "Spanish.txt"},
{"es_AR", "Spanish.txt"},
{"es_BO", "Spanish.txt"},
{"es_CL", "Spanish.txt"},
{"es_CO", "Spanish.txt"},
{"es_CR", "Spanish.txt"},
{"es_DO", "Spanish.txt"},
{"es_EC", "Spanish.txt"},
{"es_ES", "Spanish.txt"},
{"es_ES", "Spanish.txt"},
{"es_GT", "Spanish.txt"},
{"es_HN", "Spanish.txt"},
{"es_MX", "Spanish.txt"},
{"es_NI", "Spanish.txt"},
{"es_PA", "Spanish.txt"},
{"es_PE", "Spanish.txt"},
{"es_PR", "Spanish.txt"},
{"es_PY", "Spanish.txt"},
{"es_SV", "Spanish.txt"},
{"es_UY", "Spanish.txt"},
{"es_VE", "Spanish.txt"},
{"et", "English.txt"},
{"et_EE", "English.txt"},
{"eu", "English.txt"},
{"eu_ES", "English.txt"},
{"fa", "English.txt"},
{"fa_IR", "English.txt"},
{"fi", "English.txt"},
{"fi_FI", "English.txt"},
{"fo", "English.txt"},
{"fo_FO", "English.txt"},
{"fr", "French.txt"},
{"fr_BE", "French.txt"},
{"fr_CA", "French.txt"},
{"fr_CH", "French.txt"},
{"fr_FR", "French.txt"},
{"fr_LU", "French.txt"},
{"fr_MC", "French.txt"},
{"gl", "English.txt"},
{"gl_ES", "English.txt"},
{"gu", "English.txt"},
{"gu_IN", "English.txt"},
{"he", "English.txt"},
{"he_IL", "English.txt"},
{"hi", "English.txt"},
{"hi_IN", "English.txt"},
{"hr", "English.txt"},
{"hr_BA", "English.txt"},
{"hr_HR", "English.txt"},
{"hu", "English.txt"},
{"hu_HU", "English.txt"},
{"hy", "English.txt"},
{"hy_AM", "English.txt"},
{"id", "Indonesian.txt"},
{"id_ID", "Indonesian.txt"},
{"is", "English.txt"},
{"is_IS", "English.txt"},
{"it", "Italian.txt"},
{"it_CH", "Italian.txt"},
{"it_IT", "Italian.txt"},
{"ja", "Japanese.txt"},
{"ja_JP", "Japanese.txt"},
{"ka", "English.txt"},
{"ka_GE", "English.txt"},
{"kk", "English.txt"},
{"kk_KZ", "English.txt"},
{"kn", "English.txt"},
{"kn_IN", "English.txt"},
{"ko", "Korean.txt"},
{"ko_KR", "Korean.txt"},
{"kok", "English.txt"},
{"kok_IN", "English.txt"},
{"ky", "English.txt"},
{"ky_KG", "English.txt"},
{"lt", "Lithuanian.txt"},
{"lt_LT", "Lithuanian.txt"},
{"lv", "English.txt"},
{"lv_LV", "English.txt"},
{"mi", "English.txt"},
{"mi_NZ", "English.txt"},
{"mk", "English.txt"},
{"mk_MK", "English.txt"},
{"mn", "English.txt"},
{"mn_MN", "English.txt"},
{"mr", "English.txt"},
{"mr_IN", "English.txt"},
{"ms", "English.txt"},
{"ms_BN", "English.txt"},
{"ms_MY", "English.txt"},
{"mt", "English.txt"},
{"mt_MT", "English.txt"},
{"nb", "English.txt"},
{"nb_NO", "English.txt"},
{"nl", "English.txt"},
{"nl_BE", "English.txt"},
{"nl_NL", "English.txt"},
{"nn_NO", "English.txt"},
{"ns", "English.txt"},
{"ns_ZA", "English.txt"},
{"pa", "English.txt"},
{"pa_IN", "English.txt"},
{"pl", "Polish.txt"},
{"pl_PL", "Polish.txt"},
{"ps", "English.txt"},
{"ps_AR", "English.txt"},
{"pt", "Portugese.txt"},
{"pt_BR", "Portugese.txt"},
{"pt_PT", "Portugese.txt"},
{"qu", "English.txt"},
{"qu_BO", "English.txt"},
{"qu_EC", "English.txt"},
{"qu_PE", "English.txt"},
{"ro", "English.txt"},
{"ro_RO", "English.txt"},
{"ru", "Russian.txt"},
{"ru_RU", "Russian.txt"},
{"sa", "English.txt"},
{"sa_IN", "English.txt"},
{"se", "English.txt"},
{"se_FI", "English.txt"},
{"se_FI", "English.txt"},
{"se_FI", "English.txt"},
{"se_NO", "English.txt"},
{"se_NO", "English.txt"},
{"se_NO", "English.txt"},
{"se_SE", "English.txt"},
{"se_SE", "English.txt"},
{"se_SE", "English.txt"},
{"sk", "English.txt"},
{"sk_SK", "English.txt"},
{"sl", "Slovenian.txt"},
{"sl_SI", "Slovenian.txt"},
{"sq", "English.txt"},
{"sq_AL", "English.txt"},
{"sr_BA", "English.txt"},
{"sr_BA", "English.txt"},
{"sr_SP", "English.txt"},
{"sr_SP", "English.txt"},
{"sv", "English.txt"},
{"sv_FI", "English.txt"},
{"sv_SE", "English.txt"},
{"sw", "English.txt"},
{"sw_KE", "English.txt"},
{"syr", "English.txt"},
{"syr_SY", "English.txt"},
{"ta", "English.txt"},
{"ta_IN", "English.txt"},
{"te", "English.txt"},
{"te_IN", "English.txt"},
{"th", "English.txt"},
{"th_TH", "English.txt"},
{"tl", "English.txt"},
{"tl_PH", "English.txt"},
{"tn", "English.txt"},
{"tn_ZA", "English.txt"},
{"tr", "Turkish.txt"},
{"tr_TR", "Turkish.txt"},
{"tt", "English.txt"},
{"tt_RU", "English.txt"},
{"ts", "English.txt"},
{"uk", "English.txt"},
{"uk_UA", "English.txt"},
{"ur", "English.txt"},
{"ur_PK", "English.txt"},
{"uz", "English.txt"},
{"uz_UZ", "English.txt"},
{"uz_UZ", "English.txt"},
{"vi", "English.txt"},
{"vi_VN", "English.txt"},
{"xh", "English.txt"},
{"xh_ZA", "English.txt"},
{"zh", "Chinese.txt"},
{"zh_CN", "Chinese.txt"},
{"zh_HK", "Chinese.txt"},
{"zh_MO", "Chinese.txt"},
{"zh_SG", "Chinese.txt"},
{"zh_TW", "Chinese_TW.txt"},
{"zu", "English.txt"},
{"zu_ZA", "English.txt"},
}
func TestResolveLicenseFile(t *testing.T) {
for _, table := range licenseTests {
os.Setenv("LANG", table.in)
f := resolveLicenseFile()
if f != table.out {
t.Errorf("resolveLicenseFile() with LANG=%v - expected %v, got %v", table.in, table.out, f)
}
}
}

121
cmd/runmqserver/logging.go Normal file
View File

@@ -0,0 +1,121 @@
/*
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/ibm-messaging/mq-container/internal/logger"
"github.com/ibm-messaging/mq-container/internal/mqini"
)
// var debug = false
var log *logger.Logger
func logDebug(args ...interface{}) {
log.Debug(args)
}
func logDebugf(format string, args ...interface{}) {
log.Debugf(format, args...)
}
func logTerminationf(format string, args ...interface{}) {
logTermination(fmt.Sprintf(format, args))
}
func logTermination(args ...interface{}) {
msg := fmt.Sprint(args)
// Write the message to the termination log. This is the default place
// that Kubernetes will look for termination information.
log.Debugf("Writing termination message: %v", msg)
err := ioutil.WriteFile("/dev/termination-log", []byte(msg), 0660)
if err != nil {
log.Debug(err)
}
log.Error(msg)
}
func getLogFormat() string {
return os.Getenv("LOG_FORMAT")
}
func formatSimple(datetime string, message string) string {
return fmt.Sprintf("%v %v\n", datetime, message)
}
// mirrorSystemErrorLogs starts a goroutine to mirror the contents of the MQ system error logs
func mirrorSystemErrorLogs(ctx context.Context, wg *sync.WaitGroup, mf mirrorFunc) (chan error, error) {
// Always use the JSON log as the source
return mirrorLog(ctx, wg, "/var/mqm/errors/AMQERR01.json", false, mf)
}
// mirrorQueueManagerErrorLogs starts a goroutine to mirror the contents of the MQ queue manager error logs
func mirrorQueueManagerErrorLogs(ctx context.Context, wg *sync.WaitGroup, name string, fromStart bool, mf mirrorFunc) (chan error, error) {
// Always use the JSON log as the source
qm, err := mqini.GetQueueManager(name)
if err != nil {
logDebug(err)
return nil, err
}
f := filepath.Join(mqini.GetErrorLogDirectory(qm), "AMQERR01.json")
return mirrorLog(ctx, wg, f, fromStart, mf)
}
func getDebug() bool {
debug := os.Getenv("DEBUG")
if debug == "true" || debug == "1" {
return true
}
return false
}
func configureLogger(name string) (mirrorFunc, error) {
var err error
f := getLogFormat()
d := getDebug()
switch f {
case "json":
log, err = logger.NewLogger(os.Stderr, d, true, name)
if err != nil {
return nil, err
}
return log.LogDirect, nil
case "basic":
log, err = logger.NewLogger(os.Stderr, d, false, name)
if err != nil {
return nil, err
}
return func(msg string) {
// Parse the JSON message, and print a simplified version
var obj map[string]interface{}
json.Unmarshal([]byte(msg), &obj)
fmt.Printf(formatSimple(obj["ibm_datetime"].(string), obj["message"].(string)))
}, nil
default:
log, err = logger.NewLogger(os.Stdout, d, false, name)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("invalid value for LOG_FORMAT: %v", f)
}
}

View File

@@ -1,5 +1,5 @@
/*
© Copyright IBM Corporation 2017
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -18,279 +18,119 @@ limitations under the License.
package main
import (
"fmt"
"io/ioutil"
"log"
"context"
"errors"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"runtime"
"strings"
"syscall"
"sync"
"golang.org/x/sys/unix"
"github.com/ibm-messaging/mq-container/internal/name"
"github.com/ibm-messaging/mq-container/internal/ready"
)
// resolveLicenseFile returns the file name of the MQ license file, taking into
// account the language set by the LANG environment variable
func resolveLicenseFile() string {
lang, ok := os.LookupEnv("LANG")
if !ok {
return "English.txt"
}
switch {
case strings.HasPrefix(lang, "zh_TW"):
return "Chinese_TW.txt"
case strings.HasPrefix(lang, "zh"):
return "Chinese.txt"
case strings.HasPrefix(lang, "cs"):
return "Czech.txt"
case strings.HasPrefix(lang, "fr"):
return "French.txt"
case strings.HasPrefix(lang, "de"):
return "German.txt"
case strings.HasPrefix(lang, "el"):
return "Greek.txt"
case strings.HasPrefix(lang, "id"):
return "Indonesian.txt"
case strings.HasPrefix(lang, "it"):
return "Italian.txt"
case strings.HasPrefix(lang, "ja"):
return "Japanese.txt"
case strings.HasPrefix(lang, "ko"):
return "Korean.txt"
case strings.HasPrefix(lang, "lt"):
return "Lithuanian.txt"
case strings.HasPrefix(lang, "pl"):
return "Polish.txt"
case strings.HasPrefix(lang, "pt"):
return "Portugese.txt"
case strings.HasPrefix(lang, "ru"):
return "Russian.txt"
case strings.HasPrefix(lang, "sl"):
return "Slovenian.txt"
case strings.HasPrefix(lang, "es"):
return "Spanish.txt"
case strings.HasPrefix(lang, "tr"):
return "Turkish.txt"
}
return "English.txt"
}
func checkLicense() {
lic, ok := os.LookupEnv("LICENSE")
switch {
case ok && lic == "accept":
return
case ok && lic == "view":
file := filepath.Join("/opt/mqm/licenses", resolveLicenseFile())
buf, err := ioutil.ReadFile(file)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println(string(buf))
os.Exit(1)
}
fmt.Println("Error: Set environment variable LICENSE=accept to indicate acceptance of license terms and conditions.")
fmt.Println("License agreements and information can be viewed by setting the environment variable LICENSE=view. You can also set the LANG environment variable to view the license in a different language.")
os.Exit(1)
}
// sanitizeQueueManagerName removes any invalid characters from a queue manager name
func sanitizeQueueManagerName(name string) string {
var re = regexp.MustCompile("[^a-zA-Z0-9._%/]")
return re.ReplaceAllString(name, "")
}
// GetQueueManagerName resolves the queue manager name to use. Resolved from
// either an environment variable, or the hostname.
func getQueueManagerName() (string, error) {
var name string
var err error
name, ok := os.LookupEnv("MQ_QMGR_NAME")
if !ok || name == "" {
name, err = os.Hostname()
if err != nil {
return "", err
}
name = sanitizeQueueManagerName(name)
}
// TODO: What if the specified env variable is an invalid name?
return name, nil
}
// runCommand runs an OS command. On Linux it waits for the command to
// complete and returns the exit status (return code).
func runCommand(name string, arg ...string) (string, int, error) {
cmd := exec.Command(name, arg...)
// Run the command and wait for completion
out, err := cmd.CombinedOutput()
func doMain() error {
name, nameErr := name.GetQueueManagerName()
mf, err := configureLogger(name)
if err != nil {
var rc int
// Only works on Linux
if runtime.GOOS == "linux" {
var ws unix.WaitStatus
unix.Wait4(cmd.Process.Pid, &ws, 0, nil)
rc = ws.ExitStatus()
} else {
rc = -1
}
if rc == 0 {
return string(out), rc, nil
}
return string(out), rc, err
logTermination(err)
return err
}
return string(out), 0, nil
}
// createDirStructure creates the default MQ directory structure under /var/mqm
func createDirStructure() {
out, _, err := runCommand("/opt/mqm/bin/crtmqdir", "-f", "-s")
if nameErr != nil {
logTermination(err)
return err
}
err = ready.Clear()
if err != nil {
log.Fatalf("Error creating directory structure: %v\n", string(out))
logTermination(err)
return err
}
log.Println("Created directory structure under /var/mqm")
}
func createQueueManager(name string) {
log.Printf("Creating queue manager %v", name)
out, rc, err := runCommand("crtmqm", "-q", "-p", "1414", name)
accepted, err := checkLicense()
if err != nil {
// 8=Queue manager exists, which is fine
if rc != 8 {
log.Printf("crtmqm returned %v", rc)
log.Fatalln(string(out))
} else {
log.Printf("Detected existing queue manager %v", name)
return
}
logTerminationf("Error checking license acceptance: %v", err)
return err
}
}
func updateCommandLevel() {
level, ok := os.LookupEnv("MQ_CMDLEVEL")
if ok && level != "" {
out, rc, err := runCommand("strmqm", "-e", "CMDLEVEL="+level)
if err != nil {
log.Fatalf("Error %v setting CMDLEVEL: %v", rc, string(out))
}
}
}
func startQueueManager() {
log.Println("Starting queue manager")
out, rc, err := runCommand("strmqm")
if err != nil {
log.Fatalf("Error %v starting queue manager: %v", rc, string(out))
}
log.Println("Started queue manager")
}
func configureQueueManager() {
const configDir string = "/etc/mqm"
files, err := ioutil.ReadDir(configDir)
if err != nil {
log.Fatal(err)
}
for _, file := range files {
if strings.HasSuffix(file.Name(), ".mqsc") {
abs := filepath.Join(configDir, file.Name())
mqsc, err := ioutil.ReadFile(abs)
if err != nil {
log.Fatal(err)
}
cmd := exec.Command("runmqsc")
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
stdin.Write(mqsc)
stdin.Close()
// Run the command and wait for completion
out, err := cmd.CombinedOutput()
if err != nil {
log.Println(err)
}
// Print the runmqsc output, adding tab characters to make it more readable as part of the log
log.Printf("Output for \"runmqsc\" with %v:\n\t%v", abs, strings.Replace(string(out), "\n", "\n\t", -1))
}
}
}
func stopQueueManager() {
log.Println("Stopping queue manager")
out, _, err := runCommand("endmqm", "-w")
if err != nil {
log.Fatalf("Error stopping queue manager: %v", string(out))
}
log.Println("Stopped queue manager")
}
// createTerminateChannel creates a channel which will be closed when SIGTERM
// is received.
func createTerminateChannel() chan struct{} {
done := make(chan struct{})
// Handle SIGTERM
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
go func() {
sig := <-c
log.Printf("Signal received: %v", sig)
stopQueueManager()
close(done)
}()
return done
}
// createReaperChannel creates a channel which will be used to reap zombie
// (defunct) processes. This is a responsibility of processes running
// as PID 1.
func createReaper() {
// Handle SIGCHLD
c := make(chan os.Signal, 3)
signal.Notify(c, syscall.SIGCHLD)
go func() {
for {
<-c
for {
var ws unix.WaitStatus
_, err := unix.Wait4(-1, &ws, 0, nil)
// If err indicates "no child processes" left to reap, then
// wait for next SIGCHLD signal
if err == unix.ECHILD {
break
}
}
}
}()
}
func main() {
createReaper()
checkLicense()
// Start SIGTERM handler channel
done := createTerminateChannel()
name, err := getQueueManagerName()
if err != nil {
log.Fatalln(err)
if !accepted {
err = errors.New("License not accepted")
logTermination(err)
return err
}
log.Printf("Using queue manager name: %v", name)
// Start signal handler
signalControl := signalHandler(name)
logConfig()
err = createVolume("/mnt/mqm")
if err != nil {
log.Fatal(err)
logTermination(err)
return err
}
err = createDirStructure()
if err != nil {
logTermination(err)
return err
}
err = postInit(name)
if err != nil {
logTermination(err)
return err
}
newQM, err := createQueueManager(name)
if err != nil {
logTermination(err)
return err
}
var wg sync.WaitGroup
defer func() {
log.Debug("Waiting for log mirroring to complete")
wg.Wait()
}()
ctx, cancelMirror := context.WithCancel(context.Background())
defer func() {
log.Debug("Cancel log mirroring")
cancelMirror()
}()
// TODO: Use the error channel
_, err = mirrorSystemErrorLogs(ctx, &wg, mf)
if err != nil {
logTermination(err)
return err
}
_, err = mirrorQueueManagerErrorLogs(ctx, &wg, name, newQM, mf)
if err != nil {
logTermination(err)
return err
}
err = updateCommandLevel()
if err != nil {
logTermination(err)
return err
}
err = startQueueManager()
if err != nil {
logTermination(err)
return err
}
createDirStructure()
createQueueManager(name)
updateCommandLevel()
startQueueManager()
configureQueueManager()
// Start reaping zombies from now on.
// Start this here, so that we don't reap any sub-processes created
// by this process (e.g. for crtmqm or strmqm)
signalControl <- startReaping
// Reap zombies now, just in case we've already got some
signalControl <- reapNow
// Write a file to indicate that chkmqready should now work as normal
ready.Set()
// Wait for terminate signal
<-done
<-signalControl
return nil
}
var osExit = os.Exit
func main() {
err := doMain()
if err != nil {
osExit(1)
}
}

View File

@@ -1,5 +1,5 @@
/*
© Copyright IBM Corporation 2017
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,18 +17,38 @@ package main
import (
"flag"
"io/ioutil"
"os"
"strconv"
"testing"
"github.com/ibm-messaging/mq-container/internal/logger"
)
var test *bool
const filename = "/var/coverage/exitCode"
func init() {
test = flag.Bool("test", false, "Set to true when running tests for coverage")
log, _ = logger.NewLogger(os.Stdout, true, false, "test")
}
// Test started when the test binary is started. Only calls main.
func TestSystem(t *testing.T) {
if *test {
var oldExit = osExit
defer func() {
osExit = oldExit
}()
osExit = func(rc int) {
// Write the exit code to a file instead
log.Printf("Writing exit code %v to file %v", strconv.Itoa(rc), filename)
err := ioutil.WriteFile(filename, []byte(strconv.Itoa(rc)), 0644)
if err != nil {
log.Print(err)
}
}
main()
}
}

189
cmd/runmqserver/mirror.go Normal file
View File

@@ -0,0 +1,189 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bufio"
"context"
"fmt"
"os"
"sync"
"time"
)
// waitForFile waits until the specified file exists
func waitForFile(ctx context.Context, path string) (os.FileInfo, error) {
var fi os.FileInfo
var err error
// Wait for file to exist
for {
select {
// Check to see if cancellation has been requested
case <-ctx.Done():
return nil, nil
default:
fi, err = os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
time.Sleep(500 * time.Millisecond)
continue
} else {
return nil, fmt.Errorf("mirror: unable to get info on file %v", path)
}
}
log.Debugf("File exists: %v, %v", path, fi.Size())
return fi, nil
}
}
}
type mirrorFunc func(msg string)
// mirrorAvailableMessages prints lines from the file, until no more are available
func mirrorAvailableMessages(f *os.File, mf mirrorFunc) {
scanner := bufio.NewScanner(f)
count := 0
for scanner.Scan() {
t := scanner.Text()
mf(t)
count++
}
if count > 0 {
log.Debugf("Mirrored %v log entries from %v", count, f.Name())
}
err := scanner.Err()
if err != nil {
log.Errorf("Error reading file %v: %v", f.Name(), err)
return
}
}
// mirrorLog tails the specified file, and logs each line to stdout.
// This is useful for usability, as the container console log can show
// messages from the MQ error logs.
func mirrorLog(ctx context.Context, wg *sync.WaitGroup, path string, fromStart bool, mf mirrorFunc) (chan error, error) {
errorChannel := make(chan error, 1)
var offset int64 = -1
var f *os.File
var err error
var fi os.FileInfo
// Need to check if the file exists before returning, otherwise we have a
// race to see if the new file get created before we can test for it
fi, err = os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
// File doesn't exist, so ensure we start at the beginning
offset = 0
} else {
return nil, err
}
} else {
// If the file exists, open it now, before we return. This makes sure
// the file is open before the queue manager is created or started.
// Otherwise, there would be the potential for a nearly-full file to
// rotate before the goroutine had a chance to open it.
f, err = os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
// File already exists, so start reading at the end
offset = fi.Size()
}
// Increment wait group counter, only if the goroutine gets started
wg.Add(1)
go func() {
// Notify the wait group when this goroutine ends
defer func() {
log.Debugf("Finished monitoring %v", path)
wg.Done()
}()
if f == nil {
// File didn't exist, so need to wait for it
fi, err = waitForFile(ctx, path)
if err != nil {
log.Error(err)
errorChannel <- err
return
}
if fi == nil {
return
}
f, err = os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
log.Error(err)
errorChannel <- err
return
}
}
fi, err = f.Stat()
if err != nil {
log.Error(err)
errorChannel <- err
return
}
// The file now exists. If it didn't exist before we started, offset=0
// Always start at the beginning if we've been told to go from the start
if offset != 0 && !fromStart {
log.Debugf("Seeking offset %v in file %v", offset, path)
f.Seek(offset, 0)
}
closing := false
for {
// If there's already data there, mirror it now.
mirrorAvailableMessages(f, mf)
newFI, err := os.Stat(path)
if err != nil {
log.Error(err)
errorChannel <- err
return
}
if !os.SameFile(fi, newFI) {
log.Debugf("Detected log rotation in file %v", path)
// WARNING: There is a possible race condition here. If *another*
// log rotation happens before we can open the new file, then we
// could skip all those messages. This could happen with a very small
// MQ error log size.
mirrorAvailableMessages(f, mf)
f.Close()
// Re-open file
log.Debugf("Re-opening error log file %v", path)
f, err = os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
log.Error(err)
errorChannel <- err
return
}
fi = newFI
// Don't seek this time, because we know it's a new file
mirrorAvailableMessages(f, mf)
}
select {
case <-ctx.Done():
log.Debugf("Context cancelled for mirroring %v", path)
if closing {
log.Debugf("Shutting down mirror for %v", path)
return
}
// Set a flag, to allow one more time through the loop
closing = true
default:
time.Sleep(500 * time.Millisecond)
}
}
}()
return errorChannel, nil
}

View File

@@ -0,0 +1,191 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"sync"
"testing"
"time"
)
func TestMirrorLogWithoutRotation(t *testing.T) {
// Repeat the test multiple times, to help identify timing problems
for i := 0; i < 10; i++ {
t.Run(t.Name()+strconv.Itoa(i), func(t *testing.T) {
// Use just the sub-test name in the file name
tmp, err := ioutil.TempFile("", strings.Split(t.Name(), "/")[1])
if err != nil {
t.Fatal(err)
}
t.Log(tmp.Name())
defer os.Remove(tmp.Name())
count := 0
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
_, err = mirrorLog(ctx, &wg, tmp.Name(), true, func(msg string) {
count++
})
if err != nil {
t.Fatal(err)
}
f, err := os.OpenFile(tmp.Name(), os.O_WRONLY, 0700)
if err != nil {
t.Fatal(err)
}
log.Println("Logging 3 JSON messages")
fmt.Fprintln(f, "{\"message\"=\"A\"}")
fmt.Fprintln(f, "{\"message\"=\"B\"}")
fmt.Fprintln(f, "{\"message\"=\"C\"}")
f.Close()
cancel()
wg.Wait()
if count != 3 {
t.Fatalf("Expected 3 log entries; got %v", count)
}
})
}
}
func TestMirrorLogWithRotation(t *testing.T) {
// Repeat the test multiple times, to help identify timing problems
for i := 0; i < 5; i++ {
t.Run(t.Name()+strconv.Itoa(i), func(t *testing.T) {
// Use just the sub-test name in the file name
tmp, err := ioutil.TempFile("", strings.Split(t.Name(), "/")[1])
if err != nil {
t.Fatal(err)
}
t.Log(tmp.Name())
defer func() {
t.Log("Removing file")
os.Remove(tmp.Name())
}()
count := 0
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
_, err = mirrorLog(ctx, &wg, tmp.Name(), true, func(msg string) {
count++
})
if err != nil {
t.Fatal(err)
}
f, err := os.OpenFile(tmp.Name(), os.O_WRONLY, 0700)
if err != nil {
t.Fatal(err)
}
t.Log("Logging 3 JSON messages")
fmt.Fprintln(f, "{\"message\"=\"A\"}")
fmt.Fprintln(f, "{\"message\"=\"B\"}")
fmt.Fprintln(f, "{\"message\"=\"C\"}")
f.Close()
// Rotate the file, by renaming it
rotated := tmp.Name() + ".1"
os.Rename(tmp.Name(), rotated)
defer os.Remove(rotated)
// Open a new file, with the same name as before
f, err = os.OpenFile(tmp.Name(), os.O_WRONLY|os.O_CREATE, 0700)
if err != nil {
t.Fatal(err)
}
t.Log("Logging 2 more JSON messages")
fmt.Fprintln(f, "{\"message\"=\"D\"}")
fmt.Fprintln(f, "{\"message\"=\"E\"}")
f.Close()
// Shut the mirroring down
cancel()
wg.Wait()
if count != 5 {
t.Fatalf("Expected 5 log entries; got %v", count)
}
})
}
}
func testMirrorLogExistingFile(t *testing.T, newQM bool) int {
tmp, err := ioutil.TempFile("", t.Name())
if err != nil {
t.Fatal(err)
}
t.Log(tmp.Name())
log.Println("Logging 1 message before we start")
ioutil.WriteFile(tmp.Name(), []byte("{\"message\"=\"A\"}\n"), 0600)
defer os.Remove(tmp.Name())
count := 0
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
_, err = mirrorLog(ctx, &wg, tmp.Name(), newQM, func(msg string) {
count++
})
if err != nil {
t.Fatal(err)
}
f, err := os.OpenFile(tmp.Name(), os.O_APPEND|os.O_WRONLY, 0700)
if err != nil {
t.Fatal(err)
}
log.Println("Logging 2 new JSON messages")
fmt.Fprintln(f, "{\"message\"=\"B\"}")
fmt.Fprintln(f, "{\"message\"=\"C\"}")
f.Close()
cancel()
wg.Wait()
return count
}
// TestMirrorLogExistingFile tests that we only get new log messages, if the
// log file already exists
func TestMirrorLogExistingFile(t *testing.T) {
count := testMirrorLogExistingFile(t, false)
if count != 2 {
t.Fatalf("Expected 2 log entries; got %v", count)
}
}
// TestMirrorLogExistingFileButNewQueueManager tests that we only get all log
// messages, even if the file exists, if we tell it we want all messages
func TestMirrorLogExistingFileButNewQueueManager(t *testing.T) {
count := testMirrorLogExistingFile(t, true)
if count != 3 {
t.Fatalf("Expected 3 log entries; got %v", count)
}
}
func TestMirrorLogCancelWhileWaiting(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
defer func() {
cancel()
wg.Wait()
}()
_, err := mirrorLog(ctx, &wg, "fake.log", true, func(msg string) {
})
if err != nil {
t.Error(err)
}
time.Sleep(time.Second * 3)
cancel()
wg.Wait()
// No need to assert anything. If it didn't work, the code would have hung (TODO: not ideal)
}

View File

@@ -1,5 +1,5 @@
/*
© Copyright IBM Corporation 2017
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,27 +17,13 @@ package main
import (
"io/ioutil"
"log"
"os/user"
"runtime"
"strings"
"github.com/ibm-messaging/mq-container/pkg/linux/capabilities"
"golang.org/x/sys/unix"
"github.com/ibm-messaging/mq-container/internal/capabilities"
)
// fsTypes contains file system identifier codes.
// This code will not compile on some operating systems - Linux only.
var fsTypes = map[int64]string{
0x61756673: "aufs",
0xef53: "ext",
0x6969: "nfs",
0x65735546: "fuse",
0x9123683e: "btrfs",
0x01021994: "tmpfs",
0x794c7630: "overlayfs",
}
func logBaseImage() error {
buf, err := ioutil.ReadFile("/etc/os-release")
if err != nil {
@@ -86,7 +72,7 @@ func readProc(filename string) (value string, err error) {
func readMounts() error {
all, err := readProc("/proc/mounts")
if err != nil {
log.Println("Error: Couldn't read /proc/mounts")
log.Print("Error: Couldn't read /proc/mounts")
return err
}
lines := strings.Split(all, "\n")
@@ -102,43 +88,27 @@ func readMounts() error {
}
}
if !detected {
log.Println("No volume detected. Persistent messages may be lost")
log.Print("No volume detected. Persistent messages may be lost")
} else {
checkFS("/mnt/mqm")
}
return nil
}
func checkFS(path string) {
statfs := &unix.Statfs_t{}
err := unix.Statfs(path, statfs)
if err != nil {
log.Println(err)
return
}
t := fsTypes[statfs.Type]
switch t {
case "aufs", "overlayfs", "tmpfs":
log.Fatalf("Error: %v uses unsupported filesystem type %v", path, t)
default:
log.Printf("Detected %v has filesystem type '%v'", path, t)
}
}
func logConfig() {
log.Printf("CPU architecture: %v", runtime.GOARCH)
if runtime.GOOS == "linux" {
var err error
osr, err := readProc("/proc/sys/kernel/osrelease")
if err != nil {
log.Println(err)
log.Print(err)
} else {
log.Printf("Linux kernel version: %v", osr)
}
logBaseImage()
fileMax, err := readProc("/proc/sys/fs/file-max")
if err != nil {
log.Println(err)
log.Print(err)
} else {
log.Printf("Maximum file handles: %v", fileMax)
}

View File

@@ -0,0 +1,50 @@
// +build linux
/*
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"golang.org/x/sys/unix"
)
// fsTypes contains file system identifier codes.
// This code will not compile on some operating systems - Linux only.
var fsTypes = map[int64]string{
0x61756673: "aufs",
0xef53: "ext",
0x6969: "nfs",
0x65735546: "fuse",
0x9123683e: "btrfs",
0x01021994: "tmpfs",
0x794c7630: "overlayfs",
}
func checkFS(path string) {
statfs := &unix.Statfs_t{}
err := unix.Statfs(path, statfs)
if err != nil {
log.Println(err)
return
}
t := fsTypes[statfs.Type]
switch t {
case "aufs", "overlayfs", "tmpfs":
log.Fatalf("Error: %v uses unsupported filesystem type %v", path, t)
default:
log.Printf("Detected %v has filesystem type '%v'", path, t)
}
}

View File

@@ -0,0 +1,24 @@
// +build !linux
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
// Dummy version of this function, only for non-Linux systems.
// Having this allows unit tests to be run on other platforms (e.g. macOS)
func checkFS(path string) {
return
}

View File

@@ -0,0 +1,36 @@
// +build mqdev
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import "os"
// postInit is run after /var/mqm is set up
// This version of postInit is only included as part of the MQ Advanced for Developers build
func postInit(name string) error {
disable := os.Getenv("MQ_DISABLE_WEB_CONSOLE")
if disable != "true" && disable != "1" {
// Configure and start the web server, in the background (if installed)
// WARNING: No error handling or health checking available for the web server,
// which is why it's limited to use with MQ Advanced for Developers only
go func() {
configureWebServer()
startWebServer()
}()
}
return nil
}

View File

@@ -0,0 +1,22 @@
// +build !mqdev
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
func postInit(name string) error {
return nil
}

132
cmd/runmqserver/qmgr.go Normal file
View File

@@ -0,0 +1,132 @@
/*
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/ibm-messaging/mq-container/internal/command"
)
// createDirStructure creates the default MQ directory structure under /var/mqm
func createDirStructure() error {
out, _, err := command.Run("/opt/mqm/bin/crtmqdir", "-f", "-s")
if err != nil {
log.Printf("Error creating directory structure: %v\n", string(out))
return err
}
log.Println("Created directory structure under /var/mqm")
return nil
}
// createQueueManager creates a queue manager, if it doesn't already exist.
// It returns true if one was created, or false if one already existed
func createQueueManager(name string) (bool, error) {
log.Printf("Creating queue manager %v", name)
out, rc, err := command.Run("crtmqm", "-q", "-p", "1414", name)
if err != nil {
// 8=Queue manager exists, which is fine
if rc == 8 {
log.Printf("Detected existing queue manager %v", name)
return false, nil
}
log.Printf("crtmqm returned %v", rc)
log.Println(string(out))
return false, err
}
return true, nil
}
func updateCommandLevel() error {
level, ok := os.LookupEnv("MQ_CMDLEVEL")
if ok && level != "" {
log.Printf("Setting CMDLEVEL to %v", level)
out, rc, err := command.Run("strmqm", "-e", "CMDLEVEL="+level)
if err != nil {
log.Printf("Error %v setting CMDLEVEL: %v", rc, string(out))
return err
}
}
return nil
}
func startQueueManager() error {
log.Println("Starting queue manager")
out, rc, err := command.Run("strmqm")
if err != nil {
log.Printf("Error %v starting queue manager: %v", rc, string(out))
return err
}
log.Println("Started queue manager")
return nil
}
func configureQueueManager() error {
const configDir string = "/etc/mqm"
files, err := ioutil.ReadDir(configDir)
if err != nil {
log.Println(err)
return err
}
for _, file := range files {
if strings.HasSuffix(file.Name(), ".mqsc") {
abs := filepath.Join(configDir, file.Name())
cmd := exec.Command("runmqsc")
stdin, err := cmd.StdinPipe()
if err != nil {
log.Println(err)
return err
}
// Open the MQSC file for reading
f, err := os.Open(abs)
if err != nil {
log.Printf("Error opening %v: %v", abs, err)
}
// Copy the contents to stdin of the runmqsc process
_, err = io.Copy(stdin, f)
if err != nil {
log.Printf("Error reading %v: %v", abs, err)
}
f.Close()
stdin.Close()
// Run the command and wait for completion
out, err := cmd.CombinedOutput()
if err != nil {
log.Println(err)
}
// Print the runmqsc output, adding tab characters to make it more readable as part of the log
log.Printf("Output for \"runmqsc\" with %v:\n\t%v", abs, strings.Replace(string(out), "\n", "\n\t", -1))
}
}
return nil
}
func stopQueueManager(name string) error {
log.Println("Stopping queue manager")
out, _, err := command.Run("endmqm", "-w", name)
if err != nil {
log.Printf("Error stopping queue manager: %v", string(out))
return err
}
log.Println("Stopped queue manager")
return nil
}

View File

@@ -0,0 +1,81 @@
/*
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"os/signal"
"syscall"
"golang.org/x/sys/unix"
)
const (
startReaping = iota
reapNow = iota
)
func signalHandler(qmgr string) chan int {
control := make(chan int)
// Use separate channels for the signals, to avoid SIGCHLD signals swamping
// the buffer, and preventing other signals.
stopSignals := make(chan os.Signal)
reapSignals := make(chan os.Signal)
signal.Notify(stopSignals, syscall.SIGTERM, syscall.SIGINT)
go func() {
for {
select {
case sig := <-stopSignals:
log.Printf("Signal received: %v", sig)
signal.Stop(reapSignals)
signal.Stop(stopSignals)
stopQueueManager(qmgr)
// One final reap
reapZombies()
close(control)
// End the goroutine
return
case <-reapSignals:
logDebug("Received SIGCHLD signal")
reapZombies()
case job := <-control:
switch {
case job == startReaping:
// Add SIGCHLD to the list of signals we're listening to
logDebug("Listening for SIGCHLD signals")
signal.Notify(reapSignals, syscall.SIGCHLD)
case job == reapNow:
reapZombies()
}
}
}
}()
return control
}
// reapZombies reaps any zombie (terminated) processes now.
// This function should be called before exiting.
func reapZombies() {
for {
var ws unix.WaitStatus
pid, err := unix.Wait4(-1, &ws, unix.WNOHANG, nil)
// If err or pid indicate "no child processes"
if pid == 0 || err == unix.ECHILD {
return
}
logDebugf("Reaped PID %v", pid)
}
}

View File

@@ -0,0 +1,110 @@
// +build mqdev
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/ibm-messaging/mq-container/internal/command"
)
func startWebServer() error {
_, err := os.Stat("/opt/mqm/bin/strmqweb")
if err != nil && os.IsNotExist(err) {
log.Debug("Skipping web server, because it's not installed")
return nil
}
log.Println("Starting web server")
out, rc, err := command.RunAsMQM("strmqweb")
if err != nil {
log.Printf("Error %v starting web server: %v", rc, string(out))
return err
}
log.Println("Started web server")
return nil
}
func configureWebServer() error {
_, err := os.Stat("/opt/mqm/bin/strmqweb")
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
const webConfigDir string = "/etc/mqm/web"
_, err = os.Stat(webConfigDir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
uid, gid, err := lookupMQM()
if err != nil {
return err
}
const prefix string = "/etc/mqm/web"
err = filepath.Walk(prefix, func(from string, info os.FileInfo, err error) error {
if err != nil {
return err
}
to := fmt.Sprintf("/var/mqm/web%v", from[len(prefix):])
exists := true
_, err = os.Stat(to)
if err != nil {
if os.IsNotExist(err) {
exists = false
} else {
return err
}
}
if info.IsDir() {
if !exists {
err := os.MkdirAll(to, 0770)
if err != nil {
return err
}
}
log.Printf("Directory: %v --> %v", from, to)
} else {
if exists {
err := os.Remove(to)
if err != nil {
return err
}
}
// TODO: Permissions. Can't rely on them being set in Dockerfile
err := os.Link(from, to)
if err != nil {
log.Debug(err)
return err
}
log.Printf("File: %v", from)
}
err = os.Chown(to, uid, gid)
if err != nil {
return err
}
return nil
})
return err
}

43
docs/building.md Normal file
View File

@@ -0,0 +1,43 @@
# Building a Docker image
## Prerequisites
You need to ensure you have the following tools installed:
* [Docker](https://www.docker.com/) V17.06.1 or later
* [GNU make](https://www.gnu.org/software/make/)
## Building a production image
This procedure works for building the MQ Continuous Delivery release, on `x86_64`, `ppc64le` and `s390x` architectures.
1. Create a `downloads` directory in the root of this repository
2. Download MQ from IBM Passport Advantage, and place the downloaded file (for example, `IBM_MQ_9.0.4.0_UBUNTU_X86-64.tar.gz` for MQ V9.0.4 for Ubuntu on x86_64 architecture) in the `downloads` directory
2. Run `make build-advancedserver`
> **Warning**: Note that MQ offers two different sets of packaging on Linux: one is called "MQ for Linux" and contains RPM files for installing on Red Hat Enterprise Linux and SUSE Linux Enterprise Server. The other package is called "MQ for Ubuntu", and contains DEB files for installing on Ubuntu.
You can build a different version of MQ by setting the `MQ_VERSION` environment variable, for example:
```bash
MQ_VERSION=9.0.4.0 make build-advancedserver
```
If you have an MQ archive file with a different file name, you can specify a particular file (which must be in the `downloads` directory). You should also specify the MQ version, so that the resulting image is tagged correctly, for example:
```bash
MQ_ARCHIVE=mq-1.2.3.4.tar.gz MQ_VERSION=1.2.3.4 make build-advancedserver
```
## Building a developer image
Run `make build-devserver`, which will download the latest version of MQ Advanced for Developers from IBM developerWorks. This is currently only available on the `x86_64` architecture.
You can use the environment variable `MQ_ARCHIVE_DEV` to specify an alternative local file to install from (which must be in the `downloads` directory).
## Building on a different base image
By default, the MQ images use Ubuntu as the base layer. You can build using a Red Hat Enterprise Linux compatible base layer by setting the `BASE_IMAGE` environment variable. For example:
```
BASE_IMAGE=centos:7 make build-advancedserver
```
The `make` tool will try and locate the right archive file under the `downloads` directory, based on your platform architecture and your `MQ_VERSION` environment variable, for example `IBM_MQ_9.0.4.0_LINUX_X86_64.tar.gz` for MQ V9.0.4.0 on x86_64. You can also set the `MQ_ARCHIVE` environment variable to set the specific file name.
Note that if you are using Red Hat Enterprise Linux, you will need to create your own base image layer, with your subscription enabled, as described [here](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux_atomic_host/7/html/getting_started_with_containers/get_started_with_docker_formatted_container_images). The MQ image build needs to install some additional packages, and a subscription is required to access the Red Hat repositories.

View File

@@ -1,34 +0,0 @@
# Developing
## Prerequisites
You need to ensure you have the following tools installed:
* [Docker](https://www.docker.com/)
* [Go](https://golang.org/)
* [Glide](https://glide.sh/)
* [dep](https://github.com/golang/dep) (official Go dependency management tool)
* make
For running the Kubernetes tests, a Kubernetes environment is needed, for example [Minikube](https://github.com/kubernetes/minikube) or [IBM Cloud Private](https://www.ibm.com/cloud-computing/products/ibm-cloud-private/).
## Running the tests
There are three main sets of tests:
1. Unit tests
2. Docker tests, which test a complete Docker image, using the Docker API
3. Kubernetes tests, which test the Helm charts (and the Docker image) via [Helm](https://helm.sh)
### Running the tests
The unit and Docker tests can be run locally. For example:
```bash
make test-devserver
```
### Running the Kubernetes tests
For the Kubernetes tests, you need to have built the Docker image, and pushed it to the registry used by your Kubernetes cluster. Most of the configuration used by the tests is picked up from your `kubectl` configuration, but you will typically need to specify the image details. For example:
```bash
DOCKER_REPO_DEVSERVER=mycluster.icp:8500/default/mq-devserver make test-kubernetes-devserver
```

25
docs/internals.md Normal file
View File

@@ -0,0 +1,25 @@
# Internals
This page documents internal code details and design decisions.
The resulting Docker image contains the following:
* Base linux distribution - this provides standard Linux libraries (such as "glibc") and utilities (such as "ls" and "grep") required by MQ
* MQ installation (under `/opt/mqm`)
* Three additional programs, to enable running in a containerized environment:
- `runmqserver` - The main process, which creates and runs a queue manager
- `chkmqhealthy` - Checks the health of the queue manager. This can be used by (say) a Kubernetes liveness probe.
- `chkmqready` - Checks if the queue manager is ready for work. This can be used by (say) a Kubernetes readiness probe.
## runmqserver
The `runmqserver` command has the following responsibilities:
* Checks license acceptance
* Sets up `/var/mqm`
- MQ data directory needs to be set up at container creation time. This is done using the `crtmqdir` utility, which was introduced in MQ V9.0.3
- It assumes that a storage volume for data is mounted under `/mnt/mqm`. It creates a sub-directory for the MQ data, so `/var/mqm` is a symlink which resolves to `/mnt/mqm/data`. The reason for this is that it's not always possible to change the ownership of an NFS mount point directly (`/var/mqm` needs to be owned by "mqm"), but you can change the ownership of a sub-directory.
* Acts like a daemon
- Handles UNIX signals, like SIGTERM
- Works as PID 1, so is responsible for [reaping zombie processes](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/)
* Creating and starting a queue manager
* Configuring the queue manager, by running any MQSC scripts found under `/etc/mqm`

70
docs/testing.md Normal file
View File

@@ -0,0 +1,70 @@
# Testing
## Prerequisites
You need to ensure you have the following tools installed:
* [Docker](https://www.docker.com/)
* [GNU make](https://www.gnu.org/software/make/)
* [Go](https://golang.org/) - only needed for running the tests
* [dep](https://github.com/golang/dep) (official Go dependency management tool) - needed to prepare for running the tests
* [Helm](https://helm.sh) - only needed for running the Kubernetes tests
For running the Kubernetes tests, a Kubernetes environment is needed, for example [Minikube](https://github.com/kubernetes/minikube) or [IBM Cloud Private](https://www.ibm.com/cloud-computing/products/ibm-cloud-private/).
## Preparing to run the tests
The test dependencies are not included with the source code, so you need to download them before you can run them. This can be done with the following command, which uses the `dep` tool:
```
make deps
```
## Running the tests
There are three main sets of tests:
1. Unit tests, which are run during a build
2. Docker tests, which test a complete Docker image, using the Docker API
3. Kubernetes tests, which test the Helm charts (and the Docker image) via [Helm](https://helm.sh)
### Running the Docker tests
The Docker tests can be run locally on a machine with Docker. For example:
```
make test-devserver
make test-advancedserver
```
You can specify the image to use directly by using the `MQ_IMAGE_ADVANCEDSERVER` or `MQ_IMAGE_DEVSERVER` variables, for example:
```
MQ_IMAGE_ADVANCEDSERVER=mqadvanced-server9.0.4.0-x86_64-ubuntu-16.04 make test-advancedserver
```
You can pass parameters to `go test` with an environment variable. For example, to run the "TestGoldenPath" test, run the following command::
```
TEST_OPTS_DOCKER="-run TestGoldenPath" make test-advancedserver
```
You can also use the same environment variables you specified when [building](./building), for example, the following will try and test an image called `mqadvanced-server9.0.3.0-x86_64-ubuntu-16.04`:
```
MQ_VERSION=9.0.3.0 make test-advancedserver
```
### Running the Docker tests with code coverage
You can produce code coverage results from the Docker tests by running the following:
```
make build-advancedserver-cover
make test-advancedserver-cover
```
In order to generate code coverage metrics from the Docker tests, the build step creates a new Docker image with an instrumented version of the code. Each test is then run individually, producing a coverage report each under `test/docker/coverage/`. These individual reports are then combined. The combined report is written to the `coverage` directory.
### Running the Kubernetes tests
For the Kubernetes tests, you need to have built the Docker image, and pushed it to the registry used by your Kubernetes cluster. Most of the configuration used by the tests is picked up from your `kubectl` configuration, but you will typically need to specify the image details. For example:
```bash
MQ_IMAGE=mycluster.icp:8500/default/mq-devserver make test-kubernetes-devserver
```

52
glide.lock generated
View File

@@ -1,19 +1,47 @@
hash: 170f17bb34eaa2c23733b3919e76a268654e50a21e70b45e1b7b2dd2161efc67
updated: 2017-09-25T16:58:43.624633314+01:00
hash: 4905ad6acf8e593e3c4432f31dd61275b16d0b5b95f438471749ae1943af785d
updated: 2018-02-22T17:26:13.366677Z
imports:
- name: github.com/hpcloud/tail
version: a30252cb686a21eb2d0b98132633053ec2f7f1e5
- name: golang.org/x/crypto
version: 81e90905daefcd6fd217b62423c0908922eadb30
subpackages:
- ratelimiter
- util
- watch
- winfile
- acme
- acme/autocert
- blowfish
- cast5
- chacha20poly1305/internal/chacha20
- cryptobyte
- curve25519
- ed25519
- ed25519/internal/edwards25519
- hkdf
- md4
- nacl/auth
- nacl/box
- nacl/secretbox
- openpgp
- openpgp/armor
- openpgp/elgamal
- openpgp/errors
- openpgp/packet
- openpgp/s2k
- pbkdf2
- pkcs12/internal/rc2
- poly1305
- ripemd160
- salsa20/salsa
- ssh
- ssh/agent
- ssh/terminal
- ssh/testdata
- name: golang.org/x/sys
version: 7a4fde3fda8ef580a89dbae8138c26041be14299
subpackages:
- plan9
- unix
- name: gopkg.in/fsnotify.v1
version: 7be54206639f256967dd82fa767397ba5f8f48f5
- name: gopkg.in/tomb.v1
version: c131134a1947e9afd9cecfe11f4c6dff0732ae58
- windows
- windows/registry
- windows/svc
- windows/svc/debug
- windows/svc/eventlog
- windows/svc/mgr
testImports: []

View File

@@ -19,6 +19,4 @@ excludeDirs:
- coverage
- test
import:
- package: github.com/hpcloud/tail
version: v1.0.0
- package: golang.org/x/sys/unix

View File

@@ -1,14 +1,19 @@
Docker for Mac
--------------
Steps to build a Docker image containing IBM MQ Explorer:
1. Download and extract the code from [GitHub](https://codeload.github.com/ibm-messaging/mq-container/zip/master), or run the following command: `git clone https://github.com/ibm-messaging/mq-container`
2. Open a Terminal window in the `mq-container` directory
3. Run `docker build -t mq-explorer -f ./incubating/mq-explorer/Dockerfile .`
1. Install XQuartz. Version 2.7.10 works, but V2.7.11 doesn't seem to.
Steps to prepare your Mac with XQuartz:
1. Install XQuartz. Version 2.7.10 works, but V2.7.11 doesn't seem to (see [this thread](https://stackoverflow.com/questions/38686932/how-to-forward-docker-for-mac-to-x11))
2. Run XQuartz
3. Open the XQuartz "Preferences" menu, go to the "Security" tab and enable "Allow connections from network clients"
4. Add your IP address to the list of allowed hosts: `xhost + $(ipconfig getifaddr en0)`
5. Run MQ Explorer: `docker run -e DISPLAY=$(ipconfig getifaddr en0):0 -v /tmp/.X11-unix:/tmp/.X11-unix -u 0 -ti mq-explorer`
https://stackoverflow.com/questions/38686932/how-to-forward-docker-for-mac-to-x11
Steps to run IBM MQ Explorer:
1. Add your IP address to the list of allowed hosts: `xhost + $(ipconfig getifaddr en0)`
2. Run MQ Explorer: `docker run -e DISPLAY=$(ipconfig getifaddr en0):0 -v /tmp/.X11-unix:/tmp/.X11-unix -u 0 -ti mq-explorer`
docker run -e DISPLAY=docker.for.mac.localhost:0 -v /tmp/.X11-unix:/tmp/.X11-unix -u 0 -ti mq-explorer
Use DISPLAY=docker.for.mac.localhost:0 ???

View File

@@ -0,0 +1,56 @@
# © Copyright IBM Corporation 2015, 2018
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###############################################################################
# Build stage to build Go code
###############################################################################
FROM golang:1.9 as builder
WORKDIR /go/src/github.com/ibm-messaging/mq-container/
COPY cmd/ ./cmd
COPY internal/ ./internal
COPY vendor/ ./vendor
# Re-build runmqserver, with code tagged with 'mqdev' enabled
RUN go build --tags 'mqdev' ./cmd/runmqserver
RUN go build ./cmd/runmqdevserver/
# Run all unit tests
RUN go test -v ./cmd/runmqdevserver/...
###############################################################################
# Main build stage
###############################################################################
FROM mqadvanced-server-dev-base:9.0.4.0-x86_64-ubuntu-16.04
# Enable MQ developer default configuration
ENV MQ_DEV=true
# Default administrator password
ENV MQ_ADMIN_PASSWORD=passw0rd
## Add admin and app users, and set a default password for admin
RUN useradd admin -G mqm \
&& groupadd mqclient \
&& useradd app -G mqclient,mqm \
&& echo admin:$MQ_ADMIN_PASSWORD | chpasswd
COPY --from=builder /go/src/github.com/ibm-messaging/mq-container/runmqserver /usr/local/bin/
COPY --from=builder /go/src/github.com/ibm-messaging/mq-container/runmqdevserver /usr/local/bin/
# Copy template MQSC for default developer configuration
COPY incubating/mqadvanced-server-dev/dev.mqsc.tpl /etc/mqm/
# Copy web XML files for default developer configuration
COPY incubating/mqadvanced-server-dev/web /etc/mqm/web
RUN chmod +x /usr/local/bin/runmq*
EXPOSE 9443
ENTRYPOINT ["runmqdevserver"]

View File

@@ -0,0 +1,53 @@
* © Copyright IBM Corporation 2017, 2018
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
STOP LISTENER('SYSTEM.DEFAULT.LISTENER.TCP')
* Developer queues
DEFINE QLOCAL('DEV.QUEUE.1') REPLACE
DEFINE QLOCAL('DEV.QUEUE.2') REPLACE
DEFINE QLOCAL('DEV.QUEUE.3') REPLACE
DEFINE QLOCAL('DEV.DEAD.LETTER.QUEUE') REPLACE
* Use a different dead letter queue, for undeliverable messages
ALTER QMGR DEADQ('DEV.DEAD.LETTER.QUEUE')
* Developer topics
DEFINE TOPIC('DEV.BASE.TOPIC') TOPICSTR('dev/') REPLACE
* Developer connection authentication
DEFINE AUTHINFO('DEV.AUTHINFO') AUTHTYPE(IDPWOS) CHCKCLNT(REQDADM) CHCKLOCL(OPTIONAL) ADOPTCTX(YES) REPLACE
ALTER QMGR CONNAUTH('DEV.AUTHINFO')
REFRESH SECURITY(*) TYPE(CONNAUTH)
* Developer channels (Application + Admin)
DEFINE CHANNEL('DEV.ADMIN.SVRCONN') CHLTYPE(SVRCONN) REPLACE
DEFINE CHANNEL('DEV.APP.SVRCONN') CHLTYPE(SVRCONN) REPLACE
* Developer channel authentication rules
SET CHLAUTH('*') TYPE(ADDRESSMAP) ADDRESS('*') USERSRC(NOACCESS) DESCR('Back-stop rule - Blocks everyone') ACTION(REPLACE)
SET CHLAUTH('DEV.APP.SVRCONN') TYPE(ADDRESSMAP) ADDRESS('*') USERSRC(CHANNEL) CHCKCLNT({{ .ChckClnt }}) DESCR('Allows connection via APP channel') ACTION(REPLACE)
SET CHLAUTH('DEV.ADMIN.SVRCONN') TYPE(BLOCKUSER) USERLIST('nobody') DESCR('Allows admins on ADMIN channel') ACTION(REPLACE)
SET CHLAUTH('DEV.ADMIN.SVRCONN') TYPE(USERMAP) CLNTUSER('admin') USERSRC(CHANNEL) DESCR('Allows admin user to connect via ADMIN channel') ACTION(REPLACE)
* Developer authority records
SET AUTHREC PROFILE('DEV.AUTHINFO') GROUP('root') OBJTYPE(AUTHINFO) AUTHADD(CHG,DLT,DSP,INQ)
SET AUTHREC PROFILE('DEV.AUTHINFO') GROUP('mqm') OBJTYPE(AUTHINFO) AUTHADD(CHG,DLT,DSP,INQ)
SET AUTHREC PROFILE('DEV.**') GROUP('mqclient') OBJTYPE(QUEUE) AUTHADD(BROWSE,GET,INQ,PUT)
SET AUTHREC PROFILE('DEV.**') GROUP('mqclient') OBJTYPE(TOPIC) AUTHADD(PUB,SUB)
* Developer listener
DEFINE LISTENER('DEV.LISTENER.TCP') TRPTYPE(TCP) PORT(1414) CONTROL(QMGR) REPLACE
START LISTENER('DEV.LISTENER.TCP')

View File

@@ -0,0 +1,17 @@
#!/bin/bash
# Change admin password
if [ -n "${MQ_ADMIN_PASSWORD}" ]; then
echo admin:${MQ_ADMIN_PASSWORD} | chpasswd
fi
# Change app password
if [ -n "${MQ_APP_PASSWORD}" ]; then
echo app:${MQ_APP_PASSWORD} | chpasswd
fi
# Delete the MQSC with developer defaults, if requested
if [ "${MQ_DEV}" != "true" ]; then
rm -f /etc/mqm/dev.mqsc
fi
exec runmqserver

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<server>
<featureManager>
<feature>appSecurity-2.0</feature>
<feature>basicAuthenticationMQ-1.0</feature>
</featureManager>
<enterpriseApplication id="com.ibm.mq.console">
<application-bnd>
<security-role name="MQWebAdmin">
<group name="MQWebUI" realm="defaultRealm"/>
</security-role>
</application-bnd>
</enterpriseApplication>
<enterpriseApplication id="com.ibm.mq.rest">
<application-bnd>
<security-role name="MQWebAdmin">
<group name="MQWebUI" realm="defaultRealm"/>
</security-role>
</application-bnd>
</enterpriseApplication>
<basicRegistry id="basic" realm="defaultRealm">
<user name="admin" password="${env.MQ_ADMIN_PASSWORD}"/>
<group name="MQWebUI">
<member name="admin"/>
</group>
</basicRegistry>
<variable name="httpHost" value="*"/>
<httpDispatcher enableWelcomePage="false" appOrContextRootMissingMessage='Redirecting to console.&lt;script&gt;document.location.href="/ibmmq/console";&lt;/script&gt;' />
<include location="tls.xml"/>
</server>

View File

@@ -0,0 +1,4 @@
<keyStore id="MQWebKeyStore" location="/var/mqm/web/installations/Installation1/servers/mqweb/key.jks" type="JKS" password="${env.MQ_TLS_PASSPHRASE}"/>
<keyStore id="MQWebTrustStore" location="/var/mqm/web/installations/Installation1/servers/mqweb/trust.jks" type="JKS" password="${env.MQ_TLS_PASSPHRASE}"/>
<ssl id="thisSSLConfig" clientAuthenticationSupported="true" keyStoreRef="MQWebKeyStore" trustStoreRef="MQWebTrustStore" sslProtocol="TLSv1.2" serverKeyAlias="webcert"/>
<sslDefault sslRef="thisSSLConfig"/>

View File

@@ -0,0 +1 @@
<sslDefault sslRef="mqDefaultSSLConfig"/>

View File

@@ -1,6 +1,6 @@
#!/bin/bash
# -*- mode: sh -*-
# © Copyright IBM Corporation 2015, 2017
# © Copyright IBM Corporation 2015, 2018
#
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,61 +18,102 @@
# Fail on any non-zero return code
set -ex
export DEBIAN_FRONTEND=noninteractive
test -f /usr/bin/yum && RHEL=true || RHEL=false
test -f /usr/bin/apt-get && UBUNTU=true || UBUNTU=false
# If MQ_PACKAGES isn't specifically set, then choose a valid set of defaults
if [ -z "$MQ_PACKAGES" ]; then
$UBUNTU && MQ_PACKAGES="ibmmq-server ibmmq-java ibmmq-jre ibmmq-gskit ibmmq-msg-.* ibmmq-samples ibmmq-ams"
$RHEL && MQ_PACKAGES="MQSeriesRuntime-*.rpm MQSeriesServer-*.rpm MQSeriesJava*.rpm MQSeriesJRE*.rpm MQSeriesGSKit*.rpm MQSeriesMsg*.rpm MQSeriesSamples*.rpm MQSeriesAMS-*.rpm"
fi
if ($UBUNTU); then
export DEBIAN_FRONTEND=noninteractive
# Use a reduced set of apt repositories.
# This ensures no unsupported code gets installed, and makes the build faster
source /etc/os-release
echo "deb http://archive.ubuntu.com/ubuntu/ ${UBUNTU_CODENAME} main restricted" > /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu/ ${UBUNTU_CODENAME}-updates main restricted" >> /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu/ ${UBUNTU_CODENAME}-security main restricted" >> /etc/apt/sources.list
# Install additional packages required by MQ, this install process and the runtime scripts
apt-get update
apt-get install -y --no-install-recommends \
bash \
bc \
ca-certificates \
coreutils \
curl \
debianutils \
file \
findutils \
gawk \
grep \
libc-bin \
mount \
passwd \
procps \
sed \
tar \
util-linux
fi
# Install additional packages required by MQ, this install process and the runtime scripts
apt-get update
apt-get install -y --no-install-recommends \
$RHEL && yum -y install \
bash \
bc \
ca-certificates \
coreutils \
curl \
debianutils \
file \
findutils \
gawk \
glibc-common \
grep \
libc-bin \
mount \
passwd \
procps \
procps-ng \
sed \
tar \
util-linux
# Download and extract the MQ installation files
DIR_EXTRACT=/tmp/mq
mkdir -p ${DIR_EXTRACT}
mkdir -p ${DIR_EXTRACT}
cd ${DIR_EXTRACT}
curl -LO $MQ_URL
tar -zxvf ./*.tar.gz
# Remove packages only needed by this script
apt-get purge -y \
$UBUNTU && apt-get purge -y \
ca-certificates \
curl
# Note: ca-certificates and curl are installed by default in RHEL
# Remove any orphaned packages
apt-get autoremove -y
$UBUNTU && apt-get autoremove -y
# Recommended: Create the mqm user ID with a fixed UID and group, so that the file permissions work between different images
groupadd --system --gid 999 mqm
useradd --system --uid 999 --gid mqm mqm
$UBUNTU && groupadd --system --gid 999 mqm
$UBUNTU && useradd --system --uid 999 --gid mqm mqm
$RHEL && groupadd --system --gid 888 mqm
$RHEL && useradd --system --uid 888 --gid mqm mqm
usermod -G mqm root
# Find directory containing .deb files
DIR_DEB=$(find ${DIR_EXTRACT} -name "*.deb" -printf "%h\n" | sort -u | head -1)
$UBUNTU && DIR_DEB=$(find ${DIR_EXTRACT} -name "*.deb" -printf "%h\n" | sort -u | head -1)
$RHEL && DIR_RPM=$(find ${DIR_EXTRACT} -name "*.rpm" -printf "%h\n" | sort -u | head -1)
# Find location of mqlicense.sh
MQLICENSE=$(find ${DIR_EXTRACT} -name "mqlicense.sh")
# Accept the MQ license
${MQLICENSE} -text_only -accept
echo "deb [trusted=yes] file:${DIR_DEB} ./" > /etc/apt/sources.list.d/IBM_MQ.list
$UBUNTU && echo "deb [trusted=yes] file:${DIR_DEB} ./" > /etc/apt/sources.list.d/IBM_MQ.list
# Install MQ using the DEB packages
apt-get update
apt-get install -y $MQ_PACKAGES
$UBUNTU && apt-get update
$UBUNTU && apt-get install -y $MQ_PACKAGES
$RHEL && cd $DIR_RPM && rpm -ivh $MQ_PACKAGES
# Remove 32-bit libraries from 64-bit container
find /opt/mqm /var/mqm -type f -exec file {} \; | awk -F: '/ELF 32-bit/{print $1}' | xargs --no-run-if-empty rm -f
@@ -84,21 +125,21 @@ find /opt/mqm -name '*.tar.gz' -delete
/opt/mqm/bin/setmqinst -p /opt/mqm -i
# Clean up all the downloaded files
rm -f /etc/apt/sources.list.d/IBM_MQ.list
$UBUNTU && rm -f /etc/apt/sources.list.d/IBM_MQ.list
rm -rf ${DIR_EXTRACT}
# Apply any bug fixes not included in base Ubuntu or MQ image.
# Don't upgrade everything based on Docker best practices https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#run
apt-get upgrade -y libkrb5-26-heimdal
apt-get upgrade -y libexpat1
$UBUNTU && apt-get upgrade -y sensible-utils
# End of bug fixes
# Clean up cached apt files
rm -rf /var/lib/apt/lists/*
# Clean up cached files
$UBUNTU && rm -rf /var/lib/apt/lists/*
$RHEL && yum -y clean all
$RHEL && rm -rf /var/cache/yum/*
# Optional: Update the command prompt with the MQ version
echo "mq:$(dspmqver -b -f 2)" > /etc/debian_chroot
$UBUNTU && echo "mq:$(dspmqver -b -f 2)" > /etc/debian_chroot
# Remove the directory structure under /var/mqm which was created by the installer
rm -rf /var/mqm
@@ -115,4 +156,7 @@ ln -s /mnt/mqm/data /var/mqm
# Optional: Set these values for the Bluemix Vulnerability Report
sed -i 's/PASS_MAX_DAYS\t99999/PASS_MAX_DAYS\t90/' /etc/login.defs
sed -i 's/PASS_MIN_DAYS\t0/PASS_MIN_DAYS\t1/' /etc/login.defs
sed -i 's/password\t\[success=1 default=ignore\]\tpam_unix\.so obscure sha512/password\t[success=1 default=ignore]\tpam_unix.so obscure sha512 minlen=8/' /etc/pam.d/common-password
$UBUNTU && PAM_FILE=/etc/pam.d/common-password
$RHEL && PAM_FILE=/etc/pam.d/password-auth
sed -i 's/password\t\[success=1 default=ignore\]\tpam_unix\.so obscure sha512/password\t[success=1 default=ignore]\tpam_unix.so obscure sha512 minlen=8/' $PAM_FILE

View File

@@ -0,0 +1,86 @@
/*
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package command contains code to run external commands
package command
import (
"fmt"
"os/exec"
"os/user"
"runtime"
"strconv"
"syscall"
)
// RunCmd runs an OS command. On Linux it waits for the command to
// complete and returns the exit status (return code).
// Do not use this function to run shell built-ins (like "cd"), because
// the error handling works differently
func RunCmd(cmd *exec.Cmd) (string, int, error) {
// Run the command and wait for completion
out, err := cmd.CombinedOutput()
if err != nil {
// Assert that this is an ExitError
exiterr, ok := err.(*exec.ExitError)
// If the type assertion was correct, and we're on Linux
if ok && runtime.GOOS == "linux" {
status, ok := exiterr.Sys().(syscall.WaitStatus)
if ok {
return string(out), status.ExitStatus(), fmt.Errorf("%v: %v", cmd.Path, err)
}
}
return string(out), -1, err
}
return string(out), 0, nil
}
// Run runs an OS command. On Linux it waits for the command to
// complete and returns the exit status (return code).
// Do not use this function to run shell built-ins (like "cd"), because
// the error handling works differently
func Run(name string, arg ...string) (string, int, error) {
return RunCmd(exec.Command(name, arg...))
}
// RunAsMQM runs the specified command as the mqm user
func RunAsMQM(name string, arg ...string) (string, int, error) {
cmd := exec.Command(name, arg...)
cmd.SysProcAttr = &syscall.SysProcAttr{}
uid, gid, err := lookupMQM()
if err != nil {
return "", 0, err
}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
return RunCmd(cmd)
}
// TODO: Duplicated code
func lookupMQM() (int, int, error) {
mqm, err := user.Lookup("mqm")
if err != nil {
return -1, -1, err
}
mqmUID, err := strconv.Atoi(mqm.Uid)
if err != nil {
return -1, -1, err
}
mqmGID, err := strconv.Atoi(mqm.Gid)
if err != nil {
return -1, -1, err
}
return mqmUID, mqmGID, nil
}

View File

@@ -0,0 +1,47 @@
/*
© Copyright IBM Corporation 2017
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package command
import (
"runtime"
"testing"
)
var commandTests = []struct {
name string
arg []string
rc int
}{
{"ls", []string{}, 0},
{"ls", []string{"madeup"}, 2},
{"bash", []string{"-c", "exit 99"}, 99},
}
func TestRun(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("Skipping tests for package which only works on Linux")
}
for _, table := range commandTests {
arg := table.arg
_, rc, err := Run(table.name, arg...)
if rc != table.rc {
t.Errorf("Run(%v,%v) - expected %v, got %v", table.name, table.arg, table.rc, rc)
}
if rc != 0 && err == nil {
t.Errorf("Run(%v,%v) - expected error for non-zero return code (rc=%v)", table.name, table.arg, rc)
}
}
}

154
internal/logger/logger.go Normal file
View File

@@ -0,0 +1,154 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logger
import (
"encoding/json"
"fmt"
"io"
"os"
"os/user"
"sync"
"time"
)
// timestampFormat matches the format used by MQ messages (includes milliseconds)
const timestampFormat string = "2006-01-02T15:04:05.000Z07:00"
const debugLevel string = "DEBUG"
const infoLevel string = "INFO"
const errorLevel string = "ERROR"
// A Logger is used to log messages to stdout
type Logger struct {
mutex sync.Mutex
writer io.Writer
debug bool
json bool
processName string
pid int
serverName string
host string
user *user.User
}
// NewLogger creates a new logger
func NewLogger(writer io.Writer, debug bool, json bool, serverName string) (*Logger, error) {
hostname, err := os.Hostname()
if err != nil {
return nil, err
}
user, err := user.Current()
if err != nil {
return nil, err
}
return &Logger{
mutex: sync.Mutex{},
writer: writer,
debug: debug,
json: json,
processName: os.Args[0],
pid: os.Getpid(),
serverName: serverName,
host: hostname,
user: user,
}, nil
}
func (l *Logger) format(entry map[string]interface{}) (string, error) {
if l.json {
b, err := json.Marshal(entry)
if err != nil {
return "", err
}
return string(b), err
}
return fmt.Sprintf("%v %v\n", entry["ibm_datetime"], entry["message"]), nil
}
// log logs a message at the specified level. The message is enriched with
// additional fields.
func (l *Logger) log(level string, msg string) {
t := time.Now()
entry := map[string]interface{}{
"message": fmt.Sprint(msg),
"ibm_datetime": t.Format(timestampFormat),
"loglevel": level,
"host": l.host,
"ibm_serverName": l.serverName,
"ibm_processName": l.processName,
"ibm_processId": l.pid,
"ibm_userName": l.user.Username,
"type": "mq_containerlog",
}
s, err := l.format(entry)
l.mutex.Lock()
if err != nil {
// TODO: Fix this
fmt.Println(err)
}
if l.json {
fmt.Fprintln(l.writer, s)
} else {
fmt.Fprint(l.writer, s)
}
l.mutex.Unlock()
}
func (l *Logger) LogDirect(msg string) {
fmt.Println(msg)
}
func (l *Logger) Debug(args ...interface{}) {
if l.debug {
l.log(debugLevel, fmt.Sprint(args...))
}
}
func (l *Logger) Debugf(format string, args ...interface{}) {
if l.debug {
l.log(debugLevel, fmt.Sprintf(format, args...))
}
}
func (l *Logger) Print(args ...interface{}) {
l.log(infoLevel, fmt.Sprint(args...))
}
func (l *Logger) Println(args ...interface{}) {
l.Print(args...)
}
func (l *Logger) Printf(format string, args ...interface{}) {
l.log(infoLevel, fmt.Sprintf(format, args...))
}
func (l *Logger) PrintString(msg string) {
l.log(infoLevel, msg)
}
func (l *Logger) Error(args ...interface{}) {
l.log(errorLevel, fmt.Sprint(args...))
}
func (l *Logger) Errorf(format string, args ...interface{}) {
l.log(errorLevel, fmt.Sprintf(format, args...))
}
// TODO: Remove this
func (l *Logger) Fatalf(format string, args ...interface{}) {
l.log("FATAL", fmt.Sprintf(format, args...))
}

View File

@@ -0,0 +1,55 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logger
import (
"bytes"
"encoding/json"
"strings"
"testing"
)
func TestJSONLogger(t *testing.T) {
buf := new(bytes.Buffer)
l, err := NewLogger(buf, true, true, t.Name())
if err != nil {
t.Fatal(err)
}
s := "Hello world"
l.Print(s)
var e map[string]interface{}
err = json.Unmarshal([]byte(buf.String()), &e)
if err != nil {
t.Error(err)
}
if s != e["message"] {
t.Errorf("Expected JSON to contain message=%v; got %v", s, buf.String())
}
}
func TestSimpleLogger(t *testing.T) {
buf := new(bytes.Buffer)
l, err := NewLogger(buf, true, false, t.Name())
if err != nil {
t.Fatal(err)
}
s := "Hello world"
l.Print(s)
if !strings.Contains(buf.String(), s) {
t.Errorf("Expected log output to contain %v; got %v", s, buf.String())
}
}

View File

@@ -0,0 +1,5 @@
QueueManager:
Name=foo
Directory=foo
Prefix=/var/mqm
InstallationName=Installation1

View File

@@ -0,0 +1,5 @@
QueueManager:
Name=a/b
Directory=a&b
Prefix=/var/mqm
InstallationName=Installation1

View File

@@ -0,0 +1,5 @@
QueueManager:
Name=..
Directory=!!
Prefix=/var/mqm
InstallationName=Installation1

73
internal/mqini/mqini.go Normal file
View File

@@ -0,0 +1,73 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mqini
import (
"bufio"
"path/filepath"
"strings"
"github.com/ibm-messaging/mq-container/internal/command"
)
// QueueManager describe high-level configuration information for a queue manager
type QueueManager struct {
Name string
Prefix string
Directory string
DataPath string
InstallationName string
}
// getQueueManagerFromStanza parses a queue manager stanza
func getQueueManagerFromStanza(stanza string) (*QueueManager, error) {
scanner := bufio.NewScanner(strings.NewReader(stanza))
qm := QueueManager{}
for scanner.Scan() {
l := scanner.Text()
l = strings.TrimSpace(l)
t := strings.Split(l, "=")
switch t[0] {
case "Name":
qm.Name = t[1]
case "Prefix":
qm.Prefix = t[1]
case "Directory":
qm.Directory = t[1]
case "DataPath":
qm.DataPath = t[1]
case "InstallationName":
qm.InstallationName = t[1]
}
}
return &qm, scanner.Err()
}
// GetQueueManager returns queue manager configuration information
func GetQueueManager(name string) (*QueueManager, error) {
// dspmqinf essentially returns a subset of mqs.ini, but it's simpler to parse
out, _, err := command.Run("dspmqinf", "-o", "stanza", name)
if err != nil {
return nil, err
}
return getQueueManagerFromStanza(out)
}
// GetErrorLogDirectory returns the directory holding the error logs for the
// specified queue manager
func GetErrorLogDirectory(qm *QueueManager) string {
return filepath.Join(qm.Prefix, "qmgrs", qm.Directory, "errors")
}

View File

@@ -0,0 +1,64 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mqini
import (
"io/ioutil"
"testing"
)
var getQueueManagerTests = []struct {
file string
name string
prefix string
directory string
errorLogDir string
}{
{"dspmqinf1.txt", "foo", "/var/mqm", "foo", "/var/mqm/qmgrs/foo/errors"},
{"dspmqinf2.txt", "a/b", "/var/mqm", "a&b", "/var/mqm/qmgrs/a&b/errors"},
{"dspmqinf3.txt", "..", "/var/mqm", "!!", "/var/mqm/qmgrs/!!/errors"},
}
func TestGetQueueManager(t *testing.T) {
for _, table := range getQueueManagerTests {
t.Run(table.file, func(t *testing.T) {
b, err := ioutil.ReadFile(table.file)
if err != nil {
t.Fatal(err)
}
qm, err := getQueueManagerFromStanza(string(b))
if err != nil {
t.Fatal(err)
}
t.Logf("%#v", qm)
if qm.Name != table.name {
t.Errorf("Expected name=%v; got %v", table.name, qm.Name)
}
if qm.Prefix != table.prefix {
t.Errorf("Expected prefix=%v; got %v", table.prefix, qm.Prefix)
}
if qm.Directory != table.directory {
t.Errorf("Expected directory=%v; got %v", table.directory, qm.Directory)
}
// Test
d := GetErrorLogDirectory(qm)
if d != table.errorLogDir {
t.Errorf("Expected error log directory=%v; got %v", table.errorLogDir, d)
}
})
}
}

View File

@@ -20,11 +20,9 @@ package name
import (
"os"
"regexp"
//log "github.com/sirupsen/logrus"
)
// sanitizeQueueManagerName removes any invalid characters from a queue manager name
// TODO: This is duplicate code
func sanitizeQueueManagerName(name string) string {
var re = regexp.MustCompile("[^a-zA-Z0-9._%/]")
return re.ReplaceAllString(name, "")

64
internal/ready/ready.go Normal file
View File

@@ -0,0 +1,64 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package ready contains code to provide a ready signal mechanism between processes
package ready
import (
"io/ioutil"
"os"
)
const fileName string = "/run/runmqserver/ready"
func fileExists() (bool, error) {
_, err := os.Stat(fileName)
if err != nil {
if !os.IsNotExist(err) {
return false, err
}
return false, nil
}
return true, nil
}
// Clear ensures that any readiness state is cleared
func Clear() error {
exist, err := fileExists()
if err != nil {
return err
}
if exist {
return os.Remove(fileName)
}
return nil
}
// Set lets any subsequent calls to `CheckReady` know that the queue
// manager has finished its configuration step
func Set() error {
return ioutil.WriteFile(fileName, []byte("1"), 0770)
}
// Check checks whether or not the queue manager has finished its
// configuration steps
func Check() (bool, error) {
exists, err := fileExists()
if err != nil {
return false, err
}
return exists, nil
}

View File

@@ -1,4 +1,4 @@
# © Copyright IBM Corporation 2017
# © Copyright IBM Corporation 2017, 2018
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,4 +17,8 @@
version = "^1.12"
[[constraint]]
name = "github.com/docker/go-connections"
name = "github.com/docker/go-connections"
version = "0.3.0"
[prune]
go-tests = true

View File

@@ -0,0 +1,72 @@
// +build mqdev
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"crypto/tls"
"fmt"
"net/http"
"testing"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
func TestDevGoldenPath(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
waitForReady(t, cli, id)
waitForWebReady(t, cli, id)
timeout := time.Duration(30 * time.Second)
// Disable TLS verification (server uses a self-signed certificate by default,
// so verification isn't useful anyway)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
httpClient := http.Client{
Timeout: timeout,
Transport: tr,
}
url := fmt.Sprintf("https://localhost:%s/ibmmq/rest/v1/admin/installation", getWebPort(t, cli, id))
req, err := http.NewRequest("GET", url, nil)
req.SetBasicAuth("admin", "passw0rd")
resp, err := httpClient.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected HTTP status code %v from 'GET installation'; got %v", http.StatusOK, resp.StatusCode)
}
// Stop the container cleanly
stopContainer(t, cli, id)
}

View File

@@ -0,0 +1,42 @@
// +build mqdev
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"crypto/tls"
"fmt"
"testing"
"time"
"github.com/docker/docker/client"
)
func waitForWebReady(t *testing.T, cli *client.Client, ID string) {
config := tls.Config{InsecureSkipVerify: true}
a := fmt.Sprintf("localhost:%s", getWebPort(t, cli, ID))
for {
conn, err := tls.Dial("tcp", a, &config)
if err == nil {
conn.Close()
// Extra sleep to allow web apps to start
time.Sleep(3 * time.Second)
t.Log("MQ web server is ready")
return
}
}
}

View File

@@ -1,5 +1,5 @@
/*
© Copyright IBM Corporation 2017
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,9 +16,18 @@ limitations under the License.
package main
import (
"archive/tar"
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
@@ -27,6 +36,7 @@ import (
)
func TestLicenseNotSet(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
@@ -38,9 +48,11 @@ func TestLicenseNotSet(t *testing.T) {
if rc != 1 {
t.Errorf("Expected rc=1, got rc=%v", rc)
}
expectTerminationMessage(t)
}
func TestLicenseView(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
@@ -61,21 +73,44 @@ func TestLicenseView(t *testing.T) {
}
}
// TestGoldenPath starts a queue manager successfully
func TestGoldenPath(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
//ExposedPorts: ports,
ExposedPorts: nat.PortSet{
"1414/tcp": struct{}{},
},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
waitForReady(t, cli, id)
// Stop the container cleanly
stopContainer(t, cli, id)
}
// TestSecurityVulnerabilities checks for any vulnerabilities in the image, as reported
// by Ubuntu
func TestSecurityVulnerabilities(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
rc, _ := runContainerOneShot(t, cli, "bash", "-c", "test -d /etc/apt")
if rc != 0 {
t.Skip("Skipping test because container is not Ubuntu-based")
}
// Override the entrypoint to make "apt" only receive security updates, then check for updates
rc, log := runContainerOneShot(t, cli, "bash", "-c", "source /etc/os-release && echo \"deb http://security.ubuntu.com/ubuntu/ ${VERSION_CODENAME}-security main restricted\" > /etc/apt/sources.list && apt-get update 2>&1 >/dev/null && apt-get --simulate -qq upgrade")
if rc != 0 {
t.Fatalf("Expected success, got %v", rc)
}
lines := strings.Split(strings.TrimSpace(log), "\n")
if len(lines) > 0 && lines[0] != "" {
t.Errorf("Expected no vulnerabilities, found the following:\n%v", log)
}
}
func utilTestNoQueueManagerName(t *testing.T, hostName string, expectedName string) {
@@ -87,29 +122,29 @@ func utilTestNoQueueManagerName(t *testing.T, hostName string, expectedName stri
containerConfig := container.Config{
Env: []string{"LICENSE=accept"},
Hostname: hostName,
ExposedPorts: nat.PortSet{
"1414/tcp": struct{}{},
},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
waitForReady(t, cli, id)
_, out := execContainer(t, cli, id, []string{"dspmq"})
out := execContainerWithOutput(t, cli, id, "mqm", []string{"dspmq"})
if !strings.Contains(out, search) {
t.Errorf("Expected result of running dspmq to contain name=%v, got name=%v", search, out)
}
}
func TestNoQueueManagerName(t *testing.T) {
t.Parallel()
utilTestNoQueueManagerName(t, "test", "test")
}
func TestNoQueueManagerNameInvalidHostname(t *testing.T) {
t.Parallel()
utilTestNoQueueManagerName(t, "test-1", "test1")
}
// TestWithVolume runs a container with a Docker volume, then removes that
// container and starts a new one with same volume.
func TestWithVolume(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
@@ -123,7 +158,6 @@ func TestWithVolume(t *testing.T) {
hostConfig := container.HostConfig{
Binds: []string{
coverageBind(t),
//"coverage:/var/coverage",
vol.Name + ":/mnt/mqm",
},
}
@@ -149,17 +183,16 @@ func TestWithVolume(t *testing.T) {
waitForReady(t, cli, ctr2.ID)
}
// TestNoVolumeWithRestart ensures a queue manager container can be stopped
// and restarted cleanly
func TestNoVolumeWithRestart(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
//ExposedPorts: ports,
ExposedPorts: nat.PortSet{
"1414/tcp": struct{}{},
},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
@@ -168,3 +201,381 @@ func TestNoVolumeWithRestart(t *testing.T) {
startContainer(t, cli, id)
waitForReady(t, cli, id)
}
// TestCreateQueueManagerFail causes a failure of `crtmqm`
func TestCreateQueueManagerFail(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
img, _, err := cli.ImageInspectWithRaw(context.Background(), imageName())
oldEntrypoint := strings.Join(img.Config.Entrypoint, " ")
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
// Override the entrypoint to create the queue manager directory, but leave it empty.
// This will cause `crtmqm` to return with an exit code of 2.
Entrypoint: []string{"bash", "-c", "mkdir -p /mnt/mqm/data && mkdir -p /var/mqm/qmgrs/qm1 && exec " + oldEntrypoint},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
rc := waitForContainer(t, cli, id, 10)
if rc != 1 {
t.Errorf("Expected rc=1, got rc=%v", rc)
}
expectTerminationMessage(t)
}
// TestStartQueueManagerFail causes a failure of `strmqm`
func TestStartQueueManagerFail(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
img, _, err := cli.ImageInspectWithRaw(context.Background(), imageName())
oldEntrypoint := strings.Join(img.Config.Entrypoint, " ")
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1", "DEBUG=1"},
// Override the entrypoint to replace `strmqm` with a script which deletes the queue manager.
// This will cause `strmqm` to return with an exit code of 72.
Entrypoint: []string{"bash", "-c", "echo '#!/bin/bash\ndltmqm $@ && strmqm $@' > /opt/mqm/bin/strmqm && exec " + oldEntrypoint},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
rc := waitForContainer(t, cli, id, 10)
if rc != 1 {
t.Errorf("Expected rc=1, got rc=%v", rc)
}
expectTerminationMessage(t)
}
// TestVolumeUnmount runs a queue manager with a volume, and then forces an
// unmount of the volume. The health check should then fail.
// This simulates behaviour seen in some cloud environments, where network
// attached storage gets unmounted.
func TestVolumeUnmount(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
vol := createVolume(t, cli)
defer removeVolume(t, cli, vol.Name)
containerConfig := container.Config{
Image: imageName(),
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
}
hostConfig := container.HostConfig{
// SYS_ADMIN capability is required to unmount file systems
CapAdd: []string{
"SYS_ADMIN",
},
Binds: []string{
coverageBind(t),
vol.Name + ":/mnt/mqm",
},
}
networkingConfig := network.NetworkingConfig{}
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)
waitForReady(t, cli, ctr.ID)
// Unmount the volume as root
rc := execContainerWithExitCode(t, cli, ctr.ID, "root", []string{"umount", "-l", "-f", "/mnt/mqm"})
if rc != 0 {
t.Fatalf("Expected umount to work with rc=0, got %v", rc)
}
time.Sleep(3 * time.Second)
rc = execContainerWithExitCode(t, cli, ctr.ID, "mqm", []string{"chkmqhealthy"})
if rc == 0 {
t.Errorf("Expected chkmqhealthy to fail")
t.Logf(execContainerWithOutput(t, cli, ctr.ID, "mqm", []string{"df"}))
t.Logf(execContainerWithOutput(t, cli, ctr.ID, "mqm", []string{"ps", "-ef"}))
}
}
// TestZombies starts a queue manager, then causes a zombie process to be
// created, then checks that no zombies exist (runmqserver should reap them)
func TestZombies(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1", "DEBUG=true"},
//ExposedPorts: ports,
ExposedPorts: nat.PortSet{
"1414/tcp": struct{}{},
},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
waitForReady(t, cli, id)
// Kill an MQ process with children. After it is killed, its children
// will be adopted by PID 1, and should then be reaped when they die.
out := execContainerWithOutput(t, cli, id, "mqm", []string{"pkill", "--signal", "kill", "-c", "amqzxma0"})
if out == "0" {
t.Fatalf("Expected pkill to kill a process, got %v", out)
}
time.Sleep(3 * time.Second)
out = execContainerWithOutput(t, cli, id, "mqm", []string{"bash", "-c", "ps -lA | grep '^. Z'"})
if out != "" {
count := strings.Count(out, "\n") + 1
t.Errorf("Expected zombies=0, got %v", count)
t.Error(out)
t.Fail()
}
}
// TestMQSC creates a new image with an MQSC file in, starts a container based
// on that image, and checks that the MQSC has been applied correctly.
func TestMQSC(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
var files = []struct {
Name, Body string
}{
{"Dockerfile", fmt.Sprintf("FROM %v\nRUN rm -f /etc/mqm/*.mqsc\nADD test.mqsc /etc/mqm/", imageName())},
{"test.mqsc", "DEFINE QLOCAL(test)"},
}
tag := createImage(t, cli, files)
defer deleteImage(t, cli, tag)
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
Image: tag,
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
waitForReady(t, cli, id)
rc := execContainerWithExitCode(t, cli, id, "mqm", []string{"bash", "-c", "echo 'DISPLAY QLOCAL(test)' | runmqsc"})
if rc != 0 {
t.Fatalf("Expected runmqsc to exit with rc=0, got %v", rc)
}
}
// TestReadiness creates a new image with large amounts of MQSC in, to
// ensure that the readiness check doesn't pass until configuration has finished.
// WARNING: This test is sensitive to the speed of the machine it's running on.
func TestReadiness(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
const numQueues = 3
var buf bytes.Buffer
for i := 1; i <= numQueues; i++ {
fmt.Fprintf(&buf, "* Defining queue test %v\nDEFINE QLOCAL(test%v)\n", i, i)
}
var files = []struct {
Name, Body string
}{
{"Dockerfile", fmt.Sprintf("FROM %v\nRUN rm -f /etc/mqm/*.mqsc\nADD test.mqsc /etc/mqm/", imageName())},
{"test.mqsc", buf.String()},
}
tag := createImage(t, cli, files)
defer deleteImage(t, cli, tag)
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1", "DEBUG=1"},
Image: tag,
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
queueCheckCommand := fmt.Sprintf("echo 'DISPLAY QLOCAL(test%v)' | runmqsc", numQueues)
t.Log(execContainerWithOutput(t, cli, id, "root", []string{"cat", "/etc/mqm/test.mqsc"}))
for {
readyRC := execContainerWithExitCode(t, cli, id, "mqm", []string{"chkmqready"})
queueCheckRC := execContainerWithExitCode(t, cli, id, "mqm", []string{"bash", "-c", queueCheckCommand})
t.Logf("readyRC=%v,queueCheckRC=%v\n", readyRC, queueCheckRC)
if readyRC == 0 {
if queueCheckRC != 0 {
t.Fatalf("chkmqready returned %v when MQSC had not finished", readyRC)
} else {
// chkmqready says OK, and the last queue exists, so return
t.Log(execContainerWithOutput(t, cli, id, "root", []string{"bash", "-c", "echo 'DISPLAY QLOCAL(test1)' | runmqsc"}))
return
}
}
}
}
func countLines(t *testing.T, r io.Reader) int {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() {
count++
}
err := scanner.Err()
if err != nil {
t.Fatal(err)
}
return count
}
func countTarLines(t *testing.T, b []byte) int {
r := bytes.NewReader(b)
tr := tar.NewReader(r)
total := 0
for {
_, err := tr.Next()
if err == io.EOF {
// End of TAR
break
}
if err != nil {
t.Fatal(err)
}
total += countLines(t, tr)
}
return total
}
func TestErrorLogRotation(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
qmName := "qm1"
containerConfig := container.Config{
Env: []string{
"LICENSE=accept",
"MQ_QMGR_NAME=" + qmName,
"MQMAXERRORLOGSIZE=65536",
"LOG_FORMAT=json",
},
ExposedPorts: nat.PortSet{
"1414/tcp": struct{}{},
},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
waitForReady(t, cli, id)
dir := "/var/mqm/qmgrs/" + qmName + "/errors"
// Generate some content for the error logs, by trying to put messages under an unauthorized user
// execContainerWithOutput(t, cli, id, "fred", []string{"bash", "-c", "for i in {1..30} ; do /opt/mqm/samp/bin/amqsput FAKE; done"})
execContainerWithOutput(t, cli, id, "root", []string{"useradd", "fred"})
for {
execContainerWithOutput(t, cli, id, "fred", []string{"bash", "-c", "/opt/mqm/samp/bin/amqsput FAKE"})
amqerr02size, err := strconv.Atoi(execContainerWithOutput(t, cli, id, "mqm", []string{"bash", "-c", "wc -c < " + filepath.Join(dir, "AMQERR02.json")}))
if err != nil {
t.Fatal(err)
}
if amqerr02size > 0 {
// We've done enough to cause log rotation
break
}
}
out := execContainerWithOutput(t, cli, id, "root", []string{"ls", "-l", dir})
t.Log(out)
stopContainer(t, cli, id)
b := copyFromContainer(t, cli, id, filepath.Join(dir, "AMQERR01.json"))
amqerr01 := countTarLines(t, b)
b = copyFromContainer(t, cli, id, filepath.Join(dir, "AMQERR02.json"))
amqerr02 := countTarLines(t, b)
b = copyFromContainer(t, cli, id, filepath.Join(dir, "AMQERR03.json"))
amqerr03 := countTarLines(t, b)
scanner := bufio.NewScanner(strings.NewReader(inspectLogs(t, cli, id)))
totalMirrored := 0
for scanner.Scan() {
if strings.Contains(scanner.Text(), "\"message\":\"AMQ") {
totalMirrored++
}
}
err = scanner.Err()
if err != nil {
t.Fatal(err)
}
total := amqerr01 + amqerr02 + amqerr03
if totalMirrored != total {
t.Fatalf("Expected %v (%v + %v + %v) mirrored log entries; got %v", total, amqerr01, amqerr02, amqerr03, totalMirrored)
} else {
t.Logf("Found %v (%v + %v + %v) mirrored log entries", totalMirrored, amqerr01, amqerr02, amqerr03)
}
}
func TestJSONLogFormat(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
containerConfig := container.Config{
Env: []string{
"LICENSE=accept",
"LOG_FORMAT=json",
},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
waitForReady(t, cli, id)
stopContainer(t, cli, id)
scanner := bufio.NewScanner(strings.NewReader(inspectLogs(t, cli, id)))
for scanner.Scan() {
var obj map[string]interface{}
s := scanner.Text()
err := json.Unmarshal([]byte(s), &obj)
if err != nil {
t.Fatalf("Expected all log lines to be valid JSON. Got error %v for line %v", err, s)
}
}
err = scanner.Err()
if err != nil {
t.Fatal(err)
}
}
func TestBadLogFormat(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
containerConfig := container.Config{
Env: []string{
"LICENSE=accept",
"LOG_FORMAT=fake",
},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
rc := waitForContainer(t, cli, id, 5)
if rc != 1 {
t.Errorf("Expected rc=1, got rc=%v", rc)
}
expectTerminationMessage(t)
}
// TestMQJSONDisabled tests the case where MQ's JSON logging feature is
// specifically disabled (which will disable log mirroring)
func TestMQJSONDisabled(t *testing.T) {
t.SkipNow()
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
containerConfig := container.Config{
Env: []string{
"LICENSE=accept",
"MQ_QMGR_NAME=qm1",
"AMQ_ADDITIONAL_JSON_LOG=0",
},
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
waitForReady(t, cli, id)
// Stop the container (which could hang if runmqserver is still waiting for
// JSON logs to appear)
stopContainer(t, cli, id)
}

View File

@@ -1,5 +1,5 @@
/*
© Copyright IBM Corporation 2017
© Copyright IBM Corporation 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,12 +16,19 @@ limitations under the License.
package main
import (
"archive/tar"
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
@@ -30,6 +37,8 @@ import (
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
)
@@ -41,12 +50,64 @@ func imageName() string {
return image
}
func coverageBind(t *testing.T) string {
func coverage() bool {
cover := os.Getenv("TEST_COVER")
if cover == "true" || cover == "1" {
return true
}
return false
}
// coverageDir returns the host directory to use for code coverage data
func coverageDir(t *testing.T) string {
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
return filepath.Join(dir, "coverage") + ":/var/coverage"
return filepath.Join(dir, "coverage")
}
// coverageBind returns a string to use to add a bind-mounted directory for code coverage data
func coverageBind(t *testing.T) string {
return coverageDir(t) + ":/var/coverage"
}
// terminationLog returns the name of the file to use for the termination log message
func terminationLog(t *testing.T) string {
// Warning: this directory must be accessible to the Docker daemon,
// in order to enable the bind mount
return "/tmp/" + 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 := terminationLog(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 n + ":/dev/termination-log"
}
// Returns the termination message, or an empty string if not set
func terminationMessage(t *testing.T) string {
b, err := ioutil.ReadFile(terminationLog(t))
if err != nil {
t.Log(err)
}
return string(b)
}
func expectTerminationMessage(t *testing.T) {
m := terminationMessage(t)
if m == "" {
t.Error("Expected termination message to be set")
}
}
func cleanContainer(t *testing.T, cli *client.Client, ID string) {
@@ -54,20 +115,32 @@ func cleanContainer(t *testing.T, cli *client.Client, ID string) {
if err == nil {
// Log the results and continue
t.Logf("Inspected container %v: %#v", ID, i)
s, err := json.MarshalIndent(i, "", " ")
if err != nil {
t.Fatal(err)
}
t.Logf("Inspected container %v: %v", ID, string(s))
}
t.Logf("Killing container: %v", ID)
// Kill the container. This allows the coverage output to be generated.
err = cli.ContainerKill(context.Background(), ID, "SIGTERM")
t.Logf("Stopping container: %v", ID)
timeout := 10 * time.Second
// Stop the container. This allows the coverage output to be generated.
err = cli.ContainerStop(context.Background(), ID, &timeout)
if err != nil {
// Just log the error and continue
t.Log(err)
}
//waitForContainer(t, cli, ID, 20, container.WaitConditionNotRunning)
t.Log("Container stopped")
// TODO: This is probably no longer necessary
time.Sleep(20 * time.Second)
// If a code coverage file has been generated, then rename it to match the test name
os.Rename(filepath.Join(coverageDir(t), "container.cov"), filepath.Join(coverageDir(t), t.Name()+".cov"))
// Log the container output for any container we're about to delete
t.Logf("Console log from container %v:\n%v", ID, inspectLogs(t, cli, ID))
t.Logf("Console log from container %v:\n%v", ID, inspectTextLogs(t, cli, ID))
m := terminationMessage(t)
if m != "" {
t.Logf("Termination message: %v", m)
}
os.Remove(terminationLog(t))
t.Logf("Removing container: %s", ID)
opts := types.ContainerRemoveOptions{
@@ -87,12 +160,19 @@ func runContainer(t *testing.T, cli *client.Client, containerConfig *container.C
if containerConfig.Image == "" {
containerConfig.Image = imageName()
}
// if coverage
containerConfig.Env = append(containerConfig.Env, "COVERAGE_FILE="+t.Name()+".cov")
hostConfig := container.HostConfig{
Binds: []string{
coverageBind(t),
terminationBind(t),
},
// Assign a random port for the web server on the host
// TODO: Don't do this for all tests
PortBindings: nat.PortMap{
"1414/tcp": []nat.PortBinding{
"9443/tcp": []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: "1414",
HostIP: "0.0.0.0",
},
},
},
@@ -107,6 +187,15 @@ func runContainer(t *testing.T, cli *client.Client, containerConfig *container.C
return ctr.ID
}
func runContainerOneShot(t *testing.T, cli *client.Client, command ...string) (int64, string) {
containerConfig := container.Config{
Entrypoint: command,
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
return waitForContainer(t, cli, id, 10), inspectLogs(t, cli, id)
}
func startContainer(t *testing.T, cli *client.Client, ID string) {
t.Logf("Starting container: %v", ID)
startOptions := types.ContainerStartOptions{}
@@ -125,24 +214,85 @@ func stopContainer(t *testing.T, cli *client.Client, ID string) {
}
}
func getCoverageExitCode(t *testing.T, orig int64) int64 {
f := filepath.Join(coverageDir(t), "exitCode")
_, err := os.Stat(f)
if err != nil {
t.Log(err)
return orig
}
// Remove the file, ready for the next test
defer os.Remove(f)
buf, err := ioutil.ReadFile(f)
if err != nil {
t.Log(err)
return orig
}
rc, err := strconv.Atoi(string(buf))
if err != nil {
t.Log(err)
return orig
}
t.Logf("Retrieved exit code %v from file", rc)
return int64(rc)
}
// waitForContainer waits until a container has exited
func waitForContainer(t *testing.T, cli *client.Client, ID string, timeout int64) int64 {
//ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
//defer cancel()
rc, err := cli.ContainerWait(context.Background(), ID)
// err := <-errC
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)
}
// wait := <-waitC
return rc
}
// execContainer runs the specified command inside the container, returning the
// exit code and the stdout/stderr string.
func execContainer(t *testing.T, cli *client.Client, ID string, cmd []string) (int, string) {
// execContainerWithExitCode runs a command in a running container, and returns the exit code
// Note: due to a bug in Docker/Moby code, you always get an exit code of 0 if you attach to the
// container to get output. This is why these are two separate commands.
func execContainerWithExitCode(t *testing.T, cli *client.Client, ID string, user string, cmd []string) int {
config := types.ExecConfig{
User: "mqm",
User: user,
Privileged: false,
Tty: false,
AttachStdin: false,
// Note that you still need to attach stdout/stderr, even though they're not wanted
AttachStdout: true,
AttachStderr: true,
Detach: false,
Cmd: cmd,
}
resp, err := cli.ContainerExecCreate(context.Background(), ID, config)
if err != nil {
t.Fatal(err)
}
cli.ContainerExecStart(context.Background(), resp.ID, types.ExecStartCheck{
Detach: false,
Tty: false,
})
if err != nil {
t.Fatal(err)
}
inspect, err := cli.ContainerExecInspect(context.Background(), resp.ID)
if err != nil {
t.Fatal(err)
}
return inspect.ExitCode
}
// execContainerWithOutput runs a command in a running container, and returns the output from stdout/stderr
// Note: due to a bug in Docker/Moby code, you always get an exit code of 0 if you attach to the
// container to get output. This is why these are two separate commands.
func execContainerWithOutput(t *testing.T, cli *client.Client, ID string, user string, cmd []string) string {
config := types.ExecConfig{
User: user,
Privileged: false,
Tty: false,
AttachStdin: false,
@@ -159,53 +309,36 @@ func execContainer(t *testing.T, cli *client.Client, ID string, cmd []string) (i
if err != nil {
t.Fatal(err)
}
cli.ContainerExecStart(context.Background(), resp.ID, types.ExecStartCheck{
err = cli.ContainerExecStart(context.Background(), resp.ID, types.ExecStartCheck{
Detach: false,
Tty: false,
})
if err != nil {
t.Fatal(err)
}
inspect, err := cli.ContainerExecInspect(context.Background(), resp.ID)
if err != nil {
t.Fatal(err)
}
// TODO: For some reason, each line seems to start with an extra, random character
buf, err := ioutil.ReadAll(hijack.Reader)
if err != nil {
t.Fatal(err)
}
hijack.Close()
return inspect.ExitCode, string(buf)
}
func waitForReady(t *testing.T, cli *client.Client, ID string) {
// Wait for the command to finish
for {
resp, err := cli.ContainerExecCreate(context.Background(), ID, types.ExecConfig{
User: "mqm",
Privileged: false,
Tty: false,
AttachStdin: false,
AttachStdout: true,
AttachStderr: true,
Detach: false,
Cmd: []string{"chkmqready"},
})
if err != nil {
t.Fatal(err)
}
cli.ContainerExecStart(context.Background(), resp.ID, types.ExecStartCheck{
Detach: false,
Tty: false,
})
if err != nil {
t.Fatal(err)
}
inspect, err := cli.ContainerExecInspect(context.Background(), resp.ID)
if err != nil {
t.Fatal(err)
}
if inspect.ExitCode == 0 {
if !inspect.Running {
break
}
}
buf := new(bytes.Buffer)
// Each output line has a header, which needs to be removed
_, err = stdcopy.StdCopy(buf, buf, hijack.Reader)
if err != nil {
log.Fatal(err)
}
return strings.TrimSpace(buf.String())
}
func waitForReady(t *testing.T, cli *client.Client, ID string) {
for {
rc := execContainerWithExitCode(t, cli, ID, "mqm", []string{"chkmqready"})
if rc == 0 {
t.Log("MQ is ready")
return
}
@@ -262,6 +395,29 @@ func removeVolume(t *testing.T, cli *client.Client, name string) {
}
}
func inspectTextLogs(t *testing.T, cli *client.Client, ID string) string {
jsonLogs := inspectLogs(t, cli, ID)
scanner := bufio.NewScanner(strings.NewReader(jsonLogs))
b := make([]byte, 64*1024)
scanner.Buffer(b, 1024*1024)
buf := bytes.NewBuffer(b)
for scanner.Scan() {
t := scanner.Text()
if strings.HasPrefix(t, "{") {
var e map[string]interface{}
json.Unmarshal([]byte(scanner.Text()), &e)
fmt.Fprintf(buf, "{\"message\": \"%v\"}\n", e["message"])
} else {
fmt.Fprintln(buf, t)
}
}
err := scanner.Err()
if err != nil {
t.Fatal(err)
}
return buf.String()
}
func inspectLogs(t *testing.T, cli *client.Client, ID string) string {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -272,8 +428,96 @@ func inspectLogs(t *testing.T, cli *client.Client, ID string) string {
if err != nil {
log.Fatal(err)
}
buf := new(bytes.Buffer)
buf.ReadFrom(reader)
// Each output line has a header, which needs to be removed
_, err = stdcopy.StdCopy(buf, buf, reader)
if err != nil {
log.Fatal(err)
}
return buf.String()
}
// generateTAR creates a TAR-formatted []byte, with the specified files included.
func generateTAR(t *testing.T, files []struct{ Name, Body string }) []byte {
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
for _, file := range files {
hdr := &tar.Header{
Name: file.Name,
Mode: 0600,
Size: int64(len(file.Body)),
}
err := tw.WriteHeader(hdr)
if err != nil {
t.Fatal(err)
}
_, err = tw.Write([]byte(file.Body))
if err != nil {
t.Fatal(err)
}
}
err := tw.Close()
if err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
// createImage creates a new Docker image with the specified files included.
func createImage(t *testing.T, cli *client.Client, files []struct{ Name, Body string }) string {
r := bytes.NewReader(generateTAR(t, files))
tag := strings.ToLower(t.Name())
buildOptions := types.ImageBuildOptions{
Context: r,
Tags: []string{tag},
}
resp, err := cli.ImageBuild(context.Background(), r, buildOptions)
if err != nil {
t.Fatal(err)
}
// resp (ImageBuildResponse) contains a series of JSON messages
dec := json.NewDecoder(resp.Body)
for {
m := jsonmessage.JSONMessage{}
err := dec.Decode(&m)
if m.Error != nil {
t.Fatal(m.ErrorMessage)
}
t.Log(strings.TrimSpace(m.Stream))
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
}
return tag
}
// deleteImage deletes a Docker image
func deleteImage(t *testing.T, cli *client.Client, id string) {
cli.ImageRemove(context.Background(), id, types.ImageRemoveOptions{
Force: true,
})
}
func copyFromContainer(t *testing.T, cli *client.Client, id string, file string) []byte {
reader, _, err := cli.CopyFromContainer(context.Background(), id, file)
if err != nil {
t.Fatal(err)
}
defer reader.Close()
b, err := ioutil.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
return b
}
func getWebPort(t *testing.T, cli *client.Client, ID string) string {
i, err := cli.ContainerInspect(context.Background(), ID)
if err != nil {
t.Fatal(err)
}
return i.NetworkSettings.Ports["9443/tcp"][0].HostPort
}

View File

@@ -1,4 +1,4 @@
# © Copyright IBM Corporation 2017
# © Copyright IBM Corporation 2017, 2018
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -27,3 +27,7 @@
[[constraint]]
name = "k8s.io/apimachinery"
branch = "release-1.7"
[prune]
unused-packages = true
go-tests = true

View File

@@ -17,36 +17,34 @@ package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"testing"
"time"
"unicode"
"golang.org/x/sys/unix"
"github.com/ibm-messaging/mq-container/internal/command"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/tools/clientcmd"
)
func imageName() string {
v, ok := os.LookupEnv("TEST_REPO")
func image(t *testing.T) string {
v, ok := os.LookupEnv("TEST_IMAGE")
if !ok {
v = "ibmcom/mq"
t.Fatal("TEST_IMAGE environment variable not set")
}
return v
}
func imageTag() string {
v, ok := os.LookupEnv("TEST_TAG")
if !ok {
v = "latest"
}
return v
func imageName(t *testing.T) string {
return strings.Fields(strings.Replace(image(t), ":", " ", -1))[0]
}
func imageTag(t *testing.T) string {
return strings.Fields(strings.Replace(image(t), ":", " ", -1))[1]
}
func chartName() string {
@@ -57,32 +55,6 @@ func chartName() string {
return v
}
// runCommand runs an OS command. On Linux it waits for the command to
// complete and returns the exit status (return code).
// TODO: duplicated from cmd/runmqserver/main.go
func runCommand(t *testing.T, name string, arg ...string) (string, int, error) {
t.Logf("Running command: %v %v", name, strings.Trim(fmt.Sprintf("%v", arg), "[]"))
cmd := exec.Command(name, arg...)
// Run the command and wait for completion
out, err := cmd.CombinedOutput()
if err != nil {
var rc int
// Only works on Linux
if runtime.GOOS == "linux" {
var ws unix.WaitStatus
unix.Wait4(cmd.Process.Pid, &ws, 0, nil)
rc = ws.ExitStatus()
} else {
rc = -1
}
if rc == 0 {
return string(out), rc, nil
}
return string(out), rc, err
}
return string(out), 0, nil
}
func inspectLogs(t *testing.T, cs *kubernetes.Clientset, release string) string {
pods := getPodsForHelmRelease(t, cs, release)
opt := v1.PodLogOptions{}
@@ -98,7 +70,7 @@ func inspectLogs(t *testing.T, cs *kubernetes.Clientset, release string) string
func helmInstall(t *testing.T, cs *kubernetes.Clientset, release string, values ...string) {
chart := chartName()
tag := "latest"
tag := imageTag(t)
arg := []string{
"install",
"--debug",
@@ -106,7 +78,7 @@ func helmInstall(t *testing.T, cs *kubernetes.Clientset, release string, values
"--name",
release,
"--set",
"image.repository=" + imageName(),
"image.repository=" + imageName(t),
"--set",
"image.tag=" + tag,
"--set",
@@ -116,7 +88,7 @@ func helmInstall(t *testing.T, cs *kubernetes.Clientset, release string, values
for _, value := range values {
arg = append(arg, "--set", value)
}
out, _, err := runCommand(t, "helm", arg...)
out, _, err := command.Run("helm", arg...)
t.Log(out)
if err != nil {
t.Error(out)
@@ -127,7 +99,7 @@ func helmInstall(t *testing.T, cs *kubernetes.Clientset, release string, values
func helmDelete(t *testing.T, cs *kubernetes.Clientset, release string) {
t.Log("Deleting Helm release")
t.Log(inspectLogs(t, cs, release))
out, _, err := runCommand(t, "helm", "delete", "--purge", release)
out, _, err := command.Run("helm", "delete", "--purge", release)
if err != nil {
t.Error(out)
t.Fatal(err)
@@ -151,7 +123,10 @@ func helmDeletePVC(t *testing.T, cs *kubernetes.Clientset, release string) {
}
func kubeLogin(t *testing.T) *kubernetes.Clientset {
kc := os.Getenv("HOME") + "/.kube/config"
kc := os.Getenv("KUBECONFIG")
if kc == "" {
kc = os.Getenv("HOME") + "/.kube/config"
}
c, err := clientcmd.BuildConfigFromFlags("", kc)
if err != nil {
t.Fatal(err)
@@ -167,7 +142,7 @@ func kubeExec(t *testing.T, podName string, name string, arg ...string) (string,
// Current version of Kubernetes Go client doesn't support "exec", so run this via the command line
param := []string{"exec", podName, "--", name}
param = append(param, arg...)
return runCommand(t, "kubectl", param...)
return command.Run("kubectl", param...)
}
func waitForReady(t *testing.T, cs *kubernetes.Clientset, release string) {
@@ -199,12 +174,12 @@ func waitForReady(t *testing.T, cs *kubernetes.Clientset, release string) {
for {
// Current version of Kubernetes Go client doesn't support "exec", so run this via the command line
// TODO: If we run "chkmqready" here, it doesn't seem to work
//out, _, err := runCommand(t, "kubectl", "exec", podName, "--", "dspmq")
//out, _, err := command.Run(t, "kubectl", "exec", podName, "--", "dspmq")
out, _, err := kubeExec(t, podName, "dspmq")
//out, rc, err := runCommand(t, "kubectl", "exec", podName, "--", "chkmqready")
//out, rc, err := command.Run(t, "kubectl", "exec", podName, "--", "chkmqready")
if err != nil {
t.Error(out)
out2, _, err2 := runCommand(t, "kubectl", "describe", "pod", podName)
out2, _, err2 := command.Run("kubectl", "describe", "pod", podName)
if err2 == nil {
t.Log(out2)
}
@@ -258,17 +233,32 @@ func volumesAvailable(t *testing.T, cs *kubernetes.Clientset) bool {
return false
}
// digitsOnly returns only the digits from the supplied string
func digitsOnly(s string) string {
return strings.Map(
func(r rune) rune {
if unicode.IsDigit(r) {
return r
}
return -1
},
s,
)
}
// assertKubeVersion is used to assert that a test requires a specific version of Kubernetes
func assertKubeVersion(t *testing.T, cs *kubernetes.Clientset, major int, minor int) {
v, err := cs.Discovery().ServerVersion()
if err != nil {
t.Fatal(err)
}
maj, err := strconv.Atoi(v.Major)
// Make sure we use only the digits from the version, to account for things like "1.8+"
maj, err := strconv.Atoi(digitsOnly(v.Major))
if err != nil {
t.Fatal(err)
}
min, err := strconv.Atoi(v.Minor)
// Make sure we use only the digits from the version, to account for things like "1.8+"
min, err := strconv.Atoi(digitsOnly(v.Minor))
if err != nil {
t.Fatal(err)
}

View File

@@ -1,3 +0,0 @@
.test
.go

View File

@@ -1,18 +0,0 @@
language: go
script:
- go test -race -v ./...
go:
- 1.4
- 1.5
- 1.6
- tip
matrix:
allow_failures:
- go: tip
install:
- go get gopkg.in/fsnotify.v1
- go get gopkg.in/tomb.v1

View File

@@ -1,63 +0,0 @@
# API v1 (gopkg.in/hpcloud/tail.v1)
## April, 2016
* Migrated to godep, as depman is not longer supported
* Introduced golang vendoring feature
* Fixed issue [#57](https://github.com/hpcloud/tail/issues/57) related to reopen deleted file
## July, 2015
* Fix inotify watcher leak; remove `Cleanup` (#51)
# API v0 (gopkg.in/hpcloud/tail.v0)
## June, 2015
* Don't return partial lines (PR #40)
* Use stable version of fsnotify (#46)
## July, 2014
* Fix tail for Windows (PR #36)
## May, 2014
* Improved rate limiting using leaky bucket (PR #29)
* Fix odd line splitting (PR #30)
## Apr, 2014
* LimitRate now discards read buffer (PR #28)
* allow reading of longer lines if MaxLineSize is unset (PR #24)
* updated deps.json to latest fsnotify (441bbc86b1)
## Feb, 2014
* added `Config.Logger` to suppress library logging
## Nov, 2013
* add Cleanup to remove leaky inotify watches (PR #20)
## Aug, 2013
* redesigned Location field (PR #12)
* add tail.Tell (PR #14)
## July, 2013
* Rate limiting (PR #10)
## May, 2013
* Detect file deletions/renames in polling file watcher (PR #1)
* Detect file truncation
* Fix potential race condition when reopening the file (issue 5)
* Fix potential blocking of `tail.Stop` (issue 4)
* Fix uncleaned up ChangeEvents goroutines after calling tail.Stop
* Support Follow=false
## Feb, 2013
* Initial open source release

View File

@@ -1,19 +0,0 @@
FROM golang
RUN mkdir -p $GOPATH/src/github.com/hpcloud/tail/
ADD . $GOPATH/src/github.com/hpcloud/tail/
# expecting to fetch dependencies successfully.
RUN go get -v github.com/hpcloud/tail
# expecting to run the test successfully.
RUN go test -v github.com/hpcloud/tail
# expecting to install successfully
RUN go install -v github.com/hpcloud/tail
RUN go install -v github.com/hpcloud/tail/cmd/gotail
RUN $GOPATH/bin/gotail -h || true
ENV PATH $GOPATH/bin:$PATH
CMD ["gotail"]

View File

@@ -1,15 +0,0 @@
{
"ImportPath": "github.com/hpcloud/tail",
"GoVersion": "go1.5.1",
"Deps": [
{
"ImportPath": "gopkg.in/fsnotify.v1",
"Comment": "v1.2.1",
"Rev": "7be54206639f256967dd82fa767397ba5f8f48f5"
},
{
"ImportPath": "gopkg.in/tomb.v1",
"Rev": "c131134a1947e9afd9cecfe11f4c6dff0732ae58"
}
]
}

View File

@@ -1,5 +0,0 @@
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.

View File

@@ -1,21 +0,0 @@
# The MIT License (MIT)
# © Copyright 2015 Hewlett Packard Enterprise Development LP
Copyright (c) 2014 ActiveState
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,11 +0,0 @@
default: test
test: *.go
go test -v -race ./...
fmt:
gofmt -w .
# Run the test in an isolated environment.
fulltest:
docker build -t hpcloud/tail .

View File

@@ -1,28 +0,0 @@
[![Build Status](https://travis-ci.org/hpcloud/tail.svg)](https://travis-ci.org/hpcloud/tail)
[![Build status](https://ci.appveyor.com/api/projects/status/kohpsf3rvhjhrox6?svg=true)](https://ci.appveyor.com/project/HelionCloudFoundry/tail)
# Go package for tail-ing files
A Go package striving to emulate the features of the BSD `tail` program.
```Go
t, err := tail.TailFile("/var/log/nginx.log", tail.Config{Follow: true})
for line := range t.Lines {
fmt.Println(line.Text)
}
```
See [API documentation](http://godoc.org/github.com/hpcloud/tail).
## Log rotation
Tail comes with full support for truncation/move detection as it is
designed to work with log rotation tools.
## Installing
go get github.com/hpcloud/tail/...
## Windows support
This package [needs assistance](https://github.com/hpcloud/tail/labels/Windows) for full Windows support.

View File

@@ -1,11 +0,0 @@
version: 0.{build}
skip_tags: true
cache: C:\Users\appveyor\AppData\Local\NuGet\Cache
build_script:
- SET GOPATH=c:\workspace
- go test -v -race ./...
test: off
clone_folder: c:\workspace\src\github.com\hpcloud\tail
branches:
only:
- master

View File

@@ -1 +0,0 @@
gotail

View File

@@ -1,4 +0,0 @@
default: gotail
gotail: *.go ../../*.go
go build

View File

@@ -1,66 +0,0 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
package main
import (
"flag"
"fmt"
"os"
"github.com/hpcloud/tail"
)
func args2config() (tail.Config, int64) {
config := tail.Config{Follow: true}
n := int64(0)
maxlinesize := int(0)
flag.Int64Var(&n, "n", 0, "tail from the last Nth location")
flag.IntVar(&maxlinesize, "max", 0, "max line size")
flag.BoolVar(&config.Follow, "f", false, "wait for additional data to be appended to the file")
flag.BoolVar(&config.ReOpen, "F", false, "follow, and track file rename/rotation")
flag.BoolVar(&config.Poll, "p", false, "use polling, instead of inotify")
flag.Parse()
if config.ReOpen {
config.Follow = true
}
config.MaxLineSize = maxlinesize
return config, n
}
func main() {
config, n := args2config()
if flag.NFlag() < 1 {
fmt.Println("need one or more files as arguments")
os.Exit(1)
}
if n != 0 {
config.Location = &tail.SeekInfo{-n, os.SEEK_END}
}
done := make(chan bool)
for _, filename := range flag.Args() {
go tailFile(filename, config, done)
}
for _, _ = range flag.Args() {
<-done
}
}
func tailFile(filename string, config tail.Config, done chan bool) {
defer func() { done <- true }()
t, err := tail.TailFile(filename, config)
if err != nil {
fmt.Println(err)
return
}
for line := range t.Lines {
fmt.Println(line.Text)
}
err = t.Wait()
if err != nil {
fmt.Println(err)
}
}

View File

@@ -1,7 +0,0 @@
Copyright (C) 2013 99designs
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,97 +0,0 @@
// Package ratelimiter implements the Leaky Bucket ratelimiting algorithm with memcached and in-memory backends.
package ratelimiter
import (
"time"
)
type LeakyBucket struct {
Size uint16
Fill float64
LeakInterval time.Duration // time.Duration for 1 unit of size to leak
Lastupdate time.Time
Now func() time.Time
}
func NewLeakyBucket(size uint16, leakInterval time.Duration) *LeakyBucket {
bucket := LeakyBucket{
Size: size,
Fill: 0,
LeakInterval: leakInterval,
Now: time.Now,
Lastupdate: time.Now(),
}
return &bucket
}
func (b *LeakyBucket) updateFill() {
now := b.Now()
if b.Fill > 0 {
elapsed := now.Sub(b.Lastupdate)
b.Fill -= float64(elapsed) / float64(b.LeakInterval)
if b.Fill < 0 {
b.Fill = 0
}
}
b.Lastupdate = now
}
func (b *LeakyBucket) Pour(amount uint16) bool {
b.updateFill()
var newfill float64 = b.Fill + float64(amount)
if newfill > float64(b.Size) {
return false
}
b.Fill = newfill
return true
}
// The time at which this bucket will be completely drained
func (b *LeakyBucket) DrainedAt() time.Time {
return b.Lastupdate.Add(time.Duration(b.Fill * float64(b.LeakInterval)))
}
// The duration until this bucket is completely drained
func (b *LeakyBucket) TimeToDrain() time.Duration {
return b.DrainedAt().Sub(b.Now())
}
func (b *LeakyBucket) TimeSinceLastUpdate() time.Duration {
return b.Now().Sub(b.Lastupdate)
}
type LeakyBucketSer struct {
Size uint16
Fill float64
LeakInterval time.Duration // time.Duration for 1 unit of size to leak
Lastupdate time.Time
}
func (b *LeakyBucket) Serialise() *LeakyBucketSer {
bucket := LeakyBucketSer{
Size: b.Size,
Fill: b.Fill,
LeakInterval: b.LeakInterval,
Lastupdate: b.Lastupdate,
}
return &bucket
}
func (b *LeakyBucketSer) DeSerialise() *LeakyBucket {
bucket := LeakyBucket{
Size: b.Size,
Fill: b.Fill,
LeakInterval: b.LeakInterval,
Lastupdate: b.Lastupdate,
Now: time.Now,
}
return &bucket
}

View File

@@ -1,73 +0,0 @@
package ratelimiter
import (
"testing"
"time"
)
func TestPour(t *testing.T) {
bucket := NewLeakyBucket(60, time.Second)
bucket.Lastupdate = time.Unix(0, 0)
bucket.Now = func() time.Time { return time.Unix(1, 0) }
if bucket.Pour(61) {
t.Error("Expected false")
}
if !bucket.Pour(10) {
t.Error("Expected true")
}
if !bucket.Pour(49) {
t.Error("Expected true")
}
if bucket.Pour(2) {
t.Error("Expected false")
}
bucket.Now = func() time.Time { return time.Unix(61, 0) }
if !bucket.Pour(60) {
t.Error("Expected true")
}
if bucket.Pour(1) {
t.Error("Expected false")
}
bucket.Now = func() time.Time { return time.Unix(70, 0) }
if !bucket.Pour(1) {
t.Error("Expected true")
}
}
func TestTimeSinceLastUpdate(t *testing.T) {
bucket := NewLeakyBucket(60, time.Second)
bucket.Now = func() time.Time { return time.Unix(1, 0) }
bucket.Pour(1)
bucket.Now = func() time.Time { return time.Unix(2, 0) }
sinceLast := bucket.TimeSinceLastUpdate()
if sinceLast != time.Second*1 {
t.Errorf("Expected time since last update to be less than 1 second, got %d", sinceLast)
}
}
func TestTimeToDrain(t *testing.T) {
bucket := NewLeakyBucket(60, time.Second)
bucket.Now = func() time.Time { return time.Unix(1, 0) }
bucket.Pour(10)
if bucket.TimeToDrain() != time.Second*10 {
t.Error("Time to drain should be 10 seconds")
}
bucket.Now = func() time.Time { return time.Unix(2, 0) }
if bucket.TimeToDrain() != time.Second*9 {
t.Error("Time to drain should be 9 seconds")
}
}

View File

@@ -1,58 +0,0 @@
package ratelimiter
import (
"errors"
"time"
)
const GC_SIZE int = 100
type Memory struct {
store map[string]LeakyBucket
lastGCCollected time.Time
}
func NewMemory() *Memory {
m := new(Memory)
m.store = make(map[string]LeakyBucket)
m.lastGCCollected = time.Now()
return m
}
func (m *Memory) GetBucketFor(key string) (*LeakyBucket, error) {
bucket, ok := m.store[key]
if !ok {
return nil, errors.New("miss")
}
return &bucket, nil
}
func (m *Memory) SetBucketFor(key string, bucket LeakyBucket) error {
if len(m.store) > GC_SIZE {
m.GarbageCollect()
}
m.store[key] = bucket
return nil
}
func (m *Memory) GarbageCollect() {
now := time.Now()
// rate limit GC to once per minute
if now.Add(60*time.Second).Unix() > m.lastGCCollected.Unix() {
for key, bucket := range m.store {
// if the bucket is drained, then GC
if bucket.DrainedAt().Unix() > now.Unix() {
delete(m.store, key)
}
}
m.lastGCCollected = now
}
}

View File

@@ -1,6 +0,0 @@
package ratelimiter
type Storage interface {
GetBucketFor(string) (*LeakyBucket, error)
SetBucketFor(string, LeakyBucket) error
}

View File

@@ -1,438 +0,0 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
package tail
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"sync"
"time"
"github.com/hpcloud/tail/ratelimiter"
"github.com/hpcloud/tail/util"
"github.com/hpcloud/tail/watch"
"gopkg.in/tomb.v1"
)
var (
ErrStop = fmt.Errorf("tail should now stop")
)
type Line struct {
Text string
Time time.Time
Err error // Error from tail
}
// NewLine returns a Line with present time.
func NewLine(text string) *Line {
return &Line{text, time.Now(), nil}
}
// SeekInfo represents arguments to `os.Seek`
type SeekInfo struct {
Offset int64
Whence int // os.SEEK_*
}
type logger interface {
Fatal(v ...interface{})
Fatalf(format string, v ...interface{})
Fatalln(v ...interface{})
Panic(v ...interface{})
Panicf(format string, v ...interface{})
Panicln(v ...interface{})
Print(v ...interface{})
Printf(format string, v ...interface{})
Println(v ...interface{})
}
// Config is used to specify how a file must be tailed.
type Config struct {
// File-specifc
Location *SeekInfo // Seek to this location before tailing
ReOpen bool // Reopen recreated files (tail -F)
MustExist bool // Fail early if the file does not exist
Poll bool // Poll for file changes instead of using inotify
Pipe bool // Is a named pipe (mkfifo)
RateLimiter *ratelimiter.LeakyBucket
// Generic IO
Follow bool // Continue looking for new lines (tail -f)
MaxLineSize int // If non-zero, split longer lines into multiple lines
// Logger, when nil, is set to tail.DefaultLogger
// To disable logging: set field to tail.DiscardingLogger
Logger logger
}
type Tail struct {
Filename string
Lines chan *Line
Config
file *os.File
reader *bufio.Reader
watcher watch.FileWatcher
changes *watch.FileChanges
tomb.Tomb // provides: Done, Kill, Dying
lk sync.Mutex
}
var (
// DefaultLogger is used when Config.Logger == nil
DefaultLogger = log.New(os.Stderr, "", log.LstdFlags)
// DiscardingLogger can be used to disable logging output
DiscardingLogger = log.New(ioutil.Discard, "", 0)
)
// TailFile begins tailing the file. Output stream is made available
// via the `Tail.Lines` channel. To handle errors during tailing,
// invoke the `Wait` or `Err` method after finishing reading from the
// `Lines` channel.
func TailFile(filename string, config Config) (*Tail, error) {
if config.ReOpen && !config.Follow {
util.Fatal("cannot set ReOpen without Follow.")
}
t := &Tail{
Filename: filename,
Lines: make(chan *Line),
Config: config,
}
// when Logger was not specified in config, use default logger
if t.Logger == nil {
t.Logger = log.New(os.Stderr, "", log.LstdFlags)
}
if t.Poll {
t.watcher = watch.NewPollingFileWatcher(filename)
} else {
t.watcher = watch.NewInotifyFileWatcher(filename)
}
if t.MustExist {
var err error
t.file, err = OpenFile(t.Filename)
if err != nil {
return nil, err
}
}
go t.tailFileSync()
return t, nil
}
// Return the file's current position, like stdio's ftell().
// But this value is not very accurate.
// it may readed one line in the chan(tail.Lines),
// so it may lost one line.
func (tail *Tail) Tell() (offset int64, err error) {
if tail.file == nil {
return
}
offset, err = tail.file.Seek(0, os.SEEK_CUR)
if err != nil {
return
}
tail.lk.Lock()
defer tail.lk.Unlock()
if tail.reader == nil {
return
}
offset -= int64(tail.reader.Buffered())
return
}
// Stop stops the tailing activity.
func (tail *Tail) Stop() error {
tail.Kill(nil)
return tail.Wait()
}
// StopAtEOF stops tailing as soon as the end of the file is reached.
func (tail *Tail) StopAtEOF() error {
tail.Kill(errStopAtEOF)
return tail.Wait()
}
var errStopAtEOF = errors.New("tail: stop at eof")
func (tail *Tail) close() {
close(tail.Lines)
tail.closeFile()
}
func (tail *Tail) closeFile() {
if tail.file != nil {
tail.file.Close()
tail.file = nil
}
}
func (tail *Tail) reopen() error {
tail.closeFile()
for {
var err error
tail.file, err = OpenFile(tail.Filename)
if err != nil {
if os.IsNotExist(err) {
tail.Logger.Printf("Waiting for %s to appear...", tail.Filename)
if err := tail.watcher.BlockUntilExists(&tail.Tomb); err != nil {
if err == tomb.ErrDying {
return err
}
return fmt.Errorf("Failed to detect creation of %s: %s", tail.Filename, err)
}
continue
}
return fmt.Errorf("Unable to open file %s: %s", tail.Filename, err)
}
break
}
return nil
}
func (tail *Tail) readLine() (string, error) {
tail.lk.Lock()
line, err := tail.reader.ReadString('\n')
tail.lk.Unlock()
if err != nil {
// Note ReadString "returns the data read before the error" in
// case of an error, including EOF, so we return it as is. The
// caller is expected to process it if err is EOF.
return line, err
}
line = strings.TrimRight(line, "\n")
return line, err
}
func (tail *Tail) tailFileSync() {
defer tail.Done()
defer tail.close()
if !tail.MustExist {
// deferred first open.
err := tail.reopen()
if err != nil {
if err != tomb.ErrDying {
tail.Kill(err)
}
return
}
}
// Seek to requested location on first open of the file.
if tail.Location != nil {
_, err := tail.file.Seek(tail.Location.Offset, tail.Location.Whence)
tail.Logger.Printf("Seeked %s - %+v\n", tail.Filename, tail.Location)
if err != nil {
tail.Killf("Seek error on %s: %s", tail.Filename, err)
return
}
}
tail.openReader()
var offset int64 = 0
var err error
// Read line by line.
for {
// do not seek in named pipes
if !tail.Pipe {
// grab the position in case we need to back up in the event of a half-line
offset, err = tail.Tell()
if err != nil {
tail.Kill(err)
return
}
}
line, err := tail.readLine()
// Process `line` even if err is EOF.
if err == nil {
cooloff := !tail.sendLine(line)
if cooloff {
// Wait a second before seeking till the end of
// file when rate limit is reached.
msg := fmt.Sprintf(
"Too much log activity; waiting a second " +
"before resuming tailing")
tail.Lines <- &Line{msg, time.Now(), fmt.Errorf(msg)}
select {
case <-time.After(time.Second):
case <-tail.Dying():
return
}
if err := tail.seekEnd(); err != nil {
tail.Kill(err)
return
}
}
} else if err == io.EOF {
if !tail.Follow {
if line != "" {
tail.sendLine(line)
}
return
}
if tail.Follow && line != "" {
// this has the potential to never return the last line if
// it's not followed by a newline; seems a fair trade here
err := tail.seekTo(SeekInfo{Offset: offset, Whence: 0})
if err != nil {
tail.Kill(err)
return
}
}
// When EOF is reached, wait for more data to become
// available. Wait strategy is based on the `tail.watcher`
// implementation (inotify or polling).
err := tail.waitForChanges()
if err != nil {
if err != ErrStop {
tail.Kill(err)
}
return
}
} else {
// non-EOF error
tail.Killf("Error reading %s: %s", tail.Filename, err)
return
}
select {
case <-tail.Dying():
if tail.Err() == errStopAtEOF {
continue
}
return
default:
}
}
}
// waitForChanges waits until the file has been appended, deleted,
// moved or truncated. When moved or deleted - the file will be
// reopened if ReOpen is true. Truncated files are always reopened.
func (tail *Tail) waitForChanges() error {
if tail.changes == nil {
pos, err := tail.file.Seek(0, os.SEEK_CUR)
if err != nil {
return err
}
tail.changes, err = tail.watcher.ChangeEvents(&tail.Tomb, pos)
if err != nil {
return err
}
}
select {
case <-tail.changes.Modified:
return nil
case <-tail.changes.Deleted:
tail.changes = nil
if tail.ReOpen {
// XXX: we must not log from a library.
tail.Logger.Printf("Re-opening moved/deleted file %s ...", tail.Filename)
if err := tail.reopen(); err != nil {
return err
}
tail.Logger.Printf("Successfully reopened %s", tail.Filename)
tail.openReader()
return nil
} else {
tail.Logger.Printf("Stopping tail as file no longer exists: %s", tail.Filename)
return ErrStop
}
case <-tail.changes.Truncated:
// Always reopen truncated files (Follow is true)
tail.Logger.Printf("Re-opening truncated file %s ...", tail.Filename)
if err := tail.reopen(); err != nil {
return err
}
tail.Logger.Printf("Successfully reopened truncated %s", tail.Filename)
tail.openReader()
return nil
case <-tail.Dying():
return ErrStop
}
panic("unreachable")
}
func (tail *Tail) openReader() {
if tail.MaxLineSize > 0 {
// add 2 to account for newline characters
tail.reader = bufio.NewReaderSize(tail.file, tail.MaxLineSize+2)
} else {
tail.reader = bufio.NewReader(tail.file)
}
}
func (tail *Tail) seekEnd() error {
return tail.seekTo(SeekInfo{Offset: 0, Whence: os.SEEK_END})
}
func (tail *Tail) seekTo(pos SeekInfo) error {
_, err := tail.file.Seek(pos.Offset, pos.Whence)
if err != nil {
return fmt.Errorf("Seek error on %s: %s", tail.Filename, err)
}
// Reset the read buffer whenever the file is re-seek'ed
tail.reader.Reset(tail.file)
return nil
}
// sendLine sends the line(s) to Lines channel, splitting longer lines
// if necessary. Return false if rate limit is reached.
func (tail *Tail) sendLine(line string) bool {
now := time.Now()
lines := []string{line}
// Split longer lines
if tail.MaxLineSize > 0 && len(line) > tail.MaxLineSize {
lines = util.PartitionString(line, tail.MaxLineSize)
}
for _, line := range lines {
tail.Lines <- &Line{line, now, nil}
}
if tail.Config.RateLimiter != nil {
ok := tail.Config.RateLimiter.Pour(uint16(len(lines)))
if !ok {
tail.Logger.Printf("Leaky bucket full (%v); entering 1s cooloff period.\n",
tail.Filename)
return false
}
}
return true
}
// Cleanup removes inotify watches added by the tail package. This function is
// meant to be invoked from a process's exit handler. Linux kernel may not
// automatically remove inotify watches after the process exits.
func (tail *Tail) Cleanup() {
watch.Cleanup(tail.Filename)
}

View File

@@ -1,11 +0,0 @@
// +build linux darwin freebsd netbsd openbsd
package tail
import (
"os"
)
func OpenFile(name string) (file *os.File, err error) {
return os.Open(name)
}

View File

@@ -1,521 +0,0 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
// TODO:
// * repeat all the tests with Poll:true
package tail
import (
_ "fmt"
"io/ioutil"
"os"
"strings"
"testing"
"time"
"github.com/hpcloud/tail/ratelimiter"
"github.com/hpcloud/tail/watch"
)
func init() {
// Clear the temporary test directory
err := os.RemoveAll(".test")
if err != nil {
panic(err)
}
}
func TestMain(m *testing.M) {
// Use a smaller poll duration for faster test runs. Keep it below
// 100ms (which value is used as common delays for tests)
watch.POLL_DURATION = 5 * time.Millisecond
os.Exit(m.Run())
}
func TestMustExist(t *testing.T) {
tail, err := TailFile("/no/such/file", Config{Follow: true, MustExist: true})
if err == nil {
t.Error("MustExist:true is violated")
tail.Stop()
}
tail, err = TailFile("/no/such/file", Config{Follow: true, MustExist: false})
if err != nil {
t.Error("MustExist:false is violated")
}
tail.Stop()
_, err = TailFile("README.md", Config{Follow: true, MustExist: true})
if err != nil {
t.Error("MustExist:true on an existing file is violated")
}
tail.Cleanup()
}
func TestWaitsForFileToExist(t *testing.T) {
tailTest := NewTailTest("waits-for-file-to-exist", t)
tail := tailTest.StartTail("test.txt", Config{})
go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false)
<-time.After(100 * time.Millisecond)
tailTest.CreateFile("test.txt", "hello\nworld\n")
tailTest.Cleanup(tail, true)
}
func TestWaitsForFileToExistRelativePath(t *testing.T) {
tailTest := NewTailTest("waits-for-file-to-exist-relative", t)
oldWD, err := os.Getwd()
if err != nil {
tailTest.Fatal(err)
}
os.Chdir(tailTest.path)
defer os.Chdir(oldWD)
tail, err := TailFile("test.txt", Config{})
if err != nil {
tailTest.Fatal(err)
}
go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false)
<-time.After(100 * time.Millisecond)
if err := ioutil.WriteFile("test.txt", []byte("hello\nworld\n"), 0600); err != nil {
tailTest.Fatal(err)
}
tailTest.Cleanup(tail, true)
}
func TestStop(t *testing.T) {
tail, err := TailFile("_no_such_file", Config{Follow: true, MustExist: false})
if err != nil {
t.Error("MustExist:false is violated")
}
if tail.Stop() != nil {
t.Error("Should be stoped successfully")
}
tail.Cleanup()
}
func TestStopAtEOF(t *testing.T) {
tailTest := NewTailTest("maxlinesize", t)
tailTest.CreateFile("test.txt", "hello\nthere\nworld\n")
tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil})
// read "hello"
line := <-tail.Lines
if line.Text != "hello" {
t.Errorf("Expected to get 'hello', got '%s' instead", line.Text)
}
tailTest.VerifyTailOutput(tail, []string{"there", "world"}, false)
tail.StopAtEOF()
tailTest.Cleanup(tail, true)
}
func TestMaxLineSizeFollow(t *testing.T) {
// As last file line does not end with newline, it will not be present in tail's output
maxLineSize(t, true, "hello\nworld\nfin\nhe", []string{"hel", "lo", "wor", "ld", "fin"})
}
func TestMaxLineSizeNoFollow(t *testing.T) {
maxLineSize(t, false, "hello\nworld\nfin\nhe", []string{"hel", "lo", "wor", "ld", "fin", "he"})
}
func TestOver4096ByteLine(t *testing.T) {
tailTest := NewTailTest("Over4096ByteLine", t)
testString := strings.Repeat("a", 4097)
tailTest.CreateFile("test.txt", "test\n"+testString+"\nhello\nworld\n")
tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil})
go tailTest.VerifyTailOutput(tail, []string{"test", testString, "hello", "world"}, false)
// Delete after a reasonable delay, to give tail sufficient time
// to read all lines.
<-time.After(100 * time.Millisecond)
tailTest.RemoveFile("test.txt")
tailTest.Cleanup(tail, true)
}
func TestOver4096ByteLineWithSetMaxLineSize(t *testing.T) {
tailTest := NewTailTest("Over4096ByteLineMaxLineSize", t)
testString := strings.Repeat("a", 4097)
tailTest.CreateFile("test.txt", "test\n"+testString+"\nhello\nworld\n")
tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil, MaxLineSize: 4097})
go tailTest.VerifyTailOutput(tail, []string{"test", testString, "hello", "world"}, false)
// Delete after a reasonable delay, to give tail sufficient time
// to read all lines.
<-time.After(100 * time.Millisecond)
tailTest.RemoveFile("test.txt")
tailTest.Cleanup(tail, true)
}
func TestLocationFull(t *testing.T) {
tailTest := NewTailTest("location-full", t)
tailTest.CreateFile("test.txt", "hello\nworld\n")
tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil})
go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false)
// Delete after a reasonable delay, to give tail sufficient time
// to read all lines.
<-time.After(100 * time.Millisecond)
tailTest.RemoveFile("test.txt")
tailTest.Cleanup(tail, true)
}
func TestLocationFullDontFollow(t *testing.T) {
tailTest := NewTailTest("location-full-dontfollow", t)
tailTest.CreateFile("test.txt", "hello\nworld\n")
tail := tailTest.StartTail("test.txt", Config{Follow: false, Location: nil})
go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false)
// Add more data only after reasonable delay.
<-time.After(100 * time.Millisecond)
tailTest.AppendFile("test.txt", "more\ndata\n")
<-time.After(100 * time.Millisecond)
tailTest.Cleanup(tail, true)
}
func TestLocationEnd(t *testing.T) {
tailTest := NewTailTest("location-end", t)
tailTest.CreateFile("test.txt", "hello\nworld\n")
tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: &SeekInfo{0, os.SEEK_END}})
go tailTest.VerifyTailOutput(tail, []string{"more", "data"}, false)
<-time.After(100 * time.Millisecond)
tailTest.AppendFile("test.txt", "more\ndata\n")
// Delete after a reasonable delay, to give tail sufficient time
// to read all lines.
<-time.After(100 * time.Millisecond)
tailTest.RemoveFile("test.txt")
tailTest.Cleanup(tail, true)
}
func TestLocationMiddle(t *testing.T) {
// Test reading from middle.
tailTest := NewTailTest("location-middle", t)
tailTest.CreateFile("test.txt", "hello\nworld\n")
tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: &SeekInfo{-6, os.SEEK_END}})
go tailTest.VerifyTailOutput(tail, []string{"world", "more", "data"}, false)
<-time.After(100 * time.Millisecond)
tailTest.AppendFile("test.txt", "more\ndata\n")
// Delete after a reasonable delay, to give tail sufficient time
// to read all lines.
<-time.After(100 * time.Millisecond)
tailTest.RemoveFile("test.txt")
tailTest.Cleanup(tail, true)
}
// The use of polling file watcher could affect file rotation
// (detected via renames), so test these explicitly.
func TestReOpenInotify(t *testing.T) {
reOpen(t, false)
}
func TestReOpenPolling(t *testing.T) {
reOpen(t, true)
}
// The use of polling file watcher could affect file rotation
// (detected via renames), so test these explicitly.
func TestReSeekInotify(t *testing.T) {
reSeek(t, false)
}
func TestReSeekPolling(t *testing.T) {
reSeek(t, true)
}
func TestRateLimiting(t *testing.T) {
tailTest := NewTailTest("rate-limiting", t)
tailTest.CreateFile("test.txt", "hello\nworld\nagain\nextra\n")
config := Config{
Follow: true,
RateLimiter: ratelimiter.NewLeakyBucket(2, time.Second)}
leakybucketFull := "Too much log activity; waiting a second before resuming tailing"
tail := tailTest.StartTail("test.txt", config)
// TODO: also verify that tail resumes after the cooloff period.
go tailTest.VerifyTailOutput(tail, []string{
"hello", "world", "again",
leakybucketFull,
"more", "data",
leakybucketFull}, false)
// Add more data only after reasonable delay.
<-time.After(1200 * time.Millisecond)
tailTest.AppendFile("test.txt", "more\ndata\n")
// Delete after a reasonable delay, to give tail sufficient time
// to read all lines.
<-time.After(100 * time.Millisecond)
tailTest.RemoveFile("test.txt")
tailTest.Cleanup(tail, true)
}
func TestTell(t *testing.T) {
tailTest := NewTailTest("tell-position", t)
tailTest.CreateFile("test.txt", "hello\nworld\nagain\nmore\n")
config := Config{
Follow: false,
Location: &SeekInfo{0, os.SEEK_SET}}
tail := tailTest.StartTail("test.txt", config)
// read noe line
<-tail.Lines
offset, err := tail.Tell()
if err != nil {
tailTest.Errorf("Tell return error: %s", err.Error())
}
tail.Done()
// tail.close()
config = Config{
Follow: false,
Location: &SeekInfo{offset, os.SEEK_SET}}
tail = tailTest.StartTail("test.txt", config)
for l := range tail.Lines {
// it may readed one line in the chan(tail.Lines),
// so it may lost one line.
if l.Text != "world" && l.Text != "again" {
tailTest.Fatalf("mismatch; expected world or again, but got %s",
l.Text)
}
break
}
tailTest.RemoveFile("test.txt")
tail.Done()
tail.Cleanup()
}
func TestBlockUntilExists(t *testing.T) {
tailTest := NewTailTest("block-until-file-exists", t)
config := Config{
Follow: true,
}
tail := tailTest.StartTail("test.txt", config)
go func() {
time.Sleep(100 * time.Millisecond)
tailTest.CreateFile("test.txt", "hello world\n")
}()
for l := range tail.Lines {
if l.Text != "hello world" {
tailTest.Fatalf("mismatch; expected hello world, but got %s",
l.Text)
}
break
}
tailTest.RemoveFile("test.txt")
tail.Stop()
tail.Cleanup()
}
func maxLineSize(t *testing.T, follow bool, fileContent string, expected []string) {
tailTest := NewTailTest("maxlinesize", t)
tailTest.CreateFile("test.txt", fileContent)
tail := tailTest.StartTail("test.txt", Config{Follow: follow, Location: nil, MaxLineSize: 3})
go tailTest.VerifyTailOutput(tail, expected, false)
// Delete after a reasonable delay, to give tail sufficient time
// to read all lines.
<-time.After(100 * time.Millisecond)
tailTest.RemoveFile("test.txt")
tailTest.Cleanup(tail, true)
}
func reOpen(t *testing.T, poll bool) {
var name string
var delay time.Duration
if poll {
name = "reopen-polling"
delay = 300 * time.Millisecond // account for POLL_DURATION
} else {
name = "reopen-inotify"
delay = 100 * time.Millisecond
}
tailTest := NewTailTest(name, t)
tailTest.CreateFile("test.txt", "hello\nworld\n")
tail := tailTest.StartTail(
"test.txt",
Config{Follow: true, ReOpen: true, Poll: poll})
content := []string{"hello", "world", "more", "data", "endofworld"}
go tailTest.ReadLines(tail, content)
// deletion must trigger reopen
<-time.After(delay)
tailTest.RemoveFile("test.txt")
<-time.After(delay)
tailTest.CreateFile("test.txt", "more\ndata\n")
// rename must trigger reopen
<-time.After(delay)
tailTest.RenameFile("test.txt", "test.txt.rotated")
<-time.After(delay)
tailTest.CreateFile("test.txt", "endofworld\n")
// Delete after a reasonable delay, to give tail sufficient time
// to read all lines.
<-time.After(delay)
tailTest.RemoveFile("test.txt")
<-time.After(delay)
// Do not bother with stopping as it could kill the tomb during
// the reading of data written above. Timings can vary based on
// test environment.
tail.Cleanup()
}
func reSeek(t *testing.T, poll bool) {
var name string
if poll {
name = "reseek-polling"
} else {
name = "reseek-inotify"
}
tailTest := NewTailTest(name, t)
tailTest.CreateFile("test.txt", "a really long string goes here\nhello\nworld\n")
tail := tailTest.StartTail(
"test.txt",
Config{Follow: true, ReOpen: false, Poll: poll})
go tailTest.VerifyTailOutput(tail, []string{
"a really long string goes here", "hello", "world", "h311o", "w0r1d", "endofworld"}, false)
// truncate now
<-time.After(100 * time.Millisecond)
tailTest.TruncateFile("test.txt", "h311o\nw0r1d\nendofworld\n")
// Delete after a reasonable delay, to give tail sufficient time
// to read all lines.
<-time.After(100 * time.Millisecond)
tailTest.RemoveFile("test.txt")
// Do not bother with stopping as it could kill the tomb during
// the reading of data written above. Timings can vary based on
// test environment.
tailTest.Cleanup(tail, false)
}
// Test library
type TailTest struct {
Name string
path string
done chan struct{}
*testing.T
}
func NewTailTest(name string, t *testing.T) TailTest {
tt := TailTest{name, ".test/" + name, make(chan struct{}), t}
err := os.MkdirAll(tt.path, os.ModeTemporary|0700)
if err != nil {
tt.Fatal(err)
}
return tt
}
func (t TailTest) CreateFile(name string, contents string) {
err := ioutil.WriteFile(t.path+"/"+name, []byte(contents), 0600)
if err != nil {
t.Fatal(err)
}
}
func (t TailTest) RemoveFile(name string) {
err := os.Remove(t.path + "/" + name)
if err != nil {
t.Fatal(err)
}
}
func (t TailTest) RenameFile(oldname string, newname string) {
oldname = t.path + "/" + oldname
newname = t.path + "/" + newname
err := os.Rename(oldname, newname)
if err != nil {
t.Fatal(err)
}
}
func (t TailTest) AppendFile(name string, contents string) {
f, err := os.OpenFile(t.path+"/"+name, os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
t.Fatal(err)
}
defer f.Close()
_, err = f.WriteString(contents)
if err != nil {
t.Fatal(err)
}
}
func (t TailTest) TruncateFile(name string, contents string) {
f, err := os.OpenFile(t.path+"/"+name, os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
t.Fatal(err)
}
defer f.Close()
_, err = f.WriteString(contents)
if err != nil {
t.Fatal(err)
}
}
func (t TailTest) StartTail(name string, config Config) *Tail {
tail, err := TailFile(t.path+"/"+name, config)
if err != nil {
t.Fatal(err)
}
return tail
}
func (t TailTest) VerifyTailOutput(tail *Tail, lines []string, expectEOF bool) {
defer close(t.done)
t.ReadLines(tail, lines)
// It is important to do this if only EOF is expected
// otherwise we could block on <-tail.Lines
if expectEOF {
line, ok := <-tail.Lines
if ok {
t.Fatalf("more content from tail: %+v", line)
}
}
}
func (t TailTest) ReadLines(tail *Tail, lines []string) {
for idx, line := range lines {
tailedLine, ok := <-tail.Lines
if !ok {
// tail.Lines is closed and empty.
err := tail.Err()
if err != nil {
t.Fatalf("tail ended with error: %v", err)
}
t.Fatalf("tail ended early; expecting more: %v", lines[idx:])
}
if tailedLine == nil {
t.Fatalf("tail.Lines returned nil; not possible")
}
// Note: not checking .Err as the `lines` argument is designed
// to match error strings as well.
if tailedLine.Text != line {
t.Fatalf(
"unexpected line/err from tail: "+
"expecting <<%s>>>, but got <<<%s>>>",
line, tailedLine.Text)
}
}
}
func (t TailTest) Cleanup(tail *Tail, stop bool) {
<-t.done
if stop {
tail.Stop()
}
tail.Cleanup()
}

View File

@@ -1,12 +0,0 @@
// +build windows
package tail
import (
"github.com/hpcloud/tail/winfile"
"os"
)
func OpenFile(name string) (file *os.File, err error) {
return winfile.OpenFile(name, os.O_RDONLY, 0)
}

View File

@@ -1,48 +0,0 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
package util
import (
"fmt"
"log"
"os"
"runtime/debug"
)
type Logger struct {
*log.Logger
}
var LOGGER = &Logger{log.New(os.Stderr, "", log.LstdFlags)}
// fatal is like panic except it displays only the current goroutine's stack.
func Fatal(format string, v ...interface{}) {
// https://github.com/hpcloud/log/blob/master/log.go#L45
LOGGER.Output(2, fmt.Sprintf("FATAL -- "+format, v...)+"\n"+string(debug.Stack()))
os.Exit(1)
}
// partitionString partitions the string into chunks of given size,
// with the last chunk of variable size.
func PartitionString(s string, chunkSize int) []string {
if chunkSize <= 0 {
panic("invalid chunkSize")
}
length := len(s)
chunks := 1 + length/chunkSize
start := 0
end := chunkSize
parts := make([]string, 0, chunks)
for {
if end > length {
end = length
}
parts = append(parts, s[start:end])
if end == length {
break
}
start, end = end, end+chunkSize
}
return parts
}

View File

@@ -1,36 +0,0 @@
package watch
type FileChanges struct {
Modified chan bool // Channel to get notified of modifications
Truncated chan bool // Channel to get notified of truncations
Deleted chan bool // Channel to get notified of deletions/renames
}
func NewFileChanges() *FileChanges {
return &FileChanges{
make(chan bool), make(chan bool), make(chan bool)}
}
func (fc *FileChanges) NotifyModified() {
sendOnlyIfEmpty(fc.Modified)
}
func (fc *FileChanges) NotifyTruncated() {
sendOnlyIfEmpty(fc.Truncated)
}
func (fc *FileChanges) NotifyDeleted() {
sendOnlyIfEmpty(fc.Deleted)
}
// sendOnlyIfEmpty sends on a bool channel only if the channel has no
// backlog to be read by other goroutines. This concurrency pattern
// can be used to notify other goroutines if and only if they are
// looking for it (i.e., subsequent notifications can be compressed
// into one).
func sendOnlyIfEmpty(ch chan bool) {
select {
case ch <- true:
default:
}
}

Some files were not shown because too many files have changed in this diff Show More