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: 1993 lines (+1818/-1)
28 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 (+162/-0)
lib/ops (+1/-0)
metadata.yaml (+26/-0)
mod/.empty (+0/-0)
mod/charm-helpers (+1/-0)
src/charm.py (+196/-0)
src/setuppath.py (+3/-0)
tests/functional/requirements.txt (+1/-0)
tests/functional/tests/bundles/bionic.yaml (+90/-0)
tests/functional/tests/bundles/focal.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 (+243/-0)
tests/unit/test_lib_ksc.py (+171/-0)
tests/unit/test_plugins.py (+89/-0)
tox.ini (+52/-0)
Reviewer Review Type Date Requested Status
Adam Dyess (community) Approve
Giuseppe Petralia Pending
Review via email: mp+387615@code.launchpad.net

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

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

Commit message

Initial working Kubernetes Service Checks Charm.

Description of the change

Initial charm for Kubernetes Service Checks

2 NRPE checks included:
  - check_http for cert expirations
  - check_k8s_api_health for api responding

3 Relations required:
  - nrpe:nrpe-external-master
  - kubernetes-master:kube-api-endpoint
  - kubernetes-master:kube-control

Charm will block if the relations are not configured/ or the required data is missing.

+ Unit tests (passing)
+ Functional tests (passing)
+ lint (passing)

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 : Posted in a previous version of this proposal

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
Revision history for this message
Ryan Farrell (whereisrysmind) wrote :

- Fixed issues with nagios_context, and nagios_servicegroups- these configs params are used internally in the charmhelpers NRPE code.
- Fixed missing base64 decode of the SSL cert prior to writing it to disk, since it should be base64 encoded as described in the config)
- Tested that urllib will honor the systems certs without specifying any path. The plugin will work but issue a warning if host key checking is disabled. The charm handles the logic to flag that or not.
- Tested plugin for checking k8s health- it turns out it does require the client-key provided by the kube-control relation, thusly this relation is still required until we can get an improved single relation from the k8s team.
  - Plugins for kube api responding and certificate expiration are both working and uploaded to the correct location for NRPE.

Revision history for this message
Adam Dyess (addyess) wrote :

I didn't find any bugs... only nits on style, doc strings, and the like

review: Approve
296fbef... by Ryan Farrell

Some cleanup

- Merge some docstrings with neighboring comments
- Added Black section to Makefile
- Removed repeated set status calls in the charm

184e030... by Ryan Farrell

Added focal functional test bundle

05632f6... by Ryan Farrell

Replaced eval with json.loads

Unmerged commits

05632f6... by Ryan Farrell

Replaced eval with json.loads

184e030... by Ryan Farrell

Added focal functional test bundle

296fbef... by Ryan Farrell

Some cleanup

- Merge some docstrings with neighboring comments
- Added Black section to Makefile
- Removed repeated set status calls in the charm

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.

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..17cda3f
431--- /dev/null
432+++ b/lib/lib_kubernetes_service_checks.py
433@@ -0,0 +1,162 @@
434+"""Kubernetes Service Checks Helper Library."""
435+import base64
436+import json
437+import logging
438+import os
439+import subprocess
440+
441+from charmhelpers.contrib.charmsupport.nrpe import NRPE
442+from charmhelpers.core import hookenv, host
443+from charmhelpers.fetch import snap
444+
445+CERT_FILE = "/usr/local/share/ca-certificates/kubernetes-service-checks.crt"
446+NAGIOS_PLUGINS_DIR = "/usr/local/lib/nagios/plugins/"
447+
448+
449+class KSCHelper():
450+ """Kubernetes Service Checks Helper Class."""
451+
452+ def __init__(self, config, state):
453+ """Initialize the Helper with the charm config and state."""
454+ self.config = config
455+ self.state = state
456+
457+ @property
458+ def kubernetes_api_address(self):
459+ """Get kubernetes api hostname."""
460+ return self.state.kube_api_endpoint.get("hostname", None)
461+
462+ @property
463+ def kubernetes_api_port(self):
464+ """Get kubernetes api port."""
465+ return self.state.kube_api_endpoint.get("port", None)
466+
467+ @property
468+ def kubernetes_client_token(self):
469+ """Get kubernetes client token."""
470+ try:
471+ data = json.loads(self.state.kube_control.get("creds", "{}"))
472+ except json.decoder.JSONDecodeError:
473+ data = {}
474+ for creds in data.values():
475+ token = creds.get("client_token", None)
476+ if token:
477+ return token
478+ return None
479+
480+ @property
481+ def use_tls_cert(self):
482+ """Check if SSL cert is provided for use."""
483+ return bool(self._ssl_certificate)
484+
485+ @property
486+ def _ssl_certificate(self):
487+ # TODO: Expand this later to take a cert from a relation or from the config.
488+ # cert from the relation is to be prioritized
489+ ssl_cert = self.config.get("trusted_ssl_ca", None)
490+ if ssl_cert:
491+ ssl_cert = ssl_cert.strip()
492+ return ssl_cert
493+
494+ @property
495+ def ssl_cert_path(self):
496+ """Get cert file path."""
497+ return CERT_FILE
498+
499+ @property
500+ def plugins_dir(self):
501+ """Get nagios plugins directory."""
502+ return NAGIOS_PLUGINS_DIR
503+
504+ def restart_nrpe_service(self):
505+ """Restart nagios-nrpe-server service."""
506+ host.service_restart('nagios-nrpe-server')
507+
508+ def update_tls_certificates(self):
509+ """Write the trusted ssl certificate to the CERT_FILE."""
510+ if self._ssl_certificate:
511+ cert_content = base64.b64decode(self._ssl_certificate).decode()
512+ try:
513+ logging.debug('Writing ssl ca cert to {}'.format(self.ssl_cert_path))
514+ with open(self.ssl_cert_path, "w") as f:
515+ f.write(cert_content)
516+ subprocess.call(['/usr/sbin/update-ca-certificates'])
517+ return True
518+ except subprocess.CalledProcessError as e:
519+ logging.error(e)
520+ return False
521+ except PermissionError as e:
522+ logging.error(e)
523+ return False
524+ else:
525+ logging.error("Trusted SSL Certificate is not defined")
526+ return False
527+
528+ def configure(self):
529+ """Refresh configuration data."""
530+ self.update_plugins()
531+ self.render_checks()
532+
533+ def update_plugins(self):
534+ """Rsync plugins to the plugin directory."""
535+ charm_plugin_dir = os.path.join(hookenv.charm_dir(), "files", "plugins/")
536+ host.rsync(charm_plugin_dir, self.plugins_dir, options=["--executability"])
537+
538+ def render_checks(self):
539+ """Render nrpe checks."""
540+ nrpe = NRPE()
541+ if not os.path.exists(self.plugins_dir):
542+ os.makedirs(self.plugins_dir)
543+
544+ # register basic api health check
545+ check_k8s_plugin = os.path.join(self.plugins_dir, "check_kubernetes_api.py")
546+ for check in ["health"]:
547+ check_command = "{} -H {} -P {} -T {} --check {}".format(
548+ check_k8s_plugin,
549+ self.kubernetes_api_address,
550+ self.kubernetes_api_port,
551+ self.kubernetes_client_token,
552+ check
553+ ).strip()
554+ if not self.use_tls_cert:
555+ check_command += " -d"
556+
557+ nrpe.add_check(
558+ shortname="k8s_api_{}".format(check),
559+ description="Check Kubernetes API ({})".format(check),
560+ check_cmd=check_command,
561+ )
562+
563+ # register k8s host certificate expiration check
564+ check_http_plugin = "/usr/lib/nagios/plugins/check_http"
565+ check_command = "{} -I {} -p {} -C {},{}".format(
566+ check_http_plugin,
567+ self.kubernetes_api_address,
568+ self.kubernetes_api_port,
569+ self.config.get("tls_warn_days"),
570+ self.config.get("tls_crit_days")
571+ ).strip()
572+ nrpe.add_check(
573+ shortname="k8s_api_cert_expiration",
574+ description="Check Kubernetes API ({})".format(check),
575+ check_cmd=check_command,
576+ )
577+ nrpe.write()
578+
579+ def install_kubectl(self):
580+ """Attempt to install kubectl.
581+
582+ :returns: bool, indicating whether or not successful
583+ """
584+ # snap retry is excessive
585+ snap.SNAP_NO_LOCK_RETRY_DELAY = 0.5
586+ snap.SNAP_NO_LOCK_RETRY_COUNT = 3
587+ try:
588+ channel = self.config.get('channel')
589+ snap.snap_install("kubectl",
590+ "--classic",
591+ "--channel={}".format(channel)
592+ )
593+ return True
594+ except snap.CouldNotAcquireLockException:
595+ return False
596diff --git a/lib/ops b/lib/ops
597new file mode 120000
598index 0000000..d934193
599--- /dev/null
600+++ b/lib/ops
601@@ -0,0 +1 @@
602+../mod/operator/ops
603\ No newline at end of file
604diff --git a/metadata.yaml b/metadata.yaml
605new file mode 100644
606index 0000000..09b26a5
607--- /dev/null
608+++ b/metadata.yaml
609@@ -0,0 +1,26 @@
610+name: kubernetes-service-checks
611+summary: Kubernetes Services NRPE Checks
612+maintainers:
613+ - Bootstack Charmers <bootstack-charmers@lists.canonical.com>
614+description: |
615+ This charm provides NRPE Checks verifying Kubernetes API accessibility
616+ and integrates with Nagios for timely alerting.
617+tags:
618+ - kubernetes
619+ - ops
620+ - monitoring
621+series:
622+ - focal
623+ - bionic
624+ - xenial
625+requires:
626+ kube-control:
627+ interface: kube-control
628+ kube-api-endpoint:
629+ interface: http
630+provides:
631+ nrpe-external-master:
632+ interface: nrpe-external-master
633+ scope: container
634+ optional: true
635+subordinate: false
636diff --git a/mod/.empty b/mod/.empty
637new file mode 100644
638index 0000000..e69de29
639--- /dev/null
640+++ b/mod/.empty
641diff --git a/mod/charm-helpers b/mod/charm-helpers
642new file mode 160000
643index 0000000..4b3602e
644--- /dev/null
645+++ b/mod/charm-helpers
646@@ -0,0 +1 @@
647+Subproject commit 4b3602e2bdf101bf58cc808264ec0c8092a67cd0
648diff --git a/src/charm.py b/src/charm.py
649new file mode 100755
650index 0000000..5b873ba
651--- /dev/null
652+++ b/src/charm.py
653@@ -0,0 +1,196 @@
654+#! /usr/bin/env python3
655+# -*- coding: utf-8 -*-
656+# vim:fenc=utf-8
657+# Copyright © 2020 Bootstack Charmers bootstack-charmers@lists.canonical.com
658+
659+"""Operator Charm main library."""
660+# Load modules from lib directory
661+import logging
662+
663+import setuppath # noqa:F401
664+
665+from lib_kubernetes_service_checks import KSCHelper # noqa:I100
666+from ops.charm import CharmBase
667+from ops.framework import StoredState
668+from ops.main import main
669+from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
670+
671+
672+class KubernetesServiceChecksCharm(CharmBase):
673+ """Class representing this Operator charm."""
674+
675+ state = StoredState()
676+
677+ def __init__(self, *args):
678+ """Initialize charm and configure states and events to observe."""
679+ super().__init__(*args)
680+ # -- standard hook observation
681+ self.framework.observe(self.on.install, self.on_install)
682+ self.framework.observe(self.on.start, self.on_start)
683+ self.framework.observe(self.on.config_changed, self.on_config_changed)
684+ self.framework.observe(
685+ self.on.kube_api_endpoint_relation_changed,
686+ self.on_kube_api_endpoint_relation_changed,
687+ )
688+ self.framework.observe(
689+ self.on.kube_api_endpoint_relation_departed,
690+ self.on_kube_api_endpoint_relation_departed
691+ )
692+ self.framework.observe(
693+ self.on.kube_control_relation_changed,
694+ self.on_kube_control_relation_changed,
695+ )
696+ self.framework.observe(
697+ self.on.kube_control_relation_departed,
698+ self.on_kube_control_relation_departed
699+ )
700+ self.framework.observe(
701+ self.on.nrpe_external_master_relation_joined,
702+ self.on_nrpe_external_master_relation_joined
703+ )
704+ self.framework.observe(
705+ self.on.nrpe_external_master_relation_departed,
706+ self.on_nrpe_external_master_relation_departed
707+ )
708+ # -- initialize states --
709+ self.state.set_default(
710+ installed=False,
711+ configured=False,
712+ started=False,
713+ kube_control={},
714+ kube_api_endpoint={},
715+ nrpe_configured=False,
716+ )
717+ self.helper = KSCHelper(self.model.config, self.state)
718+
719+ def on_install(self, event):
720+ """Handle install state."""
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+ logging.info("Reinstalling for upgrade-charm hook")
728+ self.on_install(event)
729+ self.check_charm_status()
730+
731+ def check_charm_status(self):
732+ """
733+ Check that required data is available from relations.
734+
735+ - Check kube-api-endpoint relation and data available
736+ - Check kube-control relation and data available
737+ - Check nrpe-external-master is configured
738+ - Check any required config options
739+ - Finally, configure the charms checks and set flags
740+ """
741+ if not self.helper.kubernetes_api_address or not self.helper.kubernetes_api_port:
742+ logging.warning("kube-api-endpoint relation missing or misconfigured")
743+ self.unit.status = BlockedStatus("missing kube-api-endpoint relation")
744+ return
745+ if not self.helper.kubernetes_client_token:
746+ logging.warning("kube-control relation missing or misconfigured")
747+ self.unit.status = BlockedStatus("missing kube-control relation")
748+ return
749+ if not self.state.nrpe_configured:
750+ logging.warning("nrpe-external-master relation missing or misconfigured")
751+ self.unit.status = BlockedStatus("missing nrpe-external-master relation")
752+ return
753+
754+ # Check specific required config values
755+ # Set up TLS Certificate
756+ if self.helper.use_tls_cert:
757+ logging.info("Updating tls certificates")
758+ if self.helper.update_tls_certificates():
759+ logging.info("TLS Certificates updated successfully")
760+ else:
761+ logging.error("Failed to update TLS Certificates")
762+ self.unit.status = BlockedStatus("update-ca-certificates error. check logs")
763+ return
764+ else:
765+ logging.warn("No trusted_ssl_ca provided, SSL Host Authentication disabled")
766+
767+ # configure nrpe checks
768+ logging.info("Configuring Kubernetes Service Checks")
769+ self.helper.configure()
770+ if not self.state.configured:
771+ logging.info('Reloading nagios-nrpe-server')
772+ self.helper.restart_nrpe_service()
773+ self.state.configured = True
774+ self.unit.status = ActiveStatus("Unit is ready")
775+
776+ def on_config_changed(self, event):
777+ """Handle config changed."""
778+ if not self.state.installed:
779+ logging.warning("Config changed called before install complete, deferring event: {}.".format(event.handle))
780+ self._defer_once(event)
781+ return
782+ self.check_charm_status()
783+
784+ def on_start(self, event):
785+ """Handle start state."""
786+ if not self.state.configured:
787+ logging.warning("Start called before configuration complete, deferring event: {}".format(event.handle))
788+ event.defer()
789+ return
790+ self.unit.status = ActiveStatus("Unit is ready")
791+ self.state.started = True
792+ logging.info("Started")
793+
794+ def _defer_once(self, event):
795+ """Defer the given event, but only once."""
796+ notice_count = 0
797+ handle = str(event.handle)
798+
799+ for event_path, _, _ in self.framework._storage.notices(None):
800+ if event_path.startswith(handle.split('[')[0]):
801+ notice_count += 1
802+ logging.debug("Found event: {} x {}".format(event_path, notice_count))
803+
804+ if notice_count > 1:
805+ logging.debug("Not deferring {} notice count of {}".format(handle, notice_count))
806+ else:
807+ logging.debug("Deferring {} notice count of {}".format(handle, notice_count))
808+ event.defer()
809+
810+ def on_kube_api_endpoint_relation_changed(self, event):
811+ """Handle kube_api_endpoint relation changed."""
812+ self.unit.status = MaintenanceStatus("Updating K8S Endpoint")
813+ self.state.kube_api_endpoint.update(event.relation.data.get(event.unit, {}))
814+ self.check_charm_status()
815+
816+ def on_kube_api_endpoint_relation_departed(self, event):
817+ """Handle kube-api-endpoint relation departed."""
818+ self.state.configured = False
819+ for k in self.state.kube_api_endpoint.keys():
820+ self.state.kube_api_endpoint[k] = ""
821+ self.check_charm_status()
822+
823+ def on_kube_control_relation_changed(self, event):
824+ """Handle kube-control relation changed."""
825+ self.unit.status = MaintenanceStatus("Updating K8S Credentials")
826+ self.state.kube_control.update(event.relation.data.get(event.unit, {}))
827+ self.check_charm_status()
828+
829+ def on_kube_control_relation_departed(self, event):
830+ """Handle kube-control relation departed."""
831+ self.state.configured = False
832+ for k in self.state.kube_control.keys():
833+ self.state.kube_control[k] = ""
834+ self.check_charm_status()
835+
836+ def on_nrpe_external_master_relation_joined(self, event):
837+ """Handle nrpe-external-master relation joined."""
838+ self.state.nrpe_configured = True
839+ self.check_charm_status()
840+
841+ def on_nrpe_external_master_relation_departed(self, event):
842+ """Handle nrpe-external-master relation departed."""
843+ self.state.configured = False
844+ self.state.nrpe_configured = False
845+ self.check_charm_status()
846+
847+
848+if __name__ == "__main__":
849+ main(KubernetesServiceChecksCharm)
850diff --git a/src/setuppath.py b/src/setuppath.py
851new file mode 100644
852index 0000000..769d936
853--- /dev/null
854+++ b/src/setuppath.py
855@@ -0,0 +1,3 @@
856+"""Include ./lib in the charm's PATH."""
857+import sys
858+sys.path.append('lib')
859diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt
860new file mode 100644
861index 0000000..b7c9112
862--- /dev/null
863+++ b/tests/functional/requirements.txt
864@@ -0,0 +1 @@
865+git+https://github.com/openstack-charmers/zaza.git#egg=zaza
866diff --git a/tests/functional/tests/bundles/bionic.yaml b/tests/functional/tests/bundles/bionic.yaml
867new file mode 100644
868index 0000000..c55ba41
869--- /dev/null
870+++ b/tests/functional/tests/bundles/bionic.yaml
871@@ -0,0 +1,90 @@
872+series: bionic
873+applications:
874+ kubernetes-service-checks:
875+ charm: ../../../../
876+ num_units: 1
877+ containerd:
878+ charm: cs:~containers/containerd
879+ options:
880+ gpu_driver: none
881+ resources: {}
882+ easyrsa:
883+ charm: cs:~containers/easyrsa
884+ num_units: 1
885+ resources:
886+ easyrsa: 5
887+ etcd:
888+ charm: cs:~containers/etcd
889+ num_units: 1
890+ options:
891+ channel: 3.3/stable
892+ resources:
893+ core: 0
894+ etcd: 3
895+ snapshot: 0
896+ flannel:
897+ charm: cs:~containers/flannel
898+ resources:
899+ flannel-amd64: 625
900+ flannel-arm64: 622
901+ flannel-s390x: 609
902+ kubernetes-master:
903+ charm: cs:~containers/kubernetes-master
904+ num_units: 1
905+ constraints: cores=4 mem=4G root-disk=16G
906+ options:
907+ channel: 1.18/stable
908+ resources:
909+ cdk-addons: 0
910+ core: 0
911+ kube-apiserver: 0
912+ kube-controller-manager: 0
913+ kube-proxy: 0
914+ kube-scheduler: 0
915+ kubectl: 0
916+ kubernetes-worker:
917+ charm: cs:~containers/kubernetes-worker
918+ expose: true
919+ num_units: 1
920+ constraints: cores=4 mem=4G root-disk=16G
921+ options:
922+ channel: 1.18/stable
923+ resources:
924+ cni-amd64: 645
925+ cni-arm64: 636
926+ cni-s390x: 648
927+ core: 0
928+ kube-proxy: 0
929+ kubectl: 0
930+ kubelet: 0
931+ nrpe:
932+ charm: cs:nrpe
933+relations:
934+ - - kubernetes-master:kube-api-endpoint
935+ - kubernetes-worker:kube-api-endpoint
936+ - - kubernetes-service-checks:nrpe-external-master
937+ - nrpe:nrpe-external-master
938+ - - kubernetes-master:kube-control
939+ - kubernetes-worker:kube-control
940+ - - kubernetes-master:certificates
941+ - easyrsa:client
942+ - - etcd:certificates
943+ - easyrsa:client
944+ - - kubernetes-master:etcd
945+ - etcd:db
946+ - - kubernetes-worker:certificates
947+ - easyrsa:client
948+ - - flannel:etcd
949+ - etcd:db
950+ - - flannel:cni
951+ - kubernetes-master:cni
952+ - - flannel:cni
953+ - kubernetes-worker:cni
954+ - - containerd:containerd
955+ - kubernetes-worker:container-runtime
956+ - - containerd:containerd
957+ - kubernetes-master:container-runtime
958+ - - kubernetes-service-checks:kube-control
959+ - kubernetes-master:kube-control
960+ - - kubernetes-service-checks:kube-api-endpoint
961+ - kubernetes-master:kube-api-endpoint
962\ No newline at end of file
963diff --git a/tests/functional/tests/bundles/focal.yaml b/tests/functional/tests/bundles/focal.yaml
964new file mode 100644
965index 0000000..31888a0
966--- /dev/null
967+++ b/tests/functional/tests/bundles/focal.yaml
968@@ -0,0 +1,90 @@
969+series: focal
970+applications:
971+ kubernetes-service-checks:
972+ charm: ../../../../
973+ num_units: 1
974+ containerd:
975+ charm: cs:~containers/containerd
976+ options:
977+ gpu_driver: none
978+ resources: {}
979+ easyrsa:
980+ charm: cs:~containers/easyrsa
981+ num_units: 1
982+ resources:
983+ easyrsa: 5
984+ etcd:
985+ charm: cs:~containers/etcd
986+ num_units: 1
987+ options:
988+ channel: 3.3/stable
989+ resources:
990+ core: 0
991+ etcd: 3
992+ snapshot: 0
993+ flannel:
994+ charm: cs:~containers/flannel
995+ resources:
996+ flannel-amd64: 625
997+ flannel-arm64: 622
998+ flannel-s390x: 609
999+ kubernetes-master:
1000+ charm: cs:~containers/kubernetes-master
1001+ num_units: 1
1002+ constraints: cores=4 mem=4G root-disk=16G
1003+ options:
1004+ channel: 1.18/stable
1005+ resources:
1006+ cdk-addons: 0
1007+ core: 0
1008+ kube-apiserver: 0
1009+ kube-controller-manager: 0
1010+ kube-proxy: 0
1011+ kube-scheduler: 0
1012+ kubectl: 0
1013+ kubernetes-worker:
1014+ charm: cs:~containers/kubernetes-worker
1015+ expose: true
1016+ num_units: 1
1017+ constraints: cores=4 mem=4G root-disk=16G
1018+ options:
1019+ channel: 1.18/stable
1020+ resources:
1021+ cni-amd64: 645
1022+ cni-arm64: 636
1023+ cni-s390x: 648
1024+ core: 0
1025+ kube-proxy: 0
1026+ kubectl: 0
1027+ kubelet: 0
1028+ nrpe:
1029+ charm: cs:nrpe
1030+relations:
1031+ - - kubernetes-master:kube-api-endpoint
1032+ - kubernetes-worker:kube-api-endpoint
1033+ - - kubernetes-service-checks:nrpe-external-master
1034+ - nrpe:nrpe-external-master
1035+ - - kubernetes-master:kube-control
1036+ - kubernetes-worker:kube-control
1037+ - - kubernetes-master:certificates
1038+ - easyrsa:client
1039+ - - etcd:certificates
1040+ - easyrsa:client
1041+ - - kubernetes-master:etcd
1042+ - etcd:db
1043+ - - kubernetes-worker:certificates
1044+ - easyrsa:client
1045+ - - flannel:etcd
1046+ - etcd:db
1047+ - - flannel:cni
1048+ - kubernetes-master:cni
1049+ - - flannel:cni
1050+ - kubernetes-worker:cni
1051+ - - containerd:containerd
1052+ - kubernetes-worker:container-runtime
1053+ - - containerd:containerd
1054+ - kubernetes-master:container-runtime
1055+ - - kubernetes-service-checks:kube-control
1056+ - kubernetes-master:kube-control
1057+ - - kubernetes-service-checks:kube-api-endpoint
1058+ - kubernetes-master:kube-api-endpoint
1059diff --git a/tests/functional/tests/bundles/xenial.yaml b/tests/functional/tests/bundles/xenial.yaml
1060new file mode 100644
1061index 0000000..962bdfd
1062--- /dev/null
1063+++ b/tests/functional/tests/bundles/xenial.yaml
1064@@ -0,0 +1,90 @@
1065+series: xenial
1066+applications:
1067+ kubernetes-service-checks:
1068+ charm: ../../../../
1069+ num_units: 1
1070+ containerd:
1071+ charm: cs:~containers/containerd
1072+ options:
1073+ gpu_driver: none
1074+ resources: {}
1075+ easyrsa:
1076+ charm: cs:~containers/easyrsa
1077+ num_units: 1
1078+ resources:
1079+ easyrsa: 5
1080+ etcd:
1081+ charm: cs:~containers/etcd
1082+ num_units: 1
1083+ options:
1084+ channel: 3.3/stable
1085+ resources:
1086+ core: 0
1087+ etcd: 3
1088+ snapshot: 0
1089+ flannel:
1090+ charm: cs:~containers/flannel
1091+ resources:
1092+ flannel-amd64: 625
1093+ flannel-arm64: 622
1094+ flannel-s390x: 609
1095+ kubernetes-master:
1096+ charm: cs:~containers/kubernetes-master
1097+ num_units: 1
1098+ constraints: cores=4 mem=4G root-disk=16G
1099+ options:
1100+ channel: 1.18/stable
1101+ resources:
1102+ cdk-addons: 0
1103+ core: 0
1104+ kube-apiserver: 0
1105+ kube-controller-manager: 0
1106+ kube-proxy: 0
1107+ kube-scheduler: 0
1108+ kubectl: 0
1109+ kubernetes-worker:
1110+ charm: cs:~containers/kubernetes-worker
1111+ expose: true
1112+ num_units: 1
1113+ constraints: cores=4 mem=4G root-disk=16G
1114+ options:
1115+ channel: 1.18/stable
1116+ resources:
1117+ cni-amd64: 645
1118+ cni-arm64: 636
1119+ cni-s390x: 648
1120+ core: 0
1121+ kube-proxy: 0
1122+ kubectl: 0
1123+ kubelet: 0
1124+ nrpe:
1125+ charm: cs:nrpe
1126+relations:
1127+ - - kubernetes-master:kube-api-endpoint
1128+ - kubernetes-worker:kube-api-endpoint
1129+ - - kubernetes-service-checks:nrpe-external-master
1130+ - nrpe:nrpe-external-master
1131+ - - kubernetes-master:kube-control
1132+ - kubernetes-worker:kube-control
1133+ - - kubernetes-master:certificates
1134+ - easyrsa:client
1135+ - - etcd:certificates
1136+ - easyrsa:client
1137+ - - kubernetes-master:etcd
1138+ - etcd:db
1139+ - - kubernetes-worker:certificates
1140+ - easyrsa:client
1141+ - - flannel:etcd
1142+ - etcd:db
1143+ - - flannel:cni
1144+ - kubernetes-master:cni
1145+ - - flannel:cni
1146+ - kubernetes-worker:cni
1147+ - - containerd:containerd
1148+ - kubernetes-worker:container-runtime
1149+ - - containerd:containerd
1150+ - kubernetes-master:container-runtime
1151+ - - kubernetes-service-checks:kube-control
1152+ - kubernetes-master:kube-control
1153+ - - kubernetes-service-checks:kube-api-endpoint
1154+ - kubernetes-master:kube-api-endpoint
1155diff --git a/tests/functional/tests/kubernetes_service_checks.py b/tests/functional/tests/kubernetes_service_checks.py
1156new file mode 100644
1157index 0000000..39dc7ef
1158--- /dev/null
1159+++ b/tests/functional/tests/kubernetes_service_checks.py
1160@@ -0,0 +1,193 @@
1161+"""Charm Kubernetes Service Checks Functional Tests."""
1162+import concurrent.futures
1163+import logging
1164+import re
1165+import time
1166+import unittest
1167+
1168+from juju.errors import JujuAPIError
1169+import zaza.model
1170+
1171+
1172+class TestBase(unittest.TestCase):
1173+ """Base Class for charm functional tests."""
1174+
1175+ @classmethod
1176+ def setUpClass(cls):
1177+ """Run setup for tests."""
1178+ cls.model_name = zaza.model.get_juju_model()
1179+ cls.application_name = "kubernetes-service-checks"
1180+
1181+ def setUp(self):
1182+ """Set up functional tests & ensure all relations added."""
1183+ for local_relation_name, remote_relation_unit in [
1184+ ("kube-api-endpoint", "kubernetes-master"),
1185+ ("kube-control", "kubernetes-master"),
1186+ ("nrpe-external-master", "nrpe")]:
1187+ logging.info("Adding relation {} with {}".format(local_relation_name,
1188+ remote_relation_unit))
1189+ try:
1190+ zaza.model.add_relation(self.application_name,
1191+ local_relation_name,
1192+ remote_relation_unit,
1193+ self.model_name)
1194+ except JujuAPIError as e:
1195+ p = r"^.*cannot\ add\ relation.*already\ exists"
1196+ if re.search(p, e.message):
1197+ pass
1198+ else:
1199+ raise(e)
1200+ zaza.model.block_until_wl_status_info_starts_with(
1201+ self.application_name,
1202+ status="Unit is ready",
1203+ timeout=200
1204+ )
1205+
1206+
1207+class TestChecks(TestBase):
1208+ """Tests for availability and usefulness of nagios checks."""
1209+
1210+ expected_checks = ["check_k8s_api_health.cfg",
1211+ "check_k8s_api_cert_expiration.cfg"]
1212+ checks_dir = "/etc/nagios/nrpe.d/"
1213+ expected_plugins = ["check_kubernetes_api.py"]
1214+ plugins_dir = "/usr/local/lib/nagios/plugins/"
1215+
1216+ # TODO: Need testing around setting the trusted_ssl_ca cert
1217+ # - does it get written to /etc/ssl/certs/ca-certificates.crt?
1218+ # - does the k8s check plugin see it and use it for verification?
1219+
1220+ def test_check_plugins_exist(self):
1221+ """Verify that kubernetes service checks plugins are found."""
1222+ fail_messages = []
1223+ for plugin in self.expected_plugins:
1224+ pluginpath = self.plugins_dir + plugin
1225+ response = zaza.model.run_on_unit(
1226+ "kubernetes-service-checks/0",
1227+ "[ -f \"{}\" ]".format(pluginpath),
1228+ model_name=self.model_name,
1229+ timeout=30
1230+ )
1231+ if response["Code"] != "0":
1232+ fail_messages.append("Missing plugin: {}".format(pluginpath))
1233+ continue
1234+
1235+ # check executable
1236+ response = zaza.model.run_on_unit(
1237+ "kubernetes-service-checks/0",
1238+ "[ -x \"{}\" ]".format(pluginpath),
1239+ model_name=self.model_name,
1240+ timeout=30
1241+ )
1242+
1243+ if response["Code"] != "0":
1244+ fail_messages.append("Plugin not executable: {}".format(pluginpath))
1245+
1246+ if fail_messages:
1247+ self.fail("\n".join(fail_messages))
1248+
1249+ def test_checks_exist(self):
1250+ """Verify that kubernetes service checks nrpe checks exist."""
1251+ fail_messages = []
1252+ for check in self.expected_checks:
1253+ checkpath = self.checks_dir + check
1254+ response = zaza.model.run_on_unit(
1255+ "kubernetes-service-checks/0",
1256+ "[ -f \"{}\" ]".format(checkpath),
1257+ model_name=self.model_name,
1258+ timeout=30
1259+ )
1260+ if response["Code"] != "0":
1261+ fail_messages.append("Missing check: {}".format(checkpath))
1262+ if fail_messages:
1263+ self.fail("\n".join(fail_messages))
1264+
1265+
1266+class TestRelations(TestBase):
1267+ """Tests for charm behavior adding and removing relations."""
1268+
1269+ def _get_relation_id(self, remote_application, interface_name):
1270+ return zaza.model.get_relation_id(self.application_name,
1271+ remote_application,
1272+ model_name=self.model_name,
1273+ remote_interface_name=interface_name)
1274+
1275+ def test_remove_kube_api_endpoint(self):
1276+ """Test removing kube-api-endpoint relation."""
1277+ rel_name = "kube-api-endpoint"
1278+ remote_app = "kubernetes-master"
1279+ logging.info("Removing kube-api-endpoint relation")
1280+
1281+ zaza.model.remove_relation(self.application_name,
1282+ rel_name,
1283+ remote_app,
1284+ self.model_name)
1285+ try:
1286+ zaza.model.block_until_wl_status_info_starts_with(
1287+ self.application_name,
1288+ status="missing kube-api-endpoint relation",
1289+ timeout=180
1290+ )
1291+ except concurrent.futures._base.TimeoutError:
1292+ self.fail("Timed out waiting for Unit to become blocked")
1293+
1294+ logging.info("Waiting for relation {} to be destroyed".format(rel_name))
1295+ timeout = time.time() + 600
1296+ while self._get_relation_id(remote_app, rel_name) is not None:
1297+ time.sleep(5)
1298+ if time.time() > timeout:
1299+ self.fail("Timed out waiting for the relation {} to be destroyed".format(rel_name))
1300+
1301+ def test_remove_kube_control(self):
1302+ """Test removing kube-control relation."""
1303+ rel_name = "kube-control"
1304+ remote_app = "kubernetes-master"
1305+ logging.info("Removing kube-control relation")
1306+
1307+ zaza.model.remove_relation(self.application_name,
1308+ rel_name,
1309+ remote_app,
1310+ self.model_name)
1311+
1312+ try:
1313+ zaza.model.block_until_wl_status_info_starts_with(
1314+ self.application_name,
1315+ status="missing kube-control relation",
1316+ timeout=180
1317+ )
1318+ except concurrent.futures._base.TimeoutError:
1319+ self.fail("Timed out waiting for Unit to become blocked")
1320+
1321+ logging.info("Waiting for relation {} to be destroyed".format(rel_name))
1322+ timeout = time.time() + 600
1323+ while self._get_relation_id(remote_app, rel_name) is not None:
1324+ time.sleep(5)
1325+ if time.time() > timeout:
1326+ self.fail("Timed out waiting for the relation {} to be destroyed".format(rel_name))
1327+
1328+ def test_remove_nrpe_external_master(self):
1329+ """Test removing nrpe-external-master relation."""
1330+ rel_name = "nrpe-external-master"
1331+ remote_app = "nrpe"
1332+ logging.info("Removing nrpe-external-master relation")
1333+
1334+ zaza.model.remove_relation(self.application_name,
1335+ rel_name,
1336+ remote_app,
1337+ self.model_name)
1338+
1339+ try:
1340+ zaza.model.block_until_wl_status_info_starts_with(
1341+ self.application_name,
1342+ status="missing nrpe-external-master relation",
1343+ timeout=180
1344+ )
1345+ except concurrent.futures._base.TimeoutError:
1346+ self.fail("Timed out waiting for Unit to become blocked")
1347+
1348+ logging.info("Waiting for relation {} to be destroyed".format(rel_name))
1349+ timeout = time.time() + 600
1350+ while self._get_relation_id(remote_app, rel_name) is not None:
1351+ time.sleep(5)
1352+ if time.time() > timeout:
1353+ self.fail("Timed out waiting for the relation {} to be destroyed".format(rel_name))
1354diff --git a/tests/functional/tests/tests.yaml b/tests/functional/tests/tests.yaml
1355new file mode 100644
1356index 0000000..75a9ce8
1357--- /dev/null
1358+++ b/tests/functional/tests/tests.yaml
1359@@ -0,0 +1,33 @@
1360+tests:
1361+ - tests.kubernetes_service_checks.TestChecks
1362+ - tests.kubernetes_service_checks.TestRelations
1363+target_deploy_status:
1364+ kubernetes-service-checks:
1365+ workload-status: blocked
1366+ workload-status-message: "missing kube-api-endpoint relation"
1367+ kubernetes-master:
1368+ workload-status: active
1369+ workload-status-message: "Kubernetes master running."
1370+ kubernetes-worker:
1371+ workload-status: active
1372+ workload-status-message: "Kubernetes worker running."
1373+ flannel:
1374+ workload-status: active
1375+ workload-status-message: "Flannel subnet"
1376+ easyrsa:
1377+ workload-status: active
1378+ workload-status-message: "Certificate Authority connected."
1379+ containerd:
1380+ workload-status: active
1381+ workload-status-message: "Container runtime available"
1382+ etcd:
1383+ workload-status: active
1384+ workload-status-message: "Healthy with 1 known peer"
1385+ nrpe:
1386+ workload-status: active
1387+ workload-status-message: "ready"
1388+gate_bundles:
1389+ - xenial
1390+ - bionic
1391+smoke_bundles:
1392+ - focal
1393diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
1394new file mode 100644
1395index 0000000..b5f9fbd
1396--- /dev/null
1397+++ b/tests/unit/requirements.txt
1398@@ -0,0 +1,5 @@
1399+mock
1400+pyyaml
1401+coverage
1402+six
1403+urllib3
1404diff --git a/tests/unit/setuppath.py b/tests/unit/setuppath.py
1405new file mode 100644
1406index 0000000..5e334a3
1407--- /dev/null
1408+++ b/tests/unit/setuppath.py
1409@@ -0,0 +1,5 @@
1410+"""Include ./lib ./src and ./file/plugins in the tests' PATH."""
1411+import sys
1412+sys.path.append('lib')
1413+sys.path.append('src')
1414+sys.path.append('files/plugins')
1415diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
1416new file mode 100644
1417index 0000000..fd54384
1418--- /dev/null
1419+++ b/tests/unit/test_charm.py
1420@@ -0,0 +1,243 @@
1421+"""Charm unit tests."""
1422+import os
1423+from pathlib import Path
1424+import unittest
1425+
1426+
1427+import mock
1428+import yaml
1429+# include ./lib in the charm's PATH
1430+import setuppath # noqa:F401
1431+
1432+from charm import KubernetesServiceChecksCharm # noqa:I100
1433+import ops.main
1434+from ops.testing import Harness
1435+
1436+TEST_KUBE_CONTOL_RELATION_DATA = {"creds":
1437+ """{"system:node:juju-62684f-0":
1438+ {"client_token": "DECAFBADBEEF",
1439+ "kubelet_token": "ABCDEF012345",
1440+ "proxy_token": "BADC0FFEEDAD",
1441+ "scope": "kubernetes-worker/0"}
1442+ }""" # noqa:E127
1443+ }
1444+TEST_KUBE_API_ENDPOINT_RELATION_DATA = {"hostname": "1.1.1.1",
1445+ "port": "1111"}
1446+
1447+
1448+class TestKubernetesServiceChecksCharm(unittest.TestCase):
1449+ """Test Kubernetes Service Checks Charm Code."""
1450+
1451+ @classmethod
1452+ def setUpClass(cls):
1453+ """Prepare class fixture."""
1454+ # Stop unit test from calling fchown
1455+ fchown_patcher = mock.patch("os.fchown")
1456+ cls.mock_fchown = fchown_patcher.start()
1457+ chown_patcher = mock.patch("os.chown")
1458+ cls.mock_chown = chown_patcher.start()
1459+
1460+ # Stop charmhelpers host from logging via debug log
1461+ host_log_patcher = mock.patch("charmhelpers.core.host.log")
1462+ cls.mock_juju_log = host_log_patcher.start()
1463+
1464+ # Stop charmhelpers snap from logging via debug log
1465+ snap_log_patcher = mock.patch("charmhelpers.fetch.snap.log")
1466+ cls.mock_snap_log = snap_log_patcher.start()
1467+
1468+ charm_logger_patcher = mock.patch("charm.logging")
1469+ cls.mock_charm_log = charm_logger_patcher.start()
1470+
1471+ lib_logger_patcher = mock.patch("lib.lib_kubernetes_service_checks.logging")
1472+ cls.mock_lib_logger = lib_logger_patcher.start()
1473+
1474+ # Prevent charmhelpers from calling systemctl
1475+ host_service_patcher = mock.patch("charmhelpers.core.host.service_stop")
1476+ cls.mock_service_stop = host_service_patcher.start()
1477+ host_service_patcher = mock.patch("charmhelpers.core.host.service_start")
1478+ cls.mock_service_start = host_service_patcher.start()
1479+ host_service_patcher = mock.patch("charmhelpers.core.host.service_restart")
1480+ cls.mock_service_restart = host_service_patcher.start()
1481+
1482+ # Setup mock JUJU Environment variables
1483+ os.environ["JUJU_UNIT_NAME"] = "mock/0"
1484+ os.environ["JUJU_CHARM_DIR"] = "."
1485+
1486+ def setUp(self):
1487+ """Prepare tests."""
1488+ self.harness = Harness(KubernetesServiceChecksCharm)
1489+ # Mock config_get to return default config
1490+ with open(ops.main._get_charm_dir() / Path("config.yaml"), "r") as config_file:
1491+ config = yaml.safe_load(config_file)
1492+ charm_config = {}
1493+
1494+ for key, _ in config["options"].items():
1495+ charm_config[key] = config["options"][key]["default"]
1496+
1497+ self.harness._backend._config = charm_config
1498+
1499+ def test_harness(self):
1500+ """Verify harness."""
1501+ self.harness.begin()
1502+ self.assertFalse(self.harness.charm.state.installed)
1503+
1504+ @mock.patch("charmhelpers.fetch.snap.subprocess.check_call")
1505+ def test_install(self, mock_snap_subprocess):
1506+ """Test response to an install event."""
1507+ mock_snap_subprocess.return_value = 0
1508+ mock_snap_subprocess.side_effect = None
1509+
1510+ self.harness.begin()
1511+ self.harness.charm.on.install.emit()
1512+
1513+ self.assertEqual(self.harness.charm.unit.status.name, "maintenance")
1514+ self.assertEqual(self.harness.charm.unit.status.message, "Install complete")
1515+ self.assertTrue(self.harness.charm.state.installed)
1516+
1517+ def test_config_changed(self):
1518+ """Test response to config changed event."""
1519+ self.harness.set_leader(True)
1520+ self.harness.populate_oci_resources()
1521+ self.harness.begin()
1522+ self.harness.charm.check_charm_status = mock.MagicMock()
1523+ self.harness.charm.state.installed = True
1524+ self.harness.charm.on.config_changed.emit()
1525+ self.harness.charm.check_charm_status.assert_called_once()
1526+
1527+ def test_start_not_installed(self):
1528+ """Test response to start event without install state."""
1529+ self.harness.begin()
1530+ self.harness.charm.on.start.emit()
1531+ self.assertFalse(self.harness.charm.state.started)
1532+
1533+ def test_start_not_configured(self):
1534+ """Test response to start event without configured state."""
1535+ self.harness.begin()
1536+ self.harness.charm.state.installed = True
1537+ self.harness.charm.on.start.emit()
1538+ self.assertFalse(self.harness.charm.state.started)
1539+
1540+ def test_start(self):
1541+ """Test response to start event."""
1542+ self.harness.begin()
1543+ self.harness.charm.state.installed = True
1544+ self.harness.charm.state.configured = True
1545+ self.harness.charm.on.start.emit()
1546+ self.assertTrue(self.harness.charm.state.started)
1547+ self.assertEqual(self.harness.charm.unit.status.name, "active")
1548+
1549+ def test_on_kube_api_endpoint_relation_changed(self):
1550+ """Check kube-api-endpoint relation changed handling."""
1551+ relation_id = self.harness.add_relation('kube-api-endpoint', 'kubernetes-master')
1552+ remote_unit = "kubernetes-master/0"
1553+ self.harness.begin()
1554+ self.harness.charm.check_charm_status = mock.MagicMock()
1555+ self.harness.add_relation_unit(relation_id, remote_unit)
1556+ self.harness.update_relation_data(relation_id, remote_unit, TEST_KUBE_API_ENDPOINT_RELATION_DATA)
1557+
1558+ self.harness.charm.check_charm_status.assert_called_once()
1559+ self.assertEqual(self.harness.charm.helper.kubernetes_api_address, "1.1.1.1")
1560+ self.assertEqual(self.harness.charm.helper.kubernetes_api_port, "1111")
1561+
1562+ def test_on_kube_control_relation_changed(self):
1563+ """Check kube-control relation changed handling."""
1564+ relation_id = self.harness.add_relation('kube-control', 'kubernetes-master')
1565+ remote_unit = "kubernetes-master/0"
1566+ self.harness.begin()
1567+ self.harness.charm.check_charm_status = mock.MagicMock()
1568+ self.harness.add_relation_unit(relation_id, remote_unit)
1569+ self.harness.update_relation_data(relation_id, remote_unit, TEST_KUBE_CONTOL_RELATION_DATA)
1570+
1571+ self.harness.charm.check_charm_status.assert_called_once()
1572+ assert self.harness.charm.helper.kubernetes_client_token == "DECAFBADBEEF"
1573+
1574+ def test_nrpe_external_master_relation_joined(self):
1575+ """Check that nrpe.configure is True after nrpe relation joined."""
1576+ relation_id = self.harness.add_relation('nrpe-external-master', 'nrpe')
1577+ remote_unit = "nrpe/0"
1578+ self.harness.begin()
1579+ self.assertFalse(self.harness.charm.state.nrpe_configured)
1580+ self.harness.charm.check_charm_status = mock.MagicMock()
1581+ self.harness.add_relation_unit(relation_id, remote_unit)
1582+
1583+ self.harness.charm.check_charm_status.assert_called_once()
1584+ self.assertTrue(self.harness.charm.state.nrpe_configured)
1585+
1586+ @mock.patch("ops.model.RelationData")
1587+ def test_nrpe_external_master_relation_departed(self, mock_relation_data):
1588+ """Check that nrpe.configure is False after nrpe relation departed."""
1589+ mock_relation_data.return_value.__getitem__.return_value = {}
1590+ self.harness.begin()
1591+ self.harness.charm.check_charm_status = mock.MagicMock()
1592+ self.emit("nrpe_external_master_relation_departed")
1593+ self.harness.charm.check_charm_status.assert_called_once()
1594+
1595+ self.assertFalse(self.harness.charm.state.nrpe_configured)
1596+
1597+ def test_check_charm_status_kube_api_endpoint_relation_missing(self):
1598+ """Check that the chatm blocks without kube-api-endpoint relation."""
1599+ self.harness.begin()
1600+ self.harness.charm.state.kube_control.update(TEST_KUBE_CONTOL_RELATION_DATA)
1601+ self.harness.charm.state.nrpe_configured = True
1602+ self.harness.charm.check_charm_status()
1603+
1604+ self.assertFalse(self.harness.charm.state.configured)
1605+ self.assertEqual(self.harness.charm.unit.status.name, "blocked")
1606+ self.assertEqual(self.harness.charm.unit.status.message, "missing kube-api-endpoint relation")
1607+
1608+ def test_check_charm_status_kube_control_relation_missing(self):
1609+ """Check that the charm blocks without kube-control relation."""
1610+ self.harness.begin()
1611+ self.harness.charm.state.kube_api_endpoint.update(TEST_KUBE_API_ENDPOINT_RELATION_DATA)
1612+ self.harness.charm.state.nrpe_configured = True
1613+ self.harness.charm.check_charm_status()
1614+
1615+ self.assertFalse(self.harness.charm.state.configured)
1616+ self.assertEqual(self.harness.charm.unit.status.name, "blocked")
1617+ self.assertEqual(self.harness.charm.unit.status.message, "missing kube-control relation")
1618+
1619+ def test_check_charm_status_nrpe_relation_missing(self):
1620+ """Check that the charm bloack without nrpe relation."""
1621+ self.harness.begin()
1622+ self.harness.charm.state.kube_control.update(TEST_KUBE_CONTOL_RELATION_DATA)
1623+ self.harness.charm.state.kube_api_endpoint.update(TEST_KUBE_API_ENDPOINT_RELATION_DATA)
1624+ self.harness.charm.check_charm_status()
1625+
1626+ self.assertFalse(self.harness.charm.state.configured)
1627+ self.assertEqual(self.harness.charm.unit.status.name, "blocked")
1628+ self.assertEqual(self.harness.charm.unit.status.message, "missing nrpe-external-master relation")
1629+
1630+ def test_check_charm_status_configured(self):
1631+ """Check the charm becomes configured."""
1632+ self.harness.begin()
1633+ self.harness.charm.helper.configure = mock.MagicMock()
1634+ self.harness.charm.state.kube_control.update(TEST_KUBE_CONTOL_RELATION_DATA)
1635+ self.harness.charm.state.kube_api_endpoint.update(TEST_KUBE_API_ENDPOINT_RELATION_DATA)
1636+ self.harness.charm.state.nrpe_configured = True
1637+ self.harness.charm.check_charm_status()
1638+
1639+ self.harness.charm.helper.configure.assert_called_once()
1640+ self.assertTrue(self.harness.charm.state.configured)
1641+
1642+ def emit(self, event):
1643+ """Emit the named hook on the charm."""
1644+ self.harness.charm.framework.reemit()
1645+
1646+ if "_relation_" in event:
1647+ relation_name = event.split("_relation")[0].replace("_", "-")
1648+ with mock.patch.dict(
1649+ "os.environ",
1650+ {
1651+ "JUJU_RELATION": relation_name,
1652+ "JUJU_RELATION_ID": "1",
1653+ "JUJU_REMOTE_APP": "mock",
1654+ "JUJU_REMOTE_UNIT": "mock/0",
1655+ },
1656+ ):
1657+ ops.main._emit_charm_event(self.harness.charm, event)
1658+ else:
1659+ ops.main._emit_charm_event(self.harness.charm, event)
1660+
1661+
1662+if __name__ == "__main__":
1663+ unittest.main()
1664diff --git a/tests/unit/test_lib_ksc.py b/tests/unit/test_lib_ksc.py
1665new file mode 100644
1666index 0000000..b8586bf
1667--- /dev/null
1668+++ b/tests/unit/test_lib_ksc.py
1669@@ -0,0 +1,171 @@
1670+"""Tests for Kubernetes Service Checks Helper."""
1671+import base64
1672+import os
1673+import subprocess
1674+from subprocess import CalledProcessError
1675+import tempfile
1676+import unittest
1677+
1678+from lib import lib_kubernetes_service_checks
1679+import mock
1680+import yaml
1681+
1682+
1683+TEST_CERTIFICATE = """-----BEGIN CERTIFICATE-----
1684+MIIDOzCCAiOgAwIBAgIJAPoOXrIwH+miMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
1685+BAMMDTEwLjEzMi4yNTEuNjAwHhcNMjAwNzE3MTMzMzI0WhcNMzAwNzE1MTMzMzI0
1686+WjAYMRYwFAYDVQQDDA0xMC4xMzIuMjUxLjYwMIIBIjANBgkqhkiG9w0BAQEFAAOC
1687+AQ8AMIIBCgKCAQEAqpYVlmT/eRBhCKHaqXjY6EAzvx5GZY0PhL/YGBl9uF8YQGEF
1688+F3k3Ec7pyJMIQblmWxdCPd1uNzHU8mwApiuPG9GtYOK+olqgslLsmOU9LTi6KJWX
1689+x956VxdefXDYvr0B6K/Hdgkb1x//XwvipSV1fZ1MCDIiP/hWKi4CmEq31sVpCBdp
1690+Uiz3qdCzsiGt0f4kbgIJSVtxhWlNJ5MaCOm7gXafkF8OIUTmWhmPp2gH7pfPzzl1
1691+glOX2Z41qwPuz7Jbcxx/z/yGjdPeJTQYoqJfpDpCrT2er5xyRf66HqKx9Ld/FiqM
1692+ZksRwmzF9WvqCBK8WoRmnvFxk1FZPGt6E5gotwIDAQABo4GHMIGEMB0GA1UdDgQW
1693+BBSUCCmRxb4tKD6w8jZ3hHs4ciFizDBIBgNVHSMEQTA/gBSUCCmRxb4tKD6w8jZ3
1694+hHs4ciFizKEcpBowGDEWMBQGA1UEAwwNMTAuMTMyLjI1MS42MIIJAPoOXrIwH+mi
1695+MAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQBv
1696+BYwILWI/4dGczqG0hcqt8tW04Oi+7y0HxzeI/oaUq/HKfvCz5a+WhpykMKRDJoaZ
1697+aejR2Oc7A0OUnenpvMeIiMcUIetM3Q1Gzx0aU+vqUNNaZlooSSbe3z1VK6bUsYDo
1698+qdKhs+mSyuEticK2SEWjT+ZWpV1rjSd5zRZ/UvC1ZhDNJGZotIIqryQWd3YfYl9l
1699+7JrdzUVCbxs4ywxNp9/I+MJEiBfMHQx8FWr1M2HvLDAm6NZLfM68y5FClzfGpopV
1700+0ARirz1AfbS6xUumyXHOH2qH527PUXFdfYGSn+juDG/dRTENYJ3OPAfWdj4ze1qQ
1701+n3ajLSYPvdyKaztdB1VL
1702+-----END CERTIFICATE-----"""
1703+
1704+
1705+class TestLibKSCHelper(unittest.TestCase):
1706+ """Unittest class for Kubernetes Service Checks Helper."""
1707+
1708+ @classmethod
1709+ def setUpClass(cls):
1710+ """Prepare Class Fixture."""
1711+ # Load default config
1712+ with open("./config.yaml") as default_config:
1713+ cls.config = yaml.safe_load(default_config)
1714+
1715+ # set defaults to the config object
1716+ for key in cls.config["options"]:
1717+ if "default" in cls.config["options"][key]:
1718+ cls.config[key] = cls.config["options"][key]["default"]
1719+
1720+ # Create test state object
1721+ class FakeStateObject(object):
1722+ kube_api_endpoint = {"hostname": "1.1.1.1",
1723+ "port": "1111"}
1724+ kube_control = {"creds": """{"kube-client": {"client_token": "abcdef0123456789"}}"""}
1725+ installed = False
1726+ configured = False
1727+ started = False
1728+ nrpe_configured = False
1729+
1730+ cls.state = FakeStateObject()
1731+
1732+ # Stop unit test from calling fchown
1733+ fchown_patcher = mock.patch("os.fchown")
1734+ cls.mock_fchown = fchown_patcher.start()
1735+ chown_patcher = mock.patch("os.chown")
1736+ cls.mock_chown = chown_patcher.start()
1737+
1738+ # Stop charmhelpers host from logging via debug log
1739+ host_log_patcher = mock.patch("charmhelpers.core.host.log")
1740+ cls.mock_juju_log = host_log_patcher.start()
1741+
1742+ host_logger_patcher = mock.patch("lib.lib_kubernetes_service_checks.logging")
1743+ cls.mock_logger = host_logger_patcher.start()
1744+
1745+ # Stop charmhelpers snap from logging via debug log
1746+ snap_log_patcher = mock.patch("charmhelpers.fetch.snap.log")
1747+ cls.mock_snap_log = snap_log_patcher.start()
1748+
1749+ # Setup a tmpdir
1750+ cls.tmpdir = tempfile.TemporaryDirectory()
1751+ cls.cert_path = os.path.join(
1752+ cls.tmpdir.name,
1753+ "kubernetes-service-checks.crt"
1754+ )
1755+
1756+ lib_kubernetes_service_checks.CERT_FILE = cls.cert_path
1757+ lib_kubernetes_service_checks.NAGIOS_PLUGINS_DIR = cls.tmpdir.name
1758+
1759+ @classmethod
1760+ def tearDownClass(cls):
1761+ """Tear down class fixture."""
1762+ mock.patch.stopall()
1763+ cls.tmpdir.cleanup()
1764+
1765+ def setUp(self):
1766+ """Prepare test fixture."""
1767+ self.helper = lib_kubernetes_service_checks.KSCHelper(self.config,
1768+ self.state)
1769+
1770+ def tearDown(self):
1771+ """Clean up test fixture."""
1772+ try:
1773+ os.remove(self.cert_path)
1774+ except FileNotFoundError:
1775+ pass
1776+
1777+ def test_kube_api_endpoint_properties(self):
1778+ """Test that hostname and port properties get passed through."""
1779+ # kube_api_endpoint (relation) -> hostname & port
1780+ self.assertEqual(self.helper.kubernetes_api_address, "1.1.1.1")
1781+ self.assertEqual(self.helper.kubernetes_api_port, "1111")
1782+
1783+ self.helper.state.kube_api_endpoint = {}
1784+ self.assertEqual(self.helper.kubernetes_api_address, None)
1785+ self.assertEqual(self.helper.kubernetes_api_port, None)
1786+
1787+ def test_kube_control_endpoint_properties(self):
1788+ """Test KSCHelper client_token gets passed though."""
1789+ # kube-control (relation) -> kube client token
1790+ self.assertEqual(self.helper.kubernetes_client_token, "abcdef0123456789")
1791+
1792+ self.helper.state.kube_control = {}
1793+ self.assertEqual(self.helper.kubernetes_client_token, None)
1794+
1795+ @mock.patch("lib.lib_kubernetes_service_checks.subprocess.call")
1796+ def test_update_tls_certificates(self, mock_subprocess):
1797+ """Test that SSL certificates get updated."""
1798+ # returns False when no available trusted_ssl_cert
1799+ self.assertFalse(self.helper.update_tls_certificates())
1800+
1801+ # returns True when subprocess successful
1802+ self.helper.config["trusted_ssl_ca"] = base64.b64encode(str.encode(TEST_CERTIFICATE))
1803+ self.assertTrue(self.helper.update_tls_certificates())
1804+ with open(self.cert_path, "r") as f:
1805+ self.assertEqual(f.read(), TEST_CERTIFICATE)
1806+ mock_subprocess.assert_called_once_with(['/usr/sbin/update-ca-certificates'])
1807+ mock_subprocess.reset_mock()
1808+
1809+ # returns false when subprocess hits an exception
1810+ mock_subprocess.side_effect = CalledProcessError("Command", "Mock Subprocess Call Error")
1811+ self.assertFalse(self.helper.update_tls_certificates())
1812+
1813+ def test_render_checks(self):
1814+ """Test that NPRE is called to add KSC checks."""
1815+ # TODO
1816+ pass
1817+
1818+ @mock.patch("charmhelpers.fetch.snap.subprocess.check_call")
1819+ def test_install_kubectl(self, mock_snap_subprocess):
1820+ """Test install kubectl snap helper function."""
1821+ self.assertTrue(self.helper.install_kubectl())
1822+ channel = self.config.get("channel")
1823+ mock_snap_subprocess.assert_called_with(["snap",
1824+ "install",
1825+ "--classic",
1826+ "--channel={}".format(channel),
1827+ "kubectl"], env=os.environ)
1828+
1829+ @mock.patch("charmhelpers.fetch.snap.subprocess.check_call")
1830+ def test_install_snap_failure(self, mock_snap_subprocess):
1831+ """Test response to a failed install event."""
1832+ error = subprocess.CalledProcessError("cmd", "Install failed")
1833+ error.returncode = 1
1834+ mock_snap_subprocess.return_value = 1
1835+ mock_snap_subprocess.side_effect = error
1836+ self.assertFalse(self.helper.install_kubectl())
1837+
1838+
1839+if __name__ == "__main__":
1840+ unittest.main()
1841diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py
1842new file mode 100644
1843index 0000000..e3d6600
1844--- /dev/null
1845+++ b/tests/unit/test_plugins.py
1846@@ -0,0 +1,89 @@
1847+"""Unit tests for Kubernetes Service Checks NRPE Plugins."""
1848+import unittest
1849+
1850+import check_kubernetes_api
1851+import mock
1852+
1853+
1854+class TestKSCPlugins(unittest.TestCase):
1855+ """Test cases for Kubernetes Service Checks NRPE plugins."""
1856+
1857+ @mock.patch("check_kubernetes_api.sys.exit")
1858+ @mock.patch("check_kubernetes_api.print")
1859+ def test_nagios_exit(self, mock_print, mock_sys_exit):
1860+ """Test the nagios_exit function."""
1861+ msg = "Test message"
1862+ for code, status in check_kubernetes_api.NAGIOS_STATUS.items():
1863+ expected_output = "{}: {}".format(status, msg)
1864+ check_kubernetes_api.nagios_exit(code, msg)
1865+
1866+ mock_print.assert_called_with(expected_output)
1867+ mock_sys_exit.assert_called_with(code)
1868+
1869+ @mock.patch("check_kubernetes_api.urllib3.PoolManager")
1870+ def test_kubernetes_health_ssl(self,
1871+ mock_http_pool_manager):
1872+ """Test the check k8s health function called with expected ssl params."""
1873+ host_address = "https://1.1.1.1:1111"
1874+ token = "0123456789abcdef"
1875+ disable_ssl = True
1876+
1877+ mock_http_pool_manager.return_value.status = 200
1878+ mock_http_pool_manager.return_value.data = b"ok"
1879+
1880+ check_kubernetes_api.check_kubernetes_health(
1881+ host_address,
1882+ token,
1883+ disable_ssl
1884+ )
1885+ mock_http_pool_manager.assert_called_with(cert_reqs='CERT_NONE', assert_hostname=False)
1886+
1887+ disable_ssl = False
1888+ check_kubernetes_api.check_kubernetes_health(
1889+ host_address,
1890+ token,
1891+ disable_ssl
1892+ )
1893+ mock_http_pool_manager.assert_called_with()
1894+
1895+ @mock.patch("check_kubernetes_api.urllib3.PoolManager")
1896+ def test_kubernetes_health_status(self,
1897+ mock_http_pool_manager):
1898+ """Test kubernetes health function."""
1899+ host_address = "https://1.1.1.1:1111"
1900+ token = "0123456789abcdef"
1901+ ssl_ca = "test/cert/path"
1902+
1903+ mock_http_pool_manager.return_value.request.return_value.status = 200
1904+ mock_http_pool_manager.return_value.request.return_value.data = b"ok"
1905+
1906+ # verify status OK
1907+ status, _ = check_kubernetes_api.check_kubernetes_health(
1908+ host_address,
1909+ token,
1910+ ssl_ca
1911+ )
1912+ self.assertEqual(status, check_kubernetes_api.NAGIOS_STATUS_OK)
1913+ mock_http_pool_manager.return_value.request.assert_called_once_with(
1914+ "GET",
1915+ "{}/healthz".format(host_address),
1916+ headers={"Authorization": "Bearer {}".format(token)}
1917+ )
1918+
1919+ mock_http_pool_manager.return_value.request.return_value.status = 500
1920+ mock_http_pool_manager.return_value.request.return_value.data = b"ok"
1921+ status, _ = check_kubernetes_api.check_kubernetes_health(
1922+ host_address,
1923+ token,
1924+ ssl_ca
1925+ )
1926+ self.assertEqual(status, check_kubernetes_api.NAGIOS_STATUS_CRITICAL)
1927+
1928+ mock_http_pool_manager.return_value.request.return_value.status = 200
1929+ mock_http_pool_manager.return_value.request.return_value.data = b"not ok"
1930+ status, _ = check_kubernetes_api.check_kubernetes_health(
1931+ host_address,
1932+ token,
1933+ ssl_ca
1934+ )
1935+ self.assertEqual(status, check_kubernetes_api.NAGIOS_STATUS_WARNING)
1936diff --git a/tox.ini b/tox.ini
1937new file mode 100644
1938index 0000000..2f92371
1939--- /dev/null
1940+++ b/tox.ini
1941@@ -0,0 +1,52 @@
1942+[tox]
1943+skipsdist = True
1944+envlist = unit, func
1945+skip_missing_interpreters = True
1946+
1947+[testenv]
1948+basepython = python3
1949+setenv =
1950+ PYTHONPATH = {toxinidir}/lib/:{toxinidir}
1951+passenv =
1952+ HOME
1953+ MODEL_SETTINGS
1954+ CHARM_BUILD_DIR
1955+
1956+[testenv:unit]
1957+commands =
1958+ coverage run -m unittest discover -s {toxinidir}/tests/unit -v
1959+ coverage report \
1960+ --omit tests/*,mod/*,.tox/*
1961+ coverage html \
1962+ --omit tests/*,mod/*,.tox/*
1963+deps = -r{toxinidir}/tests/unit/requirements.txt
1964+
1965+[testenv:func]
1966+changedir = {toxinidir}/tests/functional
1967+commands = functest-run-suite {posargs}
1968+deps = -r{toxinidir}/tests/functional/requirements.txt
1969+
1970+[testenv:lint]
1971+commands =
1972+ flake8
1973+ black --check --line-length 120 --exclude /(\.eggs|\.git|\.tox|\.venv|build|dist|charmhelpers|mod)/ .
1974+deps =
1975+ black
1976+ flake8
1977+ flake8-docstrings
1978+ flake8-import-order
1979+ pep8-naming
1980+ flake8-colors
1981+
1982+[flake8]
1983+exclude =
1984+ .git,
1985+ __pycache__,
1986+ .tox,
1987+ mod,
1988+max-line-length = 120
1989+max-complexity = 10
1990+import-order-style = google
1991+
1992+[isort]
1993+force_to_top=setuppath

Subscribers

People subscribed via source and target branches

to all changes: