Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
pip install requests
pip install vcrpy
pip install prometheus_client
pip install redis
- name: Analysing the code with pylint
run: |
# Set PYTHONPATH so pylint can find the app module
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ RUN pip install --no-cache-dir -r /app/requirements.txt --require-hashes && \
chown -R appuser:appgroup /app

ENV FLASK_APP=app.main.py:app \
PYTHONUNBUFFERED=1
PYTHONUNBUFFERED=1 \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
CACHE_TTL=300

USER appuser

Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# HiveBox Project

## Introduction

DevOps project that includes most used technologies in the industry. Focusing in the CI and CD tools and integrating them simultaneously to achieve topics as the following:

- Software Production.
- Agile Planning.
- QA and Quality Gates.
- Code and Programming.
- Operating System.
- Docker Containers.
- Kubernetes and Cloud.
- Observability and Monitoring.
- Continuous Integration/Delivery/Deployment.
- Automation and Infrastructure as Code.

For more information please refer to the Project webpage: [HiveBox](https://devopsroadmap.io/projects/hivebox/)

## Technologies Used
- Python (Flask, Prometheus, Redis, Requests)
- Docker
- Kubernetes (Minikube, Kind)
- Dependabot
- GitHub Actions:
- SonarQube
- Terrascan
- Pylint
- Hadolint

## Development

To adhere to industry standards, the Repository's Project section was used, leveraging the Kanban board to process tasks in an organized manner.

[Kanban Board](https://github.com/users/GabrielPalmar/projects/1)
63 changes: 53 additions & 10 deletions app/opensense.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,56 @@
'''Module to get entries from OpenSenseMap API and get the average temperature'''
from datetime import datetime, timezone, timedelta
import os
import requests
import redis

REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379))
REDIS_DB = int(os.environ.get('REDIS_DB', 0))
CACHE_TTL = int(os.environ.get('CACHE_TTL', 300))

try:
redis_client = redis.StrictRedis(
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB,
decode_responses=True,
socket_connect_timeout=240,
socket_timeout=240
)

redis_client.ping()
REDIS_AVAILABLE = True
print("Connected to Redis successfully!")
except (redis.ConnectionError, redis.TimeoutError) as e:
REDIS_AVAILABLE = False
print("Could not connect to Redis.")

def get_temperature():
'''Function to get the average temperature from OpenSenseMap API.'''
# Ensuring that data is not older than 1 hour.
subs_time = datetime.now(timezone.utc) - timedelta(hours=1)
time_iso = subs_time.isoformat().replace("+00:00", "Z")
cache_key = "temperature_data"
if REDIS_AVAILABLE:
try:
cached_data = redis_client.get(cache_key)
if cached_data:
print("Using cached data from Redis.")
return cached_data
except redis.RedisError as e:
print(f"Redis error: {e}. Proceeding without cache.")

print("Fetching new data from OpenSenseMap API...")

api_endpoint = "https://api.opensensemap.org/boxes"
# Ensuring that data is not older than 1 hour.
time_iso = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat().replace("+00:00", "Z")

params = {
"date": time_iso,
"format": "json"
}

print('Getting data from OpenSenseMap API...')
response = requests.get(api_endpoint, params=params, timeout=240)

response = requests.get("https://api.opensensemap.org/boxes", params=params, timeout=240)
print('Data retrieved successfully!')

res = [d.get('sensors') for d in response.json() if 'sensors' in d]
Expand All @@ -35,8 +68,18 @@ def get_temperature():
total_sum = sum(temp_list)
average = total_sum / len(temp_list) if temp_list else 0

if average < 10:
return f'Average temperature: {average:.2f} °C (Warning: Too cold)\n'
if 10 < average <= 36:
return f'Average temperature: {average:.2f} °C (Good)\n'
return f'Average temperature: {average:.2f} °C (Warning: Too hot)\n'
if average <= 10:
result = f'Average temperature: {average:.2f} °C (Warning: Too cold)\n'
elif 10 < average <= 36:
result = f'Average temperature: {average:.2f} °C (Good)\n'
else:
result = f'Average temperature: {average:.2f} °C (Warning: Too hot)\n'

if REDIS_AVAILABLE:
try:
redis_client.setex(cache_key, CACHE_TTL, result)
print("Data cached in Redis.")
except redis.RedisError as e:
print(f"Redis error while caching data: {e}")

return result
28 changes: 25 additions & 3 deletions k8s/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ spec:
image: ghcr.io/gabrielpalmar/hivebox:0.4.0@sha256:31dccc066ffd02ef65850ed8125fc2dadf0bd65958fb49bee0517e40afab2e1c
ports:
- containerPort: 5000
env:
- name: FLASK_ENV
value: "production"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
Expand Down Expand Up @@ -63,6 +60,31 @@ spec:
volumeMounts:
- name: tmp-volume
mountPath: /tmp
- name: valkey
image: valkey/valkey:8-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4
ports:
- containerPort: 6379
command: ["valkey-server"]
args: ["--save", "", "--appendonly", "no"]
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 999
capabilities:
drop:
- ALL
resources:
limits:
memory: "256Mi"
cpu: "250m"
requests:
memory: "128Mi"
cpu: "100m"
volumeMounts:
- name: valkey-data
mountPath: /data
volumes:
- name: tmp-volume
emptyDir: {}
- name: valkey-data
emptyDir: {}
3 changes: 2 additions & 1 deletion requirements.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Flask==3.1.1
requests==2.32.4
prometheus-client==0.22.1
prometheus-client==0.22.1
redis==6.2.0
20 changes: 12 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --allow-unsafe --generate-hashes --output-file=requirements.txt requirements.in
# pip-compile --generate-hashes --output-file=requirements.txt requirements.in
#
blinker==1.9.0 \
--hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \
--hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc
# via flask
certifi==2025.4.26 \
--hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \
--hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3
certifi==2025.7.9 \
--hash=sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079 \
--hash=sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39
# via requests
charset-normalizer==3.4.2 \
--hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \
Expand Down Expand Up @@ -196,15 +196,19 @@ prometheus-client==0.22.1 \
--hash=sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28 \
--hash=sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094
# via -r requirements.in
redis==6.2.0 \
--hash=sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e \
--hash=sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977
# via -r requirements.in
requests==2.32.4 \
--hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \
--hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422
# via -r requirements.in
urllib3==2.4.0 \
--hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \
--hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813
urllib3==2.5.0 \
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
# via requests
werkzeug==3.1.3 \
--hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \
--hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746
# via flask
# via flask
20 changes: 19 additions & 1 deletion tests/test_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ def test_metrics_endpoint():
response = client.get('/metrics')
assert response.status_code == 200

# Add a simple test for the opensense module
def test_opensense_get_temperature():
"""Test that opensense.get_temperature returns a string"""
result = opensense.get_temperature()
Expand Down Expand Up @@ -63,3 +62,22 @@ def test_opensense_get_temperature_too_hot():
with mock.patch('app.opensense.requests.get', return_value=mock_response(40)):
result = opensense.get_temperature()
assert 'Too hot' in result

def test_opensense_cache_get():
"""Test that cached data is used if available"""
with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
mock.patch('app.opensense.redis_client.get', return_value="cached_result"), \
mock.patch('app.opensense.requests.get') as mock_requests:
result = opensense.get_temperature()
assert result == "cached_result"
mock_requests.assert_not_called()

def test_opensense_cache_setex():
"""Test that data is cached after fetching"""
with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
mock.patch('app.opensense.redis_client.get', return_value=None), \
mock.patch('app.opensense.redis_client.setex') as mock_setex, \
mock.patch('app.opensense.requests.get', return_value=mock_response(25)):
result = opensense.get_temperature()
assert 'Average temperature' in result
mock_setex.assert_called()
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.1
0.5.0