Skip to content

Commit

Permalink
fix #28 Adding coverage tests
Browse files Browse the repository at this point in the history
  • Loading branch information
LawiK974 committed Jul 21, 2024
1 parent 8cb4e77 commit a08fc3a
Show file tree
Hide file tree
Showing 18 changed files with 522 additions and 40 deletions.
Binary file added .coverage
Binary file not shown.
52 changes: 52 additions & 0 deletions .github/workflows/pytest-main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Pytest

on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow one concurrent deployment
concurrency:
group: 'pages'
cancel-in-progress: true

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup Python # Set Python version
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install dependencies
run: |
python -m pip install --upgrade pipx
python -m pipx install poetry
poetry install
- name: Test with pytest
run: export PYTHONUNBUFFERED=1 && poetry run pytest --cov --cov-report=xml --cov-report=term --cov-report=html --junit-xml=report.xml
- name: Genbadge coverage
run: poetry run genbadge coverage -i coverage.xml
- name: Genbadge tests
run: poetry run genbadge tests -i report.xml
- name: move artifacts to htmlcov
run: mv report.xml coverage.xml coverage-badge.svg tests-badge.svg ./htmlcov/
- name: Setup Pages
uses: actions/configure-pages@v5

- name: Upload coverage report
uses: actions/upload-pages-artifact@v3
with:
path: './htmlcov'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
28 changes: 28 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Pytest

on: [push]

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup Python # Set Python version
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install dependencies
run: |
python -m pip install --upgrade pipx
python -m pipx install poetry
poetry install
- name: Test with pytest
run: export PYTHONUNBUFFERED=1 && poetry run pytest --cov
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ config.yaml
*__pycache__*
*.dat
dist/
htmlcov
.coverage
coverage.xml
report.xml
28 changes: 28 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
![GitHub Release](https://img.shields.io/github/v/release/LawiK974/kube-notify?display_name=release&link=https%3A%2F%2Fgithub.com%2FLawiK974%2Fkube-notify%2Freleases%2Flatest)
![GitHub Actions Workflow Status Main](https://img.shields.io/github/actions/workflow/status/LawiK974/kube-notify/github-actions-docker.yml?branch=main&label=Build%26Push%20Main)
![GitHub Actions Workflow Status Release](https://img.shields.io/github/actions/workflow/status/LawiK974/kube-notify/github-actions-docker-tags.yml?label=Build%26Push%20Release)
[![tests](https://lawik974.github.io/kube-notify/tests-badge.svg)](https://lawik974.github.io/kube-notify/)
[![coverage](https://lawik974.github.io/kube-notify/coverage-badge.svg)](https://lawik974.github.io/kube-notify/)


An app that watches kubernetes resource creation, deletion, updates and errors events and notify selected events to gotify.
Expand Down Expand Up @@ -38,6 +40,32 @@ kubectl apply -f deployement.yaml
All configuration are in `/app/config.yaml` file.
Use [sample config](./config.sample.yaml) as an example.

## Contributing

**Installing**

After installing `poetry` and `pyenv` you should do :

```sh
poetry install
```

**Launching tests**

```sh
poetry run pytest --cov
```

**Kube-notify locally against a remote cluster**

1.
2.

```sh
export PYTHONUNBUFFERED=1
poetry run kube-notify -c config.yaml
```

## To do

- [ ] Optimize Code
Expand Down
6 changes: 4 additions & 2 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ notifications:
reasons: # list work as a `or`
- ApplyJob # Recurrent K3s HelmChart apply job
- Unhealthy
- Scheduled
# - Scheduled
- SuccessfulCreate
- SuccessfulDelete
- Created
- AddedInterface
- Pulled
Expand All @@ -32,7 +34,7 @@ notifications:
discord:
webhook: toto
username: kube-notify
# avatar_url: changeme
avatar_url: changeme
mattermost:
webhook: https://chat.local/hooks/aaaaaaaaa
# username: kube-notify
Expand Down
3 changes: 0 additions & 3 deletions kube_notify/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import argparse
import asyncio
import datetime

__version__ = "0.0.0"
STARTUP_TIME = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
type EventInfo = tuple[datetime.datetime, str, str, str, str, str, str, str]
ioloop = asyncio.new_event_loop()
asyncio.set_event_loop(ioloop)
parser = argparse.ArgumentParser(
prog=f"kube-notify-{__version__}",
description="An app that watches kubernetes resource creation, deletion, updates and errors events and notify selected events to gotify.",
Expand Down
37 changes: 21 additions & 16 deletions kube_notify/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@
from kube_notify.utils import logger, misc


def start_kube_notify_loop(kube_notify_config: dict, iterate: bool = True) -> None:
def start_kube_notify_loop(
kube_notify_config: dict,
in_cluster: bool = True,
context: str | None = None,
iterate: bool = True,
) -> None:
ioloop = asyncio.new_event_loop()
asyncio.set_event_loop(ioloop)
# Initialize Kubernetes client
if in_cluster: # pragma: no cover
config.load_incluster_config()
else:
ioloop.run_until_complete(config.load_kube_config(context=context))
logger.logger.info(
f"Starting kube-notify {kube_notify.__version__} at {kube_notify.STARTUP_TIME}"
)
Expand Down Expand Up @@ -36,32 +48,25 @@ def start_kube_notify_loop(kube_notify_config: dict, iterate: bool = True) -> No
)
)
)
else:
else: # pragma: no cover
# if crd configuration is missing "type"
error = f"Couldn't get CRD type from 'customResources' at index {index}"
logger.logger.error(error)
raise ValueError(error)
try:
kube_notify.ioloop.run_until_complete(asyncio.wait(tasks))
# kube_notify.ioloop.run_forever()
ioloop.run_until_complete(asyncio.wait(tasks))
except Exception as e:
logger.logger.error("[Error] Ignoring :" + e)
finally:
kube_notify.ioloop.run_until_complete(kube_notify.ioloop.shutdown_asyncgens())
kube_notify.ioloop.close()
ioloop.run_until_complete(ioloop.shutdown_asyncgens())
ioloop.close()


def main() -> None:
def main() -> None: # pragma: no cover
args = kube_notify.parser.parse_args()
# Initialize Kubernetes client
if args.inCluster:
config.load_incluster_config()
else:
kube_notify.ioloop.run_until_complete(
config.load_kube_config(context=args.context)
)
kube_notify_config = misc.load_kube_notify_config(args.config)
start_kube_notify_loop(kube_notify_config)
start_kube_notify_loop(kube_notify_config, args.inCluster, args.context)


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
main()
2 changes: 1 addition & 1 deletion kube_notify/notifications/gotify.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def send_gotify_message(
# Construct the HTTP request for sending a message to Gotify
url = f"{url}/message?token={token}&format=markdown"
headers = {"Content-Type": "application/json"}
message = f"**{description}**\\\n"
message = f"**{description}**\n\n"

for key, value in fields.items():
message += f"**{key} :** {value}\\\n"
Expand Down
2 changes: 1 addition & 1 deletion kube_notify/stream/core_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ async def core_stream(kube_notify_config: dict, iterate: bool = True) -> None:
)
await asyncio.sleep(0)
del stream
except Exception as e:
except Exception as e: # pragma: no cover
logger.logger.error(f"{type(e).__name__}: {e}")
if not iterate:
raise e
Expand Down
35 changes: 18 additions & 17 deletions kube_notify/stream/pod_terminations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# import datetime

from kubernetes_asyncio import client


Expand All @@ -9,7 +7,10 @@ def find_container_restart_policy(
for container in containers:
if container.name == container_name:
return container.restart_policy
raise KeyError(f"{container_name} not found in containers for pod {pod_name}")
# We should never be in the following situation :
raise KeyError(
f"{container_name} not found in containers for pod {pod_name}"
) # pragma: no cover


def get_container_state(restart_policy: str | None, exit_code: int) -> tuple[bool, str]:
Expand All @@ -19,9 +20,7 @@ def get_container_state(restart_policy: str | None, exit_code: int) -> tuple[boo
return True, "restarted"
if restart_policy == "Never" and exit_code != 0:
return True, "crashed"
if exit_code == 0:
return False, "finished"
return False, ""
return False, "" # Completed or Running


async def generate_pod_termination_events(
Expand All @@ -31,16 +30,14 @@ async def generate_pod_termination_events(
pods: client.V1PodList = await api.list_pod_for_all_namespaces()
for pod in pods.items:
containers = (
pod.spec.containers
or [] + pod.spec.init_containers
or [] + pod.spec.ephemeral_containers
or []
(pod.spec.containers or [])
+ (pod.spec.init_containers or [])
+ (pod.spec.ephemeral_containers or [])
)
for container_status in (
pod.status.container_statuses
or [] + pod.status.ephemeral_container_statuses
or [] + pod.status.init_container_statuses
or []
(pod.status.container_statuses or [])
+ (pod.status.ephemeral_container_statuses or [])
+ (pod.status.init_container_statuses or [])
):
namespace = pod.metadata.namespace
pod_name = pod.metadata.name
Expand All @@ -59,7 +56,12 @@ async def generate_pod_termination_events(
if is_error:
reason = status.terminated.reason
timestamp = status.terminated.finished_at
message = status.terminated.message
message = (
f"{reason}({exit_code}): Container {container_name} {state} "
f"(restartCount: {restart_count})"
)
if status.terminated.message:
message += f"{', message :\n' + status.terminated.message}"
events.append(
client.CoreV1Event(
kind="Event",
Expand All @@ -70,8 +72,7 @@ async def generate_pod_termination_events(
),
reason=reason,
last_timestamp=timestamp,
message=message
or f"{reason}({exit_code}): Container {container_name} {state} (restartCount: {restart_count})",
message=message,
involved_object=client.V1ObjectReference(
kind="Pod", name=pod_name, namespace=namespace
),
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CONFIG = "./config.sample.yaml"
54 changes: 54 additions & 0 deletions tests/data/events.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
test_pod_created:
type: Normal
involved_object:
kind: Pod
name: test-pod
namespace: default
reason: Scheduled
message: This is a test event
log_message: "[default_group/discord,default_group/gotify,default_group/mattermost] [✅] Pod test-pod Scheduled."

test_pod_created_excluded:
type: Normal
involved_object:
kind: Job
name: test-job
namespace: default
reason: Created
message: This is a test event
log_message: "[Excluded] [✅] Job test-job Created."

test_deleted_pod:
type: Normal
involved_object:
kind: Pod
name: unknown
namespace: default
reason: Killing
message: This is a test event
log_message: "[Excluded] [✅] Pod unknown Killing."

test_pod_oomkilled:
log_message: "[default_group/discord,default_group/gotify,default_group/mattermost] [💥] Pod test-pod OOMKilled."

test_pod_crashed:
log_message: "[default_group/discord,default_group/gotify,default_group/mattermost] [💥] Pod website Error."

test_unhealthy_pod:
type: Warning
involved_object:
kind: Pod
name: cloud
namespace: default
reason: Unhealthy
message: Liveness Probe failed.
log_message: "[Excluded] [⚠️] Pod cloud Unhealthy."

test_unknown_event:
type: Unknown
involved_object:
kind: Unknown
name: Unknown
reason: Unknown
message: Unknown
log_message: "[default_group/discord,default_group/gotify,default_group/mattermost] [🔔] Unknown Unknown Unknown."
Loading

0 comments on commit a08fc3a

Please sign in to comment.