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