Merge ~whereisrysmind/charm-kubernetes-service-checks:master into charm-kubernetes-service-checks:master

Proposed by Ryan Farrell
Status: Superseded
Proposed branch: ~whereisrysmind/charm-kubernetes-service-checks:master
Merge into: charm-kubernetes-service-checks:master
Diff against target: 1895 lines (+1726/-1)
27 files modified
.gitignore (+25/-0)
.gitmodules (+3/-1)
Makefile (+78/-0)
README.md (+96/-0)
config.yaml (+38/-0)
files/plugins/check_kubernetes_api.py (+125/-0)
hooks/install (+1/-0)
lib/.empty (+0/-0)
lib/charmhelpers (+1/-0)
lib/lib_kubernetes_service_checks.py (+161/-0)
lib/ops (+1/-0)
metadata.yaml (+26/-0)
mod/.empty (+0/-0)
mod/charm-helpers (+1/-0)
src/charm.py (+193/-0)
src/setuppath.py (+3/-0)
tests/functional/requirements.txt (+1/-0)
tests/functional/tests/bundles/bionic.yaml (+90/-0)
tests/functional/tests/bundles/xenial.yaml (+90/-0)
tests/functional/tests/kubernetes_service_checks.py (+193/-0)
tests/functional/tests/tests.yaml (+33/-0)
tests/unit/requirements.txt (+5/-0)
tests/unit/setuppath.py (+5/-0)
tests/unit/test_charm.py (+242/-0)
tests/unit/test_lib_ksc.py (+171/-0)
tests/unit/test_plugins.py (+95/-0)
tox.ini (+49/-0)
Reviewer Review Type Date Requested Status
Giuseppe Petralia Needs Fixing
Adam Dyess Pending
Review via email: mp+387569@code.launchpad.net

This proposal supersedes a proposal from 2020-07-15.

This proposal has been superseded by a proposal from 2020-07-17.

Commit message

Feedback request for APAC and EMEA teams.

To post a comment you must log in.
Revision history for this message
Giuseppe Petralia (peppepetra) wrote : Posted in a previous version of this proposal

I have deployed the bundle xenial plus added the commented relations to get it unblocked. The plugin got created in /usr/local/lib/nagios/plugins/ but it is not executable while i think it should be to be used by the nrpe check. Also i can't see the nrpe check in /etc/nagios/nrpe.d and i can not test if it is working.

Other comments inline.

Revision history for this message
Adam Dyess (addyess) wrote : Posted in a previous version of this proposal

I could work on this project. I think the style is clear for what's expected. Just a few suggestions below

review: Needs Fixing
Revision history for this message
Ryan Farrell (whereisrysmind) wrote : Posted in a previous version of this proposal

Addressed each of the inline comments.

Revision history for this message
Giuseppe Petralia (peppepetra) wrote :

Fucntional tests were failing.
Once i commented
context=self.config["nagios_context"],
servicegroups=self.config["nagios_servicegroups"],

in nrpe.add_check in lib/lib_kubernetes_service_checks.py

they passed: https://pastebin.canonical.com/p/rG3QnNWY5f/

review: Needs Fixing
7f88877... by Ryan Farrell

Removed nagios context and servicegroup params

8205896... by Ryan Farrell

Added plugin functional tests

- Added tests confirming that nagios plugins and check configs exist
- Some code lint cleanup

2e6fb0f... by Ryan Farrell

Added nagios-nrpe-server restart

Added logic to restart the nrpe service at times when the charm goes
from an unconfigured state to configured.
Cleaned up some code lint.

8172b74... by Ryan Farrell

Lint Fixes

a17ae30... by Ryan Farrell

Fixed issues with SSL Host Key verification

- Added step to base64 decode of the SSL cert
- Changed plugin default to check SSL host key
- Updated unit tests
- Fixed fixed functional tests

Unmerged commits

a17ae30... by Ryan Farrell

Fixed issues with SSL Host Key verification

- Added step to base64 decode of the SSL cert
- Changed plugin default to check SSL host key
- Updated unit tests
- Fixed fixed functional tests

8172b74... by Ryan Farrell

Lint Fixes

2e6fb0f... by Ryan Farrell

Added nagios-nrpe-server restart

Added logic to restart the nrpe service at times when the charm goes
from an unconfigured state to configured.
Cleaned up some code lint.

8205896... by Ryan Farrell

Added plugin functional tests

- Added tests confirming that nagios plugins and check configs exist
- Some code lint cleanup

7f88877... by Ryan Farrell

Removed nagios context and servicegroup params

34a5d31... by Ryan Farrell

Set executable flag to the check k8s health plugin

7f4d465... by Ryan Farrell

Fixed functional tests

Found an issue with the functests where, although relation remove events
triggered by zaza would be honored by the charms quickly, the juju model
needed time to reflect the change. Relation add events would not tigger
events in the charm again until the model was done removing the
relation. Added waits in the functional tests to ensure this occurred
prior to moving to the next test.

650936d... by Ryan Farrell

Updated Charm

Addressed comments and concerts from the team including:
- Modified functional and unit tests
- Cleaned up some code around accessing and iterating over dicts
- Added missing nrpe.write() call

3257526... by Ryan Farrell

Fix for expected model state

1c0593a... by Ryan Farrell

Charm improvements and Bugfixes

- Added some functional tests, bundles and expected states
- Fixed some state issues when relations are departed

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2new file mode 100644
3index 0000000..f04567f
4--- /dev/null
5+++ b/.gitignore
6@@ -0,0 +1,25 @@
7+# Byte-compiled / optimized / DLL files
8+__pycache__/
9+*.py[cod]
10+*$py.class
11+
12+# Log files
13+*.log
14+
15+.tox/
16+.coverage
17+
18+# vi
19+.*.swp
20+
21+# pycharm
22+.idea/
23+
24+# version data
25+repo-info
26+
27+# reports
28+report/*
29+
30+# coverage
31+htmlcov/*
32diff --git a/.gitmodules b/.gitmodules
33index 0b10c26..2970f9f 100644
34--- a/.gitmodules
35+++ b/.gitmodules
36@@ -1,4 +1,6 @@
37 [submodule "mod/operator"]
38 path = mod/operator
39 url = https://github.com/canonical/operator.git
40- branch = 0.7.0
41+[submodule "mod/charm-helpers"]
42+ path = mod/charm-helpers
43+ url = https://github.com/juju/charm-helpers.git
44diff --git a/Makefile b/Makefile
45new file mode 100644
46index 0000000..4dad760
47--- /dev/null
48+++ b/Makefile
49@@ -0,0 +1,78 @@
50+PYTHON := /usr/bin/python3
51+
52+ifndef CHARM_BUILD_DIR
53+ CHARM_BUILD_DIR=/tmp/charm-builds
54+endif
55+
56+PROJECTPATH=$(dir $(realpath $(MAKEFILE_LIST)))
57+METADATA_FILE="metadata.yaml"
58+CHARM_NAME=$(shell cat ${PROJECTPATH}/${METADATA_FILE} | grep -E '^name:' | awk '{print $$2}')
59+
60+help:
61+ @echo "This project supports the following targets"
62+ @echo ""
63+ @echo " make help - show this text"
64+ @echo " make clean - remove unneeded files"
65+ @echo " make submodules - make sure that the submodules are up-to-date"
66+ @echo " make build - build the charm"
67+ @echo " make release - run clean, submodules and build targets"
68+ @echo " make lint - run flake8 and black"
69+ @echo " make proof - run charm proof"
70+ @echo " make unittests - run the tests defined in the unittest subdirectory"
71+ @echo " make functional - run the tests defined in the functional subdirectory"
72+ @echo " make test - run lint, proof, unittests and functional targets"
73+ @echo ""
74+
75+clean:
76+ @echo "Cleaning files"
77+ @if [ -d .tox ] ; then rm -r .tox ; fi
78+ @if [ -d .pytest_cache ] ; then rm -r .pytest_cache ; fi
79+ @if [ -d .idea ] ; then rm -r .idea ; fi
80+ @if [ -d .coverage ] ; then rm -r .coverage ; fi
81+ @if [ -d report ] ; then rm -r report ; fi
82+ @find . -iname __pycache__ -exec rm -r {} +
83+ @find . -type f -name "*.py[cod]" -delete
84+ @find . -type f -name "*$py.class" -delete
85+ @find . -type f -name "*.log" -delete
86+ @find . -type f -name "*.swp" -delete
87+ @find . -type f -name ".unit-state.db" -delete
88+ @echo "Cleaning existing build"
89+ @rm -rf ${CHARM_BUILD_DIR}/${CHARM_NAME}
90+
91+submodules:
92+ @echo "Cloning submodules"
93+ @git submodule update --init --recursive
94+
95+build:
96+ @echo "Building charm to base directory ${CHARM_BUILD_DIR}/${CHARM_NAME}"
97+ @-git describe --tags > ./repo-info
98+ @mkdir -p ${CHARM_BUILD_DIR}/${CHARM_NAME}
99+ @cp -r ./* ${CHARM_BUILD_DIR}/${CHARM_NAME}
100+ @echo "Installing/updating env from requirements.txt"
101+ @mkdir -p ${CHARM_BUILD_DIR}/${CHARM_NAME}/env/
102+ @if [ -f requirements.txt ] ; then @pip3 install --target=${CHARM_BUILD_DIR}/${CHARM_NAME}/env -r requirements.txt --upgrade ; fi
103+
104+release: clean submodules build
105+ @echo "Charm is built at ${CHARM_BUILD_DIR}/${CHARM_NAME}"
106+
107+lint:
108+ @echo "Running lint checks"
109+ @tox -e lint
110+
111+proof:
112+ @echo "Running charm proof"
113+ @charm proof
114+
115+unittests:
116+ @echo "Running unit tests"
117+ @tox -e unit
118+
119+functional: build
120+ @echo "Executing functional tests in ${CHARM_BUILD_DIR}"
121+ @CHARM_BUILD_DIR=${CHARM_BUILD_DIR} tox -e func
122+
123+test: lint proof unittests functional
124+ @echo "Tests completed for charm ${CHARM_NAME}."
125+
126+# The targets below don't depend on a file
127+.PHONY: help submodules clean build release lint proof unittests functional test
128\ No newline at end of file
129diff --git a/README.md b/README.md
130new file mode 100644
131index 0000000..1a50bd9
132--- /dev/null
133+++ b/README.md
134@@ -0,0 +1,96 @@
135+# kubernetes-service-checks Charm
136+
137+Overview
138+--------
139+
140+This charm provides Kubernetes Service checks for Nagios
141+
142+Quickstart
143+----------
144+
145+ juju deploy cs:kubernetes-service-checks
146+ juju add-relation kubernetes-service-checks nrpe
147+ juju add-relation kubernetes-service-checks:kube-api-endpoint kubernetes-master
148+ juju add-relation kuberentes-service-checks:kube-control kuberentes-master
149+
150+
151+
152+
153+### Relations
154+
155+* **kubernetes-master:kube-api-endpoint** - Provides KSC with the kubernetes-api *hostname* and *port*
156+
157+* **kuberentes-master:kube-control** - Provides KSC with a kuberentes-api *client-token* for authentication
158+
159+* **nrpe:nrpe-external-master** - Required for nagios; provides additional plugins
160+
161+
162+**Note:** Future relations with kubernetes-master *may* be changed so that a
163+single relation can provide the K8S api hostname, port, client token and ssl ca
164+cert.
165+
166+### Config Options
167+
168+**trusted_ssl_ca** *(Optional)* Setting this option enables SSL host
169+certificate authentication in the api checks
170+
171+ juju config kubernetes-service-checks trusted_ssl_ca="${KUBERNETES_API_CA}"
172+
173+
174+Service Checks
175+--------------
176+The plugin *check_kubernetes_api.py* ships with this charm and contains an array of checks for the k8s api health.
177+
178+```
179+check_kubernetes_api.py --help
180+usage: check_kubernetes_api.py [-h] [-H HOST] [-P PORT] [-T CLIENT_TOKEN]
181+ [--check health] [-C SSL_CA_PATH]
182+
183+Check Kubernetes API status
184+
185+optional arguments:
186+ -h, --help show this help message and exit
187+ -H HOST, --host HOST Hostname or IP of the kube-api-server (default: None)
188+ -P PORT, --port PORT Port of the kube-api-server (default: 6443)
189+ -T CLIENT_TOKEN, --token CLIENT_TOKEN
190+ Client access token for authenticate with the
191+ Kubernetes API (default: None)
192+ --check health which check to run (default: health)
193+ -C SSL_CA_PATH, --trusted-ca-cert SSL_CA_PATH
194+ String containing path to the trusted CA certificate
195+ (default: None)
196+
197+```
198+
199+**health** - This polls the kubernetes-api */healthz* endpoint. Posting a GET to this URL endpoint is expected to
200+return 200 - 'ok' if the api is healthy, otherwise 500.
201+
202+
203+Other Checks
204+------------
205+
206+**Certificate Expiration:** The *check_http* plugin is shipped with nrpe, and contains a built in cert expiration check. The warning and crit
207+thesholds are configurable:
208+
209+ juju config kubernetes-service-checks tls_warn_days=90
210+ juju config kubernetes-service-checks tls_crit_days=30
211+
212+Testing
213+-------
214+
215+Juju should be installed and bootstrapped on the system to run functional tests.
216+
217+
218+```
219+ export MODEL_SETTINGS=<semicolon-separated list of "juju model-config" settings>
220+ make test
221+```
222+
223+NOTE: If you are behind a proxy, be sure to export a MODEL_SETTINGS variable as
224+described above. Note that you will need to use the juju-http-proxy, juju-https-proxy, juju-no-proxy
225+and similar settings.
226+
227+Contact
228+-------
229+ - Author: **Bootstack Charmers** *<bootstack-charmers@lists.canonical.com>*
230+ - Bug Tracker: [lp:charm-kubernetes-service-checks](https://launchpad.net/charm-kubernetes-service-checks)
231diff --git a/config.yaml b/config.yaml
232new file mode 100644
233index 0000000..8560b6e
234--- /dev/null
235+++ b/config.yaml
236@@ -0,0 +1,38 @@
237+options:
238+ channel:
239+ type: string
240+ default: 1.18/stable
241+ description: |
242+ Snap channel to install kubectl from
243+ nagios_context:
244+ default: "juju"
245+ type: string
246+ description: |
247+ Used by the nrpe subordinate charms.
248+ A string that will be prepended to instance name to set the host name
249+ in nagios. So for instance the hostname would be something like:
250+ juju-myservice-0
251+ If you're running multiple environments with the same services in them
252+ this allows you to differentiate between them.
253+ nagios_servicegroups:
254+ default: ""
255+ type: string
256+ description: |
257+ A comma-separated list of nagios servicegroups.
258+ If left empty, the nagios_context will be used as the servicegroup
259+ tls_warn_days:
260+ type: int
261+ default: 60
262+ description: |
263+ Number of days left for the TLS certificate to expire before Warning.
264+ tls_crit_days:
265+ type: int
266+ default: 30
267+ description: |
268+ Number of days left for the TLS certificate to expire before alerting Critical.
269+ # temporary config setting for trusted SSL CA (see LP1886982)
270+ trusted_ssl_ca:
271+ type: string
272+ default: ""
273+ description: |
274+ base64 encoded SSL ca cert to use for Kubernetes API client connections.
275\ No newline at end of file
276diff --git a/files/plugins/check_kubernetes_api.py b/files/plugins/check_kubernetes_api.py
277new file mode 100755
278index 0000000..7e77fb4
279--- /dev/null
280+++ b/files/plugins/check_kubernetes_api.py
281@@ -0,0 +1,125 @@
282+#!/usr/bin/python3
283+"""NRPE Plugin for checking Kubernetes API."""
284+
285+import argparse
286+import sys
287+
288+import urllib3
289+
290+NAGIOS_STATUS_OK = 0
291+NAGIOS_STATUS_WARNING = 1
292+NAGIOS_STATUS_CRITICAL = 2
293+NAGIOS_STATUS_UNKNOWN = 3
294+
295+NAGIOS_STATUS = {
296+ NAGIOS_STATUS_OK: "OK",
297+ NAGIOS_STATUS_WARNING: "WARNING",
298+ NAGIOS_STATUS_CRITICAL: "CRITICAL",
299+ NAGIOS_STATUS_UNKNOWN: "UNKNOWN",
300+}
301+
302+
303+def nagios_exit(status, message):
304+ """Return the check status in Nagios preferred format.
305+
306+ :param status: Nagios Check status code (in [0, 1, 2, 3])
307+ :param message: Message describing the status
308+ :return: sys.exit("{status_string}: {message}")
309+ """
310+ assert status in NAGIOS_STATUS, "Invalid Nagios status code"
311+ # prefix status name to message
312+ output = "{}: {}".format(NAGIOS_STATUS[status], message)
313+ print(output) # nagios requires print to stdout, no stderr
314+ sys.exit(status)
315+
316+
317+def check_kubernetes_health(k8s_address, client_token, disable_ssl):
318+ """Call <kubernetes-api>/healthz endpoint and check return value is 'ok'.
319+
320+ :param k8s_address: Address to kube-api-server formatted 'https://<IP>:<PORT>'
321+ :param client_token: Token for authenticating with the kube-api
322+ :param disable_ssl: Disables SSL Host Key verification
323+ """
324+ url = k8s_address + "/healthz"
325+ if disable_ssl:
326+ # perform check without SSL verification
327+ http = urllib3.PoolManager(
328+ cert_reqs="CERT_NONE",
329+ assert_hostname=False
330+ )
331+ else:
332+ http = urllib3.PoolManager()
333+
334+ try:
335+ req = http.request(
336+ "GET",
337+ url,
338+ headers={"Authorization": "Bearer {}".format(client_token)}
339+ )
340+ except urllib3.exceptions.MaxRetryError as e:
341+ return NAGIOS_STATUS_CRITICAL, e
342+
343+ if req.status != 200:
344+ return NAGIOS_STATUS_CRITICAL, "Unexpected HTTP Response code ({})".format(req.status)
345+ elif req.data != b"ok":
346+ return NAGIOS_STATUS_WARNING, "Unexpected Kubernetes healthz status '{}'".format(req.data)
347+ return NAGIOS_STATUS_OK, "Kubernetes health 'ok'"
348+
349+
350+if __name__ == "__main__":
351+ parser = argparse.ArgumentParser(
352+ description="Check Kubernetes API status",
353+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
354+ )
355+
356+ parser.add_argument(
357+ "-H", "--host", dest="host",
358+ help="Hostname or IP of the kube-api-server"
359+ )
360+
361+ parser.add_argument(
362+ "-P", "--port", dest="port", type=int, default=6443,
363+ help="Port of the kube-api-server"
364+ )
365+
366+ parser.add_argument(
367+ "-T", "--token", dest="client_token",
368+ help="Client access token for authenticate with the Kubernetes API"
369+ )
370+
371+ check_choices = ["health"]
372+ parser.add_argument(
373+ "--check", dest="check", metavar="|".join(check_choices),
374+ type=str, choices=check_choices,
375+ default=check_choices[0],
376+ help="which check to run")
377+
378+ parser.add_argument(
379+ "-d", "--disable-host-key-check", dest="disable_ssl", default=False,
380+ action="store_true", help="Disables Host SSL Key Authentication"
381+ )
382+ args = parser.parse_args()
383+
384+ checks = {
385+ "health": check_kubernetes_health,
386+ }
387+
388+ k8s_url = "https://{}:{}".format(args.host, args.port)
389+ nagios_exit(*checks[args.check](k8s_url,
390+ args.client_token,
391+ args.disable_ssl))
392+
393+"""
394+TODO: Future Checks
395+
396+GET /api/v1/componentstatuses HTTP/1.1
397+Authorization: Bearer $TOKEN
398+Accept: application/json
399+Connection: close
400+
401+GET /api/va/nodes HTTP/1.1
402+Authorization: Bearer $TOKEN
403+Accept: application/json
404+Connection: close
405+
406+"""
407diff --git a/hooks/install b/hooks/install
408new file mode 120000
409index 0000000..25b1f68
410--- /dev/null
411+++ b/hooks/install
412@@ -0,0 +1 @@
413+../src/charm.py
414\ No newline at end of file
415diff --git a/lib/.empty b/lib/.empty
416new file mode 100644
417index 0000000..e69de29
418--- /dev/null
419+++ b/lib/.empty
420diff --git a/lib/charmhelpers b/lib/charmhelpers
421new file mode 120000
422index 0000000..cedc09e
423--- /dev/null
424+++ b/lib/charmhelpers
425@@ -0,0 +1 @@
426+../mod/charm-helpers/charmhelpers
427\ No newline at end of file
428diff --git a/lib/lib_kubernetes_service_checks.py b/lib/lib_kubernetes_service_checks.py
429new file mode 100644
430index 0000000..48f36b8
431--- /dev/null
432+++ b/lib/lib_kubernetes_service_checks.py
433@@ -0,0 +1,161 @@
434+"""Kubernetes Service Checks Helper Library."""
435+import base64
436+import logging
437+import os
438+import subprocess
439+
440+from charmhelpers.contrib.charmsupport.nrpe import NRPE
441+from charmhelpers.core import hookenv, host
442+from charmhelpers.fetch import snap
443+
444+CERT_FILE = "/usr/local/share/ca-certificates/kubernetes-service-checks.crt"
445+NAGIOS_PLUGINS_DIR = "/usr/local/lib/nagios/plugins/"
446+
447+
448+class KSCHelper():
449+ """Kubernetes Service Checks Helper Class."""
450+
451+ def __init__(self, config, state):
452+ """Initialize the Helper with the charm config and state."""
453+ self.config = config
454+ self.state = state
455+
456+ @property
457+ def kubernetes_api_address(self):
458+ """Get kubernetes api hostname."""
459+ return self.state.kube_api_endpoint.get("hostname", None)
460+
461+ @property
462+ def kubernetes_api_port(self):
463+ """Get kubernetes api port."""
464+ return self.state.kube_api_endpoint.get("port", None)
465+
466+ @property
467+ def kubernetes_client_token(self):
468+ """Get kubernetes client token."""
469+ try:
470+ data = eval(self.state.kube_control.get("creds", "{}"))
471+ except SyntaxError:
472+ data = {}
473+ for creds in data.values():
474+ token = creds.get("client_token", None)
475+ if token:
476+ return token
477+ return None
478+
479+ @property
480+ def use_tls_cert(self):
481+ """Check if SSL cert is provided for use."""
482+ return bool(self._ssl_certificate)
483+
484+ @property
485+ def _ssl_certificate(self):
486+ # TODO: Expand this later to take a cert from a relation or from the config.
487+ # cert from the relation is to be prioritized
488+ ssl_cert = self.config.get("trusted_ssl_ca", None)
489+ if ssl_cert:
490+ ssl_cert = ssl_cert.strip()
491+ return ssl_cert
492+
493+ @property
494+ def ssl_cert_path(self):
495+ """Get cert file path."""
496+ return CERT_FILE
497+
498+ @property
499+ def plugins_dir(self):
500+ """Get nagios plugins directory."""
501+ return NAGIOS_PLUGINS_DIR
502+
503+ def restart_nrpe_service(self):
504+ """Restart nagios-nrpe-server service."""
505+ host.service_restart('nagios-nrpe-server')
506+
507+ def update_tls_certificates(self):
508+ """Write the trusted ssl certificate to the CERT_FILE."""
509+ if self._ssl_certificate:
510+ cert_content = base64.b64decode(self._ssl_certificate).decode()
511+ try:
512+ logging.debug('Writing ssl ca cert to {}'.format(self.ssl_cert_path))
513+ with open(self.ssl_cert_path, "w") as f:
514+ f.write(cert_content)
515+ subprocess.call(['/usr/sbin/update-ca-certificates'])
516+ return True
517+ except subprocess.CalledProcessError as e:
518+ logging.error(e)
519+ return False
520+ except PermissionError as e:
521+ logging.error(e)
522+ return False
523+ else:
524+ logging.error("Trusted SSL Certificate is not defined")
525+ return False
526+
527+ def configure(self):
528+ """Refresh configuration data."""
529+ self.update_plugins()
530+ self.render_checks()
531+
532+ def update_plugins(self):
533+ """Rsync plugins to the plugin directory."""
534+ charm_plugin_dir = os.path.join(hookenv.charm_dir(), "files", "plugins/")
535+ host.rsync(charm_plugin_dir, self.plugins_dir, options=["--executability"])
536+
537+ def render_checks(self):
538+ """Render nrpe checks."""
539+ nrpe = NRPE()
540+ if not os.path.exists(self.plugins_dir):
541+ os.makedirs(self.plugins_dir)
542+
543+ # register basic api health check
544+ check_k8s_plugin = os.path.join(self.plugins_dir, "check_kubernetes_api.py")
545+ for check in ["health"]:
546+ check_command = "{} -H {} -P {} -T {} --check {}".format(
547+ check_k8s_plugin,
548+ self.kubernetes_api_address,
549+ self.kubernetes_api_port,
550+ self.kubernetes_client_token,
551+ check
552+ ).strip()
553+ if not self.use_tls_cert:
554+ check_command += " -d"
555+
556+ nrpe.add_check(
557+ shortname="k8s_api_{}".format(check),
558+ description="Check Kubernetes API ({})".format(check),
559+ check_cmd=check_command,
560+ )
561+
562+ # register k8s host certificate expiration check
563+ check_http_plugin = "/usr/lib/nagios/plugins/check_http"
564+ check_command = "{} -I {} -p {} -C {},{}".format(
565+ check_http_plugin,
566+ self.kubernetes_api_address,
567+ self.kubernetes_api_port,
568+ self.config.get("tls_warn_days"),
569+ self.config.get("tls_crit_days")
570+ ).strip()
571+ nrpe.add_check(
572+ shortname="k8s_api_cert_expiration",
573+ description="Check Kubernetes API ({})".format(check),
574+ check_cmd=check_command,
575+ )
576+ nrpe.write()
577+
578+ def install_kubectl(self):
579+ """Attempt to install kubectl.
580+
581+ :returns: bool, indicating whether or not successful
582+ """
583+ # snap retry is excessive
584+ snap.SNAP_NO_LOCK_RETRY_DELAY = 0.5
585+ snap.SNAP_NO_LOCK_RETRY_COUNT = 3
586+ try:
587+ channel = self.config.get('channel')
588+ snap.snap_install("kubectl",
589+ "--classic",
590+ "--channel={}".format(channel)
591+ )
592+ return True
593+ except snap.CouldNotAcquireLockException:
594+ return False
595diff --git a/lib/ops b/lib/ops
596new file mode 120000
597index 0000000..d934193
598--- /dev/null
599+++ b/lib/ops
600@@ -0,0 +1 @@
601+../mod/operator/ops
602\ No newline at end of file
603diff --git a/metadata.yaml b/metadata.yaml
604new file mode 100644
605index 0000000..09b26a5
606--- /dev/null
607+++ b/metadata.yaml
608@@ -0,0 +1,26 @@
609+name: kubernetes-service-checks
610+summary: Kubernetes Services NRPE Checks
611+maintainers:
612+ - Bootstack Charmers <bootstack-charmers@lists.canonical.com>
613+description: |
614+ This charm provides NRPE Checks verifying Kubernetes API accessibility
615+ and integrates with Nagios for timely alerting.
616+tags:
617+ - kubernetes
618+ - ops
619+ - monitoring
620+series:
621+ - focal
622+ - bionic
623+ - xenial
624+requires:
625+ kube-control:
626+ interface: kube-control
627+ kube-api-endpoint:
628+ interface: http
629+provides:
630+ nrpe-external-master:
631+ interface: nrpe-external-master
632+ scope: container
633+ optional: true
634+subordinate: false
635diff --git a/mod/.empty b/mod/.empty
636new file mode 100644
637index 0000000..e69de29
638--- /dev/null
639+++ b/mod/.empty
640diff --git a/mod/charm-helpers b/mod/charm-helpers
641new file mode 160000
642index 0000000..4b3602e
643--- /dev/null
644+++ b/mod/charm-helpers
645@@ -0,0 +1 @@
646+Subproject commit 4b3602e2bdf101bf58cc808264ec0c8092a67cd0
647diff --git a/src/charm.py b/src/charm.py
648new file mode 100755
649index 0000000..1e0a1ff
650--- /dev/null
651+++ b/src/charm.py
652@@ -0,0 +1,193 @@
653+#! /usr/bin/env python3
654+# -*- coding: utf-8 -*-
655+# vim:fenc=utf-8
656+# Copyright © 2020 Bootstack Charmers bootstack-charmers@lists.canonical.com
657+
658+"""Operator Charm main library."""
659+# Load modules from lib directory
660+import logging
661+
662+import setuppath # noqa:F401
663+
664+from lib_kubernetes_service_checks import KSCHelper # noqa:I100
665+from ops.charm import CharmBase
666+from ops.framework import StoredState
667+from ops.main import main
668+from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
669+
670+
671+class Kubernetes_Service_ChecksCharm(CharmBase): # noqa:N801
672+ """Class representing this Operator charm."""
673+
674+ state = StoredState()
675+
676+ def __init__(self, *args):
677+ """Initialize charm and configure states and events to observe."""
678+ super().__init__(*args)
679+ # -- standard hook observation
680+ self.framework.observe(self.on.install, self.on_install)
681+ self.framework.observe(self.on.start, self.on_start)
682+ self.framework.observe(self.on.config_changed, self.on_config_changed)
683+ self.framework.observe(
684+ self.on.kube_api_endpoint_relation_changed,
685+ self.on_kube_api_endpoint_relation_changed,
686+ )
687+ self.framework.observe(
688+ self.on.kube_api_endpoint_relation_departed,
689+ self.on_kube_api_endpoint_relation_departed
690+ )
691+ self.framework.observe(
692+ self.on.kube_control_relation_changed,
693+ self.on_kube_control_relation_changed,
694+ )
695+ self.framework.observe(
696+ self.on.kube_control_relation_departed,
697+ self.on_kube_control_relation_departed
698+ )
699+ self.framework.observe(
700+ self.on.nrpe_external_master_relation_joined,
701+ self.on_nrpe_external_master_relation_joined
702+ )
703+ self.framework.observe(
704+ self.on.nrpe_external_master_relation_departed,
705+ self.on_nrpe_external_master_relation_departed
706+ )
707+ # -- initialize states --
708+ self.state.set_default(
709+ installed=False,
710+ configured=False,
711+ started=False,
712+ kube_control={},
713+ kube_api_endpoint={},
714+ nrpe_configured=False,
715+ )
716+ self.helper = KSCHelper(self.model.config, self.state)
717+
718+ def on_install(self, event):
719+ """Handle install state."""
720+ self.unit.status = MaintenanceStatus("Installing charm software")
721+ self.unit.status = MaintenanceStatus("Install complete")
722+ logging.info("Install of software complete")
723+ self.state.installed = True
724+
725+ def on_upgrade_charm(self, event):
726+ """Handle upgrade and resource updates."""
727+ # Re-install for new snaps
728+ logging.info("Reinstalling for upgrade-charm hook")
729+ self.on_install(event)
730+ self.check_charm_status()
731+
732+ def check_charm_status(self):
733+ """Check that required data is available from relations."""
734+ # check that relations are configured with expected data
735+
736+ if not self.helper.kubernetes_api_address or not self.helper.kubernetes_api_port:
737+ logging.warning("kube-api-endpoint relation missing or misconfigured")
738+ self.unit.status = BlockedStatus("missing kube-api-endpoint relation")
739+ return
740+ if not self.helper.kubernetes_client_token:
741+ logging.warning("kube-control relation missing or misconfigured")
742+ self.unit.status = BlockedStatus("missing kube-control relation")
743+ return
744+ if not self.state.nrpe_configured:
745+ logging.warning("nrpe-external-master relation missing or misconfigured")
746+ self.unit.status = BlockedStatus("missing nrpe-external-master relation")
747+ return
748+
749+ # Check specific required config values
750+ # Set up TLS Certificate
751+ if self.helper.use_tls_cert:
752+ logging.info("Updating tls certificates")
753+ if self.helper.update_tls_certificates():
754+ logging.info("TLS Certificates updated successfully")
755+ else:
756+ logging.error("Failed to update TLS Certificates")
757+ self.unit.status = BlockedStatus("update-ca-certificates error. check logs")
758+ return
759+ else:
760+ logging.warn("No trusted_ssl_ca provided, SSL Host Authentication disabled")
761+
762+ # configure nrpe checks
763+ logging.info("Configuring Kubernetes Service Checks")
764+ self.helper.configure()
765+ if not self.state.configured:
766+ logging.info('Reloading nagios-nrpe-server')
767+ self.helper.restart_nrpe_service()
768+ self.state.configured = True
769+ self.unit.status = ActiveStatus("Unit is ready")
770+
771+ def on_config_changed(self, event):
772+ """Handle config changed."""
773+ if not self.state.installed:
774+ logging.warning("Config changed called before install complete, deferring event: {}.".format(event.handle))
775+ self._defer_once(event)
776+ return
777+ self.check_charm_status()
778+
779+ def on_start(self, event):
780+ """Handle start state."""
781+ if not self.state.configured:
782+ logging.warning("Start called before configuration complete, deferring event: {}".format(event.handle))
783+ event.defer()
784+ return
785+ self.unit.status = MaintenanceStatus("Starting charm software")
786+ self.unit.status = ActiveStatus("Unit is ready")
787+ self.state.started = True
788+ logging.info("Started")
789+
790+ def _defer_once(self, event):
791+ """Defer the given event, but only once."""
792+ notice_count = 0
793+ handle = str(event.handle)
794+
795+ for event_path, _, _ in self.framework._storage.notices(None):
796+ if event_path.startswith(handle.split('[')[0]):
797+ notice_count += 1
798+ logging.debug("Found event: {} x {}".format(event_path, notice_count))
799+
800+ if notice_count > 1:
801+ logging.debug("Not deferring {} notice count of {}".format(handle, notice_count))
802+ else:
803+ logging.debug("Deferring {} notice count of {}".format(handle, notice_count))
804+ event.defer()
805+
806+ def on_kube_api_endpoint_relation_changed(self, event):
807+ """Handle kube_api_endpoint relation changed."""
808+ self.unit.status = MaintenanceStatus("Updating K8S Endpoint")
809+ self.state.kube_api_endpoint.update(event.relation.data.get(event.unit, {}))
810+ self.check_charm_status()
811+
812+ def on_kube_api_endpoint_relation_departed(self, event):
813+ """Handle kube-api-endpoint relation departed."""
814+ self.state.configured = False
815+ for k in self.state.kube_api_endpoint.keys():
816+ self.state.kube_api_endpoint[k] = ""
817+ self.check_charm_status()
818+
819+ def on_kube_control_relation_changed(self, event):
820+ """Handle kube-control relation changed."""
821+ self.unit.status = MaintenanceStatus("Updating K8S Credentials")
822+ self.state.kube_control.update(event.relation.data.get(event.unit, {}))
823+ self.check_charm_status()
824+
825+ def on_kube_control_relation_departed(self, event):
826+ """Handle kube-control relation departed."""
827+ self.state.configured = False
828+ for k in self.state.kube_control.keys():
829+ self.state.kube_control[k] = ""
830+ self.check_charm_status()
831+
832+ def on_nrpe_external_master_relation_joined(self, event):
833+ """Handle nrpe-external-master relation joined."""
834+ self.state.nrpe_configured = True
835+ self.check_charm_status()
836+
837+ def on_nrpe_external_master_relation_departed(self, event):
838+ """Handle nrpe-external-master relation departed."""
839+ self.state.configured = False
840+ self.state.nrpe_configured = False
841+ self.check_charm_status()
842+
843+
844+if __name__ == "__main__":
845+ main(Kubernetes_Service_ChecksCharm)
846diff --git a/src/setuppath.py b/src/setuppath.py
847new file mode 100644
848index 0000000..769d936
849--- /dev/null
850+++ b/src/setuppath.py
851@@ -0,0 +1,3 @@
852+"""Include ./lib in the charm's PATH."""
853+import sys
854+sys.path.append('lib')
855diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt
856new file mode 100644
857index 0000000..b7c9112
858--- /dev/null
859+++ b/tests/functional/requirements.txt
860@@ -0,0 +1 @@
861+git+https://github.com/openstack-charmers/zaza.git#egg=zaza
862diff --git a/tests/functional/tests/bundles/bionic.yaml b/tests/functional/tests/bundles/bionic.yaml
863new file mode 100644
864index 0000000..c55ba41
865--- /dev/null
866+++ b/tests/functional/tests/bundles/bionic.yaml
867@@ -0,0 +1,90 @@
868+series: bionic
869+applications:
870+ kubernetes-service-checks:
871+ charm: ../../../../
872+ num_units: 1
873+ containerd:
874+ charm: cs:~containers/containerd
875+ options:
876+ gpu_driver: none
877+ resources: {}
878+ easyrsa:
879+ charm: cs:~containers/easyrsa
880+ num_units: 1
881+ resources:
882+ easyrsa: 5
883+ etcd:
884+ charm: cs:~containers/etcd
885+ num_units: 1
886+ options:
887+ channel: 3.3/stable
888+ resources:
889+ core: 0
890+ etcd: 3
891+ snapshot: 0
892+ flannel:
893+ charm: cs:~containers/flannel
894+ resources:
895+ flannel-amd64: 625
896+ flannel-arm64: 622
897+ flannel-s390x: 609
898+ kubernetes-master:
899+ charm: cs:~containers/kubernetes-master
900+ num_units: 1
901+ constraints: cores=4 mem=4G root-disk=16G
902+ options:
903+ channel: 1.18/stable
904+ resources:
905+ cdk-addons: 0
906+ core: 0
907+ kube-apiserver: 0
908+ kube-controller-manager: 0
909+ kube-proxy: 0
910+ kube-scheduler: 0
911+ kubectl: 0
912+ kubernetes-worker:
913+ charm: cs:~containers/kubernetes-worker
914+ expose: true
915+ num_units: 1
916+ constraints: cores=4 mem=4G root-disk=16G
917+ options:
918+ channel: 1.18/stable
919+ resources:
920+ cni-amd64: 645
921+ cni-arm64: 636
922+ cni-s390x: 648
923+ core: 0
924+ kube-proxy: 0
925+ kubectl: 0
926+ kubelet: 0
927+ nrpe:
928+ charm: cs:nrpe
929+relations:
930+ - - kubernetes-master:kube-api-endpoint
931+ - kubernetes-worker:kube-api-endpoint
932+ - - kubernetes-service-checks:nrpe-external-master
933+ - nrpe:nrpe-external-master
934+ - - kubernetes-master:kube-control
935+ - kubernetes-worker:kube-control
936+ - - kubernetes-master:certificates
937+ - easyrsa:client
938+ - - etcd:certificates
939+ - easyrsa:client
940+ - - kubernetes-master:etcd
941+ - etcd:db
942+ - - kubernetes-worker:certificates
943+ - easyrsa:client
944+ - - flannel:etcd
945+ - etcd:db
946+ - - flannel:cni
947+ - kubernetes-master:cni
948+ - - flannel:cni
949+ - kubernetes-worker:cni
950+ - - containerd:containerd
951+ - kubernetes-worker:container-runtime
952+ - - containerd:containerd
953+ - kubernetes-master:container-runtime
954+ - - kubernetes-service-checks:kube-control
955+ - kubernetes-master:kube-control
956+ - - kubernetes-service-checks:kube-api-endpoint
957+ - kubernetes-master:kube-api-endpoint
958\ No newline at end of file
959diff --git a/tests/functional/tests/bundles/xenial.yaml b/tests/functional/tests/bundles/xenial.yaml
960new file mode 100644
961index 0000000..962bdfd
962--- /dev/null
963+++ b/tests/functional/tests/bundles/xenial.yaml
964@@ -0,0 +1,90 @@
965+series: xenial
966+applications:
967+ kubernetes-service-checks:
968+ charm: ../../../../
969+ num_units: 1
970+ containerd:
971+ charm: cs:~containers/containerd
972+ options:
973+ gpu_driver: none
974+ resources: {}
975+ easyrsa:
976+ charm: cs:~containers/easyrsa
977+ num_units: 1
978+ resources:
979+ easyrsa: 5
980+ etcd:
981+ charm: cs:~containers/etcd
982+ num_units: 1
983+ options:
984+ channel: 3.3/stable
985+ resources:
986+ core: 0
987+ etcd: 3
988+ snapshot: 0
989+ flannel:
990+ charm: cs:~containers/flannel
991+ resources:
992+ flannel-amd64: 625
993+ flannel-arm64: 622
994+ flannel-s390x: 609
995+ kubernetes-master:
996+ charm: cs:~containers/kubernetes-master
997+ num_units: 1
998+ constraints: cores=4 mem=4G root-disk=16G
999+ options:
1000+ channel: 1.18/stable
1001+ resources:
1002+ cdk-addons: 0
1003+ core: 0
1004+ kube-apiserver: 0
1005+ kube-controller-manager: 0
1006+ kube-proxy: 0
1007+ kube-scheduler: 0
1008+ kubectl: 0
1009+ kubernetes-worker:
1010+ charm: cs:~containers/kubernetes-worker
1011+ expose: true
1012+ num_units: 1
1013+ constraints: cores=4 mem=4G root-disk=16G
1014+ options:
1015+ channel: 1.18/stable
1016+ resources:
1017+ cni-amd64: 645
1018+ cni-arm64: 636
1019+ cni-s390x: 648
1020+ core: 0
1021+ kube-proxy: 0
1022+ kubectl: 0
1023+ kubelet: 0
1024+ nrpe:
1025+ charm: cs:nrpe
1026+relations:
1027+ - - kubernetes-master:kube-api-endpoint
1028+ - kubernetes-worker:kube-api-endpoint
1029+ - - kubernetes-service-checks:nrpe-external-master
1030+ - nrpe:nrpe-external-master
1031+ - - kubernetes-master:kube-control
1032+ - kubernetes-worker:kube-control
1033+ - - kubernetes-master:certificates
1034+ - easyrsa:client
1035+ - - etcd:certificates
1036+ - easyrsa:client
1037+ - - kubernetes-master:etcd
1038+ - etcd:db
1039+ - - kubernetes-worker:certificates
1040+ - easyrsa:client
1041+ - - flannel:etcd
1042+ - etcd:db
1043+ - - flannel:cni
1044+ - kubernetes-master:cni
1045+ - - flannel:cni
1046+ - kubernetes-worker:cni
1047+ - - containerd:containerd
1048+ - kubernetes-worker:container-runtime
1049+ - - containerd:containerd
1050+ - kubernetes-master:container-runtime
1051+ - - kubernetes-service-checks:kube-control
1052+ - kubernetes-master:kube-control
1053+ - - kubernetes-service-checks:kube-api-endpoint
1054+ - kubernetes-master:kube-api-endpoint
1055diff --git a/tests/functional/tests/kubernetes_service_checks.py b/tests/functional/tests/kubernetes_service_checks.py
1056new file mode 100644
1057index 0000000..39dc7ef
1058--- /dev/null
1059+++ b/tests/functional/tests/kubernetes_service_checks.py
1060@@ -0,0 +1,193 @@
1061+"""Charm Kubernetes Service Checks Functional Tests."""
1062+import concurrent.futures
1063+import logging
1064+import re
1065+import time
1066+import unittest
1067+
1068+from juju.errors import JujuAPIError
1069+import zaza.model
1070+
1071+
1072+class TestBase(unittest.TestCase):
1073+ """Base Class for charm functional tests."""
1074+
1075+ @classmethod
1076+ def setUpClass(cls):
1077+ """Run setup for tests."""
1078+ cls.model_name = zaza.model.get_juju_model()
1079+ cls.application_name = "kubernetes-service-checks"
1080+
1081+ def setUp(self):
1082+ """Set up functional tests & ensure all relations added."""
1083+ for local_relation_name, remote_relation_unit in [
1084+ ("kube-api-endpoint", "kubernetes-master"),
1085+ ("kube-control", "kubernetes-master"),
1086+ ("nrpe-external-master", "nrpe")]:
1087+ logging.info("Adding relation {} with {}".format(local_relation_name,
1088+ remote_relation_unit))
1089+ try:
1090+ zaza.model.add_relation(self.application_name,
1091+ local_relation_name,
1092+ remote_relation_unit,
1093+ self.model_name)
1094+ except JujuAPIError as e:
1095+ p = r"^.*cannot\ add\ relation.*already\ exists"
1096+ if re.search(p, e.message):
1097+ pass
1098+ else:
1099+ raise(e)
1100+ zaza.model.block_until_wl_status_info_starts_with(
1101+ self.application_name,
1102+ status="Unit is ready",
1103+ timeout=200
1104+ )
1105+
1106+
1107+class TestChecks(TestBase):
1108+ """Tests for availability and usefulness of nagios checks."""
1109+
1110+ expected_checks = ["check_k8s_api_health.cfg",
1111+ "check_k8s_api_cert_expiration.cfg"]
1112+ checks_dir = "/etc/nagios/nrpe.d/"
1113+ expected_plugins = ["check_kubernetes_api.py"]
1114+ plugins_dir = "/usr/local/lib/nagios/plugins/"
1115+
1116+ # TODO: Need testing around setting the trusted_ssl_ca cert
1117+ # - does it get written to /etc/ssl/certs/ca-certificates.crt?
1118+ # - does the k8s check plugin see it and use it for verification?
1119+
1120+ def test_check_plugins_exist(self):
1121+ """Verify that kubernetes service checks plugins are found."""
1122+ fail_messages = []
1123+ for plugin in self.expected_plugins:
1124+ pluginpath = self.plugins_dir + plugin
1125+ response = zaza.model.run_on_unit(
1126+ "kubernetes-service-checks/0",
1127+ "[ -f \"{}\" ]".format(pluginpath),
1128+ model_name=self.model_name,
1129+ timeout=30
1130+ )
1131+ if response["Code"] != "0":
1132+ fail_messages.append("Missing plugin: {}".format(pluginpath))
1133+ continue
1134+
1135+ # check executable
1136+ response = zaza.model.run_on_unit(
1137+ "kubernetes-service-checks/0",
1138+ "[ -x \"{}\" ]".format(pluginpath),
1139+ model_name=self.model_name,
1140+ timeout=30
1141+ )
1142+
1143+ if response["Code"] != "0":
1144+ fail_messages.append("Plugin not executable: {}".format(pluginpath))
1145+
1146+ if fail_messages:
1147+ self.fail("\n".join(fail_messages))
1148+
1149+ def test_checks_exist(self):
1150+ """Verify that kubernetes service checks nrpe checks exist."""
1151+ fail_messages = []
1152+ for check in self.expected_checks:
1153+ checkpath = self.checks_dir + check
1154+ response = zaza.model.run_on_unit(
1155+ "kubernetes-service-checks/0",
1156+ "[ -f \"{}\" ]".format(checkpath),
1157+ model_name=self.model_name,
1158+ timeout=30
1159+ )
1160+ if response["Code"] != "0":
1161+ fail_messages.append("Missing check: {}".format(checkpath))
1162+ if fail_messages:
1163+ self.fail("\n".join(fail_messages))
1164+
1165+
1166+class TestRelations(TestBase):
1167+ """Tests for charm behavior adding and removing relations."""
1168+
1169+ def _get_relation_id(self, remote_application, interface_name):
1170+ return zaza.model.get_relation_id(self.application_name,
1171+ remote_application,
1172+ model_name=self.model_name,
1173+ remote_interface_name=interface_name)
1174+
1175+ def test_remove_kube_api_endpoint(self):
1176+ """Test removing kube-api-endpoint relation."""
1177+ rel_name = "kube-api-endpoint"
1178+ remote_app = "kubernetes-master"
1179+ logging.info("Removing kube-api-endpoint relation")
1180+
1181+ zaza.model.remove_relation(self.application_name,
1182+ rel_name,
1183+ remote_app,
1184+ self.model_name)
1185+ try:
1186+ zaza.model.block_until_wl_status_info_starts_with(
1187+ self.application_name,
1188+ status="missing kube-api-endpoint relation",
1189+ timeout=180
1190+ )
1191+ except concurrent.futures._base.TimeoutError:
1192+ self.fail("Timed out waiting for Unit to become blocked")
1193+
1194+ logging.info("Waiting for relation {} to be destroyed".format(rel_name))
1195+ timeout = time.time() + 600
1196+ while self._get_relation_id(remote_app, rel_name) is not None:
1197+ time.sleep(5)
1198+ if time.time() > timeout:
1199+ self.fail("Timed out waiting for the relation {} to be destroyed".format(rel_name))
1200+
1201+ def test_remove_kube_control(self):
1202+ """Test removing kube-control relation."""
1203+ rel_name = "kube-control"
1204+ remote_app = "kubernetes-master"
1205+ logging.info("Removing kube-control relation")
1206+
1207+ zaza.model.remove_relation(self.application_name,
1208+ rel_name,
1209+ remote_app,
1210+ self.model_name)
1211+
1212+ try:
1213+ zaza.model.block_until_wl_status_info_starts_with(
1214+ self.application_name,
1215+ status="missing kube-control relation",
1216+ timeout=180
1217+ )
1218+ except concurrent.futures._base.TimeoutError:
1219+ self.fail("Timed out waiting for Unit to become blocked")
1220+
1221+ logging.info("Waiting for relation {} to be destroyed".format(rel_name))
1222+ timeout = time.time() + 600
1223+ while self._get_relation_id(remote_app, rel_name) is not None:
1224+ time.sleep(5)
1225+ if time.time() > timeout:
1226+ self.fail("Timed out waiting for the relation {} to be destroyed".format(rel_name))
1227+
1228+ def test_remove_nrpe_external_master(self):
1229+ """Test removing nrpe-external-master relation."""
1230+ rel_name = "nrpe-external-master"
1231+ remote_app = "nrpe"
1232+ logging.info("Removing nrpe-external-master relation")
1233+
1234+ zaza.model.remove_relation(self.application_name,
1235+ rel_name,
1236+ remote_app,
1237+ self.model_name)
1238+
1239+ try:
1240+ zaza.model.block_until_wl_status_info_starts_with(
1241+ self.application_name,
1242+ status="missing nrpe-external-master relation",
1243+ timeout=180
1244+ )
1245+ except concurrent.futures._base.TimeoutError:
1246+ self.fail("Timed out waiting for Unit to become blocked")
1247+
1248+ logging.info("Waiting for relation {} to be destroyed".format(rel_name))
1249+ timeout = time.time() + 600
1250+ while self._get_relation_id(remote_app, rel_name) is not None:
1251+ time.sleep(5)
1252+ if time.time() > timeout:
1253+ self.fail("Timed out waiting for the relation {} to be destroyed".format(rel_name))
1254diff --git a/tests/functional/tests/tests.yaml b/tests/functional/tests/tests.yaml
1255new file mode 100644
1256index 0000000..75a9ce8
1257--- /dev/null
1258+++ b/tests/functional/tests/tests.yaml
1259@@ -0,0 +1,33 @@
1260+tests:
1261+ - tests.kubernetes_service_checks.TestChecks
1262+ - tests.kubernetes_service_checks.TestRelations
1263+target_deploy_status:
1264+ kubernetes-service-checks:
1265+ workload-status: blocked
1266+ workload-status-message: "missing kube-api-endpoint relation"
1267+ kubernetes-master:
1268+ workload-status: active
1269+ workload-status-message: "Kubernetes master running."
1270+ kubernetes-worker:
1271+ workload-status: active
1272+ workload-status-message: "Kubernetes worker running."
1273+ flannel:
1274+ workload-status: active
1275+ workload-status-message: "Flannel subnet"
1276+ easyrsa:
1277+ workload-status: active
1278+ workload-status-message: "Certificate Authority connected."
1279+ containerd:
1280+ workload-status: active
1281+ workload-status-message: "Container runtime available"
1282+ etcd:
1283+ workload-status: active
1284+ workload-status-message: "Healthy with 1 known peer"
1285+ nrpe:
1286+ workload-status: active
1287+ workload-status-message: "ready"
1288+gate_bundles:
1289+ - xenial
1290+ - bionic
1291+smoke_bundles:
1292+ - focal
1293diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
1294new file mode 100644
1295index 0000000..b5f9fbd
1296--- /dev/null
1297+++ b/tests/unit/requirements.txt
1298@@ -0,0 +1,5 @@
1299+mock
1300+pyyaml
1301+coverage
1302+six
1303+urllib3
1304diff --git a/tests/unit/setuppath.py b/tests/unit/setuppath.py
1305new file mode 100644
1306index 0000000..5e334a3
1307--- /dev/null
1308+++ b/tests/unit/setuppath.py
1309@@ -0,0 +1,5 @@
1310+"""Include ./lib ./src and ./file/plugins in the tests' PATH."""
1311+import sys
1312+sys.path.append('lib')
1313+sys.path.append('src')
1314+sys.path.append('files/plugins')
1315diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
1316new file mode 100644
1317index 0000000..93fd21d
1318--- /dev/null
1319+++ b/tests/unit/test_charm.py
1320@@ -0,0 +1,242 @@
1321+"""Charm unit tests."""
1322+import os
1323+from pathlib import Path
1324+import unittest
1325+
1326+
1327+import mock
1328+import yaml
1329+import setuppath # noqa:F401
1330+
1331+from charm import Kubernetes_Service_ChecksCharm # noqa:I100
1332+import ops.main
1333+from ops.testing import Harness
1334+
1335+TEST_KUBE_CONTOL_RELATION_DATA = {"creds":
1336+ """{"system:node:juju-62684f-0":
1337+ {"client_token": "DECAFBADBEEF",
1338+ "kubelet_token": "ABCDEF012345",
1339+ "proxy_token": "BADC0FFEEDAD",
1340+ "scope": "kubernetes-worker/0"}
1341+ }""" # noqa:E127
1342+ }
1343+TEST_KUBE_API_ENDPOINT_RELATION_DATA = {"hostname": "1.1.1.1",
1344+ "port": "1111"}
1345+
1346+
1347+class TestKubernetes_Service_ChecksCharm(unittest.TestCase): # noqa:N801
1348+ """Test Kubernetes Service Checks Charm Code."""
1349+
1350+ @classmethod
1351+ def setUpClass(cls):
1352+ """Prepare class fixture."""
1353+ # Stop unit test from calling fchown
1354+ fchown_patcher = mock.patch("os.fchown")
1355+ cls.mock_fchown = fchown_patcher.start()
1356+ chown_patcher = mock.patch("os.chown")
1357+ cls.mock_chown = chown_patcher.start()
1358+
1359+ # Stop charmhelpers host from logging via debug log
1360+ host_log_patcher = mock.patch("charmhelpers.core.host.log")
1361+ cls.mock_juju_log = host_log_patcher.start()
1362+
1363+ # Stop charmhelpers snap from logging via debug log
1364+ snap_log_patcher = mock.patch("charmhelpers.fetch.snap.log")
1365+ cls.mock_snap_log = snap_log_patcher.start()
1366+
1367+ charm_logger_patcher = mock.patch("charm.logging")
1368+ cls.mock_charm_log = charm_logger_patcher.start()
1369+
1370+ lib_logger_patcher = mock.patch("lib.lib_kubernetes_service_checks.logging")
1371+ cls.mock_lib_logger = lib_logger_patcher.start()
1372+
1373+ # Prevent charmhelpers from calling systemctl
1374+ host_service_patcher = mock.patch("charmhelpers.core.host.service_stop")
1375+ cls.mock_service_stop = host_service_patcher.start()
1376+ host_service_patcher = mock.patch("charmhelpers.core.host.service_start")
1377+ cls.mock_service_start = host_service_patcher.start()
1378+ host_service_patcher = mock.patch("charmhelpers.core.host.service_restart")
1379+ cls.mock_service_restart = host_service_patcher.start()
1380+
1381+ # Setup mock JUJU Environment variables
1382+ os.environ["JUJU_UNIT_NAME"] = "mock/0"
1383+ os.environ["JUJU_CHARM_DIR"] = "."
1384+
1385+ def setUp(self):
1386+ """Prepare tests."""
1387+ self.harness = Harness(Kubernetes_Service_ChecksCharm)
1388+ # Mock config_get to return default config
1389+ with open(ops.main._get_charm_dir() / Path("config.yaml"), "r") as config_file:
1390+ config = yaml.safe_load(config_file)
1391+ charm_config = {}
1392+
1393+ for key, _ in config["options"].items():
1394+ charm_config[key] = config["options"][key]["default"]
1395+
1396+ self.harness._backend._config = charm_config
1397+
1398+ def test_harness(self):
1399+ """Verify harness."""
1400+ self.harness.begin()
1401+ self.assertFalse(self.harness.charm.state.installed)
1402+
1403+ @mock.patch("charmhelpers.fetch.snap.subprocess.check_call")
1404+ def test_install(self, mock_snap_subprocess):
1405+ """Test response to an install event."""
1406+ mock_snap_subprocess.return_value = 0
1407+ mock_snap_subprocess.side_effect = None
1408+
1409+ self.harness.begin()
1410+ self.harness.charm.on.install.emit()
1411+
1412+ self.assertEqual(self.harness.charm.unit.status.name, "maintenance")
1413+ self.assertEqual(self.harness.charm.unit.status.message, "Install complete")
1414+ self.assertTrue(self.harness.charm.state.installed)
1415+
1416+ def test_config_changed(self):
1417+ """Test response to config changed event."""
1418+ self.harness.set_leader(True)
1419+ self.harness.populate_oci_resources()
1420+ self.harness.begin()
1421+ self.harness.charm.check_charm_status = mock.MagicMock()
1422+ self.harness.charm.state.installed = True
1423+ self.harness.charm.on.config_changed.emit()
1424+ self.harness.charm.check_charm_status.assert_called_once()
1425+
1426+ def test_start_not_installed(self):
1427+ """Test response to start event without install state."""
1428+ self.harness.begin()
1429+ self.harness.charm.on.start.emit()
1430+ self.assertFalse(self.harness.charm.state.started)
1431+
1432+ def test_start_not_configured(self):
1433+ """Test response to start event without configured state."""
1434+ self.harness.begin()
1435+ self.harness.charm.state.installed = True
1436+ self.harness.charm.on.start.emit()
1437+ self.assertFalse(self.harness.charm.state.started)
1438+
1439+ def test_start(self):
1440+ """Test response to start event."""
1441+ self.harness.begin()
1442+ self.harness.charm.state.installed = True
1443+ self.harness.charm.state.configured = True
1444+ self.harness.charm.on.start.emit()
1445+ self.assertTrue(self.harness.charm.state.started)
1446+ self.assertEqual(self.harness.charm.unit.status.name, "active")
1447+
1448+ def test_on_kube_api_endpoint_relation_changed(self):
1449+ """Check kube-api-endpoint relation changed handling."""
1450+ relation_id = self.harness.add_relation('kube-api-endpoint', 'kubernetes-master')
1451+ remote_unit = "kubernetes-master/0"
1452+ self.harness.begin()
1453+ self.harness.charm.check_charm_status = mock.MagicMock()
1454+ self.harness.add_relation_unit(relation_id, remote_unit)
1455+ self.harness.update_relation_data(relation_id, remote_unit, TEST_KUBE_API_ENDPOINT_RELATION_DATA)
1456+
1457+ self.harness.charm.check_charm_status.assert_called_once()
1458+ self.assertEqual(self.harness.charm.helper.kubernetes_api_address, "1.1.1.1")
1459+ self.assertEqual(self.harness.charm.helper.kubernetes_api_port, "1111")
1460+
1461+ def test_on_kube_control_relation_changed(self):
1462+ """Check kube-control relation changed handling."""
1463+ relation_id = self.harness.add_relation('kube-control', 'kubernetes-master')
1464+ remote_unit = "kubernetes-master/0"
1465+ self.harness.begin()
1466+ self.harness.charm.check_charm_status = mock.MagicMock()
1467+ self.harness.add_relation_unit(relation_id, remote_unit)
1468+ self.harness.update_relation_data(relation_id, remote_unit, TEST_KUBE_CONTOL_RELATION_DATA)
1469+
1470+ self.harness.charm.check_charm_status.assert_called_once()
1471+ assert self.harness.charm.helper.kubernetes_client_token == "DECAFBADBEEF"
1472+
1473+ def test_nrpe_external_master_relation_joined(self):
1474+ """Check that nrpe.configure is True after nrpe relation joined."""
1475+ relation_id = self.harness.add_relation('nrpe-external-master', 'nrpe')
1476+ remote_unit = "nrpe/0"
1477+ self.harness.begin()
1478+ self.assertFalse(self.harness.charm.state.nrpe_configured)
1479+ self.harness.charm.check_charm_status = mock.MagicMock()
1480+ self.harness.add_relation_unit(relation_id, remote_unit)
1481+
1482+ self.harness.charm.check_charm_status.assert_called_once()
1483+ self.assertTrue(self.harness.charm.state.nrpe_configured)
1484+
1485+ @mock.patch("ops.model.RelationData")
1486+ def test_nrpe_external_master_relation_departed(self, mock_relation_data):
1487+ """Check that nrpe.configure is False after nrpe relation departed."""
1488+ mock_relation_data.return_value.__getitem__.return_value = {}
1489+ self.harness.begin()
1490+ self.harness.charm.check_charm_status = mock.MagicMock()
1491+ self.emit("nrpe_external_master_relation_departed")
1492+ self.harness.charm.check_charm_status.assert_called_once()
1493+
1494+ self.assertFalse(self.harness.charm.state.nrpe_configured)
1495+
1496+ def test_check_charm_status_kube_api_endpoint_relation_missing(self):
1497+ """Check that the chatm blocks without kube-api-endpoint relation."""
1498+ self.harness.begin()
1499+ self.harness.charm.state.kube_control.update(TEST_KUBE_CONTOL_RELATION_DATA)
1500+ self.harness.charm.state.nrpe_configured = True
1501+ self.harness.charm.check_charm_status()
1502+
1503+ self.assertFalse(self.harness.charm.state.configured)
1504+ self.assertEqual(self.harness.charm.unit.status.name, "blocked")
1505+ self.assertEqual(self.harness.charm.unit.status.message, "missing kube-api-endpoint relation")
1506+
1507+ def test_check_charm_status_kube_control_relation_missing(self):
1508+ """Check that the charm blocks without kube-control relation."""
1509+ self.harness.begin()
1510+ self.harness.charm.state.kube_api_endpoint.update(TEST_KUBE_API_ENDPOINT_RELATION_DATA)
1511+ self.harness.charm.state.nrpe_configured = True
1512+ self.harness.charm.check_charm_status()
1513+
1514+ self.assertFalse(self.harness.charm.state.configured)
1515+ self.assertEqual(self.harness.charm.unit.status.name, "blocked")
1516+ self.assertEqual(self.harness.charm.unit.status.message, "missing kube-control relation")
1517+
1518+ def test_check_charm_status_nrpe_relation_missing(self):
1519+ """Check that the charm bloack without nrpe relation."""
1520+ self.harness.begin()
1521+ self.harness.charm.state.kube_control.update(TEST_KUBE_CONTOL_RELATION_DATA)
1522+ self.harness.charm.state.kube_api_endpoint.update(TEST_KUBE_API_ENDPOINT_RELATION_DATA)
1523+ self.harness.charm.check_charm_status()
1524+
1525+ self.assertFalse(self.harness.charm.state.configured)
1526+ self.assertEqual(self.harness.charm.unit.status.name, "blocked")
1527+ self.assertEqual(self.harness.charm.unit.status.message, "missing nrpe-external-master relation")
1528+
1529+ def test_check_charm_status_configured(self):
1530+ """Check the charm becomes configured."""
1531+ self.harness.begin()
1532+ self.harness.charm.helper.configure = mock.MagicMock()
1533+ self.harness.charm.state.kube_control.update(TEST_KUBE_CONTOL_RELATION_DATA)
1534+ self.harness.charm.state.kube_api_endpoint.update(TEST_KUBE_API_ENDPOINT_RELATION_DATA)
1535+ self.harness.charm.state.nrpe_configured = True
1536+ self.harness.charm.check_charm_status()
1537+
1538+ self.harness.charm.helper.configure.assert_called_once()
1539+ self.assertTrue(self.harness.charm.state.configured)
1540+
1541+ def emit(self, event):
1542+ """Emit the named hook on the charm."""
1543+ self.harness.charm.framework.reemit()
1544+
1545+ if "_relation_" in event:
1546+ relation_name = event.split("_relation")[0].replace("_", "-")
1547+ with mock.patch.dict(
1548+ "os.environ",
1549+ {
1550+ "JUJU_RELATION": relation_name,
1551+ "JUJU_RELATION_ID": "1",
1552+ "JUJU_REMOTE_APP": "mock",
1553+ "JUJU_REMOTE_UNIT": "mock/0",
1554+ },
1555+ ):
1556+ ops.main._emit_charm_event(self.harness.charm, event)
1557+ else:
1558+ ops.main._emit_charm_event(self.harness.charm, event)
1559+
1560+
1561+if __name__ == "__main__":
1562+ unittest.main()
1563diff --git a/tests/unit/test_lib_ksc.py b/tests/unit/test_lib_ksc.py
1564new file mode 100644
1565index 0000000..b8586bf
1566--- /dev/null
1567+++ b/tests/unit/test_lib_ksc.py
1568@@ -0,0 +1,171 @@
1569+"""Tests for Kubernetes Service Checks Helper."""
1570+import base64
1571+import os
1572+import subprocess
1573+from subprocess import CalledProcessError
1574+import tempfile
1575+import unittest
1576+
1577+from lib import lib_kubernetes_service_checks
1578+import mock
1579+import yaml
1580+
1581+
1582+TEST_CERTIFICATE = """-----BEGIN CERTIFICATE-----
1583+MIIDOzCCAiOgAwIBAgIJAPoOXrIwH+miMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
1584+BAMMDTEwLjEzMi4yNTEuNjAwHhcNMjAwNzE3MTMzMzI0WhcNMzAwNzE1MTMzMzI0
1585+WjAYMRYwFAYDVQQDDA0xMC4xMzIuMjUxLjYwMIIBIjANBgkqhkiG9w0BAQEFAAOC
1586+AQ8AMIIBCgKCAQEAqpYVlmT/eRBhCKHaqXjY6EAzvx5GZY0PhL/YGBl9uF8YQGEF
1587+F3k3Ec7pyJMIQblmWxdCPd1uNzHU8mwApiuPG9GtYOK+olqgslLsmOU9LTi6KJWX
1588+x956VxdefXDYvr0B6K/Hdgkb1x//XwvipSV1fZ1MCDIiP/hWKi4CmEq31sVpCBdp
1589+Uiz3qdCzsiGt0f4kbgIJSVtxhWlNJ5MaCOm7gXafkF8OIUTmWhmPp2gH7pfPzzl1
1590+glOX2Z41qwPuz7Jbcxx/z/yGjdPeJTQYoqJfpDpCrT2er5xyRf66HqKx9Ld/FiqM
1591+ZksRwmzF9WvqCBK8WoRmnvFxk1FZPGt6E5gotwIDAQABo4GHMIGEMB0GA1UdDgQW
1592+BBSUCCmRxb4tKD6w8jZ3hHs4ciFizDBIBgNVHSMEQTA/gBSUCCmRxb4tKD6w8jZ3
1593+hHs4ciFizKEcpBowGDEWMBQGA1UEAwwNMTAuMTMyLjI1MS42MIIJAPoOXrIwH+mi
1594+MAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQBv
1595+BYwILWI/4dGczqG0hcqt8tW04Oi+7y0HxzeI/oaUq/HKfvCz5a+WhpykMKRDJoaZ
1596+aejR2Oc7A0OUnenpvMeIiMcUIetM3Q1Gzx0aU+vqUNNaZlooSSbe3z1VK6bUsYDo
1597+qdKhs+mSyuEticK2SEWjT+ZWpV1rjSd5zRZ/UvC1ZhDNJGZotIIqryQWd3YfYl9l
1598+7JrdzUVCbxs4ywxNp9/I+MJEiBfMHQx8FWr1M2HvLDAm6NZLfM68y5FClzfGpopV
1599+0ARirz1AfbS6xUumyXHOH2qH527PUXFdfYGSn+juDG/dRTENYJ3OPAfWdj4ze1qQ
1600+n3ajLSYPvdyKaztdB1VL
1601+-----END CERTIFICATE-----"""
1602+
1603+
1604+class TestLibKSCHelper(unittest.TestCase):
1605+ """Unittest class for Kubernetes Service Checks Helper."""
1606+
1607+ @classmethod
1608+ def setUpClass(cls):
1609+ """Prepare Class Fixture."""
1610+ # Load default config
1611+ with open("./config.yaml") as default_config:
1612+ cls.config = yaml.safe_load(default_config)
1613+
1614+ # set defaults to the config object
1615+ for key in cls.config["options"]:
1616+ if "default" in cls.config["options"][key]:
1617+ cls.config[key] = cls.config["options"][key]["default"]
1618+
1619+ # Create test state object
1620+ class FakeStateObject(object):
1621+ kube_api_endpoint = {"hostname": "1.1.1.1",
1622+ "port": "1111"}
1623+ kube_control = {"creds": """{"kube-client": {"client_token": "abcdef0123456789"}}"""}
1624+ installed = False
1625+ configured = False
1626+ started = False
1627+ nrpe_configured = False
1628+
1629+ cls.state = FakeStateObject()
1630+
1631+ # Stop unit test from calling fchown
1632+ fchown_patcher = mock.patch("os.fchown")
1633+ cls.mock_fchown = fchown_patcher.start()
1634+ chown_patcher = mock.patch("os.chown")
1635+ cls.mock_chown = chown_patcher.start()
1636+
1637+ # Stop charmhelpers host from logging via debug log
1638+ host_log_patcher = mock.patch("charmhelpers.core.host.log")
1639+ cls.mock_juju_log = host_log_patcher.start()
1640+
1641+ host_logger_patcher = mock.patch("lib.lib_kubernetes_service_checks.logging")
1642+ cls.mock_logger = host_logger_patcher.start()
1643+
1644+ # Stop charmhelpers snap from logging via debug log
1645+ snap_log_patcher = mock.patch("charmhelpers.fetch.snap.log")
1646+ cls.mock_snap_log = snap_log_patcher.start()
1647+
1648+ # Setup a tmpdir
1649+ cls.tmpdir = tempfile.TemporaryDirectory()
1650+ cls.cert_path = os.path.join(
1651+ cls.tmpdir.name,
1652+ "kubernetes-service-checks.crt"
1653+ )
1654+
1655+ lib_kubernetes_service_checks.CERT_FILE = cls.cert_path
1656+ lib_kubernetes_service_checks.NAGIOS_PLUGINS_DIR = cls.tmpdir.name
1657+
1658+ @classmethod
1659+ def tearDownClass(cls):
1660+ """Tear down class fixture."""
1661+ mock.patch.stopall()
1662+ cls.tmpdir.cleanup()
1663+
1664+ def setUp(self):
1665+ """Prepare test fixture."""
1666+ self.helper = lib_kubernetes_service_checks.KSCHelper(self.config,
1667+ self.state)
1668+
1669+ def tearDown(self):
1670+ """Clean up test fixture."""
1671+ try:
1672+ os.remove(self.cert_path)
1673+ except FileNotFoundError:
1674+ pass
1675+
1676+ def test_kube_api_endpoint_properties(self):
1677+ """Test that hostname and port properties get passed through."""
1678+ # kube_api_endpoint (relation) -> hostname & port
1679+ self.assertEqual(self.helper.kubernetes_api_address, "1.1.1.1")
1680+ self.assertEqual(self.helper.kubernetes_api_port, "1111")
1681+
1682+ self.helper.state.kube_api_endpoint = {}
1683+ self.assertEqual(self.helper.kubernetes_api_address, None)
1684+ self.assertEqual(self.helper.kubernetes_api_port, None)
1685+
1686+ def test_kube_control_endpoint_properties(self):
1687+ """Test KSCHelper client_token gets passed though."""
1688+ # kube-control (relation) -> kube client token
1689+ self.assertEqual(self.helper.kubernetes_client_token, "abcdef0123456789")
1690+
1691+ self.helper.state.kube_control = {}
1692+ self.assertEqual(self.helper.kubernetes_client_token, None)
1693+
1694+ @mock.patch("lib.lib_kubernetes_service_checks.subprocess.call")
1695+ def test_update_tls_certificates(self, mock_subprocess):
1696+ """Test that SSL certificates get updated."""
1697+ # returns False when no available trusted_ssl_cert
1698+ self.assertFalse(self.helper.update_tls_certificates())
1699+
1700+ # returns True when subprocess successful
1701+ self.helper.config["trusted_ssl_ca"] = base64.b64encode(str.encode(TEST_CERTIFICATE))
1702+ self.assertTrue(self.helper.update_tls_certificates())
1703+ with open(self.cert_path, "r") as f:
1704+ self.assertEqual(f.read(), TEST_CERTIFICATE)
1705+ mock_subprocess.assert_called_once_with(['/usr/sbin/update-ca-certificates'])
1706+ mock_subprocess.reset_mock()
1707+
1708+ # returns false when subprocess hits an exception
1709+ mock_subprocess.side_effect = CalledProcessError("Command", "Mock Subprocess Call Error")
1710+ self.assertFalse(self.helper.update_tls_certificates())
1711+
1712+ def test_render_checks(self):
1713+ """Test that NPRE is called to add KSC checks."""
1714+ # TODO
1715+ pass
1716+
1717+ @mock.patch("charmhelpers.fetch.snap.subprocess.check_call")
1718+ def test_install_kubectl(self, mock_snap_subprocess):
1719+ """Test install kubectl snap helper function."""
1720+ self.assertTrue(self.helper.install_kubectl())
1721+ channel = self.config.get("channel")
1722+ mock_snap_subprocess.assert_called_with(["snap",
1723+ "install",
1724+ "--classic",
1725+ "--channel={}".format(channel),
1726+ "kubectl"], env=os.environ)
1727+
1728+ @mock.patch("charmhelpers.fetch.snap.subprocess.check_call")
1729+ def test_install_snap_failure(self, mock_snap_subprocess):
1730+ """Test response to a failed install event."""
1731+ error = subprocess.CalledProcessError("cmd", "Install failed")
1732+ error.returncode = 1
1733+ mock_snap_subprocess.return_value = 1
1734+ mock_snap_subprocess.side_effect = error
1735+ self.assertFalse(self.helper.install_kubectl())
1736+
1737+
1738+if __name__ == "__main__":
1739+ unittest.main()
1740diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py
1741new file mode 100644
1742index 0000000..d229902
1743--- /dev/null
1744+++ b/tests/unit/test_plugins.py
1745@@ -0,0 +1,95 @@
1746+"""Unit tests for Kubernetes Service Checks NRPE Plugins."""
1747+import unittest
1748+
1749+import check_kubernetes_api
1750+import mock
1751+
1752+
1753+class TestKSCPlugins(unittest.TestCase):
1754+ """Test cases for Kubernetes Service Checks NRPE plugins."""
1755+
1756+ @mock.patch("check_kubernetes_api.sys.exit")
1757+ @mock.patch("check_kubernetes_api.print")
1758+ def test_nagios_exit(self, mock_print, mock_sys_exit):
1759+ """Test the nagios_exit function."""
1760+ msg = "Test message"
1761+ for code, status in check_kubernetes_api.NAGIOS_STATUS.items():
1762+ expected_output = "{}: {}".format(status, msg)
1763+ check_kubernetes_api.nagios_exit(code, msg)
1764+
1765+ mock_print.assert_called_with(expected_output)
1766+ mock_sys_exit.assert_called_with(code)
1767+
1768+ @mock.patch("check_kubernetes_api.urllib3.PoolManager")
1769+ @mock.patch("check_kubernetes_api.os.path.exists")
1770+ def test_kubernetes_health_ssl(self,
1771+ mock_os_path_exists,
1772+ mock_http_pool_manager):
1773+ """Test the check k8s health function called with expected ssl params."""
1774+ host_address = "https://1.1.1.1:1111"
1775+ token = "0123456789abcdef"
1776+ disable_ssl = True
1777+
1778+ mock_http_pool_manager.return_value.status = 200
1779+ mock_http_pool_manager.return_value.data = b"ok"
1780+
1781+ check_kubernetes_api.check_kubernetes_health(
1782+ host_address,
1783+ token,
1784+ disable_ssl
1785+ )
1786+ mock_http_pool_manager.assert_called_with(cert_reqs='CERT_NONE', assert_hostname=False)
1787+
1788+ disable_ssl = False
1789+ mock_os_path_exists.return_value = True
1790+ check_kubernetes_api.check_kubernetes_health(
1791+ host_address,
1792+ token,
1793+ disable_ssl
1794+ )
1795+ mock_http_pool_manager.assert_called_with()
1796+
1797+ @mock.patch("check_kubernetes_api.urllib3.PoolManager")
1798+ @mock.patch("check_kubernetes_api.os.path.exists")
1799+ def test_kubernetes_health_status(self,
1800+ mock_os_path_exists,
1801+ mock_http_pool_manager):
1802+ """Test kubernetes health function."""
1803+ host_address = "https://1.1.1.1:1111"
1804+ token = "0123456789abcdef"
1805+ ssl_ca = "test/cert/path"
1806+
1807+ mock_os_path_exists.return_value = True
1808+ mock_http_pool_manager.return_value.request.return_value.status = 200
1809+ mock_http_pool_manager.return_value.request.return_value.data = b"ok"
1810+
1811+ # verify status OK
1812+ status, _ = check_kubernetes_api.check_kubernetes_health(
1813+ host_address,
1814+ token,
1815+ ssl_ca
1816+ )
1817+ self.assertEqual(status, check_kubernetes_api.NAGIOS_STATUS_OK)
1818+ mock_http_pool_manager.return_value.request.assert_called_once_with(
1819+ "GET",
1820+ "{}/healthz".format(host_address),
1821+ headers={"Authorization": "Bearer {}".format(token)}
1822+ )
1823+
1824+ mock_http_pool_manager.return_value.request.return_value.status = 500
1825+ mock_http_pool_manager.return_value.request.return_value.data = b"ok"
1826+ status, _ = check_kubernetes_api.check_kubernetes_health(
1827+ host_address,
1828+ token,
1829+ ssl_ca
1830+ )
1831+ self.assertEqual(status, check_kubernetes_api.NAGIOS_STATUS_CRITICAL)
1832+
1833+ mock_http_pool_manager.return_value.request.return_value.status = 200
1834+ mock_http_pool_manager.return_value.request.return_value.data = b"not ok"
1835+ status, _ = check_kubernetes_api.check_kubernetes_health(
1836+ host_address,
1837+ token,
1838+ ssl_ca
1839+ )
1840+ self.assertEqual(status, check_kubernetes_api.NAGIOS_STATUS_WARNING)
1841diff --git a/tox.ini b/tox.ini
1842new file mode 100644
1843index 0000000..2d5bb8f
1844--- /dev/null
1845+++ b/tox.ini
1846@@ -0,0 +1,49 @@
1847+[tox]
1848+skipsdist = True
1849+envlist = unit, func
1850+skip_missing_interpreters = True
1851+
1852+[testenv]
1853+basepython = python3
1854+setenv =
1855+ PYTHONPATH = {toxinidir}/lib/:{toxinidir}
1856+passenv =
1857+ HOME
1858+ MODEL_SETTINGS
1859+ CHARM_BUILD_DIR
1860+
1861+[testenv:unit]
1862+commands =
1863+ coverage run -m unittest discover -s {toxinidir}/tests/unit -v
1864+ coverage report \
1865+ --omit tests/*,mod/*,.tox/*
1866+ coverage html \
1867+ --omit tests/*,mod/*,.tox/*
1868+deps = -r{toxinidir}/tests/unit/requirements.txt
1869+
1870+[testenv:func]
1871+changedir = {toxinidir}/tests/functional
1872+commands = functest-run-suite {posargs}
1873+deps = -r{toxinidir}/tests/functional/requirements.txt
1874+
1875+[testenv:lint]
1876+commands = flake8
1877+deps =
1878+ flake8
1879+ flake8-docstrings
1880+ flake8-import-order
1881+ pep8-naming
1882+ flake8-colors
1883+
1884+[flake8]
1885+exclude =
1886+ .git,
1887+ __pycache__,
1888+ .tox,
1889+ mod,
1890+max-line-length = 120
1891+max-complexity = 10
1892+import-order-style = google
1893+
1894+[isort]
1895+force_to_top=setuppath

Subscribers

People subscribed via source and target branches

to all changes: