From 2b7e62786a0771de3a82d6d477ff33f10b5b8816 Mon Sep 17 00:00:00 2001
From: Rich Megginson <rmeggins@redhat.com>
Date: Thu, 5 Apr 2018 21:57:17 -0600
Subject: [PATCH] mmkubernetes: fix lnrules, add defaults, add test

Fix lnrules for CONTAINER_NAME

Add pkg check for lognorm >= 2.0.3 so we can set the macro
to enable ln_loadSamplesFromString

Add some reasonable default values for parameters, such as
kubernetesurl https://kubernetes.default.svc.cluster.local:443

Clean up sample.conf configuration file

Add test for mmkubernetes, including mock kubernetes service

(cherry picked from commit 1d49aac5cb101704486bfb065fac362ca69f06bc)
---
 configure.ac                        |  14 +++++
 contrib/mmkubernetes/mmkubernetes.c |  69 ++++++++++++++++----
 contrib/mmkubernetes/sample.conf    |  22 ++-----
 tests/Makefile.am                   |  13 +++-
 tests/mmkubernetes-basic-vg.sh      |  89 ++++++++++++++++++++++++++
 tests/mmkubernetes-basic.out.json   | 110 ++++++++++++++++++++++++++++++++
 tests/mmkubernetes-basic.sh         |  88 ++++++++++++++++++++++++++
 tests/mmkubernetes_test_server.py   | 121 ++++++++++++++++++++++++++++++++++++
 8 files changed, 495 insertions(+), 31 deletions(-)
 create mode 100755 tests/mmkubernetes-basic-vg.sh
 create mode 100644 tests/mmkubernetes-basic.out.json
 create mode 100755 tests/mmkubernetes-basic.sh
 create mode 100644 tests/mmkubernetes_test_server.py

diff --git a/configure.ac b/configure.ac
index de679b3ef..17548b9fe 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2099,6 +2099,20 @@ AC_ARG_ENABLE(mmkubernetes,
 )
 if test "x$enable_mmkubernetes" = "xyes"; then
         PKG_CHECK_MODULES([CURL], [libcurl])
+        PKG_CHECK_MODULES(LIBLOGNORM, lognorm >= 2.0.3)
+
+        save_CFLAGS="$CFLAGS"
+        save_LIBS="$LIBS"
+
+        CFLAGS="$CFLAGS $LIBLOGNORM_CFLAGS"
+        LIBS="$LIBS $LIBLOGNORM_LIBS"
+
+        AC_CHECK_FUNC([ln_loadSamplesFromString],
+                      [AC_DEFINE([HAVE_LOADSAMPLESFROMSTRING], [1], [Define if ln_loadSamplesFromString exists.])],
+                      [AC_DEFINE([NO_LOADSAMPLESFROMSTRING], [1], [Define if ln_loadSamplesFromString does not exist.])])
+
+        CFLAGS="$save_CFLAGS"
+        LIBS="$save_LIBS"
 fi
 AM_CONDITIONAL(ENABLE_MMKUBERNETES, test x$enable_mmkubernetes = xyes)
 
diff --git a/contrib/mmkubernetes/mmkubernetes.c b/contrib/mmkubernetes/mmkubernetes.c
index 5012c54f6..6aba930e3 100644
--- a/contrib/mmkubernetes/mmkubernetes.c
+++ b/contrib/mmkubernetes/mmkubernetes.c
@@ -75,10 +75,10 @@ DEFobjCurrIf(regexp)
  * this is for _tag_ match, not actual filename match - in_tail turns filename
  * into a fluentd tag
  */
-#define DFLT_FILENAME_LNRULES ":/var/log/containers/%pod_name:char-to:.%."\
+#define DFLT_FILENAME_LNRULES "rule=:/var/log/containers/%pod_name:char-to:.%."\
 	"%container_hash:char-to:_%_"\
 	"%namespace_name:char-to:_%_%container_name:char-to:-%-%container_id:char-to:.%.log\n"\
-	":/var/log/containers/%pod_name:char-to:_%_"\
+	"rule=:/var/log/containers/%pod_name:char-to:_%_"\
 	"%namespace_name:char-to:_%_%container_name:char-to:-%-%container_id:char-to:.%.log"
 #define DFLT_FILENAME_RULEBASE "/etc/rsyslog.d/k8s_filename.rulebase"
 /* original from fluentd plugin:
@@ -86,10 +86,10 @@ DEFobjCurrIf(regexp)
  *     (\.(?<container_hash>[^_]+))?_(?<pod_name>[^_]+)_\
  *     (?<namespace>[^_]+)_[^_]+_[^_]+$'
  */
-#define DFLT_CONTAINER_LNRULES ":%k8s_prefix:char-to:_%_%container_name:char-to:.%."\
-	"%container_hash:char-to:_%_%"\
+#define DFLT_CONTAINER_LNRULES "rule=:%k8s_prefix:char-to:_%_%container_name:char-to:.%."\
+	"%container_hash:char-to:_%_"\
 	"%pod_name:char-to:_%_%namespace_name:char-to:_%_%not_used_1:char-to:_%_%not_used_2:rest%\n"\
-	":%k8s_prefix:char-to:_%_%container_name:char-to:_%_"\
+	"rule=:%k8s_prefix:char-to:_%_%container_name:char-to:_%_"\
 	"%pod_name:char-to:_%_%namespace_name:char-to:_%_%not_used_1:char-to:_%_%not_used_2:rest%"
 #define DFLT_CONTAINER_RULEBASE "/etc/rsyslog.d/k8s_container_name.rulebase"
 #define DFLT_SRCMD_PATH "$!metadata!filename"
@@ -98,6 +98,7 @@ DEFobjCurrIf(regexp)
 #define DFLT_DE_DOT_SEPARATOR "_"
 #define DFLT_CONTAINER_NAME "$!CONTAINER_NAME" /* name of variable holding CONTAINER_NAME value */
 #define DFLT_CONTAINER_ID_FULL "$!CONTAINER_ID_FULL" /* name of variable holding CONTAINER_ID_FULL value */
+#define DFLT_KUBERNETES_URL "https://kubernetes.default.svc.cluster.local:443"
 
 static struct cache_s {
 	const uchar *kbUrl;
@@ -953,9 +954,11 @@ CODESTARTnewActInst
 			loadModConf->contRules, loadModConf->contRulebase));
 
 	if(pData->kubernetesUrl == NULL) {
-		if(loadModConf->kubernetesUrl == NULL)
-			ABORT_FINALIZE(RS_RET_CONFIG_ERROR);
-		pData->kubernetesUrl = (uchar *) strdup((char *) loadModConf->kubernetesUrl);
+		if(loadModConf->kubernetesUrl == NULL) {
+			CHKmalloc(pData->kubernetesUrl = (uchar *) strdup(DFLT_KUBERNETES_URL));
+		} else {
+			CHKmalloc(pData->kubernetesUrl = (uchar *) strdup((char *) loadModConf->kubernetesUrl));
+		}
 	}
 	if(pData->srcMetadataDescr == NULL) {
 		CHKmalloc(pData->srcMetadataDescr = MALLOC(sizeof(msgPropDescr_t)));
@@ -1125,10 +1128,10 @@ extractMsgMetadata(smsg_t *pMsg, instanceData *pData, struct json_object **json)
 
 	/* extract metadata from the file name */
 	filename = MsgGetProp(pMsg, NULL, pData->srcMetadataDescr, &fnLen, &freeFn, NULL);
-	dbgprintf("mmkubernetes: filename: '%s'.\n", filename);
-	if(filename == NULL)
+	if((filename == NULL) || (fnLen == 0))
 		ABORT_FINALIZE(RS_RET_NOT_FOUND);
 
+	dbgprintf("mmkubernetes: filename: '%s' len %d.\n", filename, fnLen);
 	if ((lnret = ln_normalize(pData->fnCtxln, (char*)filename, fnLen, json))) {
 		ABORT_FINALIZE(RS_RET_ERR);
 	}
@@ -1173,18 +1176,58 @@ queryKB(wrkrInstanceData_t *pWrkrData, char *url, struct json_object **rply)
 	CURLcode ccode;
 	struct json_tokener *jt = NULL;
 	struct json_object *jo;
+	long resp_code = 400;
 
 	/* query kubernetes for pod info */
 	ccode = curl_easy_setopt(pWrkrData->curlCtx, CURLOPT_URL, url);
 	if(ccode != CURLE_OK)
 		ABORT_FINALIZE(RS_RET_ERR);
-	if (CURLE_OK != (ccode = curl_easy_perform(pWrkrData->curlCtx))) {
+	if(CURLE_OK != (ccode = curl_easy_perform(pWrkrData->curlCtx))) {
 		errmsg.LogMsg(0, RS_RET_ERR, LOG_ERR,
 			      "mmkubernetes: failed to connect to [%s] - %d:%s\n",
 			      url, ccode, curl_easy_strerror(ccode));
 		ABORT_FINALIZE(RS_RET_ERR);
 	}
-
+	if(CURLE_OK != (ccode = curl_easy_getinfo(pWrkrData->curlCtx,
+					CURLINFO_RESPONSE_CODE, &resp_code))) {
+		errmsg.LogMsg(0, RS_RET_ERR, LOG_ERR,
+			      "mmkubernetes: could not get response code from query to [%s] - %d:%s\n",
+			      url, ccode, curl_easy_strerror(ccode));
+		ABORT_FINALIZE(RS_RET_ERR);
+	}
+	if(resp_code == 401) {
+		errmsg.LogMsg(0, RS_RET_ERR, LOG_ERR,
+			      "mmkubernetes: Unauthorized: not allowed to view url - "
+			      "check token/auth credentials [%s]\n",
+			      url);
+		ABORT_FINALIZE(RS_RET_ERR);
+	}
+	if(resp_code == 403) {
+		errmsg.LogMsg(0, RS_RET_ERR, LOG_ERR,
+			      "mmkubernetes: Forbidden: no access - "
+			      "check permissions to view url [%s]\n",
+			      url);
+		ABORT_FINALIZE(RS_RET_ERR);
+	}
+	if(resp_code == 404) {
+		errmsg.LogMsg(0, RS_RET_ERR, LOG_ERR,
+			      "mmkubernetes: Not Found: the resource does not exist at url [%s]\n",
+			      url);
+		ABORT_FINALIZE(RS_RET_ERR);
+	}
+	if(resp_code == 429) {
+		errmsg.LogMsg(0, RS_RET_ERR, LOG_ERR,
+			      "mmkubernetes: Too Many Requests: the server is too heavily loaded "
+			      "to provide the data for the requested url [%s]\n",
+			      url);
+		ABORT_FINALIZE(RS_RET_ERR);
+	}
+	if(resp_code != 200) {
+		errmsg.LogMsg(0, RS_RET_ERR, LOG_ERR,
+			      "mmkubernetes: server returned unexpected code [%ld] for url [%s]\n",
+			      resp_code, url);
+		ABORT_FINALIZE(RS_RET_ERR);
+	}
 	/* parse retrieved data */
 	jt = json_tokener_new();
 	json_tokener_reset(jt);
@@ -1384,7 +1427,7 @@ CODESTARTdoAction
 	msgAddJSON(pMsg, (uchar *) pWrkrData->pData->dstMetadataPath + 1, jMetadataCopy, 0, 0);
 
 finalize_it:
-    json_object_put(jMsgMeta);
+	json_object_put(jMsgMeta);
 	free(mdKey);
 ENDdoAction
 
diff --git a/contrib/mmkubernetes/sample.conf b/contrib/mmkubernetes/sample.conf
index 4c400ed51..55946003a 100644
--- a/contrib/mmkubernetes/sample.conf
+++ b/contrib/mmkubernetes/sample.conf
@@ -1,19 +1,7 @@
-module(load="imfile" mode="inotify")
-module(load="mmkubernetes" kubernetesurl="https://localhost:8443"
-       tls.cacert="/etc/rsyslog.d/mmk8s.ca.crt"
-       tokenfile="/etc/rsyslog.d/mmk8s.token" annotation_match=["."])
+module(load="mmkubernetes") # see docs for all module and action parameters
 
-template(name="tpl" type="list") {
-    property(name="jsonmesg")
-    constant(value="\n")
-}
+# $!metadata!filename added by imfile using addmetadata="on"
+# e.g. input(type="imfile" file="/var/log/containers/*.log" tag="kubernetes" addmetadata="on")
+# $!CONTAINER_NAME and $!CONTAINER_ID_FULL added by imjournal
 
-ruleset(name="k8s") {
-    action(type="mmkubernetes")
-    action(type="omfile" file="/var/log/k8s.log" template="tpl")
-}
-
-input(type="imfile" file="/var/log/containers/*.log" tag="kubernetes" addmetadata="on" ruleset="k8s")
-if ($!_SYSTEMD_UNIT == "docker.service") and (strlen($!CONTAINER_NAME) > 0) then {
-    call k8s
-}
+action(type="mmkubernetes")
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 4386c626d..bdee87134 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -810,6 +810,15 @@ TESTS += \
 	 omtcl.sh
 endif
 
+if ENABLE_MMKUBERNETES
+TESTS += \
+	mmkubernetes-basic.sh
+if HAVE_VALGRIND
+TESTS += \
+	mmkubernetes-basic-vg.sh
+endif
+endif
+
 endif # if ENABLE_TESTBENCH
 
 TESTS_ENVIRONMENT = RSYSLOG_MODDIR='$(abs_top_builddir)'/runtime/.libs/
@@ -1841,7 +1850,9 @@ EXTRA_DIST= \
 	pgsql-basic-cnf6-vg.sh \
 	pgsql-template-cnf6-vg.sh \
 	pgsql-actq-mt-withpause-vg.sh \
-	../devtools/prep-mysql-db.sh
+	../devtools/prep-mysql-db.sh \
+	mmkubernetes-basic.sh \
+	mmkubernetes-basic-vg.sh
 
 ourtail_SOURCES = ourtail.c
 msleep_SOURCES = msleep.c
diff --git a/tests/mmkubernetes-basic-vg.sh b/tests/mmkubernetes-basic-vg.sh
new file mode 100755
index 000000000..33f3d3588
--- /dev/null
+++ b/tests/mmkubernetes-basic-vg.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+# added 2018-04-06 by richm, released under ASL 2.0
+#export RSYSLOG_DEBUG="debug"
+. $srcdir/diag.sh init
+
+testsrv=mmk8s-test-server
+python ./mmkubernetes_test_server.py 18443 rsyslog${testsrv}.pid rsyslogd${testsrv}.started > mmk8s_srv.log 2>&1 &
+BGPROCESS=$!
+. $srcdir/diag.sh wait-startup $testsrv
+echo background mmkubernetes_test_server.py process id is $BGPROCESS
+
+pwd=$( pwd )
+. $srcdir/diag.sh generate-conf
+. $srcdir/diag.sh add-conf '
+module(load="../plugins/imfile/.libs/imfile")
+module(load="../plugins/mmjsonparse/.libs/mmjsonparse")
+module(load="../contrib/mmkubernetes/.libs/mmkubernetes" token="dummy" kubernetesurl="http://localhost:18443"
+       filenamerules=["rule=:'$pwd'/%pod_name:char-to:.%.%container_hash:char-to:_%_%namespace_name:char-to:_%_%container_name_and_id:char-to:.%.log",
+	                  "rule=:'$pwd'/%pod_name:char-to:_%_%namespace_name:char-to:_%_%container_name_and_id:char-to:.%.log"]
+)
+
+template(name="mmk8s_template" type="list") {
+    property(name="$!all-json-plain")
+    constant(value="\n")
+}
+
+input(type="imfile" file="'$pwd'/pod-*.log" tag="kubernetes" addmetadata="on")
+action(type="mmjsonparse" cookie="")
+action(type="mmkubernetes")
+action(type="omfile" file="rsyslog.out.log" template="mmk8s_template")
+'
+cat > pod-name1_namespace-name1_container-name1-id1.log <<EOF
+{"log":"{\"type\":\"response\",\"@timestamp\":\"2018-04-06T17:26:34Z\",\"tags\":[],\"pid\":75,\"method\":\"head\",\"statusCode\":200,\"req\":{\"url\":\"/\",\"method\":\"head\",\"headers\":{\"user-agent\":\"curl/7.29.0\",\"host\":\"localhost:5601\",\"accept\":\"*/*\"},\"remoteAddress\":\"127.0.0.1\",\"userAgent\":\"127.0.0.1\"},\"res\":{\"statusCode\":200,\"responseTime\":1,\"contentLength\":9},\"message\":\"HEAD1 / 200 1ms - 9.0B\"}\n","stream":"stdout","time":"2018-04-06T17:26:34.492083106Z"}
+EOF
+cat > pod-name2.container-hash2_namespace-name2_container-name2-id2.log <<EOF
+{"log":"{\"type\":\"response\",\"@timestamp\":\"2018-04-06T17:26:34Z\",\"tags\":[],\"pid\":75,\"method\":\"head\",\"statusCode\":200,\"req\":{\"url\":\"/\",\"method\":\"head\",\"headers\":{\"user-agent\":\"curl/7.29.0\",\"host\":\"localhost:5601\",\"accept\":\"*/*\"},\"remoteAddress\":\"127.0.0.1\",\"userAgent\":\"127.0.0.1\"},\"res\":{\"statusCode\":200,\"responseTime\":1,\"contentLength\":9},\"message\":\"HEAD2 / 200 1ms - 9.0B\"}\n","stream":"stdout","time":"2018-04-06T17:26:34.492083106Z"}
+EOF
+cat > pod-name3.log <<EOF
+{"message":"a message from container 3","CONTAINER_NAME":"some-prefix_container-name3.container-hash3_pod-name3_namespace-name3_unused3_unused33","CONTAINER_ID_FULL":"id3"}
+EOF
+cat > pod-name4.log <<EOF
+{"message":"a message from container 4","CONTAINER_NAME":"some-prefix_container-name4_pod-name4_namespace-name4_unused4_unused44","CONTAINER_ID_FULL":"id4"}
+EOF
+rm -f imfile-state\:*
+. $srcdir/diag.sh startup-vg-noleak
+sleep 10 || :
+. $srcdir/diag.sh shutdown-when-empty
+. $srcdir/diag.sh wait-shutdown-vg
+. $srcdir/diag.sh check-exit-vg
+
+kill $BGPROCESS
+. $srcdir/diag.sh wait-pid-termination rsyslog${testsrv}.pid
+cat mmk8s_srv.log
+
+# for each record in mmkubernetes-basic.out.json, see if the matching
+# record is found in rsyslog.out.log
+python -c 'import sys,json
+expected = {}
+for hsh in json.load(open(sys.argv[1])):
+	if "kubernetes" in hsh and "pod_name" in hsh["kubernetes"]:
+		expected[hsh["kubernetes"]["pod_name"]] = hsh
+rc = 0
+actual = {}
+for line in open(sys.argv[2]):
+	hsh = json.loads(line)
+	if "kubernetes" in hsh and "pod_name" in hsh["kubernetes"]:
+		actual[hsh["kubernetes"]["pod_name"]] = hsh
+for pod,hsh in expected.items():
+	if not pod in actual:
+		print("Error: record for pod {0} not found in output".format(pod))
+		rc = 1
+	else:
+		for kk,vv in hsh.items():
+			if not kk in actual[pod]:
+				print("Error: key {0} in record for pod {1} not found in output".format(kk, pod))
+				rc = 1
+			elif not vv == actual[pod][kk]:
+				print("Error: value {0} for key {1} in record for pod {2} does not match the expected value {3}".format(actual[pod][kk], kk, pod, vv))
+				rc = 1
+sys.exit(rc)
+' mmkubernetes-basic.out.json rsyslog.out.log
+if [ $? -ne 0 ]; then
+	echo
+	echo "FAIL: expected data not found. rsyslog.out.log is:"
+	cat rsyslog.out.log
+	. $srcdir/diag.sh error-exit 1
+fi
+
+. $srcdir/diag.sh exit
diff --git a/tests/mmkubernetes-basic.out.json b/tests/mmkubernetes-basic.out.json
new file mode 100644
index 000000000..e5876ef21
--- /dev/null
+++ b/tests/mmkubernetes-basic.out.json
@@ -0,0 +1,110 @@
+[{
+  "kubernetes": {
+    "namespace_id": "namespace-name2-id",
+    "namespace_labels": {
+      "label_1_key": "label 1 value",
+      "label_with_empty_value": "",
+      "label_2_key": "label 2 value"
+    },
+    "creation_timestamp": "2018-04-09T21:56:39Z",
+    "pod_id": "pod-name2-id",
+    "labels": {
+      "custom_label": "pod-name2-label-value",
+      "deploymentconfig": "pod-name2-dc",
+      "component": "pod-name2-component",
+      "label_with_empty_value": "",
+      "deployment": "pod-name2-deployment"
+    },
+    "pod_name": "pod-name2",
+    "namespace_name": "namespace-name2",
+    "container_name": "container-name2",
+    "master_url": "http://localhost:18443"
+  },
+  "docker": {
+    "container_id": "id2"
+  }
+},
+{
+  "message": "a message from container 4",
+  "CONTAINER_NAME": "some-prefix_container-name4_pod-name4_namespace-name4_unused4_unused44",
+  "CONTAINER_ID_FULL": "id4",
+  "kubernetes": {
+    "namespace_id": "namespace-name4-id",
+    "namespace_labels": {
+      "label_1_key": "label 1 value",
+      "label_with_empty_value": "",
+      "label_2_key": "label 2 value"
+    },
+    "creation_timestamp": "2018-04-09T21:56:39Z",
+    "pod_id": "pod-name4-id",
+    "labels": {
+      "custom_label": "pod-name4-label-value",
+      "deploymentconfig": "pod-name4-dc",
+      "component": "pod-name4-component",
+      "label_with_empty_value": "",
+      "deployment": "pod-name4-deployment"
+    },
+    "pod_name": "pod-name4",
+    "namespace_name": "namespace-name4",
+    "container_name": "container-name4",
+    "master_url": "http://localhost:18443"
+  },
+  "docker": {
+    "container_id": "id4"
+  }
+},
+{
+  "kubernetes": {
+    "namespace_id": "namespace-name1-id",
+    "namespace_labels": {
+      "label_1_key": "label 1 value",
+      "label_with_empty_value": "",
+      "label_2_key": "label 2 value"
+    },
+    "creation_timestamp": "2018-04-09T21:56:39Z",
+    "pod_id": "pod-name1-id",
+    "labels": {
+      "custom_label": "pod-name1-label-value",
+      "deploymentconfig": "pod-name1-dc",
+      "component": "pod-name1-component",
+      "label_with_empty_value": "",
+      "deployment": "pod-name1-deployment"
+    },
+    "pod_name": "pod-name1",
+    "namespace_name": "namespace-name1",
+    "container_name": "container-name1",
+    "master_url": "http://localhost:18443"
+  },
+  "docker": {
+    "container_id": "id1"
+  }
+},
+{
+  "message": "a message from container 3",
+  "CONTAINER_NAME": "some-prefix_container-name3.container-hash3_pod-name3_namespace-name3_unused3_unused33",
+  "CONTAINER_ID_FULL": "id3",
+  "kubernetes": {
+    "namespace_id": "namespace-name3-id",
+    "namespace_labels": {
+      "label_1_key": "label 1 value",
+      "label_with_empty_value": "",
+      "label_2_key": "label 2 value"
+    },
+    "creation_timestamp": "2018-04-09T21:56:39Z",
+    "pod_id": "pod-name3-id",
+    "labels": {
+      "custom_label": "pod-name3-label-value",
+      "deploymentconfig": "pod-name3-dc",
+      "component": "pod-name3-component",
+      "label_with_empty_value": "",
+      "deployment": "pod-name3-deployment"
+    },
+    "pod_name": "pod-name3",
+    "namespace_name": "namespace-name3",
+    "container_name": "container-name3",
+    "master_url": "http://localhost:18443"
+  },
+  "docker": {
+    "container_id": "id3"
+  }
+}]
diff --git a/tests/mmkubernetes-basic.sh b/tests/mmkubernetes-basic.sh
new file mode 100755
index 000000000..0bbfd08ca
--- /dev/null
+++ b/tests/mmkubernetes-basic.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+# added 2018-04-06 by richm, released under ASL 2.0
+#export RSYSLOG_DEBUG="debug"
+. $srcdir/diag.sh init
+
+testsrv=mmk8s-test-server
+python ./mmkubernetes_test_server.py 18443 rsyslog${testsrv}.pid rsyslogd${testsrv}.started > mmk8s_srv.log 2>&1 &
+BGPROCESS=$!
+. $srcdir/diag.sh wait-startup $testsrv
+echo background mmkubernetes_test_server.py process id is $BGPROCESS
+
+pwd=$( pwd )
+. $srcdir/diag.sh generate-conf
+. $srcdir/diag.sh add-conf '
+module(load="../plugins/imfile/.libs/imfile")
+module(load="../plugins/mmjsonparse/.libs/mmjsonparse")
+module(load="../contrib/mmkubernetes/.libs/mmkubernetes" token="dummy" kubernetesurl="http://localhost:18443"
+       filenamerules=["rule=:'$pwd'/%pod_name:char-to:.%.%container_hash:char-to:_%_%namespace_name:char-to:_%_%container_name_and_id:char-to:.%.log",
+	                  "rule=:'$pwd'/%pod_name:char-to:_%_%namespace_name:char-to:_%_%container_name_and_id:char-to:.%.log"]
+)
+
+template(name="mmk8s_template" type="list") {
+    property(name="$!all-json-plain")
+    constant(value="\n")
+}
+
+input(type="imfile" file="'$pwd'/pod-*.log" tag="kubernetes" addmetadata="on")
+action(type="mmjsonparse" cookie="")
+action(type="mmkubernetes")
+action(type="omfile" file="rsyslog.out.log" template="mmk8s_template")
+'
+cat > pod-name1_namespace-name1_container-name1-id1.log <<EOF
+{"log":"{\"type\":\"response\",\"@timestamp\":\"2018-04-06T17:26:34Z\",\"tags\":[],\"pid\":75,\"method\":\"head\",\"statusCode\":200,\"req\":{\"url\":\"/\",\"method\":\"head\",\"headers\":{\"user-agent\":\"curl/7.29.0\",\"host\":\"localhost:5601\",\"accept\":\"*/*\"},\"remoteAddress\":\"127.0.0.1\",\"userAgent\":\"127.0.0.1\"},\"res\":{\"statusCode\":200,\"responseTime\":1,\"contentLength\":9},\"message\":\"HEAD1 / 200 1ms - 9.0B\"}\n","stream":"stdout","time":"2018-04-06T17:26:34.492083106Z"}
+EOF
+cat > pod-name2.container-hash2_namespace-name2_container-name2-id2.log <<EOF
+{"log":"{\"type\":\"response\",\"@timestamp\":\"2018-04-06T17:26:34Z\",\"tags\":[],\"pid\":75,\"method\":\"head\",\"statusCode\":200,\"req\":{\"url\":\"/\",\"method\":\"head\",\"headers\":{\"user-agent\":\"curl/7.29.0\",\"host\":\"localhost:5601\",\"accept\":\"*/*\"},\"remoteAddress\":\"127.0.0.1\",\"userAgent\":\"127.0.0.1\"},\"res\":{\"statusCode\":200,\"responseTime\":1,\"contentLength\":9},\"message\":\"HEAD2 / 200 1ms - 9.0B\"}\n","stream":"stdout","time":"2018-04-06T17:26:34.492083106Z"}
+EOF
+cat > pod-name3.log <<EOF
+{"message":"a message from container 3","CONTAINER_NAME":"some-prefix_container-name3.container-hash3_pod-name3_namespace-name3_unused3_unused33","CONTAINER_ID_FULL":"id3"}
+EOF
+cat > pod-name4.log <<EOF
+{"message":"a message from container 4","CONTAINER_NAME":"some-prefix_container-name4_pod-name4_namespace-name4_unused4_unused44","CONTAINER_ID_FULL":"id4"}
+EOF
+rm -f imfile-state\:*
+. $srcdir/diag.sh startup
+sleep 10 || :
+. $srcdir/diag.sh shutdown-when-empty
+. $srcdir/diag.sh wait-shutdown
+
+kill $BGPROCESS
+. $srcdir/diag.sh wait-pid-termination rsyslog${testsrv}.pid
+cat mmk8s_srv.log
+
+# for each record in mmkubernetes-basic.out.json, see if the matching
+# record is found in rsyslog.out.log
+python -c 'import sys,json
+expected = {}
+for hsh in json.load(open(sys.argv[1])):
+	if "kubernetes" in hsh and "pod_name" in hsh["kubernetes"]:
+		expected[hsh["kubernetes"]["pod_name"]] = hsh
+rc = 0
+actual = {}
+for line in open(sys.argv[2]):
+	hsh = json.loads(line)
+	if "kubernetes" in hsh and "pod_name" in hsh["kubernetes"]:
+		actual[hsh["kubernetes"]["pod_name"]] = hsh
+for pod,hsh in expected.items():
+	if not pod in actual:
+		print("Error: record for pod {0} not found in output".format(pod))
+		rc = 1
+	else:
+		for kk,vv in hsh.items():
+			if not kk in actual[pod]:
+				print("Error: key {0} in record for pod {1} not found in output".format(kk, pod))
+				rc = 1
+			elif not vv == actual[pod][kk]:
+				print("Error: value {0} for key {1} in record for pod {2} does not match the expected value {3}".format(actual[pod][kk], kk, pod, vv))
+				rc = 1
+sys.exit(rc)
+' mmkubernetes-basic.out.json rsyslog.out.log
+if [ $? -ne 0 ]; then
+	echo
+	echo "FAIL: expected data not found. rsyslog.out.log is:"
+	cat rsyslog.out.log
+	. $srcdir/diag.sh error-exit 1
+fi
+
+. $srcdir/diag.sh exit
diff --git a/tests/mmkubernetes_test_server.py b/tests/mmkubernetes_test_server.py
new file mode 100644
index 000000000..0de215603
--- /dev/null
+++ b/tests/mmkubernetes_test_server.py
@@ -0,0 +1,121 @@
+# Used by the mmkubernetes tests
+# This is a simple http server which responds to kubernetes api requests
+# and responds with kubernetes api server responses
+# added 2018-04-06 by richm, released under ASL 2.0
+import os
+import json
+import sys
+
+try:
+    from http.server import HTTPServer, BaseHTTPRequestHandler
+except ImportError:
+    from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+
+ns_template = '''{{
+  "kind": "Namespace",
+  "apiVersion": "v1",
+  "metadata": {{
+    "name": "{namespace_name}",
+    "selfLink": "/api/v1/namespaces/{namespace_name}",
+    "uid": "{namespace_name}-id",
+    "resourceVersion": "2988",
+    "creationTimestamp": "2018-04-09T21:56:39Z",
+    "labels": {{
+      "label.1.key":"label 1 value",
+      "label.2.key":"label 2 value",
+      "label.with.empty.value":""
+    }},
+    "annotations": {{
+      "k8s.io/description": "",
+      "k8s.io/display-name": "",
+      "k8s.io/node-selector": "",
+      "k8s.io/sa.scc.mcs": "s0:c9,c4",
+      "k8s.io/sa.scc.supplemental-groups": "1000080000/10000",
+      "k8s.io/sa.scc.uid-range": "1000080000/10000",
+      "quota.k8s.io/cluster-resource-override-enabled": "false"
+    }}
+  }},
+  "spec": {{
+    "finalizers": [
+      "openshift.io/origin",
+      "kubernetes"
+    ]
+  }},
+  "status": {{
+    "phase": "Active"
+  }}
+}}'''
+
+pod_template = '''{{
+  "kind": "Pod",
+  "apiVersion": "v1",
+  "metadata": {{
+    "name": "{pod_name}",
+    "generateName": "{pod_name}-prefix",
+    "namespace": "{namespace_name}",
+    "selfLink": "/api/v1/namespaces/{namespace_name}/pods/{pod_name}",
+    "uid": "{pod_name}-id",
+    "resourceVersion": "3486",
+    "creationTimestamp": "2018-04-09T21:56:39Z",
+    "labels": {{
+      "component": "{pod_name}-component",
+      "deployment": "{pod_name}-deployment",
+      "deploymentconfig": "{pod_name}-dc",
+      "custom.label": "{pod_name}-label-value",
+      "label.with.empty.value":""
+    }},
+    "annotations": {{
+      "k8s.io/deployment-config.latest-version": "1",
+      "k8s.io/deployment-config.name": "{pod_name}-dc",
+      "k8s.io/deployment.name": "{pod_name}-deployment",
+      "k8s.io/custom.name": "custom value",
+      "annotation.with.empty.value":""
+    }}
+  }},
+  "status": {{
+    "phase": "Running",
+    "hostIP": "172.18.4.32",
+    "podIP": "10.128.0.14",
+    "startTime": "2018-04-09T21:57:39Z"
+  }}
+}}'''
+
+class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
+
+    def do_GET(self):
+        # "http://localhost:18443/api/v1/namespaces/namespace-name2"
+        # parse url - either /api/v1/namespaces/$ns_name
+        # or
+        # /api/v1/namespaces/$ns_name/pods/$pod_name
+        comps = self.path.split('/')
+        status = 400
+        if len(comps) >= 5 and comps[1] == 'api' and comps[2] == 'v1':
+            if len(comps) == 5 and comps[3] == 'namespaces': # namespace
+                resp = ns_template.format(namespace_name=comps[4])
+                status = 200
+            elif len(comps) == 7 and comps[3] == 'namespaces' and comps[5] == 'pods':
+                resp = pod_template.format(namespace_name=comps[4], pod_name=comps[6])
+                status = 200
+            else:
+                resp = '{{"error":"do not recognize {0}"}}'.format(self.path)
+        else:
+            resp = '{{"error":"do not recognize {0}"}}'.format(self.path)
+        if not status == 200:
+            self.log_error(resp)
+        self.send_response(status)
+        self.end_headers()
+        self.wfile.write(json.dumps(json.loads(resp), separators=(',',':')))
+
+port = int(sys.argv[1])
+
+httpd = HTTPServer(('localhost', port), SimpleHTTPRequestHandler)
+
+# write "started" to file named in argv[3]
+with open(sys.argv[3], "w") as ff:
+    ff.write("started\n")
+
+# write pid to file named in argv[2]
+with open(sys.argv[2], "w") as ff:
+    ff.write('{0}\n'.format(os.getpid()))
+
+httpd.serve_forever()
-- 
2.16.4

