Merge ~whereisrysmind/charm-kubernetes-service-checks:master into charm-kubernetes-service-checks:master
- Git
- lp:~whereisrysmind/charm-kubernetes-service-checks
- master
- Merge into master
Status: | Superseded |
---|---|
Proposed branch: | ~whereisrysmind/charm-kubernetes-service-checks:master |
Merge into: | charm-kubernetes-service-checks:master |
Diff against target: |
1918 lines (+1743/-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 (+191/-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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Giuseppe Petralia | Pending | ||
Joe Guo | Pending | ||
Adam Dyess | Pending | ||
Review via email: mp+387688@code.launchpad.net |
This proposal supersedes a proposal from 2020-07-20.
This proposal has been superseded by a proposal from 2020-07-20.
Commit message
Initial charm for Kubernetes Service Checks
Currently 2 NRPE checks:
- check_http for cert expirations
- check_k8s_
3 Relations required:
- nrpe:nrpe-
- kubernetes-
- kubernetes-
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
Giuseppe Petralia (peppepetra) wrote : Posted in a previous version of this proposal | # |
- 5e57fec... by Ryan Farrell
-
Edited when ssl configuration occurs
Giuseppe Petralia (peppepetra) wrote : Posted in a previous version of this proposal | # |
All tests passes. `make test` is blocked by charm proof that is testing things that are not valid for operator charm. I would suggest adding @-charm proof to ignore any error.
I am +1 to merge it.
Thanks.
Unmerged commits
- 5e57fec... by Ryan Farrell
-
Edited when ssl configuration occurs
- 003175a... by Ryan Farrell
-
Fixed charm configured flag handling
- 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.
Preview Diff
1 | diff --git a/.gitignore b/.gitignore |
2 | new file mode 100644 |
3 | index 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/* |
32 | diff --git a/.gitmodules b/.gitmodules |
33 | index 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 |
44 | diff --git a/Makefile b/Makefile |
45 | new file mode 100644 |
46 | index 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 |
129 | diff --git a/README.md b/README.md |
130 | new file mode 100644 |
131 | index 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) |
231 | diff --git a/config.yaml b/config.yaml |
232 | new file mode 100644 |
233 | index 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 |
276 | diff --git a/files/plugins/check_kubernetes_api.py b/files/plugins/check_kubernetes_api.py |
277 | new file mode 100755 |
278 | index 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 | +""" |
400 | diff --git a/hooks/install b/hooks/install |
401 | new file mode 120000 |
402 | index 0000000..25b1f68 |
403 | --- /dev/null |
404 | +++ b/hooks/install |
405 | @@ -0,0 +1 @@ |
406 | +../src/charm.py |
407 | \ No newline at end of file |
408 | diff --git a/lib/.empty b/lib/.empty |
409 | new file mode 100644 |
410 | index 0000000..e69de29 |
411 | --- /dev/null |
412 | +++ b/lib/.empty |
413 | diff --git a/lib/charmhelpers b/lib/charmhelpers |
414 | new file mode 120000 |
415 | index 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 |
421 | diff --git a/lib/lib_kubernetes_service_checks.py b/lib/lib_kubernetes_service_checks.py |
422 | new file mode 100644 |
423 | index 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 |
586 | diff --git a/lib/ops b/lib/ops |
587 | new file mode 120000 |
588 | index 0000000..d934193 |
589 | --- /dev/null |
590 | +++ b/lib/ops |
591 | @@ -0,0 +1 @@ |
592 | +../mod/operator/ops |
593 | \ No newline at end of file |
594 | diff --git a/metadata.yaml b/metadata.yaml |
595 | new file mode 100644 |
596 | index 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 |
626 | diff --git a/mod/.empty b/mod/.empty |
627 | new file mode 100644 |
628 | index 0000000..e69de29 |
629 | --- /dev/null |
630 | +++ b/mod/.empty |
631 | diff --git a/mod/charm-helpers b/mod/charm-helpers |
632 | new file mode 160000 |
633 | index 0000000..4b3602e |
634 | --- /dev/null |
635 | +++ b/mod/charm-helpers |
636 | @@ -0,0 +1 @@ |
637 | +Subproject commit 4b3602e2bdf101bf58cc808264ec0c8092a67cd0 |
638 | diff --git a/src/charm.py b/src/charm.py |
639 | new file mode 100755 |
640 | index 0000000..b06381d |
641 | --- /dev/null |
642 | +++ b/src/charm.py |
643 | @@ -0,0 +1,191 @@ |
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 | + self.state.configured = False |
708 | + logging.info("Reinstalling for upgrade-charm hook") |
709 | + self.on_install(event) |
710 | + self.check_charm_status() |
711 | + |
712 | + def check_charm_status(self): |
713 | + """ |
714 | + Check that required data is available from relations. |
715 | + |
716 | + - Check kube-api-endpoint relation and data available |
717 | + - Check kube-control relation and data available |
718 | + - Check nrpe-external-master is configured |
719 | + - Check any required config options |
720 | + - Finally, configure the charms checks and set flags |
721 | + """ |
722 | + if not self.helper.kubernetes_api_address or not self.helper.kubernetes_api_port: |
723 | + logging.warning("kube-api-endpoint relation missing or misconfigured") |
724 | + self.unit.status = BlockedStatus("missing kube-api-endpoint relation") |
725 | + return |
726 | + if not self.helper.kubernetes_client_token: |
727 | + logging.warning("kube-control relation missing or misconfigured") |
728 | + self.unit.status = BlockedStatus("missing kube-control relation") |
729 | + return |
730 | + if not self.state.nrpe_configured: |
731 | + logging.warning("nrpe-external-master relation missing or misconfigured") |
732 | + self.unit.status = BlockedStatus("missing nrpe-external-master relation") |
733 | + return |
734 | + |
735 | + # Check specific required config values |
736 | + # Set up TLS Certificate |
737 | + if self.helper.use_tls_cert: |
738 | + logging.info("Updating tls certificates") |
739 | + if self.helper.update_tls_certificates(): |
740 | + logging.info("TLS Certificates updated successfully") |
741 | + else: |
742 | + logging.error("Failed to update TLS Certificates") |
743 | + self.unit.status = BlockedStatus("update-ca-certificates error. check logs") |
744 | + return |
745 | + else: |
746 | + logging.warning("No trusted_ssl_ca provided, SSL Host Authentication disabled") |
747 | + |
748 | + # configure nrpe checks |
749 | + if not self.state.configured: |
750 | + logging.info("Configuring Kubernetes Service Checks") |
751 | + self.helper.configure() |
752 | + |
753 | + logging.info("Reloading nagios-nrpe-server") |
754 | + self.helper.restart_nrpe_service() |
755 | + self.state.configured = True |
756 | + self.unit.status = ActiveStatus("Unit is ready") |
757 | + |
758 | + def on_config_changed(self, event): |
759 | + """Handle config changed.""" |
760 | + self.state.configured = False |
761 | + if not self.state.installed: |
762 | + logging.warning("Config changed called before install complete, deferring event: {}.".format(event.handle)) |
763 | + self._defer_once(event) |
764 | + return |
765 | + self.check_charm_status() |
766 | + |
767 | + def on_start(self, event): |
768 | + """Handle start state.""" |
769 | + if not self.state.configured: |
770 | + logging.warning("Start called before configuration complete, deferring event: {}".format(event.handle)) |
771 | + event.defer() |
772 | + return |
773 | + self.unit.status = ActiveStatus("Unit is ready") |
774 | + self.state.started = True |
775 | + logging.info("Started") |
776 | + |
777 | + def _defer_once(self, event): |
778 | + """Defer the given event, but only once.""" |
779 | + notice_count = 0 |
780 | + handle = str(event.handle) |
781 | + |
782 | + for event_path, _, _ in self.framework._storage.notices(None): |
783 | + if event_path.startswith(handle.split("[")[0]): |
784 | + notice_count += 1 |
785 | + logging.debug("Found event: {} x {}".format(event_path, notice_count)) |
786 | + |
787 | + if notice_count > 1: |
788 | + logging.debug("Not deferring {} notice count of {}".format(handle, notice_count)) |
789 | + else: |
790 | + logging.debug("Deferring {} notice count of {}".format(handle, notice_count)) |
791 | + event.defer() |
792 | + |
793 | + def on_kube_api_endpoint_relation_changed(self, event): |
794 | + """Handle kube_api_endpoint relation changed.""" |
795 | + self.state.configured = False |
796 | + self.unit.status = MaintenanceStatus("Updating K8S Endpoint") |
797 | + self.state.kube_api_endpoint.update(event.relation.data.get(event.unit, {})) |
798 | + self.check_charm_status() |
799 | + |
800 | + def on_kube_api_endpoint_relation_departed(self, event): |
801 | + """Handle kube-api-endpoint relation departed.""" |
802 | + self.state.configured = False |
803 | + for k in self.state.kube_api_endpoint.keys(): |
804 | + self.state.kube_api_endpoint[k] = "" |
805 | + self.check_charm_status() |
806 | + |
807 | + def on_kube_control_relation_changed(self, event): |
808 | + """Handle kube-control relation changed.""" |
809 | + self.state.configured = False |
810 | + self.unit.status = MaintenanceStatus("Updating K8S Credentials") |
811 | + self.state.kube_control.update(event.relation.data.get(event.unit, {})) |
812 | + self.check_charm_status() |
813 | + |
814 | + def on_kube_control_relation_departed(self, event): |
815 | + """Handle kube-control relation departed.""" |
816 | + self.state.configured = False |
817 | + for k in self.state.kube_control.keys(): |
818 | + self.state.kube_control[k] = "" |
819 | + self.check_charm_status() |
820 | + |
821 | + def on_nrpe_external_master_relation_joined(self, event): |
822 | + """Handle nrpe-external-master relation joined.""" |
823 | + self.state.nrpe_configured = True |
824 | + self.check_charm_status() |
825 | + |
826 | + def on_nrpe_external_master_relation_departed(self, event): |
827 | + """Handle nrpe-external-master relation departed.""" |
828 | + self.state.configured = False |
829 | + self.state.nrpe_configured = False |
830 | + self.check_charm_status() |
831 | + |
832 | + |
833 | +if __name__ == "__main__": |
834 | + main(KubernetesServiceChecksCharm) |
835 | diff --git a/src/setuppath.py b/src/setuppath.py |
836 | new file mode 100644 |
837 | index 0000000..768e049 |
838 | --- /dev/null |
839 | +++ b/src/setuppath.py |
840 | @@ -0,0 +1,4 @@ |
841 | +"""Include ./lib in the charm's PATH.""" |
842 | +import sys |
843 | + |
844 | +sys.path.append("lib") |
845 | diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt |
846 | new file mode 100644 |
847 | index 0000000..b7c9112 |
848 | --- /dev/null |
849 | +++ b/tests/functional/requirements.txt |
850 | @@ -0,0 +1 @@ |
851 | +git+https://github.com/openstack-charmers/zaza.git#egg=zaza |
852 | diff --git a/tests/functional/tests/bundles/bionic.yaml b/tests/functional/tests/bundles/bionic.yaml |
853 | new file mode 100644 |
854 | index 0000000..c55ba41 |
855 | --- /dev/null |
856 | +++ b/tests/functional/tests/bundles/bionic.yaml |
857 | @@ -0,0 +1,90 @@ |
858 | +series: bionic |
859 | +applications: |
860 | + kubernetes-service-checks: |
861 | + charm: ../../../../ |
862 | + num_units: 1 |
863 | + containerd: |
864 | + charm: cs:~containers/containerd |
865 | + options: |
866 | + gpu_driver: none |
867 | + resources: {} |
868 | + easyrsa: |
869 | + charm: cs:~containers/easyrsa |
870 | + num_units: 1 |
871 | + resources: |
872 | + easyrsa: 5 |
873 | + etcd: |
874 | + charm: cs:~containers/etcd |
875 | + num_units: 1 |
876 | + options: |
877 | + channel: 3.3/stable |
878 | + resources: |
879 | + core: 0 |
880 | + etcd: 3 |
881 | + snapshot: 0 |
882 | + flannel: |
883 | + charm: cs:~containers/flannel |
884 | + resources: |
885 | + flannel-amd64: 625 |
886 | + flannel-arm64: 622 |
887 | + flannel-s390x: 609 |
888 | + kubernetes-master: |
889 | + charm: cs:~containers/kubernetes-master |
890 | + num_units: 1 |
891 | + constraints: cores=4 mem=4G root-disk=16G |
892 | + options: |
893 | + channel: 1.18/stable |
894 | + resources: |
895 | + cdk-addons: 0 |
896 | + core: 0 |
897 | + kube-apiserver: 0 |
898 | + kube-controller-manager: 0 |
899 | + kube-proxy: 0 |
900 | + kube-scheduler: 0 |
901 | + kubectl: 0 |
902 | + kubernetes-worker: |
903 | + charm: cs:~containers/kubernetes-worker |
904 | + expose: true |
905 | + num_units: 1 |
906 | + constraints: cores=4 mem=4G root-disk=16G |
907 | + options: |
908 | + channel: 1.18/stable |
909 | + resources: |
910 | + cni-amd64: 645 |
911 | + cni-arm64: 636 |
912 | + cni-s390x: 648 |
913 | + core: 0 |
914 | + kube-proxy: 0 |
915 | + kubectl: 0 |
916 | + kubelet: 0 |
917 | + nrpe: |
918 | + charm: cs:nrpe |
919 | +relations: |
920 | + - - kubernetes-master:kube-api-endpoint |
921 | + - kubernetes-worker:kube-api-endpoint |
922 | + - - kubernetes-service-checks:nrpe-external-master |
923 | + - nrpe:nrpe-external-master |
924 | + - - kubernetes-master:kube-control |
925 | + - kubernetes-worker:kube-control |
926 | + - - kubernetes-master:certificates |
927 | + - easyrsa:client |
928 | + - - etcd:certificates |
929 | + - easyrsa:client |
930 | + - - kubernetes-master:etcd |
931 | + - etcd:db |
932 | + - - kubernetes-worker:certificates |
933 | + - easyrsa:client |
934 | + - - flannel:etcd |
935 | + - etcd:db |
936 | + - - flannel:cni |
937 | + - kubernetes-master:cni |
938 | + - - flannel:cni |
939 | + - kubernetes-worker:cni |
940 | + - - containerd:containerd |
941 | + - kubernetes-worker:container-runtime |
942 | + - - containerd:containerd |
943 | + - kubernetes-master:container-runtime |
944 | + - - kubernetes-service-checks:kube-control |
945 | + - kubernetes-master:kube-control |
946 | + - - kubernetes-service-checks:kube-api-endpoint |
947 | + - kubernetes-master:kube-api-endpoint |
948 | \ No newline at end of file |
949 | diff --git a/tests/functional/tests/bundles/focal.yaml b/tests/functional/tests/bundles/focal.yaml |
950 | new file mode 100644 |
951 | index 0000000..31888a0 |
952 | --- /dev/null |
953 | +++ b/tests/functional/tests/bundles/focal.yaml |
954 | @@ -0,0 +1,90 @@ |
955 | +series: focal |
956 | +applications: |
957 | + kubernetes-service-checks: |
958 | + charm: ../../../../ |
959 | + num_units: 1 |
960 | + containerd: |
961 | + charm: cs:~containers/containerd |
962 | + options: |
963 | + gpu_driver: none |
964 | + resources: {} |
965 | + easyrsa: |
966 | + charm: cs:~containers/easyrsa |
967 | + num_units: 1 |
968 | + resources: |
969 | + easyrsa: 5 |
970 | + etcd: |
971 | + charm: cs:~containers/etcd |
972 | + num_units: 1 |
973 | + options: |
974 | + channel: 3.3/stable |
975 | + resources: |
976 | + core: 0 |
977 | + etcd: 3 |
978 | + snapshot: 0 |
979 | + flannel: |
980 | + charm: cs:~containers/flannel |
981 | + resources: |
982 | + flannel-amd64: 625 |
983 | + flannel-arm64: 622 |
984 | + flannel-s390x: 609 |
985 | + kubernetes-master: |
986 | + charm: cs:~containers/kubernetes-master |
987 | + num_units: 1 |
988 | + constraints: cores=4 mem=4G root-disk=16G |
989 | + options: |
990 | + channel: 1.18/stable |
991 | + resources: |
992 | + cdk-addons: 0 |
993 | + core: 0 |
994 | + kube-apiserver: 0 |
995 | + kube-controller-manager: 0 |
996 | + kube-proxy: 0 |
997 | + kube-scheduler: 0 |
998 | + kubectl: 0 |
999 | + kubernetes-worker: |
1000 | + charm: cs:~containers/kubernetes-worker |
1001 | + expose: true |
1002 | + num_units: 1 |
1003 | + constraints: cores=4 mem=4G root-disk=16G |
1004 | + options: |
1005 | + channel: 1.18/stable |
1006 | + resources: |
1007 | + cni-amd64: 645 |
1008 | + cni-arm64: 636 |
1009 | + cni-s390x: 648 |
1010 | + core: 0 |
1011 | + kube-proxy: 0 |
1012 | + kubectl: 0 |
1013 | + kubelet: 0 |
1014 | + nrpe: |
1015 | + charm: cs:nrpe |
1016 | +relations: |
1017 | + - - kubernetes-master:kube-api-endpoint |
1018 | + - kubernetes-worker:kube-api-endpoint |
1019 | + - - kubernetes-service-checks:nrpe-external-master |
1020 | + - nrpe:nrpe-external-master |
1021 | + - - kubernetes-master:kube-control |
1022 | + - kubernetes-worker:kube-control |
1023 | + - - kubernetes-master:certificates |
1024 | + - easyrsa:client |
1025 | + - - etcd:certificates |
1026 | + - easyrsa:client |
1027 | + - - kubernetes-master:etcd |
1028 | + - etcd:db |
1029 | + - - kubernetes-worker:certificates |
1030 | + - easyrsa:client |
1031 | + - - flannel:etcd |
1032 | + - etcd:db |
1033 | + - - flannel:cni |
1034 | + - kubernetes-master:cni |
1035 | + - - flannel:cni |
1036 | + - kubernetes-worker:cni |
1037 | + - - containerd:containerd |
1038 | + - kubernetes-worker:container-runtime |
1039 | + - - containerd:containerd |
1040 | + - kubernetes-master:container-runtime |
1041 | + - - kubernetes-service-checks:kube-control |
1042 | + - kubernetes-master:kube-control |
1043 | + - - kubernetes-service-checks:kube-api-endpoint |
1044 | + - kubernetes-master:kube-api-endpoint |
1045 | diff --git a/tests/functional/tests/bundles/xenial.yaml b/tests/functional/tests/bundles/xenial.yaml |
1046 | new file mode 100644 |
1047 | index 0000000..962bdfd |
1048 | --- /dev/null |
1049 | +++ b/tests/functional/tests/bundles/xenial.yaml |
1050 | @@ -0,0 +1,90 @@ |
1051 | +series: xenial |
1052 | +applications: |
1053 | + kubernetes-service-checks: |
1054 | + charm: ../../../../ |
1055 | + num_units: 1 |
1056 | + containerd: |
1057 | + charm: cs:~containers/containerd |
1058 | + options: |
1059 | + gpu_driver: none |
1060 | + resources: {} |
1061 | + easyrsa: |
1062 | + charm: cs:~containers/easyrsa |
1063 | + num_units: 1 |
1064 | + resources: |
1065 | + easyrsa: 5 |
1066 | + etcd: |
1067 | + charm: cs:~containers/etcd |
1068 | + num_units: 1 |
1069 | + options: |
1070 | + channel: 3.3/stable |
1071 | + resources: |
1072 | + core: 0 |
1073 | + etcd: 3 |
1074 | + snapshot: 0 |
1075 | + flannel: |
1076 | + charm: cs:~containers/flannel |
1077 | + resources: |
1078 | + flannel-amd64: 625 |
1079 | + flannel-arm64: 622 |
1080 | + flannel-s390x: 609 |
1081 | + kubernetes-master: |
1082 | + charm: cs:~containers/kubernetes-master |
1083 | + num_units: 1 |
1084 | + constraints: cores=4 mem=4G root-disk=16G |
1085 | + options: |
1086 | + channel: 1.18/stable |
1087 | + resources: |
1088 | + cdk-addons: 0 |
1089 | + core: 0 |
1090 | + kube-apiserver: 0 |
1091 | + kube-controller-manager: 0 |
1092 | + kube-proxy: 0 |
1093 | + kube-scheduler: 0 |
1094 | + kubectl: 0 |
1095 | + kubernetes-worker: |
1096 | + charm: cs:~containers/kubernetes-worker |
1097 | + expose: true |
1098 | + num_units: 1 |
1099 | + constraints: cores=4 mem=4G root-disk=16G |
1100 | + options: |
1101 | + channel: 1.18/stable |
1102 | + resources: |
1103 | + cni-amd64: 645 |
1104 | + cni-arm64: 636 |
1105 | + cni-s390x: 648 |
1106 | + core: 0 |
1107 | + kube-proxy: 0 |
1108 | + kubectl: 0 |
1109 | + kubelet: 0 |
1110 | + nrpe: |
1111 | + charm: cs:nrpe |
1112 | +relations: |
1113 | + - - kubernetes-master:kube-api-endpoint |
1114 | + - kubernetes-worker:kube-api-endpoint |
1115 | + - - kubernetes-service-checks:nrpe-external-master |
1116 | + - nrpe:nrpe-external-master |
1117 | + - - kubernetes-master:kube-control |
1118 | + - kubernetes-worker:kube-control |
1119 | + - - kubernetes-master:certificates |
1120 | + - easyrsa:client |
1121 | + - - etcd:certificates |
1122 | + - easyrsa:client |
1123 | + - - kubernetes-master:etcd |
1124 | + - etcd:db |
1125 | + - - kubernetes-worker:certificates |
1126 | + - easyrsa:client |
1127 | + - - flannel:etcd |
1128 | + - etcd:db |
1129 | + - - flannel:cni |
1130 | + - kubernetes-master:cni |
1131 | + - - flannel:cni |
1132 | + - kubernetes-worker:cni |
1133 | + - - containerd:containerd |
1134 | + - kubernetes-worker:container-runtime |
1135 | + - - containerd:containerd |
1136 | + - kubernetes-master:container-runtime |
1137 | + - - kubernetes-service-checks:kube-control |
1138 | + - kubernetes-master:kube-control |
1139 | + - - kubernetes-service-checks:kube-api-endpoint |
1140 | + - kubernetes-master:kube-api-endpoint |
1141 | diff --git a/tests/functional/tests/kubernetes_service_checks.py b/tests/functional/tests/kubernetes_service_checks.py |
1142 | new file mode 100644 |
1143 | index 0000000..e1520f3 |
1144 | --- /dev/null |
1145 | +++ b/tests/functional/tests/kubernetes_service_checks.py |
1146 | @@ -0,0 +1,162 @@ |
1147 | +"""Charm Kubernetes Service Checks Functional Tests.""" |
1148 | +import concurrent.futures |
1149 | +import logging |
1150 | +import re |
1151 | +import time |
1152 | +import unittest |
1153 | + |
1154 | +from juju.errors import JujuAPIError |
1155 | +import zaza.model |
1156 | + |
1157 | + |
1158 | +class TestBase(unittest.TestCase): |
1159 | + """Base Class for charm functional tests.""" |
1160 | + |
1161 | + @classmethod |
1162 | + def setUpClass(cls): |
1163 | + """Run setup for tests.""" |
1164 | + cls.model_name = zaza.model.get_juju_model() |
1165 | + cls.application_name = "kubernetes-service-checks" |
1166 | + |
1167 | + def setUp(self): |
1168 | + """Set up functional tests & ensure all relations added.""" |
1169 | + for local_relation_name, remote_relation_unit in [ |
1170 | + ("kube-api-endpoint", "kubernetes-master"), |
1171 | + ("kube-control", "kubernetes-master"), |
1172 | + ("nrpe-external-master", "nrpe"), |
1173 | + ]: |
1174 | + logging.info("Adding relation {} with {}".format(local_relation_name, remote_relation_unit)) |
1175 | + try: |
1176 | + zaza.model.add_relation( |
1177 | + self.application_name, local_relation_name, remote_relation_unit, self.model_name |
1178 | + ) |
1179 | + except JujuAPIError as e: |
1180 | + p = r"^.*cannot\ add\ relation.*already\ exists" |
1181 | + if re.search(p, e.message): |
1182 | + pass |
1183 | + else: |
1184 | + raise (e) |
1185 | + zaza.model.block_until_wl_status_info_starts_with(self.application_name, status="Unit is ready", timeout=200) |
1186 | + |
1187 | + |
1188 | +class TestChecks(TestBase): |
1189 | + """Tests for availability and usefulness of nagios checks.""" |
1190 | + |
1191 | + expected_checks = ["check_k8s_api_health.cfg", "check_k8s_api_cert_expiration.cfg"] |
1192 | + checks_dir = "/etc/nagios/nrpe.d/" |
1193 | + expected_plugins = ["check_kubernetes_api.py"] |
1194 | + plugins_dir = "/usr/local/lib/nagios/plugins/" |
1195 | + |
1196 | + # TODO: Need testing around setting the trusted_ssl_ca cert |
1197 | + # - does it get written to /etc/ssl/certs/ca-certificates.crt? |
1198 | + # - does the k8s check plugin see it and use it for verification? |
1199 | + |
1200 | + def test_check_plugins_exist(self): |
1201 | + """Verify that kubernetes service checks plugins are found.""" |
1202 | + fail_messages = [] |
1203 | + for plugin in self.expected_plugins: |
1204 | + pluginpath = self.plugins_dir + plugin |
1205 | + response = zaza.model.run_on_unit( |
1206 | + "kubernetes-service-checks/0", '[ -f "{}" ]'.format(pluginpath), model_name=self.model_name, timeout=30 |
1207 | + ) |
1208 | + if response["Code"] != "0": |
1209 | + fail_messages.append("Missing plugin: {}".format(pluginpath)) |
1210 | + continue |
1211 | + |
1212 | + # check executable |
1213 | + response = zaza.model.run_on_unit( |
1214 | + "kubernetes-service-checks/0", '[ -x "{}" ]'.format(pluginpath), model_name=self.model_name, timeout=30 |
1215 | + ) |
1216 | + |
1217 | + if response["Code"] != "0": |
1218 | + fail_messages.append("Plugin not executable: {}".format(pluginpath)) |
1219 | + |
1220 | + if fail_messages: |
1221 | + self.fail("\n".join(fail_messages)) |
1222 | + |
1223 | + def test_checks_exist(self): |
1224 | + """Verify that kubernetes service checks nrpe checks exist.""" |
1225 | + fail_messages = [] |
1226 | + for check in self.expected_checks: |
1227 | + checkpath = self.checks_dir + check |
1228 | + response = zaza.model.run_on_unit( |
1229 | + "kubernetes-service-checks/0", '[ -f "{}" ]'.format(checkpath), model_name=self.model_name, timeout=30 |
1230 | + ) |
1231 | + if response["Code"] != "0": |
1232 | + fail_messages.append("Missing check: {}".format(checkpath)) |
1233 | + if fail_messages: |
1234 | + self.fail("\n".join(fail_messages)) |
1235 | + |
1236 | + |
1237 | +class TestRelations(TestBase): |
1238 | + """Tests for charm behavior adding and removing relations.""" |
1239 | + |
1240 | + def _get_relation_id(self, remote_application, interface_name): |
1241 | + return zaza.model.get_relation_id( |
1242 | + self.application_name, remote_application, model_name=self.model_name, remote_interface_name=interface_name |
1243 | + ) |
1244 | + |
1245 | + def test_remove_kube_api_endpoint(self): |
1246 | + """Test removing kube-api-endpoint relation.""" |
1247 | + rel_name = "kube-api-endpoint" |
1248 | + remote_app = "kubernetes-master" |
1249 | + logging.info("Removing kube-api-endpoint relation") |
1250 | + |
1251 | + zaza.model.remove_relation(self.application_name, rel_name, remote_app, self.model_name) |
1252 | + try: |
1253 | + zaza.model.block_until_wl_status_info_starts_with( |
1254 | + self.application_name, status="missing kube-api-endpoint relation", timeout=180 |
1255 | + ) |
1256 | + except concurrent.futures._base.TimeoutError: |
1257 | + self.fail("Timed out waiting for Unit to become blocked") |
1258 | + |
1259 | + logging.info("Waiting for relation {} to be destroyed".format(rel_name)) |
1260 | + timeout = time.time() + 600 |
1261 | + while self._get_relation_id(remote_app, rel_name) is not None: |
1262 | + time.sleep(5) |
1263 | + if time.time() > timeout: |
1264 | + self.fail("Timed out waiting for the relation {} to be destroyed".format(rel_name)) |
1265 | + |
1266 | + def test_remove_kube_control(self): |
1267 | + """Test removing kube-control relation.""" |
1268 | + rel_name = "kube-control" |
1269 | + remote_app = "kubernetes-master" |
1270 | + logging.info("Removing kube-control relation") |
1271 | + |
1272 | + zaza.model.remove_relation(self.application_name, rel_name, remote_app, self.model_name) |
1273 | + |
1274 | + try: |
1275 | + zaza.model.block_until_wl_status_info_starts_with( |
1276 | + self.application_name, status="missing kube-control relation", timeout=180 |
1277 | + ) |
1278 | + except concurrent.futures._base.TimeoutError: |
1279 | + self.fail("Timed out waiting for Unit to become blocked") |
1280 | + |
1281 | + logging.info("Waiting for relation {} to be destroyed".format(rel_name)) |
1282 | + timeout = time.time() + 600 |
1283 | + while self._get_relation_id(remote_app, rel_name) is not None: |
1284 | + time.sleep(5) |
1285 | + if time.time() > timeout: |
1286 | + self.fail("Timed out waiting for the relation {} to be destroyed".format(rel_name)) |
1287 | + |
1288 | + def test_remove_nrpe_external_master(self): |
1289 | + """Test removing nrpe-external-master relation.""" |
1290 | + rel_name = "nrpe-external-master" |
1291 | + remote_app = "nrpe" |
1292 | + logging.info("Removing nrpe-external-master relation") |
1293 | + |
1294 | + zaza.model.remove_relation(self.application_name, rel_name, remote_app, self.model_name) |
1295 | + |
1296 | + try: |
1297 | + zaza.model.block_until_wl_status_info_starts_with( |
1298 | + self.application_name, status="missing nrpe-external-master relation", timeout=180 |
1299 | + ) |
1300 | + except concurrent.futures._base.TimeoutError: |
1301 | + self.fail("Timed out waiting for Unit to become blocked") |
1302 | + |
1303 | + logging.info("Waiting for relation {} to be destroyed".format(rel_name)) |
1304 | + timeout = time.time() + 600 |
1305 | + while self._get_relation_id(remote_app, rel_name) is not None: |
1306 | + time.sleep(5) |
1307 | + if time.time() > timeout: |
1308 | + self.fail("Timed out waiting for the relation {} to be destroyed".format(rel_name)) |
1309 | diff --git a/tests/functional/tests/tests.yaml b/tests/functional/tests/tests.yaml |
1310 | new file mode 100644 |
1311 | index 0000000..75a9ce8 |
1312 | --- /dev/null |
1313 | +++ b/tests/functional/tests/tests.yaml |
1314 | @@ -0,0 +1,33 @@ |
1315 | +tests: |
1316 | + - tests.kubernetes_service_checks.TestChecks |
1317 | + - tests.kubernetes_service_checks.TestRelations |
1318 | +target_deploy_status: |
1319 | + kubernetes-service-checks: |
1320 | + workload-status: blocked |
1321 | + workload-status-message: "missing kube-api-endpoint relation" |
1322 | + kubernetes-master: |
1323 | + workload-status: active |
1324 | + workload-status-message: "Kubernetes master running." |
1325 | + kubernetes-worker: |
1326 | + workload-status: active |
1327 | + workload-status-message: "Kubernetes worker running." |
1328 | + flannel: |
1329 | + workload-status: active |
1330 | + workload-status-message: "Flannel subnet" |
1331 | + easyrsa: |
1332 | + workload-status: active |
1333 | + workload-status-message: "Certificate Authority connected." |
1334 | + containerd: |
1335 | + workload-status: active |
1336 | + workload-status-message: "Container runtime available" |
1337 | + etcd: |
1338 | + workload-status: active |
1339 | + workload-status-message: "Healthy with 1 known peer" |
1340 | + nrpe: |
1341 | + workload-status: active |
1342 | + workload-status-message: "ready" |
1343 | +gate_bundles: |
1344 | + - xenial |
1345 | + - bionic |
1346 | +smoke_bundles: |
1347 | + - focal |
1348 | diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt |
1349 | new file mode 100644 |
1350 | index 0000000..b5f9fbd |
1351 | --- /dev/null |
1352 | +++ b/tests/unit/requirements.txt |
1353 | @@ -0,0 +1,5 @@ |
1354 | +mock |
1355 | +pyyaml |
1356 | +coverage |
1357 | +six |
1358 | +urllib3 |
1359 | diff --git a/tests/unit/setuppath.py b/tests/unit/setuppath.py |
1360 | new file mode 100644 |
1361 | index 0000000..c3404e5 |
1362 | --- /dev/null |
1363 | +++ b/tests/unit/setuppath.py |
1364 | @@ -0,0 +1,6 @@ |
1365 | +"""Include ./lib ./src and ./file/plugins in the tests' PATH.""" |
1366 | +import sys |
1367 | + |
1368 | +sys.path.append("lib") |
1369 | +sys.path.append("src") |
1370 | +sys.path.append("files/plugins") |
1371 | diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py |
1372 | new file mode 100644 |
1373 | index 0000000..24f83f5 |
1374 | --- /dev/null |
1375 | +++ b/tests/unit/test_charm.py |
1376 | @@ -0,0 +1,243 @@ |
1377 | +"""Charm unit tests.""" |
1378 | +import os |
1379 | +from pathlib import Path |
1380 | +import unittest |
1381 | + |
1382 | + |
1383 | +import mock |
1384 | +import yaml |
1385 | + |
1386 | +# include ./lib in the charm's PATH |
1387 | +import setuppath # noqa:F401 |
1388 | + |
1389 | +from charm import KubernetesServiceChecksCharm # noqa:I100 |
1390 | +import ops.main |
1391 | +from ops.testing import Harness |
1392 | + |
1393 | +TEST_KUBE_CONTOL_RELATION_DATA = { |
1394 | + "creds": """{"system:node:juju-62684f-0": |
1395 | + {"client_token": "DECAFBADBEEF", |
1396 | + "kubelet_token": "ABCDEF012345", |
1397 | + "proxy_token": "BADC0FFEEDAD", |
1398 | + "scope": "kubernetes-worker/0"} |
1399 | + }""" # noqa:E127 |
1400 | +} |
1401 | +TEST_KUBE_API_ENDPOINT_RELATION_DATA = {"hostname": "1.1.1.1", "port": "1111"} |
1402 | + |
1403 | + |
1404 | +class TestKubernetesServiceChecksCharm(unittest.TestCase): |
1405 | + """Test Kubernetes Service Checks Charm Code.""" |
1406 | + |
1407 | + @classmethod |
1408 | + def setUpClass(cls): |
1409 | + """Prepare class fixture.""" |
1410 | + # Stop unit test from calling fchown |
1411 | + fchown_patcher = mock.patch("os.fchown") |
1412 | + cls.mock_fchown = fchown_patcher.start() |
1413 | + chown_patcher = mock.patch("os.chown") |
1414 | + cls.mock_chown = chown_patcher.start() |
1415 | + |
1416 | + # Stop charmhelpers host from logging via debug log |
1417 | + host_log_patcher = mock.patch("charmhelpers.core.host.log") |
1418 | + cls.mock_juju_log = host_log_patcher.start() |
1419 | + |
1420 | + # Stop charmhelpers snap from logging via debug log |
1421 | + snap_log_patcher = mock.patch("charmhelpers.fetch.snap.log") |
1422 | + cls.mock_snap_log = snap_log_patcher.start() |
1423 | + |
1424 | + charm_logger_patcher = mock.patch("charm.logging") |
1425 | + cls.mock_charm_log = charm_logger_patcher.start() |
1426 | + |
1427 | + lib_logger_patcher = mock.patch("lib.lib_kubernetes_service_checks.logging") |
1428 | + cls.mock_lib_logger = lib_logger_patcher.start() |
1429 | + |
1430 | + # Prevent charmhelpers from calling systemctl |
1431 | + host_service_patcher = mock.patch("charmhelpers.core.host.service_stop") |
1432 | + cls.mock_service_stop = host_service_patcher.start() |
1433 | + host_service_patcher = mock.patch("charmhelpers.core.host.service_start") |
1434 | + cls.mock_service_start = host_service_patcher.start() |
1435 | + host_service_patcher = mock.patch("charmhelpers.core.host.service_restart") |
1436 | + cls.mock_service_restart = host_service_patcher.start() |
1437 | + |
1438 | + # Setup mock JUJU Environment variables |
1439 | + os.environ["JUJU_UNIT_NAME"] = "mock/0" |
1440 | + os.environ["JUJU_CHARM_DIR"] = "." |
1441 | + |
1442 | + def setUp(self): |
1443 | + """Prepare tests.""" |
1444 | + self.harness = Harness(KubernetesServiceChecksCharm) |
1445 | + # Mock config_get to return default config |
1446 | + with open(ops.main._get_charm_dir() / Path("config.yaml"), "r") as config_file: |
1447 | + config = yaml.safe_load(config_file) |
1448 | + charm_config = {} |
1449 | + |
1450 | + for key, _ in config["options"].items(): |
1451 | + charm_config[key] = config["options"][key]["default"] |
1452 | + |
1453 | + self.harness._backend._config = charm_config |
1454 | + |
1455 | + def test_harness(self): |
1456 | + """Verify harness.""" |
1457 | + self.harness.begin() |
1458 | + self.assertFalse(self.harness.charm.state.installed) |
1459 | + |
1460 | + @mock.patch("charmhelpers.fetch.snap.subprocess.check_call") |
1461 | + def test_install(self, mock_snap_subprocess): |
1462 | + """Test response to an install event.""" |
1463 | + mock_snap_subprocess.return_value = 0 |
1464 | + mock_snap_subprocess.side_effect = None |
1465 | + |
1466 | + self.harness.begin() |
1467 | + self.harness.charm.on.install.emit() |
1468 | + |
1469 | + self.assertEqual(self.harness.charm.unit.status.name, "maintenance") |
1470 | + self.assertEqual(self.harness.charm.unit.status.message, "Install complete") |
1471 | + self.assertTrue(self.harness.charm.state.installed) |
1472 | + |
1473 | + def test_config_changed(self): |
1474 | + """Test response to config changed event.""" |
1475 | + self.harness.set_leader(True) |
1476 | + self.harness.populate_oci_resources() |
1477 | + self.harness.begin() |
1478 | + self.harness.charm.check_charm_status = mock.MagicMock() |
1479 | + self.harness.charm.state.installed = True |
1480 | + self.harness.charm.on.config_changed.emit() |
1481 | + self.harness.charm.check_charm_status.assert_called_once() |
1482 | + |
1483 | + def test_start_not_installed(self): |
1484 | + """Test response to start event without install state.""" |
1485 | + self.harness.begin() |
1486 | + self.harness.charm.on.start.emit() |
1487 | + self.assertFalse(self.harness.charm.state.started) |
1488 | + |
1489 | + def test_start_not_configured(self): |
1490 | + """Test response to start event without configured state.""" |
1491 | + self.harness.begin() |
1492 | + self.harness.charm.state.installed = True |
1493 | + self.harness.charm.on.start.emit() |
1494 | + self.assertFalse(self.harness.charm.state.started) |
1495 | + |
1496 | + def test_start(self): |
1497 | + """Test response to start event.""" |
1498 | + self.harness.begin() |
1499 | + self.harness.charm.state.installed = True |
1500 | + self.harness.charm.state.configured = True |
1501 | + self.harness.charm.on.start.emit() |
1502 | + self.assertTrue(self.harness.charm.state.started) |
1503 | + self.assertEqual(self.harness.charm.unit.status.name, "active") |
1504 | + |
1505 | + def test_on_kube_api_endpoint_relation_changed(self): |
1506 | + """Check kube-api-endpoint relation changed handling.""" |
1507 | + relation_id = self.harness.add_relation("kube-api-endpoint", "kubernetes-master") |
1508 | + remote_unit = "kubernetes-master/0" |
1509 | + self.harness.begin() |
1510 | + self.harness.charm.check_charm_status = mock.MagicMock() |
1511 | + self.harness.add_relation_unit(relation_id, remote_unit) |
1512 | + self.harness.update_relation_data(relation_id, remote_unit, TEST_KUBE_API_ENDPOINT_RELATION_DATA) |
1513 | + |
1514 | + self.harness.charm.check_charm_status.assert_called_once() |
1515 | + self.assertEqual(self.harness.charm.helper.kubernetes_api_address, "1.1.1.1") |
1516 | + self.assertEqual(self.harness.charm.helper.kubernetes_api_port, "1111") |
1517 | + |
1518 | + def test_on_kube_control_relation_changed(self): |
1519 | + """Check kube-control relation changed handling.""" |
1520 | + relation_id = self.harness.add_relation("kube-control", "kubernetes-master") |
1521 | + remote_unit = "kubernetes-master/0" |
1522 | + self.harness.begin() |
1523 | + self.harness.charm.check_charm_status = mock.MagicMock() |
1524 | + self.harness.add_relation_unit(relation_id, remote_unit) |
1525 | + self.harness.update_relation_data(relation_id, remote_unit, TEST_KUBE_CONTOL_RELATION_DATA) |
1526 | + |
1527 | + self.harness.charm.check_charm_status.assert_called_once() |
1528 | + assert self.harness.charm.helper.kubernetes_client_token == "DECAFBADBEEF" |
1529 | + |
1530 | + def test_nrpe_external_master_relation_joined(self): |
1531 | + """Check that nrpe.configure is True after nrpe relation joined.""" |
1532 | + relation_id = self.harness.add_relation("nrpe-external-master", "nrpe") |
1533 | + remote_unit = "nrpe/0" |
1534 | + self.harness.begin() |
1535 | + self.assertFalse(self.harness.charm.state.nrpe_configured) |
1536 | + self.harness.charm.check_charm_status = mock.MagicMock() |
1537 | + self.harness.add_relation_unit(relation_id, remote_unit) |
1538 | + |
1539 | + self.harness.charm.check_charm_status.assert_called_once() |
1540 | + self.assertTrue(self.harness.charm.state.nrpe_configured) |
1541 | + |
1542 | + @mock.patch("ops.model.RelationData") |
1543 | + def test_nrpe_external_master_relation_departed(self, mock_relation_data): |
1544 | + """Check that nrpe.configure is False after nrpe relation departed.""" |
1545 | + mock_relation_data.return_value.__getitem__.return_value = {} |
1546 | + self.harness.begin() |
1547 | + self.harness.charm.check_charm_status = mock.MagicMock() |
1548 | + self.emit("nrpe_external_master_relation_departed") |
1549 | + self.harness.charm.check_charm_status.assert_called_once() |
1550 | + |
1551 | + self.assertFalse(self.harness.charm.state.nrpe_configured) |
1552 | + |
1553 | + def test_check_charm_status_kube_api_endpoint_relation_missing(self): |
1554 | + """Check that the chatm blocks without kube-api-endpoint relation.""" |
1555 | + self.harness.begin() |
1556 | + self.harness.charm.state.kube_control.update(TEST_KUBE_CONTOL_RELATION_DATA) |
1557 | + self.harness.charm.state.nrpe_configured = True |
1558 | + self.harness.charm.check_charm_status() |
1559 | + |
1560 | + self.assertFalse(self.harness.charm.state.configured) |
1561 | + self.assertEqual(self.harness.charm.unit.status.name, "blocked") |
1562 | + self.assertEqual(self.harness.charm.unit.status.message, "missing kube-api-endpoint relation") |
1563 | + |
1564 | + def test_check_charm_status_kube_control_relation_missing(self): |
1565 | + """Check that the charm blocks without kube-control relation.""" |
1566 | + self.harness.begin() |
1567 | + self.harness.charm.state.kube_api_endpoint.update(TEST_KUBE_API_ENDPOINT_RELATION_DATA) |
1568 | + self.harness.charm.state.nrpe_configured = True |
1569 | + self.harness.charm.check_charm_status() |
1570 | + |
1571 | + self.assertFalse(self.harness.charm.state.configured) |
1572 | + self.assertEqual(self.harness.charm.unit.status.name, "blocked") |
1573 | + self.assertEqual(self.harness.charm.unit.status.message, "missing kube-control relation") |
1574 | + |
1575 | + def test_check_charm_status_nrpe_relation_missing(self): |
1576 | + """Check that the charm bloack without nrpe relation.""" |
1577 | + self.harness.begin() |
1578 | + self.harness.charm.state.kube_control.update(TEST_KUBE_CONTOL_RELATION_DATA) |
1579 | + self.harness.charm.state.kube_api_endpoint.update(TEST_KUBE_API_ENDPOINT_RELATION_DATA) |
1580 | + self.harness.charm.check_charm_status() |
1581 | + |
1582 | + self.assertFalse(self.harness.charm.state.configured) |
1583 | + self.assertEqual(self.harness.charm.unit.status.name, "blocked") |
1584 | + self.assertEqual(self.harness.charm.unit.status.message, "missing nrpe-external-master relation") |
1585 | + |
1586 | + def test_check_charm_status_configured(self): |
1587 | + """Check the charm becomes configured.""" |
1588 | + self.harness.begin() |
1589 | + self.harness.charm.helper.configure = mock.MagicMock() |
1590 | + self.harness.charm.state.kube_control.update(TEST_KUBE_CONTOL_RELATION_DATA) |
1591 | + self.harness.charm.state.kube_api_endpoint.update(TEST_KUBE_API_ENDPOINT_RELATION_DATA) |
1592 | + self.harness.charm.state.nrpe_configured = True |
1593 | + self.harness.charm.check_charm_status() |
1594 | + |
1595 | + self.harness.charm.helper.configure.assert_called_once() |
1596 | + self.assertTrue(self.harness.charm.state.configured) |
1597 | + |
1598 | + def emit(self, event): |
1599 | + """Emit the named hook on the charm.""" |
1600 | + self.harness.charm.framework.reemit() |
1601 | + |
1602 | + if "_relation_" in event: |
1603 | + relation_name = event.split("_relation")[0].replace("_", "-") |
1604 | + with mock.patch.dict( |
1605 | + "os.environ", |
1606 | + { |
1607 | + "JUJU_RELATION": relation_name, |
1608 | + "JUJU_RELATION_ID": "1", |
1609 | + "JUJU_REMOTE_APP": "mock", |
1610 | + "JUJU_REMOTE_UNIT": "mock/0", |
1611 | + }, |
1612 | + ): |
1613 | + ops.main._emit_charm_event(self.harness.charm, event) |
1614 | + else: |
1615 | + ops.main._emit_charm_event(self.harness.charm, event) |
1616 | + |
1617 | + |
1618 | +if __name__ == "__main__": |
1619 | + unittest.main() |
1620 | diff --git a/tests/unit/test_lib_ksc.py b/tests/unit/test_lib_ksc.py |
1621 | new file mode 100644 |
1622 | index 0000000..94f67bc |
1623 | --- /dev/null |
1624 | +++ b/tests/unit/test_lib_ksc.py |
1625 | @@ -0,0 +1,164 @@ |
1626 | +"""Tests for Kubernetes Service Checks Helper.""" |
1627 | +import base64 |
1628 | +import os |
1629 | +import subprocess |
1630 | +from subprocess import CalledProcessError |
1631 | +import tempfile |
1632 | +import unittest |
1633 | + |
1634 | +from lib import lib_kubernetes_service_checks |
1635 | +import mock |
1636 | +import yaml |
1637 | + |
1638 | + |
1639 | +TEST_CERTIFICATE = """-----BEGIN CERTIFICATE----- |
1640 | +MIIDOzCCAiOgAwIBAgIJAPoOXrIwH+miMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV |
1641 | +BAMMDTEwLjEzMi4yNTEuNjAwHhcNMjAwNzE3MTMzMzI0WhcNMzAwNzE1MTMzMzI0 |
1642 | +WjAYMRYwFAYDVQQDDA0xMC4xMzIuMjUxLjYwMIIBIjANBgkqhkiG9w0BAQEFAAOC |
1643 | +AQ8AMIIBCgKCAQEAqpYVlmT/eRBhCKHaqXjY6EAzvx5GZY0PhL/YGBl9uF8YQGEF |
1644 | +F3k3Ec7pyJMIQblmWxdCPd1uNzHU8mwApiuPG9GtYOK+olqgslLsmOU9LTi6KJWX |
1645 | +x956VxdefXDYvr0B6K/Hdgkb1x//XwvipSV1fZ1MCDIiP/hWKi4CmEq31sVpCBdp |
1646 | +Uiz3qdCzsiGt0f4kbgIJSVtxhWlNJ5MaCOm7gXafkF8OIUTmWhmPp2gH7pfPzzl1 |
1647 | +glOX2Z41qwPuz7Jbcxx/z/yGjdPeJTQYoqJfpDpCrT2er5xyRf66HqKx9Ld/FiqM |
1648 | +ZksRwmzF9WvqCBK8WoRmnvFxk1FZPGt6E5gotwIDAQABo4GHMIGEMB0GA1UdDgQW |
1649 | +BBSUCCmRxb4tKD6w8jZ3hHs4ciFizDBIBgNVHSMEQTA/gBSUCCmRxb4tKD6w8jZ3 |
1650 | +hHs4ciFizKEcpBowGDEWMBQGA1UEAwwNMTAuMTMyLjI1MS42MIIJAPoOXrIwH+mi |
1651 | +MAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQBv |
1652 | +BYwILWI/4dGczqG0hcqt8tW04Oi+7y0HxzeI/oaUq/HKfvCz5a+WhpykMKRDJoaZ |
1653 | +aejR2Oc7A0OUnenpvMeIiMcUIetM3Q1Gzx0aU+vqUNNaZlooSSbe3z1VK6bUsYDo |
1654 | +qdKhs+mSyuEticK2SEWjT+ZWpV1rjSd5zRZ/UvC1ZhDNJGZotIIqryQWd3YfYl9l |
1655 | +7JrdzUVCbxs4ywxNp9/I+MJEiBfMHQx8FWr1M2HvLDAm6NZLfM68y5FClzfGpopV |
1656 | +0ARirz1AfbS6xUumyXHOH2qH527PUXFdfYGSn+juDG/dRTENYJ3OPAfWdj4ze1qQ |
1657 | +n3ajLSYPvdyKaztdB1VL |
1658 | +-----END CERTIFICATE-----""" |
1659 | + |
1660 | + |
1661 | +class TestLibKSCHelper(unittest.TestCase): |
1662 | + """Unittest class for Kubernetes Service Checks Helper.""" |
1663 | + |
1664 | + @classmethod |
1665 | + def setUpClass(cls): |
1666 | + """Prepare Class Fixture.""" |
1667 | + # Load default config |
1668 | + with open("./config.yaml") as default_config: |
1669 | + cls.config = yaml.safe_load(default_config) |
1670 | + |
1671 | + # set defaults to the config object |
1672 | + for key in cls.config["options"]: |
1673 | + if "default" in cls.config["options"][key]: |
1674 | + cls.config[key] = cls.config["options"][key]["default"] |
1675 | + |
1676 | + # Create test state object |
1677 | + class FakeStateObject(object): |
1678 | + kube_api_endpoint = {"hostname": "1.1.1.1", "port": "1111"} |
1679 | + kube_control = {"creds": """{"kube-client": {"client_token": "abcdef0123456789"}}"""} |
1680 | + installed = False |
1681 | + configured = False |
1682 | + started = False |
1683 | + nrpe_configured = False |
1684 | + |
1685 | + cls.state = FakeStateObject() |
1686 | + |
1687 | + # Stop unit test from calling fchown |
1688 | + fchown_patcher = mock.patch("os.fchown") |
1689 | + cls.mock_fchown = fchown_patcher.start() |
1690 | + chown_patcher = mock.patch("os.chown") |
1691 | + cls.mock_chown = chown_patcher.start() |
1692 | + |
1693 | + # Stop charmhelpers host from logging via debug log |
1694 | + host_log_patcher = mock.patch("charmhelpers.core.host.log") |
1695 | + cls.mock_juju_log = host_log_patcher.start() |
1696 | + |
1697 | + host_logger_patcher = mock.patch("lib.lib_kubernetes_service_checks.logging") |
1698 | + cls.mock_logger = host_logger_patcher.start() |
1699 | + |
1700 | + # Stop charmhelpers snap from logging via debug log |
1701 | + snap_log_patcher = mock.patch("charmhelpers.fetch.snap.log") |
1702 | + cls.mock_snap_log = snap_log_patcher.start() |
1703 | + |
1704 | + # Setup a tmpdir |
1705 | + cls.tmpdir = tempfile.TemporaryDirectory() |
1706 | + cls.cert_path = os.path.join(cls.tmpdir.name, "kubernetes-service-checks.crt") |
1707 | + |
1708 | + lib_kubernetes_service_checks.CERT_FILE = cls.cert_path |
1709 | + lib_kubernetes_service_checks.NAGIOS_PLUGINS_DIR = cls.tmpdir.name |
1710 | + |
1711 | + @classmethod |
1712 | + def tearDownClass(cls): |
1713 | + """Tear down class fixture.""" |
1714 | + mock.patch.stopall() |
1715 | + cls.tmpdir.cleanup() |
1716 | + |
1717 | + def setUp(self): |
1718 | + """Prepare test fixture.""" |
1719 | + self.helper = lib_kubernetes_service_checks.KSCHelper(self.config, self.state) |
1720 | + |
1721 | + def tearDown(self): |
1722 | + """Clean up test fixture.""" |
1723 | + try: |
1724 | + os.remove(self.cert_path) |
1725 | + except FileNotFoundError: |
1726 | + pass |
1727 | + |
1728 | + def test_kube_api_endpoint_properties(self): |
1729 | + """Test that hostname and port properties get passed through.""" |
1730 | + # kube_api_endpoint (relation) -> hostname & port |
1731 | + self.assertEqual(self.helper.kubernetes_api_address, "1.1.1.1") |
1732 | + self.assertEqual(self.helper.kubernetes_api_port, "1111") |
1733 | + |
1734 | + self.helper.state.kube_api_endpoint = {} |
1735 | + self.assertEqual(self.helper.kubernetes_api_address, None) |
1736 | + self.assertEqual(self.helper.kubernetes_api_port, None) |
1737 | + |
1738 | + def test_kube_control_endpoint_properties(self): |
1739 | + """Test KSCHelper client_token gets passed though.""" |
1740 | + # kube-control (relation) -> kube client token |
1741 | + self.assertEqual(self.helper.kubernetes_client_token, "abcdef0123456789") |
1742 | + |
1743 | + self.helper.state.kube_control = {} |
1744 | + self.assertEqual(self.helper.kubernetes_client_token, None) |
1745 | + |
1746 | + @mock.patch("lib.lib_kubernetes_service_checks.subprocess.call") |
1747 | + def test_update_tls_certificates(self, mock_subprocess): |
1748 | + """Test that SSL certificates get updated.""" |
1749 | + # returns False when no available trusted_ssl_cert |
1750 | + self.assertFalse(self.helper.update_tls_certificates()) |
1751 | + |
1752 | + # returns True when subprocess successful |
1753 | + self.helper.config["trusted_ssl_ca"] = base64.b64encode(str.encode(TEST_CERTIFICATE)) |
1754 | + self.assertTrue(self.helper.update_tls_certificates()) |
1755 | + with open(self.cert_path, "r") as f: |
1756 | + self.assertEqual(f.read(), TEST_CERTIFICATE) |
1757 | + mock_subprocess.assert_called_once_with(["/usr/sbin/update-ca-certificates"]) |
1758 | + mock_subprocess.reset_mock() |
1759 | + |
1760 | + # returns false when subprocess hits an exception |
1761 | + mock_subprocess.side_effect = CalledProcessError("Command", "Mock Subprocess Call Error") |
1762 | + self.assertFalse(self.helper.update_tls_certificates()) |
1763 | + |
1764 | + def test_render_checks(self): |
1765 | + """Test that NPRE is called to add KSC checks.""" |
1766 | + # TODO |
1767 | + pass |
1768 | + |
1769 | + @mock.patch("charmhelpers.fetch.snap.subprocess.check_call") |
1770 | + def test_install_kubectl(self, mock_snap_subprocess): |
1771 | + """Test install kubectl snap helper function.""" |
1772 | + self.assertTrue(self.helper.install_kubectl()) |
1773 | + channel = self.config.get("channel") |
1774 | + mock_snap_subprocess.assert_called_with( |
1775 | + ["snap", "install", "--classic", "--channel={}".format(channel), "kubectl"], env=os.environ |
1776 | + ) |
1777 | + |
1778 | + @mock.patch("charmhelpers.fetch.snap.subprocess.check_call") |
1779 | + def test_install_snap_failure(self, mock_snap_subprocess): |
1780 | + """Test response to a failed install event.""" |
1781 | + error = subprocess.CalledProcessError("cmd", "Install failed") |
1782 | + error.returncode = 1 |
1783 | + mock_snap_subprocess.return_value = 1 |
1784 | + mock_snap_subprocess.side_effect = error |
1785 | + self.assertFalse(self.helper.install_kubectl()) |
1786 | + |
1787 | + |
1788 | +if __name__ == "__main__": |
1789 | + unittest.main() |
1790 | diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py |
1791 | new file mode 100644 |
1792 | index 0000000..42d1697 |
1793 | --- /dev/null |
1794 | +++ b/tests/unit/test_plugins.py |
1795 | @@ -0,0 +1,65 @@ |
1796 | +"""Unit tests for Kubernetes Service Checks NRPE Plugins.""" |
1797 | +import unittest |
1798 | + |
1799 | +import check_kubernetes_api |
1800 | +import mock |
1801 | + |
1802 | + |
1803 | +class TestKSCPlugins(unittest.TestCase): |
1804 | + """Test cases for Kubernetes Service Checks NRPE plugins.""" |
1805 | + |
1806 | + @mock.patch("check_kubernetes_api.sys.exit") |
1807 | + @mock.patch("check_kubernetes_api.print") |
1808 | + def test_nagios_exit(self, mock_print, mock_sys_exit): |
1809 | + """Test the nagios_exit function.""" |
1810 | + msg = "Test message" |
1811 | + for code, status in check_kubernetes_api.NAGIOS_STATUS.items(): |
1812 | + expected_output = "{}: {}".format(status, msg) |
1813 | + check_kubernetes_api.nagios_exit(code, msg) |
1814 | + |
1815 | + mock_print.assert_called_with(expected_output) |
1816 | + mock_sys_exit.assert_called_with(code) |
1817 | + |
1818 | + @mock.patch("check_kubernetes_api.urllib3.PoolManager") |
1819 | + def test_kubernetes_health_ssl(self, mock_http_pool_manager): |
1820 | + """Test the check k8s health function called with expected ssl params.""" |
1821 | + host_address = "https://1.1.1.1:1111" |
1822 | + token = "0123456789abcdef" |
1823 | + disable_ssl = True |
1824 | + |
1825 | + mock_http_pool_manager.return_value.status = 200 |
1826 | + mock_http_pool_manager.return_value.data = b"ok" |
1827 | + |
1828 | + check_kubernetes_api.check_kubernetes_health(host_address, token, disable_ssl) |
1829 | + mock_http_pool_manager.assert_called_with(cert_reqs="CERT_NONE", assert_hostname=False) |
1830 | + |
1831 | + disable_ssl = False |
1832 | + check_kubernetes_api.check_kubernetes_health(host_address, token, disable_ssl) |
1833 | + mock_http_pool_manager.assert_called_with() |
1834 | + |
1835 | + @mock.patch("check_kubernetes_api.urllib3.PoolManager") |
1836 | + def test_kubernetes_health_status(self, mock_http_pool_manager): |
1837 | + """Test kubernetes health function.""" |
1838 | + host_address = "https://1.1.1.1:1111" |
1839 | + token = "0123456789abcdef" |
1840 | + ssl_ca = "test/cert/path" |
1841 | + |
1842 | + mock_http_pool_manager.return_value.request.return_value.status = 200 |
1843 | + mock_http_pool_manager.return_value.request.return_value.data = b"ok" |
1844 | + |
1845 | + # verify status OK |
1846 | + status, _ = check_kubernetes_api.check_kubernetes_health(host_address, token, ssl_ca) |
1847 | + self.assertEqual(status, check_kubernetes_api.NAGIOS_STATUS_OK) |
1848 | + mock_http_pool_manager.return_value.request.assert_called_once_with( |
1849 | + "GET", "{}/healthz".format(host_address), headers={"Authorization": "Bearer {}".format(token)} |
1850 | + ) |
1851 | + |
1852 | + mock_http_pool_manager.return_value.request.return_value.status = 500 |
1853 | + mock_http_pool_manager.return_value.request.return_value.data = b"ok" |
1854 | + status, _ = check_kubernetes_api.check_kubernetes_health(host_address, token, ssl_ca) |
1855 | + self.assertEqual(status, check_kubernetes_api.NAGIOS_STATUS_CRITICAL) |
1856 | + |
1857 | + mock_http_pool_manager.return_value.request.return_value.status = 200 |
1858 | + mock_http_pool_manager.return_value.request.return_value.data = b"not ok" |
1859 | + status, _ = check_kubernetes_api.check_kubernetes_health(host_address, token, ssl_ca) |
1860 | + self.assertEqual(status, check_kubernetes_api.NAGIOS_STATUS_WARNING) |
1861 | diff --git a/tox.ini b/tox.ini |
1862 | new file mode 100644 |
1863 | index 0000000..2f92371 |
1864 | --- /dev/null |
1865 | +++ b/tox.ini |
1866 | @@ -0,0 +1,52 @@ |
1867 | +[tox] |
1868 | +skipsdist = True |
1869 | +envlist = unit, func |
1870 | +skip_missing_interpreters = True |
1871 | + |
1872 | +[testenv] |
1873 | +basepython = python3 |
1874 | +setenv = |
1875 | + PYTHONPATH = {toxinidir}/lib/:{toxinidir} |
1876 | +passenv = |
1877 | + HOME |
1878 | + MODEL_SETTINGS |
1879 | + CHARM_BUILD_DIR |
1880 | + |
1881 | +[testenv:unit] |
1882 | +commands = |
1883 | + coverage run -m unittest discover -s {toxinidir}/tests/unit -v |
1884 | + coverage report \ |
1885 | + --omit tests/*,mod/*,.tox/* |
1886 | + coverage html \ |
1887 | + --omit tests/*,mod/*,.tox/* |
1888 | +deps = -r{toxinidir}/tests/unit/requirements.txt |
1889 | + |
1890 | +[testenv:func] |
1891 | +changedir = {toxinidir}/tests/functional |
1892 | +commands = functest-run-suite {posargs} |
1893 | +deps = -r{toxinidir}/tests/functional/requirements.txt |
1894 | + |
1895 | +[testenv:lint] |
1896 | +commands = |
1897 | + flake8 |
1898 | + black --check --line-length 120 --exclude /(\.eggs|\.git|\.tox|\.venv|build|dist|charmhelpers|mod)/ . |
1899 | +deps = |
1900 | + black |
1901 | + flake8 |
1902 | + flake8-docstrings |
1903 | + flake8-import-order |
1904 | + pep8-naming |
1905 | + flake8-colors |
1906 | + |
1907 | +[flake8] |
1908 | +exclude = |
1909 | + .git, |
1910 | + __pycache__, |
1911 | + .tox, |
1912 | + mod, |
1913 | +max-line-length = 120 |
1914 | +max-complexity = 10 |
1915 | +import-order-style = google |
1916 | + |
1917 | +[isort] |
1918 | +force_to_top=setuppath |
Generally code looks good.
The only comment I have is on the self.state. configured. It is not changed to False when a config changed is happening or a relation has joined, changed or departed (it is set to false only on departed), while it should be to reflect that this state is controlling whether check_charm_status should reconfigure or not the unit.
Also the self.helper. configure( ) is called in check_charm_status without checking if this state is true or false, while seems that this should be called only if configured is false.
Same for self.helper. update_ tls_certificate s(). This is needed only on initial deploy (and configured is False at the beginning) or later if a config_changed has occurred as the certificate can be updated only on a config changed at this stage.
Maybe i am not reading this state correctly. In this case more information are needed.