Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Release #87

Merged
merged 19 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions .github/workflows/ci-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
cache-to: type=inline
- uses: actions/[email protected]
with:
node-version: "20.12.0"
node-version: "20.12"

build-backend:
name: build-backend
Expand Down Expand Up @@ -128,4 +128,4 @@ jobs:
cache-to: type=inline
- uses: actions/[email protected]
with:
node-version: "20.12.0"
node-version: "20.12"
2 changes: 1 addition & 1 deletion .github/workflows/ci-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
strategy:
matrix:
node-version:
- 20.12.0
- 20.12
steps:
- uses: actions/checkout@v4
- name: Set Node.js ${{ matrix.node-version }}
Expand Down
19 changes: 11 additions & 8 deletions .github/workflows/ci-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ on:
- "main"
- "develop"
- "next"
tags-ignore:
tags:
- "v*"

jobs:
Expand Down Expand Up @@ -61,6 +61,7 @@ jobs:
type=semver,pattern={{major}}
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
- name: Build and push action - frontend
id: docker_action_build_frontend
uses: docker/build-push-action@v5
Expand All @@ -73,7 +74,7 @@ jobs:
cache-to: type=inline
- uses: actions/[email protected]
with:
node-version: "20.12.0"
node-version: "20.12"

build-backend:
name: build-backend
Expand Down Expand Up @@ -111,6 +112,7 @@ jobs:
type=semver,pattern={{major}}
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
- name: Build and push action - backend
id: docker_action_build_backend
uses: docker/build-push-action@v5
Expand All @@ -123,7 +125,7 @@ jobs:
cache-to: type=inline
- uses: actions/[email protected]
with:
node-version: "20.12.0"
node-version: "20.12"
release:
name: Release
needs: [build-frontend, build-backend]
Expand All @@ -138,15 +140,16 @@ jobs:
persist-credentials: false
- uses: actions/[email protected]
with:
node-version: "20.12.0"
node-version: "20.12"
- name: Semantic Release
id: version
uses: cycjimmy/semantic-release-action@v4.1.0
uses: splunk/semantic-release-action@v1.3.4
with:
semantic_version: 21.1.1
git_committer_name: ${{ secrets.SA_GH_USER_NAME }}
git_committer_email: ${{ secrets.SA_GH_USER_EMAIL }}
gpg_private_key: ${{ secrets.SA_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.SA_GPG_PASSPHRASE }}
extra_plugins: |
@semantic-release/exec
@semantic-release/git
[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_ADMIN }}
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.1.0

### Changed
- add error handling for apply changes action
- after clicking 'Apply changes' workflow is initially attempting to create new job immediately, if it is impossible, schedule it for the future

## [1.0.2]

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ source venv/bin/activate
Next step is to install required `python3` packages:

```shell
cd backend
pip3 install -r requirements.txt
```

Expand All @@ -80,7 +81,6 @@ docker run --rm -d -p 27017:27017 --name example-mongo mongo:4.4.6
To start backend service run:

```yaml
cd backend
flask run
```

Expand Down
2 changes: 1 addition & 1 deletion backend/SC4SNMP_UI_backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

load_dotenv()

__version__ = "1.0.2"
__version__ = "1.1.0-beta.1"

MONGO_URI = os.getenv("MONGO_URI")
mongo_client = MongoClient(MONGO_URI)
Expand Down
6 changes: 4 additions & 2 deletions backend/SC4SNMP_UI_backend/apply_changes/apply_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ def __init__(self) -> None:
mongo_config_collection.update_one(
{
"previous_job_start_time": {"$exists": True},
"currently_scheduled": {"$exists": True}}
"currently_scheduled": {"$exists": True},
"task_id": {"$exists": True}}
,{
"$set":{
"previous_job_start_time": None,
"currently_scheduled": False
"currently_scheduled": False,
"task_id": None
}
},
upsert=True
Expand Down
91 changes: 68 additions & 23 deletions backend/SC4SNMP_UI_backend/apply_changes/handling_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import ruamel.yaml
from flask import current_app
from SC4SNMP_UI_backend import mongo_client
from SC4SNMP_UI_backend.apply_changes.tasks import run_job
from SC4SNMP_UI_backend.apply_changes.tasks import run_job, get_job_config
from SC4SNMP_UI_backend.apply_changes.kubernetes_job import create_job
from kubernetes.client import ApiException
import datetime
import os

Expand All @@ -13,11 +15,23 @@
VALUES_DIRECTORY = os.getenv("VALUES_DIRECTORY", "")
VALUES_FILE = os.getenv("VALUES_FILE", "")
KEEP_TEMP_FILES = os.getenv("KEEP_TEMP_FILES", "false")
JOB_NAMESPACE = os.getenv("JOB_NAMESPACE", "sc4snmp")
mongo_config_collection = mongo_client.sc4snmp.config_collection
mongo_groups = mongo_client.sc4snmp.groups_ui
mongo_inventory = mongo_client.sc4snmp.inventory_ui
mongo_profiles = mongo_client.sc4snmp.profiles_ui


class EmptyValuesFileException(Exception):
def __init__(self, filename):
self.message = f"{filename} cannot be empty. Check sc4snmp documentation for template."
super().__init__(self.message)

class YamlParserException(Exception):
def __init__(self, filename):
self.message = f"Error occurred while reading {filename}. Check yaml syntax."
super().__init__(self.message)

class Handler(ABC):
@abstractmethod
def set_next(self, handler):
Expand Down Expand Up @@ -71,8 +85,15 @@ def handle(self, request: dict):
values_file_resolved = False
values = {}
if values_file_resolved:
with open(values_file_path, "r") as file:
values = yaml.load(file)
try:
with open(values_file_path, "r") as file:
values = yaml.load(file)
except ruamel.yaml.parser.ParserError as e:
current_app.logger.error(f"Error occurred while reading {VALUES_FILE}. Check yaml syntax.")
raise YamlParserException(VALUES_FILE)
if values is None:
current_app.logger.error(f"{VALUES_FILE} cannot be empty. Check sc4snmp documentation for template.")
raise EmptyValuesFileException(VALUES_FILE)

if not values_file_resolved or KEEP_TEMP_FILES.lower() in ["t", "true", "y", "yes", "1"]:
delete_temp_files = False
Expand Down Expand Up @@ -120,31 +141,55 @@ def handle(self, request: dict = None):
:return: pass dictionary with job_delay in seconds to the next handler
"""
record = list(mongo_config_collection.find())[0]
last_update = record["previous_job_start_time"]
if last_update is None:
# If it's the first time that the job is run (record in mongo_config_collection has been created
# in ApplyChanges class and last_update attribute is None) then job delay should be equal to
# CHANGES_INTERVAL_SECONDS. Update the mongo record with job state accordingly.
job_delay = CHANGES_INTERVAL_SECONDS
schedule_new_job = True
# get_job_config return job configuration in "job" variable and BatchV1Api from kubernetes client
job, batch_v1 = get_job_config()
if job is None or batch_v1 is None:
raise ValueError("CheckJobHandler: Job configuration is empty")
try:
# Try creating a new kubernetes job immediately. If the previous job is still present in the namespace,
# ApiException will be thrown.
create_job(batch_v1, job, JOB_NAMESPACE)
task_id = record["task_id"]
if task_id is not None:
# revoke existing Celery task with the previously scheduled job
current_app.extensions["celery"].control.revoke(task_id,
terminate=True, signal='SIGKILL')
mongo_config_collection.update_one({"_id": record["_id"]},
{"$set": {"previous_job_start_time": datetime.datetime.utcnow()}})
# time from the last update
{"$set": {"previous_job_start_time": datetime.datetime.utcnow(),
"currently_scheduled": False,
"task_id": None}})
job_delay = 1
time_difference = 0
else:
schedule_new_job = False
except ApiException:
# Check how many seconds have elapsed since the last time that the job was run. If the time difference
# is greater than CHANGES_INTERVAL_SECONDS then job can be run immediately. Otherwise, calculate how
# is greater than CHANGES_INTERVAL_SECONDS then job can be scheduled within 1 second. Otherwise, calculate how
# many seconds are left until minimum time difference between updates (CHANGES_INTERVAL_SECONDS).
current_time = datetime.datetime.utcnow()
delta = current_time - last_update
time_difference = delta.total_seconds()
if time_difference > CHANGES_INTERVAL_SECONDS:
job_delay = 1
last_update = record["previous_job_start_time"]
if last_update is None:
# If it's the first time that the job is run (record in mongo_config_collection has been created
# in ApplyChanges class and last_update attribute is None) but the previous job is still in the namespace
# then job delay should be equal to CHANGES_INTERVAL_SECONDS.
# Update the mongo record with job state accordingly.
job_delay = CHANGES_INTERVAL_SECONDS
mongo_config_collection.update_one({"_id": record["_id"]},
{"$set": {"previous_job_start_time": datetime.datetime.utcnow()}})
# time from the last update
time_difference = 0
else:
job_delay = int(CHANGES_INTERVAL_SECONDS - time_difference)
current_time = datetime.datetime.utcnow()
delta = current_time - last_update
time_difference = delta.total_seconds()
if time_difference > CHANGES_INTERVAL_SECONDS:
job_delay = 1
else:
job_delay = int(CHANGES_INTERVAL_SECONDS - time_difference)

result = {
"job_delay": job_delay,
"time_from_last_update": time_difference
"time_from_last_update": time_difference,
"schedule_new_job": schedule_new_job
}

current_app.logger.info(f"CheckJobHandler: {result}")
Expand All @@ -157,11 +202,11 @@ def handle(self, request: dict):
ScheduleHandler schedules the kubernetes job with updated sc4snmp configuration
"""
record = list(mongo_config_collection.find())[0]
if not record["currently_scheduled"]:
if not record["currently_scheduled"] and request["schedule_new_job"]:
# If the task isn't currently scheduled, schedule it and update its state in mongo.
async_result = run_job.apply_async(countdown=request["job_delay"], queue='apply_changes')
mongo_config_collection.update_one({"_id": record["_id"]},
{"$set": {"currently_scheduled": True}})
run_job.apply_async(countdown=request["job_delay"], queue='apply_changes')
{"$set": {"currently_scheduled": True, "task_id": async_result.id}})
current_app.logger.info(
f"ScheduleHandler: scheduling new task with the delay of {request['job_delay']} seconds.")
else:
Expand Down
17 changes: 15 additions & 2 deletions backend/SC4SNMP_UI_backend/apply_changes/routes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from flask import Blueprint, jsonify
from flask import Blueprint, jsonify, current_app
from flask_cors import cross_origin
from SC4SNMP_UI_backend.apply_changes.apply_changes import ApplyChanges
from SC4SNMP_UI_backend.apply_changes.handling_chain import EmptyValuesFileException, YamlParserException
import os
import traceback

apply_changes_blueprint = Blueprint('common_blueprint', __name__)
JOB_CREATION_RETRIES = int(os.getenv("JOB_CREATION_RETRIES", 10))
Expand All @@ -19,4 +21,15 @@ def apply_changes():
else:
message = f"Configuration will be updated in approximately {job_delay} seconds."
result = jsonify({"message": message})
return result, 200
return result, 200

@apply_changes_blueprint.errorhandler(Exception)
@cross_origin()
def handle_exception(e):
current_app.logger.error(traceback.format_exc())
if isinstance(e, (EmptyValuesFileException, YamlParserException)):
result = jsonify({"message": e.message})
return result, 400

result = jsonify({"message": "Undentified error. Check logs."})
return result, 400
23 changes: 17 additions & 6 deletions backend/SC4SNMP_UI_backend/apply_changes/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
JOB_CONFIG_PATH = os.getenv("JOB_CONFIG_PATH", "/config/job_config.yaml")
celery_logger = get_task_logger(__name__)

@shared_task()
def run_job():
def get_job_config():
"""
:return: job - configuration of the job
batch_v1 - BatchV1Api object from kubernetes client
"""
job = None
batch_v1 = None
with open(JOB_CONFIG_PATH, encoding="utf-8") as file:
Expand All @@ -26,6 +29,13 @@ def run_job():
config.load_incluster_config()
batch_v1 = client.BatchV1Api()
job = create_job_object(config_file)
return job, batch_v1

@shared_task()
def run_job():
job, batch_v1 = get_job_config()
if job is None or batch_v1 is None:
raise ValueError("Scheduled kubernetes job: Job configuration is empty")

with MongoClient(MONGO_URI) as connection:
try_creating = True
Expand All @@ -39,8 +49,9 @@ def run_job():
try:
record = list(connection.sc4snmp.config_collection.find())[0]
connection.sc4snmp.config_collection.update_one({"_id": record["_id"]},
{"$set": {"previous_job_start_time": datetime.datetime.utcnow(),
"currently_scheduled": False}})
{"$set": {"previous_job_start_time": datetime.datetime.utcnow(),
"currently_scheduled": False,
"task_id": None}})
except Exception as e:
celery_logger.info(f"Error occurred while updating job state after job creation: {str(e)}")
except ApiException:
Expand All @@ -50,6 +61,6 @@ def run_job():
celery_logger.info(f"Kubernetes job was not created. Max retries ({JOB_CREATION_RETRIES}) exceeded.")
record = list(connection.sc4snmp.config_collection.find())[0]
connection.sc4snmp.config_collection.update_one({"_id": record["_id"]},
{"$set": {"currently_scheduled": False}})
{"$set": {"currently_scheduled": False, "task_id": None}})
else:
time.sleep(10)
time.sleep(10)
8 changes: 4 additions & 4 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
click==8.1.3
Flask==2.2.5
Flask-Cors==3.0.10
Flask-Cors==4.0.1
itsdangerous==2.1.2
Jinja2==3.1.3
Jinja2==3.1.4
MarkupSafe==2.1.1
pymongo==4.1.1
pymongo==4.6.3
six==1.16.0
Werkzeug==2.3.8
Werkzeug==3.0.3
pytest~=7.2.0
gunicorn
kubernetes~=26.1.0
Expand Down
Loading
Loading