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: 1913 lines (+1738/-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 (+118/-0)
hooks/install (+1/-0)
lib/.empty (+0/-0)
lib/charmhelpers (+1/-0)
lib/lib_kubernetes_service_checks.py (+159/-0)
lib/ops (+1/-0)
metadata.yaml (+26/-0)
mod/.empty (+0/-0)
mod/charm-helpers (+1/-0)
src/charm.py (+186/-0)
src/setuppath.py (+4/-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 (+162/-0)
tests/functional/tests/tests.yaml (+33/-0)
tests/unit/requirements.txt (+5/-0)
tests/unit/setuppath.py (+6/-0)
tests/unit/test_charm.py (+243/-0)
tests/unit/test_lib_ksc.py (+164/-0)
tests/unit/test_plugins.py (+65/-0)
tox.ini (+52/-0)
Reviewer Review Type Date Requested Status
Joe Guo Pending
Giuseppe Petralia Pending
Adam Dyess Pending
Review via email: mp+387666@code.launchpad.net

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

Commit message

Initial charm for Kubernetes Service Checks

Currently 2 NRPE checks:
  - 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 if required data
is missing.

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

Description of the change

Initial charm for Kubernetes Service Checks

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

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

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

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

Black is complaining at current commit (05632f67469f5416c4e16be6fc7acd2521c9a503)
https://paste.ubuntu.com/p/Gm3yfyjQ2q/

review: Needs Fixing
Revision history for this message
Joe Guo (guoqiao) wrote : Posted in a previous version of this proposal

Hi Ryan, I have a few inline comments for minor changes.

review: Needs Fixing

Unmerged commits

51c4121... by Ryan Farrell

Set required args in nrpe plugin

6f49451... by Ryan Farrell

Blackened code

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

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

Subscribers

People subscribed via source and target branches

to all changes: