From 75c7a948602280ed2685e86a85dc88fed652e2e2 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Wed, 28 Feb 2024 13:13:42 +0100 Subject: [PATCH 001/310] output changed and onboarding date added --- .../core/apiinvokerenrolmentdetails.py | 11 +++++++++-- .../core/provider_enrolment_details_api.py | 13 +++++++++---- .../service_apis/core/discoveredapis.py | 2 +- .../published_apis/core/serviceapidescriptions.py | 12 +++++++++--- .../register_service/core/register_operations.py | 3 ++- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py index 8969ec1..1d79405 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py @@ -5,10 +5,12 @@ import requests from .responses import bad_request_error, not_found_error, forbidden_error, internal_server_error, make_response from flask import current_app, Flask, Response import json +from datetime import datetime from ..util import dict_to_camel_case from .auth_manager import AuthManager from .resources import Resource from ..config import Config +from api_invoker_management.models.api_invoker_enrolment_details import APIInvokerEnrolmentDetails @@ -77,10 +79,14 @@ class InvokerManagementOperations(Resource): apiinvokerenrolmentdetail.api_invoker_id = api_invoker_id current_app.logger.debug(cert) apiinvokerenrolmentdetail.onboarding_information.api_invoker_certificate = cert['data']['certificate'] + + # Onboarding Date Record + invoker_dict = apiinvokerenrolmentdetail.to_dict() + invoker_dict["onboarding_date"] = datetime.now() + mycol.insert_one(apiinvokerenrolmentdetail.to_dict()) register.update_one({'username':username}, {"$push":{'list_invokers':api_invoker_id}}) - current_app.logger.debug("Invoker inserted in database") current_app.logger.debug("Netapp onboarded sucessfuly") @@ -123,7 +129,8 @@ class InvokerManagementOperations(Resource): } current_app.logger.debug("Invoker Resource inserted in database") - res = make_response(object=dict_to_camel_case(result), status=200) + + res = make_response(object=APIInvokerEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) return res except Exception as e: diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py index 2077003..18c5e5b 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py @@ -4,11 +4,13 @@ import secrets from flask import current_app, Flask, Response from ..core.sign_certificate import sign_certificate from .responses import internal_server_error, not_found_error, forbidden_error, make_response, bad_request_error - +from datetime import datetime from ..util import dict_to_camel_case, clean_empty from .resources import Resource from .auth_manager import AuthManager +from api_provider_management.models.api_provider_enrolment_details import APIProviderEnrolmentDetails # noqa: E501 + class ProviderManagementOperations(Resource): @@ -53,8 +55,11 @@ class ProviderManagementOperations(Resource): self.auth_manager.add_auth_provider(certificate, api_provider_func.api_prov_func_id, api_provider_func.api_prov_func_role, api_provider_enrolment_details.api_prov_dom_id) + # Onboarding Date Record + provider_dict = api_provider_enrolment_details.to_dict() + provider_dict["onboarding_date"] = datetime.now() - mycol.insert_one(api_provider_enrolment_details.to_dict()) + mycol.insert_one(provider_dict) register.update_one({'username':username}, {"$push":{'list_providers':api_provider_enrolment_details.api_prov_dom_id}}) current_app.logger.debug("Provider inserted in database") @@ -135,7 +140,7 @@ class ProviderManagementOperations(Resource): result = clean_empty(result) current_app.logger.debug("Provider domain updated in database") - return make_response(object=dict_to_camel_case(result), status=200) + return make_response(object=APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) except Exception as e: exception = "An exception occurred in update provider" @@ -161,7 +166,7 @@ class ProviderManagementOperations(Resource): current_app.logger.debug("Provider domain updated in database") - return make_response(object=dict_to_camel_case(result), status=200) + return make_response(object=APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) except Exception as e: exception = "An exception occurred in patch provider" diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py index 5114c30..f755e54 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py @@ -43,7 +43,7 @@ class DiscoverApisOperations(Resource): if my_params: my_query = {"$and": my_params} - discoved_apis = services.find(my_query, {"_id":0, "apf_id":0}) + discoved_apis = services.find(my_query, {"onboarding_date":0, "_id":0, "apf_id":0}) json_docs = [] for discoved_api in discoved_apis: my_api = dict_to_camel_case(discoved_api) diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py index e2892e6..307ba34 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py @@ -11,6 +11,8 @@ from ..db.db import MongoDatabse from ..encoder import JSONEncoder from ..models.problem_details import ProblemDetails from .resources import Resource +from published_apis.models.service_api_description import ServiceAPIDescription +from datetime import datetime from ..util import dict_to_camel_case, clean_empty from .responses import bad_request_error, internal_server_error, forbidden_error, not_found_error, unauthorized_error, make_response from bson import json_util @@ -55,7 +57,8 @@ class PublishServiceOperations(Resource): if result != None: return result - service = mycol.find({"apf_id": apf_id}, {"apf_id":0, "_id":0}) + service = mycol.find({"apf_id": apf_id}, {"onboarding_date":0, "apf_id":0, "_id":0}) + current_app.logger.debug(service) if service is None: current_app.logger.error("Not found services for this apf id") return not_found_error(detail="Not exist published services for this apf_id", cause="Not exist service with this apf_id") @@ -67,6 +70,7 @@ class PublishServiceOperations(Resource): json_docs.append(my_service_api) current_app.logger.debug("Obtained services apis") + current_app.logger.debug(json_docs) res = make_response(object=json_docs, status=200) return res @@ -97,7 +101,9 @@ class PublishServiceOperations(Resource): serviceapidescription.api_id = api_id rec = dict() rec['apf_id'] = apf_id + rec['onboarding_date'] = datetime.now() rec.update(serviceapidescription.to_dict()) + mycol.insert_one(rec) self.auth_manager.add_auth_service(api_id, apf_id) @@ -127,7 +133,7 @@ class PublishServiceOperations(Resource): return result my_query = {'apf_id': apf_id, 'api_id': service_api_id} - service_api = mycol.find_one(my_query, {"apf_id":0, "_id":0}) + service_api = mycol.find_one(my_query, {"onboarding_date":0, "apf_id":0, "_id":0}) if service_api is None: current_app.logger.error(service_api_not_found_message) return not_found_error(detail=service_api_not_found_message, cause="No Service with specific credentials exists") @@ -201,7 +207,7 @@ class PublishServiceOperations(Resource): service_api_description = service_api_description.to_dict() service_api_description = clean_empty(service_api_description) - result = mycol.find_one_and_update(serviceapidescription, {"$set":service_api_description}, projection={"apf_id":0, "_id":0},return_document=ReturnDocument.AFTER ,upsert=False) + result = mycol.find_one_and_update(serviceapidescription, {"$set":service_api_description}, projection={"onboarding_date":0, "apf_id":0, "_id":0},return_document=ReturnDocument.AFTER ,upsert=False) result = clean_empty(result) diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index f929820..488481d 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -1,6 +1,7 @@ from flask import Flask, jsonify, request, current_app from flask_jwt_extended import create_access_token from ..db.db import MongoDatabse +from datetime import datetime from ..config import Config import secrets import requests @@ -21,7 +22,7 @@ class RegisterOperations: if exist_user: return jsonify("user already exists"), 409 - user_info = dict(_id=secrets.token_hex(7), username=username, password=password, role=role, description=description, cn=cn, list_invokers=[], list_providers=[]) + user_info = dict(_id=secrets.token_hex(7), username=username, password=password, role=role, description=description, cn=cn, list_invokers=[], list_providers=[], onboarding_date=datetime.now()) obj = mycol.insert_one(user_info) if role == "invoker": -- GitLab From 5eab19d4d273bc9ebb51d64f2057456b95bf08fa Mon Sep 17 00:00:00 2001 From: Alex Kakyris Date: Thu, 29 Feb 2024 14:26:11 +0200 Subject: [PATCH 002/310] Resolve "Register user password must be hashed before store on DB" --- .../core/register_operations.py | 26 ++++++++++++++++--- services/register/requirements.txt | 1 + 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index f929820..707828b 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -6,6 +6,7 @@ import secrets import requests import json import sys +import bcrypt class RegisterOperations: @@ -14,6 +15,10 @@ class RegisterOperations: self.mimetype = 'application/json' self.config = Config().get_config() + def hash_password(self, password): + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + return hashed_password + def register_user(self, username, password, description, cn, role): mycol = self.db.get_col_by_name(self.db.capif_users) @@ -21,7 +26,8 @@ class RegisterOperations: if exist_user: return jsonify("user already exists"), 409 - user_info = dict(_id=secrets.token_hex(7), username=username, password=password, role=role, description=description, cn=cn, list_invokers=[], list_providers=[]) + hashed_password = self.hash_password(password) + user_info = dict(_id=secrets.token_hex(7), username=username, password=hashed_password, role=role, description=description, cn=cn, list_invokers=[], list_providers=[]) obj = mycol.insert_one(user_info) if role == "invoker": @@ -42,11 +48,16 @@ class RegisterOperations: try: - exist_user = mycol.find_one({"username": username, "password": password}) + #exist_user = mycol.find_one({"username": username, "password": password}) + exist_user = mycol.find_one({"username": username}) if exist_user is None: return jsonify("Not exister user with this credentials"), 400 + stored_password = exist_user["password"] + if not bcrypt.checkpw(password.encode('utf-8'), stored_password): + return jsonify("Not exister user with this credentials"), 400 + access_token = create_access_token(identity=(username + " " + exist_user["role"])) url = f"http://{self.config['ca_factory']['url']}:{self.config['ca_factory']['port']}/v1/secret/data/ca" headers = { @@ -64,7 +75,16 @@ class RegisterOperations: mycol = self.db.get_col_by_name(self.db.capif_users) try: - mycol.delete_one({"username": username, "password": password}) + exist_user = mycol.find_one({"username": username}) + + if exist_user is None: + return jsonify("Not exister user with this username"), 400 + + stored_password = exist_user["password"] + if not bcrypt.checkpw(password.encode('utf-8'), stored_password): + return jsonify("Not exister user with this password"), 400 + + mycol.delete_one({"username": username}) return jsonify(message="User removed successfully"), 204 except Exception as e: return jsonify(message=f"Errors when try remove user: {e}"), 500 diff --git a/services/register/requirements.txt b/services/register/requirements.txt index c5a4f37..05b9f7d 100644 --- a/services/register/requirements.txt +++ b/services/register/requirements.txt @@ -6,3 +6,4 @@ flask_jwt_extended pyopenssl pyyaml requests +bcrypt -- GitLab From c19b4db88fce0b4bc279e9f320b75ddea3b4a85c Mon Sep 17 00:00:00 2001 From: Alex Kakyris Date: Fri, 1 Mar 2024 10:00:59 +0200 Subject: [PATCH 003/310] Fix response messages for register operations --- .../register/register_service/core/register_operations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index 707828b..d1d0921 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -52,11 +52,11 @@ class RegisterOperations: exist_user = mycol.find_one({"username": username}) if exist_user is None: - return jsonify("Not exister user with this credentials"), 400 + return jsonify("No user with these credentials"), 400 stored_password = exist_user["password"] if not bcrypt.checkpw(password.encode('utf-8'), stored_password): - return jsonify("Not exister user with this credentials"), 400 + return jsonify("No user with these credentials"), 400 access_token = create_access_token(identity=(username + " " + exist_user["role"])) url = f"http://{self.config['ca_factory']['url']}:{self.config['ca_factory']['port']}/v1/secret/data/ca" @@ -78,11 +78,11 @@ class RegisterOperations: exist_user = mycol.find_one({"username": username}) if exist_user is None: - return jsonify("Not exister user with this username"), 400 + return jsonify("No user with these credentials"), 400 stored_password = exist_user["password"] if not bcrypt.checkpw(password.encode('utf-8'), stored_password): - return jsonify("Not exister user with this password"), 400 + return jsonify("No user with these credentials"), 400 mycol.delete_one({"username": username}) return jsonify(message="User removed successfully"), 204 -- GitLab From ef1a274071e0f9423afba34366e86f21ac0b2454 Mon Sep 17 00:00:00 2001 From: Stavros Charismiadis Date: Fri, 1 Mar 2024 15:16:24 +0200 Subject: [PATCH 004/310] fix some bugs on postman files --- docs/testing_with_postman/CAPIF.postman_collection.json | 2 +- docs/testing_with_postman/CAPIF.postman_environment.json | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/testing_with_postman/CAPIF.postman_collection.json b/docs/testing_with_postman/CAPIF.postman_collection.json index e65c826..dcbd5ad 100644 --- a/docs/testing_with_postman/CAPIF.postman_collection.json +++ b/docs/testing_with_postman/CAPIF.postman_collection.json @@ -806,7 +806,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\"name\": {{USERNAME_INVOKER}}\n}", + "raw": "{\n\"name\": \"{{USERNAME_INVOKER}}\"\n}", "options": { "raw": { "language": "json" diff --git a/docs/testing_with_postman/CAPIF.postman_environment.json b/docs/testing_with_postman/CAPIF.postman_environment.json index ab3839e..fd084b3 100644 --- a/docs/testing_with_postman/CAPIF.postman_environment.json +++ b/docs/testing_with_postman/CAPIF.postman_environment.json @@ -32,6 +32,12 @@ "type": "default", "enabled": true }, + { + "key": "USERNAME_INVOKER", + "value": "InvokerONE", + "type": "default", + "enabled": true + }, { "key": "PASSWORD", "value": "pass", -- GitLab From b115ba91ed963e36b464ce83999e74a85b3943a3 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Mon, 4 Mar 2024 16:17:58 +0100 Subject: [PATCH 005/310] Use of necessary parameters in DB --- .../service_apis/core/discoveredapis.py | 2 +- .../published_apis/core/serviceapidescriptions.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py index f755e54..f56b585 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py @@ -43,7 +43,7 @@ class DiscoverApisOperations(Resource): if my_params: my_query = {"$and": my_params} - discoved_apis = services.find(my_query, {"onboarding_date":0, "_id":0, "apf_id":0}) + discoved_apis = services.find(my_query, {"_id":0, "api_name":1, "api_id":1, "aef_profiles":1, "description":1, "supported_features":1, "shareable_info":1, "service_api_category":1, "api_supp_feats":1, "pub_api_path":1, "ccf_id":1}) json_docs = [] for discoved_api in discoved_apis: my_api = dict_to_camel_case(discoved_api) diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py index 307ba34..5c903d1 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py @@ -57,7 +57,7 @@ class PublishServiceOperations(Resource): if result != None: return result - service = mycol.find({"apf_id": apf_id}, {"onboarding_date":0, "apf_id":0, "_id":0}) + service = mycol.find({"apf_id": apf_id}, {"_id":0, "api_name":1, "api_id":1, "aef_profiles":1, "description":1, "supported_features":1, "shareable_info":1, "service_api_category":1, "api_supp_feats":1, "pub_api_path":1, "ccf_id":1}) current_app.logger.debug(service) if service is None: current_app.logger.error("Not found services for this apf id") @@ -133,7 +133,7 @@ class PublishServiceOperations(Resource): return result my_query = {'apf_id': apf_id, 'api_id': service_api_id} - service_api = mycol.find_one(my_query, {"onboarding_date":0, "apf_id":0, "_id":0}) + service_api = mycol.find_one(my_query, {"_id":0, "api_name":1, "api_id":1, "aef_profiles":1, "description":1, "supported_features":1, "shareable_info":1, "service_api_category":1, "api_supp_feats":1, "pub_api_path":1, "ccf_id":1}) if service_api is None: current_app.logger.error(service_api_not_found_message) return not_found_error(detail=service_api_not_found_message, cause="No Service with specific credentials exists") @@ -207,7 +207,7 @@ class PublishServiceOperations(Resource): service_api_description = service_api_description.to_dict() service_api_description = clean_empty(service_api_description) - result = mycol.find_one_and_update(serviceapidescription, {"$set":service_api_description}, projection={"onboarding_date":0, "apf_id":0, "_id":0},return_document=ReturnDocument.AFTER ,upsert=False) + result = mycol.find_one_and_update(serviceapidescription, {"$set":service_api_description}, projection={"_id":0, "api_name":1, "api_id":1, "aef_profiles":1, "description":1, "supported_features":1, "shareable_info":1, "service_api_category":1, "api_supp_feats":1, "pub_api_path":1, "ccf_id":1},return_document=ReturnDocument.AFTER ,upsert=False) result = clean_empty(result) -- GitLab From 102093d23da0e5d4740a92ac3aa1b7270279aa27 Mon Sep 17 00:00:00 2001 From: Alex Kakyris Date: Tue, 5 Mar 2024 14:11:52 +0200 Subject: [PATCH 006/310] Refactor "Register user password must be hashed before store on DB" --- .../register/register_service/auth_utils.py | 8 ++++++++ .../core/register_operations.py | 17 ++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 services/register/register_service/auth_utils.py diff --git a/services/register/register_service/auth_utils.py b/services/register/register_service/auth_utils.py new file mode 100644 index 0000000..f799772 --- /dev/null +++ b/services/register/register_service/auth_utils.py @@ -0,0 +1,8 @@ +import bcrypt + +def hash_password(password): + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + return hashed_password + +def check_password(input_password, stored_password): + return bcrypt.checkpw(input_password.encode('utf-8'), stored_password) \ No newline at end of file diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index d1d0921..4cc5c37 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -2,12 +2,12 @@ from flask import Flask, jsonify, request, current_app from flask_jwt_extended import create_access_token from ..db.db import MongoDatabse from ..config import Config +from register_service import auth_utils import secrets import requests import json import sys -import bcrypt - + class RegisterOperations: def __init__(self): @@ -15,10 +15,6 @@ class RegisterOperations: self.mimetype = 'application/json' self.config = Config().get_config() - def hash_password(self, password): - hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) - return hashed_password - def register_user(self, username, password, description, cn, role): mycol = self.db.get_col_by_name(self.db.capif_users) @@ -26,7 +22,7 @@ class RegisterOperations: if exist_user: return jsonify("user already exists"), 409 - hashed_password = self.hash_password(password) + hashed_password = auth_utils.hash_password(password) user_info = dict(_id=secrets.token_hex(7), username=username, password=hashed_password, role=role, description=description, cn=cn, list_invokers=[], list_providers=[]) obj = mycol.insert_one(user_info) @@ -48,15 +44,14 @@ class RegisterOperations: try: - #exist_user = mycol.find_one({"username": username, "password": password}) exist_user = mycol.find_one({"username": username}) if exist_user is None: return jsonify("No user with these credentials"), 400 stored_password = exist_user["password"] - if not bcrypt.checkpw(password.encode('utf-8'), stored_password): - return jsonify("No user with these credentials"), 400 + if not auth_utils.check_password(password, stored_password): + return jsonify("No user with these credentials"), 400 access_token = create_access_token(identity=(username + " " + exist_user["role"])) url = f"http://{self.config['ca_factory']['url']}:{self.config['ca_factory']['port']}/v1/secret/data/ca" @@ -81,7 +76,7 @@ class RegisterOperations: return jsonify("No user with these credentials"), 400 stored_password = exist_user["password"] - if not bcrypt.checkpw(password.encode('utf-8'), stored_password): + if not auth_utils.check_password(password, stored_password): return jsonify("No user with these credentials"), 400 mycol.delete_one({"username": username}) -- GitLab From 734a1fe0b2b2f83b111b6fd8c57496cdfae6bab1 Mon Sep 17 00:00:00 2001 From: Alex Kakyris Date: Wed, 13 Mar 2024 09:10:44 +0200 Subject: [PATCH 007/310] Resolve "Access with credentials to DB" --- services/docker-compose-capif.yml | 3 +++ services/docker-compose-register.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/services/docker-compose-capif.yml b/services/docker-compose-capif.yml index d552105..f667d8c 100644 --- a/services/docker-compose-capif.yml +++ b/services/docker-compose-capif.yml @@ -220,6 +220,9 @@ services: ports: - 8082:8081 environment: + ME_CONFIG_MONGODB_ENABLE_ADMIN: true + ME_CONFIG_BASICAUTH_USERNAME: admin + ME_CONFIG_BASICAUTH_PASSWORD: admin ME_CONFIG_MONGODB_ADMINUSERNAME: root ME_CONFIG_MONGODB_ADMINPASSWORD: example ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/ diff --git a/services/docker-compose-register.yml b/services/docker-compose-register.yml index 173d661..5363e01 100644 --- a/services/docker-compose-register.yml +++ b/services/docker-compose-register.yml @@ -36,6 +36,9 @@ services: ports: - 8083:8081 environment: + ME_CONFIG_MONGODB_ENABLE_ADMIN: true + ME_CONFIG_BASICAUTH_USERNAME: admin + ME_CONFIG_BASICAUTH_PASSWORD: admin ME_CONFIG_MONGODB_ADMINUSERNAME: root ME_CONFIG_MONGODB_ADMINPASSWORD: example ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo_register:27017/ -- GitLab From fe7c5c64b182557d885d3242e97ece8bb9c06b90 Mon Sep 17 00:00:00 2001 From: Stavros Charismiadis Date: Wed, 13 Mar 2024 11:58:33 +0200 Subject: [PATCH 008/310] Create script to remove temporary files and folders in project --- .gitignore | 1 + services/clean_capif_temporary_files.sh | 80 +++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100755 services/clean_capif_temporary_files.sh diff --git a/.gitignore b/.gitignore index 9a67eb9..66e4e33 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ monitoring/tempo/tempo-data/* docs/testing_with_postman/*node_modules* +docs/testing_with_postman/Responses docs/testing_with_postman/package-lock.json results diff --git a/services/clean_capif_temporary_files.sh b/services/clean_capif_temporary_files.sh new file mode 100755 index 0000000..d626273 --- /dev/null +++ b/services/clean_capif_temporary_files.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +help() { + echo "Usage: $1 " + echo " -c : Clean capif services tmp files" + echo " -m : Clean monitoring service tmp files" + echo " -t : Clean robot-test service tmp files" + echo " -d : Clean docs folder tmp files" + echo " -a : Clean all tmp files" + echo " -h : show this help" + exit 1 +} + +if [[ $# -lt 1 ]] +then + echo "You must specify an option before run script." + help +fi + +cd .. + +FILES=() +echo "${FILES[@]}" + +# Read params +while getopts "cmtdah" opt; do + case $opt in + c) + echo "Remove capif services temporary files" + FILES+=("services") + ;; + m) + echo "Remove monitoring service temporary files" + FILES+=("monitoring") + ;; + t) + echo "Remove robot-test service temporary files" + FILES+=("tests") + ;; + d) + echo "Remove docs folder temporary files" + FILES+=("docs") + ;; + a) + echo "Remove all temporary files" + FILES=("services" "monitoring" "tests" "docs") + ;; + h) + help + ;; + \?) + echo "Not valid option: -$OPTARG" >&2 + help + exit 1 + ;; + :) + echo "The -$OPTARG option requires an argument." >&2 + help + exit 1 + ;; + esac +done +echo "after check" +echo "${FILES[@]}" + +for FILE in "${FILES[@]}"; do + echo "Remove temporary files for $FILE" + sudo rm -r $(git ls-files . --ignored --exclude-standard --others --directory | grep "$FILE") + status=$? + echo $status + if [ $status -eq 0 ]; then + echo "*** Removed tmp files from $FILE ***" + else + echo "*** Some files from $FILE failed on removing ***" + fi +done + + +echo "Clean complete." +cd ./services \ No newline at end of file -- GitLab From 57f3c12494d6a48645b494c3d55c12fa22fe08eb Mon Sep 17 00:00:00 2001 From: Stavros Charismiadis Date: Wed, 13 Mar 2024 15:39:13 +0200 Subject: [PATCH 009/310] Some more fixes --- services/clean_capif_temporary_files.sh | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/services/clean_capif_temporary_files.sh b/services/clean_capif_temporary_files.sh index d626273..747be29 100755 --- a/services/clean_capif_temporary_files.sh +++ b/services/clean_capif_temporary_files.sh @@ -17,7 +17,6 @@ then help fi -cd .. FILES=() echo "${FILES[@]}" @@ -65,16 +64,15 @@ echo "${FILES[@]}" for FILE in "${FILES[@]}"; do echo "Remove temporary files for $FILE" - sudo rm -r $(git ls-files . --ignored --exclude-standard --others --directory | grep "$FILE") + sudo rm -r $(git ls-files . --ignored --exclude-standard --others --directory | grep $FILE) status=$? - echo $status - if [ $status -eq 0 ]; then - echo "*** Removed tmp files from $FILE ***" - else - echo "*** Some files from $FILE failed on removing ***" - fi + if [ $status -eq 0 ]; then + echo "*** Removed tmp files from $FILE ***" + else + echo "*** Some files from $FILE failed on removing ***" + fi done -echo "Clean complete." +echo "Remove tmp files complete." cd ./services \ No newline at end of file -- GitLab From 1dba822499f58ba674145299d637435c7af93f78 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Thu, 14 Mar 2024 12:05:47 +0200 Subject: [PATCH 010/310] Check if tmp files exist to supress rm command errors --- services/clean_capif_temporary_files.sh | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/services/clean_capif_temporary_files.sh b/services/clean_capif_temporary_files.sh index 747be29..4becd8b 100755 --- a/services/clean_capif_temporary_files.sh +++ b/services/clean_capif_temporary_files.sh @@ -62,14 +62,21 @@ done echo "after check" echo "${FILES[@]}" +cd .. + for FILE in "${FILES[@]}"; do echo "Remove temporary files for $FILE" - sudo rm -r $(git ls-files . --ignored --exclude-standard --others --directory | grep $FILE) - status=$? - if [ $status -eq 0 ]; then - echo "*** Removed tmp files from $FILE ***" + tmp_files=$(git ls-files . --ignored --exclude-standard --others --directory | grep $FILE) + if [[ $tmp_files ]]; then + sudo rm -r $tmp_files + status=$? + if [ $status -eq 0 ]; then + echo "*** Removed tmp files from $FILE ***" + else + echo "*** Some files from $FILE failed on removing ***" + fi else - echo "*** Some files from $FILE failed on removing ***" + echo "No files found" fi done -- GitLab From 57e24c00ce6357156dee0d53fe502c3aed3fbee5 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 26 Mar 2024 09:42:12 +0100 Subject: [PATCH 011/310] Remove not used prometheus folder --- prometheus/kustomization.yaml | 11 --- ...ometheus-claim1-persistentvolumeclaim.yaml | 14 ---- ...ometheus-claim2-persistentvolumeclaim.yaml | 14 ---- ...ometheus-claim3-persistentvolumeclaim.yaml | 14 ---- ...ometheus-claim4-persistentvolumeclaim.yaml | 14 ---- prometheus/prometheus-configmap.yaml | 38 ---------- prometheus/prometheus-deployment.yaml | 71 ------------------- prometheus/prometheus-service.yaml | 19 ----- 8 files changed, 195 deletions(-) delete mode 100644 prometheus/kustomization.yaml delete mode 100644 prometheus/prometheus-claim1-persistentvolumeclaim.yaml delete mode 100644 prometheus/prometheus-claim2-persistentvolumeclaim.yaml delete mode 100644 prometheus/prometheus-claim3-persistentvolumeclaim.yaml delete mode 100644 prometheus/prometheus-claim4-persistentvolumeclaim.yaml delete mode 100644 prometheus/prometheus-configmap.yaml delete mode 100644 prometheus/prometheus-deployment.yaml delete mode 100644 prometheus/prometheus-service.yaml diff --git a/prometheus/kustomization.yaml b/prometheus/kustomization.yaml deleted file mode 100644 index bebcddc..0000000 --- a/prometheus/kustomization.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: evol5-capif-mon -resources: - - prometheus-claim1-persistentvolumeclaim.yaml - - prometheus-claim2-persistentvolumeclaim.yaml - - prometheus-claim3-persistentvolumeclaim.yaml - - prometheus-claim4-persistentvolumeclaim.yaml - - prometheus-configmap.yaml - - prometheus-deployment.yaml - - prometheus-service.yaml \ No newline at end of file diff --git a/prometheus/prometheus-claim1-persistentvolumeclaim.yaml b/prometheus/prometheus-claim1-persistentvolumeclaim.yaml deleted file mode 100644 index 99ca0f1..0000000 --- a/prometheus/prometheus-claim1-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: prometheus-claim1 - name: prometheus-claim1 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/prometheus/prometheus-claim2-persistentvolumeclaim.yaml b/prometheus/prometheus-claim2-persistentvolumeclaim.yaml deleted file mode 100644 index 3a1a586..0000000 --- a/prometheus/prometheus-claim2-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: prometheus-claim2 - name: prometheus-claim2 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/prometheus/prometheus-claim3-persistentvolumeclaim.yaml b/prometheus/prometheus-claim3-persistentvolumeclaim.yaml deleted file mode 100644 index 755a66b..0000000 --- a/prometheus/prometheus-claim3-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: prometheus-claim3 - name: prometheus-claim3 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/prometheus/prometheus-claim4-persistentvolumeclaim.yaml b/prometheus/prometheus-claim4-persistentvolumeclaim.yaml deleted file mode 100644 index 9a6cb2d..0000000 --- a/prometheus/prometheus-claim4-persistentvolumeclaim.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - creationTimestamp: null - labels: - io.kompose.service: prometheus-claim4 - name: prometheus-claim4 -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi -status: {} diff --git a/prometheus/prometheus-configmap.yaml b/prometheus/prometheus-configmap.yaml deleted file mode 100644 index ccfb013..0000000 --- a/prometheus/prometheus-configmap.yaml +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: prometheus-configmap -data: - prometheus.yml: | - # global: - # scrape_interval: 5s - # external_labels: - # monitor: 'prakash-monitor' - # scrape_configs: - # - job_name: 'prometheus' - # static_configs: - # - targets: ['0.0.0.0:9090'] ## IP Address of the localhost - # - job_name: 'cAdvisor' - # static_configs: - # - targets: ['0.0.0.0:8090'] - # #- cadvisor:8090 - - global: - scrape_interval: 15s - evaluation_interval: 15s - alerting: - alertmanagers: - - static_configs: - - targets: - # whatever you want - scrape_configs: - - job_name: 'prometheus' - static_configs: - - targets: ['prometheus:9090'] - labels: - alias: 'prometheus' - - job_name: 'cadvisor' - static_configs: - - targets: ['cadvisor:8080'] - labels: - alias: 'cadvisor' \ No newline at end of file diff --git a/prometheus/prometheus-deployment.yaml b/prometheus/prometheus-deployment.yaml deleted file mode 100644 index 7ae939f..0000000 --- a/prometheus/prometheus-deployment.yaml +++ /dev/null @@ -1,71 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - creationTimestamp: null - labels: - io.kompose.service: prometheus - name: prometheus -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: prometheus - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - creationTimestamp: null - labels: - io.kompose.network/monitoring-default: "true" - io.kompose.service: prometheus - spec: - containers: - - args: - - --config.file=/tmp/prometheus.yml - - --web.route-prefix=/ - - --storage.tsdb.retention.time=200h - - --web.enable-lifecycle - image: prom/prometheus:latest - name: prometheus - ports: - - containerPort: 9090 - resources: {} - volumeMounts: - - name: prometheus-config - mountPath: /tmp/prometheus.yml - subPath: prometheus.yml - - mountPath: /var/lib/prometheus - name: prometheus-claim1 - - mountPath: /prometheus - name: prometheus-claim2 - - mountPath: /etc/prometheus - name: prometheus-claim3 - - mountPath: /etc/prometheus/alert.rules - name: prometheus-claim4 - restartPolicy: Always - volumes: - - name: prometheus-config - configMap: - name: prometheus-configmap - items: - - key: "prometheus.yml" - path: "prometheus.yml" - - name: prometheus-claim1 - persistentVolumeClaim: - claimName: prometheus-claim1 - - name: prometheus-claim2 - persistentVolumeClaim: - claimName: prometheus-claim2 - - name: prometheus-claim3 - persistentVolumeClaim: - claimName: prometheus-claim3 - - name: prometheus-claim4 - persistentVolumeClaim: - claimName: prometheus-claim4 -status: {} diff --git a/prometheus/prometheus-service.yaml b/prometheus/prometheus-service.yaml deleted file mode 100644 index d7fa32d..0000000 --- a/prometheus/prometheus-service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - creationTimestamp: null - labels: - io.kompose.service: prometheus - name: prometheus -spec: - ports: - - name: "9090" - port: 9090 - targetPort: 9090 - selector: - io.kompose.service: prometheus -status: - loadBalancer: {} -- GitLab From f9baebbd05491e7964a72b2b6710cc270c6085bc Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 12:27:22 +0100 Subject: [PATCH 012/310] File to test tiggers of CICD --- helm/DELLETE.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 helm/DELLETE.txt diff --git a/helm/DELLETE.txt b/helm/DELLETE.txt new file mode 100644 index 0000000..8f7fc5b --- /dev/null +++ b/helm/DELLETE.txt @@ -0,0 +1 @@ +File to test tiggers of CICD \ No newline at end of file -- GitLab From c8de8e199f688054bc85b82220591461285ff937 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 12:30:45 +0100 Subject: [PATCH 013/310] space --- helm/DELLETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELLETE.txt b/helm/DELLETE.txt index 8f7fc5b..99310ec 100644 --- a/helm/DELLETE.txt +++ b/helm/DELLETE.txt @@ -1 +1 @@ -File to test tiggers of CICD \ No newline at end of file +File to test tiggers of CICD -- GitLab From 2dfcdc38817cf9e5e6191577635968792234104d Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 12:34:40 +0100 Subject: [PATCH 014/310] delete file --- helm/{DELLETE.txt => DELETE.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename helm/{DELLETE.txt => DELETE.txt} (100%) diff --git a/helm/DELLETE.txt b/helm/DELETE.txt similarity index 100% rename from helm/DELLETE.txt rename to helm/DELETE.txt -- GitLab From c80f062506d4aab9bff708fa577fb147064f2adc Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 12:36:18 +0100 Subject: [PATCH 015/310] delete --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 99310ec..8f7fc5b 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD +File to test tiggers of CICD \ No newline at end of file -- GitLab From ac1559fe4206765de3394af123304e90621a9b4b Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 12:38:34 +0100 Subject: [PATCH 016/310] delete --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 8f7fc5b..99310ec 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD \ No newline at end of file +File to test tiggers of CICD -- GitLab From 5b813849de5c969b900eb71528c1a4d4b558e43c Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 12:41:07 +0100 Subject: [PATCH 017/310] delete --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 99310ec..8f7fc5b 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD +File to test tiggers of CICD \ No newline at end of file -- GitLab From 52d76d46691d729b8d27091723ae42657b257855 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 12:41:50 +0100 Subject: [PATCH 018/310] delete --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 8f7fc5b..99310ec 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD \ No newline at end of file +File to test tiggers of CICD -- GitLab From 119d5af1abc20a45daba2482f4e5823dca501ba4 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 12:43:21 +0100 Subject: [PATCH 019/310] delete --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 99310ec..8f7fc5b 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD +File to test tiggers of CICD \ No newline at end of file -- GitLab From a398af0d2d7724b8c7d22486a7fd7969ec0f69e4 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 12:47:37 +0100 Subject: [PATCH 020/310] delete --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 8f7fc5b..99310ec 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD \ No newline at end of file +File to test tiggers of CICD -- GitLab From c05b488073926ff9b74b4fc01961ca85c90464b0 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 13:01:55 +0100 Subject: [PATCH 021/310] delete --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 99310ec..8f7fc5b 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD +File to test tiggers of CICD \ No newline at end of file -- GitLab From 0c0ac53e8c752b79a80a40b69a746df52e36c1c6 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 13:12:18 +0100 Subject: [PATCH 022/310] delete --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 8f7fc5b..99310ec 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD \ No newline at end of file +File to test tiggers of CICD -- GitLab From 12f7ceaaf9cebee5f1aafebf73640d5f0264c1bb Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 13:20:08 +0100 Subject: [PATCH 023/310] delete --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 99310ec..8f7fc5b 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD +File to test tiggers of CICD \ No newline at end of file -- GitLab From 0d5b848b89b5cead70f81621ff107286ec7c2fd3 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 13:35:08 +0100 Subject: [PATCH 024/310] delete --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 8f7fc5b..99310ec 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD \ No newline at end of file +File to test tiggers of CICD -- GitLab From 67e94b737f18620174d693e2aa8636cbf7713e6f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 13:36:29 +0100 Subject: [PATCH 025/310] artifact --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 99310ec..8f7fc5b 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD +File to test tiggers of CICD \ No newline at end of file -- GitLab From 52ff2f542e0b0365f2c7f76e8dbd7faa5534bb55 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 13:39:40 +0100 Subject: [PATCH 026/310] grype output table --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 8f7fc5b..99310ec 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD \ No newline at end of file +File to test tiggers of CICD -- GitLab From df94cf686a9fb2b559886c8d0aa401d83d85dfab Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 13:46:38 +0100 Subject: [PATCH 027/310] artifact folder --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 99310ec..8f7fc5b 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD +File to test tiggers of CICD \ No newline at end of file -- GitLab From 3caa4a3a04146a11cbabcb82dc80a96b0a3cf410 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 13:53:30 +0100 Subject: [PATCH 028/310] cat artifact --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 8f7fc5b..99310ec 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD \ No newline at end of file +File to test tiggers of CICD -- GitLab From 6e5fb3e490c7846f8bd7e49c79861269773b8ea6 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 13:57:42 +0100 Subject: [PATCH 029/310] artifacts --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 99310ec..8f7fc5b 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD +File to test tiggers of CICD \ No newline at end of file -- GitLab From 8cc98feaa923b047b5bf5bfd3a2ee7e139f9a87a Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 14:28:06 +0100 Subject: [PATCH 030/310] grype nginx scanning --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 8f7fc5b..99310ec 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD \ No newline at end of file +File to test tiggers of CICD -- GitLab From 8e92d14fff0e41c1bf37be965fd4a5dacd2839dd Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 14:33:38 +0100 Subject: [PATCH 031/310] improving code grype --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 99310ec..8f7fc5b 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD +File to test tiggers of CICD \ No newline at end of file -- GitLab From 2847f6357b9f2d8dd61c9c66e2ed0916bfb8659f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 14:45:03 +0100 Subject: [PATCH 032/310] improving code --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 8f7fc5b..99310ec 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD \ No newline at end of file +File to test tiggers of CICD -- GitLab From 790f394370452efe7a952faa30980fc36faf3801 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 14:54:52 +0100 Subject: [PATCH 033/310] IMAGE_LOWER --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 99310ec..8f7fc5b 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -File to test tiggers of CICD +File to test tiggers of CICD \ No newline at end of file -- GitLab From 39a5d984a4435293bb73932979f771e86dcb1d9e Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 26 Mar 2024 15:39:58 +0100 Subject: [PATCH 034/310] merge_request_staging_into_main --- helm/DELETE.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 helm/DELETE.txt diff --git a/helm/DELETE.txt b/helm/DELETE.txt deleted file mode 100644 index 8f7fc5b..0000000 --- a/helm/DELETE.txt +++ /dev/null @@ -1 +0,0 @@ -File to test tiggers of CICD \ No newline at end of file -- GitLab From 48e8c9de34e89b479f8549608a458b700ef16b71 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 27 Mar 2024 08:28:54 +0100 Subject: [PATCH 035/310] sast template --- helm/DELETE.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 helm/DELETE.txt diff --git a/helm/DELETE.txt b/helm/DELETE.txt new file mode 100644 index 0000000..f6c4fd0 --- /dev/null +++ b/helm/DELETE.txt @@ -0,0 +1 @@ +delete me \ No newline at end of file -- GitLab From cd972174da0bce294cadf95865f532ba20252d6f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 27 Mar 2024 08:33:07 +0100 Subject: [PATCH 036/310] staging_sca --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 41f81fe718f8078d7b4dd55d4909ab343a1c5a63 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 27 Mar 2024 08:35:51 +0100 Subject: [PATCH 037/310] <<: *staging_common --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 00c1a6f7692135343f5ccafedda0b2b930c41070 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 27 Mar 2024 08:49:25 +0100 Subject: [PATCH 038/310] sast --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From ceb9c71e1fef666aea6602615732a4060679c83f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 27 Mar 2024 08:51:56 +0100 Subject: [PATCH 039/310] script sast --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 855547dcbdaa0322f30360451bb7566a5bf98b9f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 27 Mar 2024 09:07:56 +0100 Subject: [PATCH 040/310] ls -lrt --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 43f978d17b79809ae28b628d710e397d99e3f99a Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 27 Mar 2024 10:06:51 +0100 Subject: [PATCH 041/310] artifact --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From da5ea1ec46da63dc415493b8e3a6791ce7a4ff1d Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 27 Mar 2024 10:26:16 +0100 Subject: [PATCH 042/310] main_build_pip --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From e7d5f6bfe0e6a518b583e5a9c4737e29643fa575 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 4 Apr 2024 16:45:48 +0200 Subject: [PATCH 043/310] force --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From e29ba86b7cca5ac589fdc9cfda2d1149d091a741 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 4 Apr 2024 17:15:37 +0200 Subject: [PATCH 044/310] sast --- services/register/pytest.ini | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 services/register/pytest.ini diff --git a/services/register/pytest.ini b/services/register/pytest.ini new file mode 100644 index 0000000..62fc4e2 --- /dev/null +++ b/services/register/pytest.ini @@ -0,0 +1,4 @@ +# content of pytest.ini +[pytest] +python_files = register_services/config.py + -- GitLab From 63e22a0994b403ec6c7a6e84fa028dff3bcc50ba Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 4 Apr 2024 17:17:25 +0200 Subject: [PATCH 045/310] force --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 8ff20ebdc20ec35efd9a00936faa657439a093fe Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 4 Apr 2024 17:40:31 +0200 Subject: [PATCH 046/310] force --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 1c487ac400b587ac312c2d6ab5495d815ddcdb0d Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 4 Apr 2024 17:47:50 +0200 Subject: [PATCH 047/310] force --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From b9a55c0eb2c9e587b7c8dac52eb94bfafe9369ba Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 4 Apr 2024 18:04:28 +0200 Subject: [PATCH 048/310] test --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From ed1d8c986f270575ea2f940ab2914ff73200dde6 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 10:36:41 +0200 Subject: [PATCH 049/310] force commit --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 380df77951aa85864c74ae25803963fb5b002b6c Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 10:48:35 +0200 Subject: [PATCH 050/310] force commit --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 0d19a2e428ef6529d797b30b3fd75125ada0fc5f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 11:01:59 +0200 Subject: [PATCH 051/310] force commit --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 219bf46b3da3afb38699aa8d34d842ea40dfa848 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 11:50:44 +0200 Subject: [PATCH 052/310] force --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 86da3b7b1bedd700288d812c49ccc8fbe94c35d2 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 11:51:36 +0200 Subject: [PATCH 053/310] force pipeline --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 3ef8d813a0717b5e61a2b1e92d0f3757d3ac205c Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 11:52:46 +0200 Subject: [PATCH 054/310] container_scanning --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 424fb3c9fcf377a6aaca7047996d9cd4166a479e Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 11:58:56 +0200 Subject: [PATCH 055/310] container_scanning_nginx --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 2c3fc16779c1d27c8bdfb79bb1148da5c980c70a Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 12:03:41 +0200 Subject: [PATCH 056/310] extends: container_scanning --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 512aac1959f074127a26fbcb79888b892edaeff3 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 12:09:43 +0200 Subject: [PATCH 057/310] CS_DOCKERFILE_PATH --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 261ebb55b3b156c735f42a86570a05706c75ae20 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 12:15:36 +0200 Subject: [PATCH 058/310] ls -lrta --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 3baa03e77df456602843adc90c47bef06329f229 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 12:24:56 +0200 Subject: [PATCH 059/310] # CS_DOCKERFILE_PATH: --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From f2b1115a5926f19dd3012f58db9e3c0752747542 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 12:45:40 +0200 Subject: [PATCH 060/310] CI_IMAGE: --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 6d259d7452ed12f683d54110b0d4271260057e37 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 12:51:41 +0200 Subject: [PATCH 061/310] CS_DEFAULT_BRANCH_IMAGE: --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 1305dd92c1f37b0895906bdcdffc7d96ea46c07b Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 13:06:10 +0200 Subject: [PATCH 062/310] CS_IMAGE --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 5bda70aad69e81e9202fe4404fc79ad0629aec69 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 13:11:30 +0200 Subject: [PATCH 063/310] docker login --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 3be9f3ace35c4199a6ac0d91a371fcc9b43f0456 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 13:25:00 +0200 Subject: [PATCH 064/310] no docker login --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 79b284b327d9e0b5d1dfc4da520175850e2d72a3 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 13:51:42 +0200 Subject: [PATCH 065/310] SECURE_LOG_LEVEL --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From f74137072d51561b7d9814dfeb8a2cd58c03d291 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 14:02:56 +0200 Subject: [PATCH 066/310] $CI_COMMIT_REF_SLUG --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From c3f06e715695e5fd46c9f4aea93352d63c4f878b Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 5 Apr 2024 14:18:55 +0200 Subject: [PATCH 067/310] # CS_DOCKERFILE_PATH: --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 12d3398fc9e4cb5aec1f8b48ae3c7c9369b9c639 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 8 Apr 2024 12:30:28 +0200 Subject: [PATCH 068/310] Removed documents from repository and README modified to link online documentation --- README.md | 74 +- docs/images/flows/01 - Register of AEF.png | Bin 71625 -> 0 bytes .../flows/01a - Register (Only) AEF.png | Bin 52092 -> 0 bytes .../flows/01b - Register of AEF GetAuth.png | Bin 38871 -> 0 bytes .../02 - AEF API Provider registration.png | Bin 49215 -> 0 bytes docs/images/flows/03 - AEF Publish.png | Bin 43268 -> 0 bytes docs/images/flows/04 - Invoker Register.png | Bin 71642 -> 0 bytes .../flows/04a - Invoker (Only) Register.png | Bin 51974 -> 0 bytes .../flows/04b - Invoker Register GetAuth.png | Bin 39061 -> 0 bytes docs/images/flows/05 - Invoker Onboarding.png | Bin 64059 -> 0 bytes .../flows/06 - Invoker Discover AEF.png | Bin 43087 -> 0 bytes .../07 - Invoker Create Security Context.png | Bin 43568 -> 0 bytes docs/images/flows/08 - Invoker Get Token.png | Bin 55546 -> 0 bytes ...nvoker Send Request to AEF Service API.png | Bin 26214 -> 0 bytes docs/images/robot_log_example.png | Bin 158079 -> 0 bytes docs/images/robot_report_example.png | Bin 183065 -> 0 bytes docs/test_plan/README.md | 16 - .../api_access_control_policy/README.md | 813 ----------- .../service_api_description_post_example.json | 113 -- docs/test_plan/api_auditing_service/README.md | 244 ---- docs/test_plan/api_discover_service/README.md | 336 ----- docs/test_plan/api_events_service/README.md | 265 ---- .../event_subscription.json | 31 - .../api_invoker_management/README.md | 306 ---- .../invoker_details_post_example.json | 15 - .../invoker_details_put_example.json | 393 ------ .../invoker_getauth_example.json | 4 - .../invoker_register_body.json | 7 - docs/test_plan/api_logging_service/README.md | 241 ---- .../api_logging_service/invocation_log.json | 45 - .../api_provider_management/README.md | 398 ------ ...tails_enrolment_details_patch_example.json | 29 - .../provider_details_post_example.json | 17 - .../provider_getauth_example.json | 4 - .../provider_register_body.json | 7 - docs/test_plan/api_publish_service/README.md | 599 -------- .../publisher_register_body.json | 7 - .../service_api_description_post_example.json | 113 -- docs/test_plan/api_security_service/README.md | 1244 ----------------- .../access_token_req.json | 6 - .../access_token_req_example.json | 5 - .../security_notification.json | 9 - .../service_security.json | 25 - docs/test_plan/common_operations/README.md | 86 -- docs/testing_with_curl/README.md | 369 ----- .../capif_tls_curls_exposer.sh | 205 --- .../capif_tls_curls_invoker.sh | 86 -- docs/testing_with_curl/exposer.key | 28 - docs/testing_with_curl/invoker.key | 28 - .../CAPIF.postman_collection.json | 982 ------------- .../CAPIF.postman_environment.json | 237 ---- docs/testing_with_postman/README.md | 160 --- docs/testing_with_postman/hello_api.py | 38 - docs/testing_with_postman/package.json | 16 - docs/testing_with_postman/script.js | 199 --- docs/testing_with_robot/README.md | 74 - 56 files changed, 7 insertions(+), 7867 deletions(-) delete mode 100644 docs/images/flows/01 - Register of AEF.png delete mode 100644 docs/images/flows/01a - Register (Only) AEF.png delete mode 100644 docs/images/flows/01b - Register of AEF GetAuth.png delete mode 100644 docs/images/flows/02 - AEF API Provider registration.png delete mode 100644 docs/images/flows/03 - AEF Publish.png delete mode 100644 docs/images/flows/04 - Invoker Register.png delete mode 100644 docs/images/flows/04a - Invoker (Only) Register.png delete mode 100644 docs/images/flows/04b - Invoker Register GetAuth.png delete mode 100644 docs/images/flows/05 - Invoker Onboarding.png delete mode 100644 docs/images/flows/06 - Invoker Discover AEF.png delete mode 100644 docs/images/flows/07 - Invoker Create Security Context.png delete mode 100644 docs/images/flows/08 - Invoker Get Token.png delete mode 100644 docs/images/flows/09 - Invoker Send Request to AEF Service API.png delete mode 100644 docs/images/robot_log_example.png delete mode 100644 docs/images/robot_report_example.png delete mode 100644 docs/test_plan/README.md delete mode 100644 docs/test_plan/api_access_control_policy/README.md delete mode 100644 docs/test_plan/api_access_control_policy/service_api_description_post_example.json delete mode 100644 docs/test_plan/api_auditing_service/README.md delete mode 100644 docs/test_plan/api_discover_service/README.md delete mode 100644 docs/test_plan/api_events_service/README.md delete mode 100644 docs/test_plan/api_events_service/event_subscription.json delete mode 100644 docs/test_plan/api_invoker_management/README.md delete mode 100644 docs/test_plan/api_invoker_management/invoker_details_post_example.json delete mode 100644 docs/test_plan/api_invoker_management/invoker_details_put_example.json delete mode 100644 docs/test_plan/api_invoker_management/invoker_getauth_example.json delete mode 100644 docs/test_plan/api_invoker_management/invoker_register_body.json delete mode 100644 docs/test_plan/api_logging_service/README.md delete mode 100644 docs/test_plan/api_logging_service/invocation_log.json delete mode 100644 docs/test_plan/api_provider_management/README.md delete mode 100644 docs/test_plan/api_provider_management/provider_details_enrolment_details_patch_example.json delete mode 100644 docs/test_plan/api_provider_management/provider_details_post_example.json delete mode 100644 docs/test_plan/api_provider_management/provider_getauth_example.json delete mode 100644 docs/test_plan/api_provider_management/provider_register_body.json delete mode 100644 docs/test_plan/api_publish_service/README.md delete mode 100644 docs/test_plan/api_publish_service/publisher_register_body.json delete mode 100644 docs/test_plan/api_publish_service/service_api_description_post_example.json delete mode 100644 docs/test_plan/api_security_service/README.md delete mode 100644 docs/test_plan/api_security_service/access_token_req.json delete mode 100644 docs/test_plan/api_security_service/access_token_req_example.json delete mode 100644 docs/test_plan/api_security_service/security_notification.json delete mode 100644 docs/test_plan/api_security_service/service_security.json delete mode 100644 docs/test_plan/common_operations/README.md delete mode 100644 docs/testing_with_curl/README.md delete mode 100755 docs/testing_with_curl/capif_tls_curls_exposer.sh delete mode 100755 docs/testing_with_curl/capif_tls_curls_invoker.sh delete mode 100644 docs/testing_with_curl/exposer.key delete mode 100644 docs/testing_with_curl/invoker.key delete mode 100644 docs/testing_with_postman/CAPIF.postman_collection.json delete mode 100644 docs/testing_with_postman/CAPIF.postman_environment.json delete mode 100644 docs/testing_with_postman/README.md delete mode 100644 docs/testing_with_postman/hello_api.py delete mode 100644 docs/testing_with_postman/package.json delete mode 100644 docs/testing_with_postman/script.js delete mode 100644 docs/testing_with_robot/README.md diff --git a/README.md b/README.md index 577a12d..3066edc 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,28 @@ # Common API Framework (CAPIF) - [Common API Framework (CAPIF)](#common-api-framework-capif) -- [Repository structure](#repository-structure) - [CAPIF\_API\_Services](#capif_api_services) +- [Documentation](#documentation) - [How to run CAPIF services in this Repository](#how-to-run-capif-services-in-this-repository) - [Run All CAPIF Services locally with Docker images](#run-all-capif-services-locally-with-docker-images) - [Run All CAPIF Services locally with Docker images and deploy monitoring stack](#run-all-capif-services-locally-with-docker-images-and-deploy-monitoring-stack) - [Run each service using Docker](#run-each-service-using-docker) - [Run each service using Python](#run-each-service-using-python) -- [How to test CAPIF APIs](#how-to-test-capif-apis) - - [Test Plan Documentation](#test-plan-documentation) - - [Robot Framework](#robot-framework) - - [Using Curl](#using-curl) - - [Using PostMan](#using-postman) - [Important urls:](#important-urls) - [Mongo CAPIF's DB Dashboard](#mongo-capifs-db-dashboard) - [Mongo Register's DB Dashboard](#mongo-registers-db-dashboard) - [FAQ Documentation](#faq-documentation) -- [CAPIF Release 0](#capif-release-0) -# Repository structure - -``` -CAPIF_API_Services -└───docs -│ └───test_plan -│ └───testing_with_postman -└───services -└───tests -└───tools - └───robot - └───open_api_script -``` -* **services**: Services developed following CAPIF API specifications. Also, other complementary services (e.g., NGINX and JWTauth services for the authentication of API consuming entities). -* **tools**: Auxiliary tools. Robot Framework related code and OpenAPI scripts. -* **test**: Tests developed using Robot Framework. - -* **docs**: Documents related to the code in the repository. - * images: images used in the repository - * test_plan: test plan descriptions for each API service referring to the test that are executed with the Robot Framework. - * testing_with_postman: auxiliary JSON file needed for the Postman-based examples. # CAPIF_API_Services This repository has the python-flask Mockup servers created with openapi-generator related with CAPIF APIS defined here: [Open API Descriptions of 3GPP 5G APIs] +# Documentation + +Please refer to [OCF Documentation] for more detailed information. + ## How to run CAPIF services in this Repository Capif services are developed under /service/ folder. @@ -147,29 +124,6 @@ pip3 install -r requirements.txt python3 -m ``` -# How to test CAPIF APIs -The above APIs can be tested either with "curl" command, POSTMAN tool or running developed tests with Robot Framework. -## Test Plan Documentation - -Complete documentation of tests is here: [Test Plan Directory] -## Robot Framework - -In order to ensure modifications over CAPIF services still accomplish the required functionality, Robot Framework test suite must be success. - -Test suite implemented accomplish requirements described under test plan at [Test Plan Directory] folder. - -Please go to [Testing with Robot Framework] Section - -## Using Curl - -Please go to [Testing Using Curl] section. - -## Using PostMan -You can test the CAPIF flow using the Postman tool. To do this, we have created a collection with some examples of CAPIF requests with everything necessary to carry them out. - -For more information on how to test the APIs with POSTMAN, follow this [Document](docs/testing_with_postman/README.md). -Also you have here the [POSTMAN Collection](docs/testing_with_postman/CAPIF.postman_collection.json) - # Important urls: ## Mongo CAPIF's DB Dashboard @@ -194,24 +148,10 @@ http://:8083/ (if accessed from another host) Frequently asked questions can be found here: [FAQ Directory] -# CAPIF Release 0 - -The APIs included in release 0 are: -- JWT Authentication APIs -- CAPIF Invoker Management API -- CAPIF Publish API -- CAPIF Discover API -- CAPIF Security API -- CAPIF Events API -- CAPIF Provider Management API - -Testing Suite of all services with robot. -Also Postman suite to a simple test. - - [Open API Descriptions of 3GPP 5G APIs]: https://forge.3gpp.org/rep/all/5G_APIs "Open API Descriptions of 3GPP 5G APIs" [Test Plan Directory]: ./docs/test_plan/README.md "Test Plan Directory" [Testing Using Curl]: ./docs/testing_with_curl/README.md "Testing Using Curl" [Testing with Robot Framework]: ./docs/testing_with_robot/README.md "Testing with Robot Framework" -[FAQ Directory]: ./FAQ.md "FAQ directory" \ No newline at end of file +[FAQ Directory]: https://ocf.etsi.org/documentation/latest/FAQ/ "FAQ Url" +[OCF Documentation]: (https://ocf.etsi.org/documentation/latest/) "OCF Documentation" \ No newline at end of file diff --git a/docs/images/flows/01 - Register of AEF.png b/docs/images/flows/01 - Register of AEF.png deleted file mode 100644 index 391cb43fe8657f9965c66391f716cfc06a5e9eb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71625 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
x%*>~>w4M(+ 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 n`=87$eEHQ9=X+q#L9|N*W|) z^ZVaBcjnHWHEY()rwj3m^PTg)JD$Dw^PC_Bx#zdANU;zExg{kjri38Z1P}zJ6XO~@ zb9=(!6#hXslzA?OTweY8*q9rGAe4xdn254V%Eq*_{>$MDWM^BaQ#PqT%U%BC({b9u zSW%7a2UxF#DdcyK#5~Hlx7kod6!Dfil|`NgQvM){rTN7!^Oh(}!Nk-wBio@m@#x~B zCYrcjQf;DQwok9Kr}OU7;*|APPp2Si-<}VyD8~4;vL=SBUo^1%h(s}{lHZD8Q~5{s zeEk@JEBfP&{x5jWh=bLH${)AvE=k4JCvM)<#GvxGKu2ft#}$2Y{r~x64xVZV7ry;U zudJLLIucEttz;TP#B_0S)LdUvGsHTnm??qswZe3;&S6bSG3gx+g;}L03kwTBe?2LY zaS;69YNmmghDMw@!F%W14JOSBT5b(Z&556h94Pu{$A8cN9dTQ7Z{{hDl;{+y7Iu4b zQBwZSj1Z!)j%{gafmijKJa}1H`Z_!FWfGX9qN3tA-q|^8iaVB=S>1W`f|j2CbTLNW z!^4BI50xrp2V*$UX>%g()FfZIAcUA@gjI{GP|*EQIa&haUPm~2p@BwxjqL(@cUjJp zt=Z=0PqBrn#rOj^@3OafxQ~<5NXp6?P4iG@6qGO@*h~KgoK(N-R(`} zxPkck`kLkYa#flQ(}zjyO_-N9o*z!~^74NB_Kh%ms@}PD=lAj7g@Awn_QaoO!_~81 z{G6Oa9wC~=C95jWpLdjRf& zswzv45_wdx@n$b`)u59O-~=Y2spPS&f(=c(%4+}w_~ zHc4uH7M9Yswzk%HI0?ZIrt6(AJA^M3`>l%3RKES&ZY~}D-jHp18l_!p-=eB!X*z!J z=+W|gdq^C?W3m@_1Mfbu7~2>xFV(46t)dba7jJp%^&^r>_Q3vL@B=_FYkxPb1v(dhJ-|o@;55G>+8dX zGE2>-gO3cXl?NWiZM-YvFW%+bi$gDlUHmuR+iSJ_GqF3K=}zw%c)p61C0-e>?tQUJ zQ&>}Q%0}-?FC``XR@QXy3tr^x^iS#0_N=p$Q}2%-V|9)U$k^m$ZdTUW#lLNE5h*Dt z>FizbX1iK4q&Jm6AwFL9$x3W20isYi3^rI@T}{E~u-f3dx3RgY5l_f`9~}+NadUzY z2`A@QRkRgV{dcq-5gJ;Qhxt653@psS*?G3y7^ju>*}L04y}fX;2g^OlIAlDODR0rx z(Xn2~F>r9~P1V}t2T`-LD?N%qyK$qVql1Fq35>xYhcoCukDT1)82+v%&BKK9jSUNg zwWp`vVQs({4f{oD)8Fs+E>B0B>Bl!;b%Zlj4l?H3i%)uu;=# zzDAiI%_lkUo~mvgMSnyWQ?og+^(`w=+cb=6W`%QW>21&oAR0<3l!d#jP6@r-Bh;98v*zK21$frR1; zzg1gJ+u7SQ#Fa(Q!{!|h_&V^BpB)T6Kfd*1%ql3|>l72&-P?j0J;fOkq@~=Sv)dHzc)8;AE|6OXD_CZ<$&W;Mx};C}P-1B7`BB2<4I}5-j z275RdR?~A_8wj9$4_SG0vYO@b+0&RJrxzb*0;xPHI)a~K`(6c`)M@w!u)*G`Jq;3zspK5`COPE`mM6XP2tP4@^AX9 z1KHiMr!Bg;Iort>XlMw!pU9qCyph#|gznrIiPx^uhhB}1fty?1H~k+!e|j8lB+|() z{rl#T%cN1ZnC(ck+^yZ{Mu=QR!eOlD&icAK zA+NKsGiI@z*8&8~p8ThJ$3 zgF`3rb`6(Y^f`*2$QRG<$E~y1u&4q!$ZyDk!XIy z28S;GzCB3iL*B@X9Jy5btbC*Dy0pxNy-OvBrxcf0KCgx#VAcI=T2WtQY1j{`T zUb3ouiTd^=GHw5i6hmfJ%~!tJ3K=exl*?>;rg8c5eSt}JvQtw2wYZ_g71u`HX8mv1 zz(#_^2CA*~=I+7C#jRh1smyYlLa0b7A7bv;vW++fwbO$?YU=7AVq+Iq zR@7!yo?&qD@VMg+mmmBYg>OeiB%>F3ZX+{pf0#nmF*587eu*A1LVSX2;^o!I9fXhz zzU~B@bf81$uW&se;RfCnCQjK6|Njjy|G$OVtMu8+u4yZ-nRmk%c*5`K79^84p^D%l z;22WTv3Gqb{S$D@bZ*(SX*_SeJ1Sd+y{;^KT8$+lv&!43tEcp-HYUaW($>H>NN!a$ zL(hEZ{rUq8-waF~uBDYO>1_ObW%?*d_4wB$5@TowN(}yHB?jRgb$BQVXOm zRhdlI4o)j_PxLNRl1^XfRgtQcx?0Ym+`K)o615qY#w;Btovlottwbk+OCUb1SBzT_ zg)T5(lzGwN7_;rmr_ytgO~j&WVStJ5mywruTt3~o9b-qCyY2};}+ab zCb1n+)LS5}|H@Y|)U1M`N!D>U^!r4f0r|kX0~#7ydwV-y>g~RYNZIoL`N%Y9{25dn zBsamYhwN@@{%{&^dtV-YN;*1*Hlmn)Q9MD;M(TC*H&3rV>xWAeT&l_8o#!czLkl(E znLqo|A6OIm#uum5?64qSzayE-Z+*A z!!!36=@u|~=udisY&0x+>3kC7s)oz>{@*G<#ma+3PouD_;c{aC2W(@t$`Ri=NwuJDQIlL8%|pR>mI zirZ#2A2P>Eqd77eyoKDR;LoRv{A~A5>5uJ>KrT~PB6og$b-ml}yUA@KVyVq($ug_v zTih?mlP3SwO(a^ox<~fSJTq3auGe27`7x2tB|cs(=$Z8Ia_PsR+xvgtDZ;n;5+B*` z7_<%8CGhvhEuy(|^mttEey5ryd&D#rshbzef68iDUr-X$vX_!WDYH5S2zw>#4%TDu zU^>qN{UP(^2+O6V0*}kt**4(;hLkfBRXN-P*PxQy!deo3CC~poowHT%Qw!^UVj5zn zo_Jk6gtzy{9vO1>+5PgspCB+U3Kb1ifwS=8B7z>ig6OZLJ+pOIG7Utd#7OEV8F!o2 z!{E{vKrF_ajx;^WLdsifqVRO@m+W1PTep0={vgPgNXoU>gmX32?N-NK4Bsphw)Xc@ z!6?6ZGr#4HlHB4;Tfw08p_B}Mgb_`d-y@vHzRneqws(Rj$5Clkdb?W2h= zWrcVjZ{~d@I4zgx4F|P2N0IL*aRK8q4jPe2f@f@ozF9n(X@lmjY1b(c7vC&@GtQ*S z{3q7aj!WMKopDMp3`(0Oz6|e-wi-Eo3eY20t3|teKsr}b{rcZbH+QrVd%Mr)@VUmi zP>Nya(I|lQ*LyxMc~$@Bm2+8GwyZC0o8H^mA>*cR8JvFIA8U}AYk@nNPa8-h)n)SQ z-T`TE-i@@!bV){S1Wa%tE1?p8BPPH8O87-mi6~LG?;w+2O?gbcQk_4j=!lWup>CNX z-FF=XI`F8altM++e%hiVv_IuK`xrCss($)y@+Fp1GE)T1>g@_<^aE2o@er4^m79EX zt;pBwxYSIaNNhK~g7RBGX2zU#twVYV!c#Co&$X{J^xRd{4Q>y>L%cSJ$n7(#m z`jiw}&6}tq(FBFN+{;;*rQc`W@$DlLXctjM@KL4M7#U)sm0zCWzl%h}KqOyuhRZ(s zG2}b+xSqQxW#H%R%NGRp=&4azcWE~U<<=I=(i7Bno-3V>j|o#t`Ou2TeQ&M=`L&_o-KW1V_j``w#37R-f5M5v9owQ#?hPyxXlYUvxoNHBw zGo3Z29$nxc@kGQ;)F)FASxjuY=d)qDb{Wexe^1?jcc1T;uO4?4O~nyOEgE{J%h5k}e{}HoWfHY#LrO%N zY|Y7Mf1=6GP4(y5F*9FLq)2XQHa;qS<-^JIlnL+UTB56QsSw#fbz^2DIdZnQU242S z6cxIdc`Nrdy?kQq+GuBchf18D!hVNe?uzxB;MZ~jI4>p8IS8s>_a$19C!-zLoO_(= zy1iAVRvDS^l+Zy#!Z{pFi>Wc@t2z3ew;uH*%~SMO|Eujg`ok4)lYv{l(_SKN!DU7g z5yvGEiF_K#;ugN*8ot6q>0;1@A%YvWeDAKPz{FSL)F+@_OG{&h7HVh5Vleup=)gP~U1x-mw zwqCj#aXi7Xx$e_&*78$3Y#^0N*U9D~odmUt6hZF<@e$vEStX+}r%A9jVUoOYLr&G@ z?CoY78iSd2JwGjUFZSZnsV$b#b7tL!*!08)_B&fA(jWaAH~vjGFA@>sBbRBvYSj|r zbBtnU=6S>K#8<8NZ-SkY*79~Y9qcJwIRDi$b5EpOI+5Wv_Gn0;Y|kidV6KkwZyL~E zo!wL0$`W8OqONoeDC+f%7a<;)!9l_=+=Nb~{xbUP=uW?ns5T3+53qI;E%Od{w_n}6 z7H#MQ0r0{l@?`p(a4355&hByl^>>Q|SS#JAQabP5&;A@-iyFFLiu*trFH_`YN`k!N z(ASGpD5j@xxZ&cZcwJ1UZW0b~{i#l?cbOjVbls}>D_CE-J#*StTjPI1!4%m|Jvxtn zNX7#I?2^qaGPy8Dq?B!ylK@A+2GJifojC4%#&^2BM9g@;*etHOdT91e(K;}sePbhg zw&-M4P~JJ=(&P9fb#bNWXArf1`_Bw|_wl4#lb=ed^J2?R1|K_YPZhZT*%R17o+=K_ zy~@3By!={VG^kCwY`wXf%LEkybo+aa#MAlW(4G616P2bJOuYu`i8e<6+=qNF3~Hg* zVqZ^mf);S8+w@j{k+Zd>JO0&jsGIB@heX>U`4Az3e^FzNut~9VYsj{i_oyQL)EOEI5(=Kfw%a!6+X$NoRTu&g$;PgbRG zPJFuEL+<8inh0^Mzd%-;MmwSRTjtEhzis)ZboMz%9PV7S5aDq77YVdFH0;A2<$?LJ zPUcT6>TUk!_+`4y3{^j?tJuWzI6bbN{A!Op?H}sDv%;Q62-mAvv77y^MtYszq*LsB zmB@Y4u*0EkHIjhQ#v7(RXM**PI}tKH(fNY1o!8Dhub*8WF&9e|GW60&O4smF{9Ymd zeblT*A<)IX%Jm?v!=A~%tM&TV*}y~cvHGta-8~)Mvev9cr_1k^=LVVr{BjG=%QsfS z&0L2fFQ}zfR)~G-WpbZf)ViYSZiT~2-pTQ#cQg|p@JQ&5nAT2}E_Uv{zFhaWLwIjk2B}*AMxkUXT(y0hvh;dd&va>FE&70Qa$J$i!iJ~MzSi|5 z?Y&MxFZ+BRBE$Xd~&RpL;1W!Dv1yQw@6F z5f7EZPkB&*HdA}P& zR8|8P!~X_ZN7$wQnjcwqVNnQeDW7xr$&?MjrB9>MG8~B$Aw*JU=Pl|wXvOz8wNaijCmgH| zw^n@7OHaGJaCle3lMr=wGVb{y-t)1a5>LItO&KII_J^D}k&!UI(@N&!fTJ{qEG%P6 zNnc7+iI&fdy7$ELuAw4D8r4f~Ev=U&DGYJa=s2qQUD15WVls{Uzc)&;LDPny5Bq^RQX{fzaGZda<`z47a8*cO{ zzjX&5YFa$+H=O$s|F`H^F>NY*Uosdv#@;7k?<1Kl&wJZ+RyGc(RZhu%OeL5%H{QS0 z-(0zjk{;tmKe}{TkV1aD>Wal&Wy<~C;Yk{4wIm|U$&RMG>}Qo}0pjfhOgScOG-tRk z??=BW3u)ts-%R1*qs~i|uOE54sd?je#DfWaZw8gYo+-{0-Y2ZEH$m3St#RO5SZ4fbJ}T6 zje1>2`ZSV&DLPo4w_OtPp~ML1NGg$02vvVjo3Ksi*NzQ(6EsH*Fg~xB?BLjL`%+qg?JNFTr<2KizsVE|;08Uh?enWn| zLRL}-iQ9;e0c|+Pj=%wQYsTImBZyCZa!CKjVe=4&Bwu0U#ytVOx_6kG)WPnN{8qiHwUV=4 zdX*;2!em1QX`6!n+iU`TcMs4rb(2qup3*Wb)XLE)j!8{p`cm_xs<&`hNRe;!53Ey@ zG)ySTDjIEs`I|elQxN8*lXwg|3ME))qdXN$w`zBlzcukm_~?UTZjU@eSDo4i*Id>; zxxaf^(Pu9*Mc#3@i{-h|CdK79GS3WejCqqH%S{wZne@Eg2POSKT-|DzQiY?2jEPrr z(0Kl)_pI)@<;{lqy9@L=TZ|UCYZ0IC)idc-id3HG8QY24RDj8+H`pY+^k-^$-@NuSrs{6){~ADniaagWN{(y-O7OV2bI{(14t z4UO|O!*a+R+kT+N_npr5P`%#GT&1opVHby;MJ9fC=fF73`k{|Z%r5cT`x{#fC=O!j zy@<``8+@i{e3p78X{AuHIWl{-y@e zjcubd!^5xfUUH%z{TJyi5&5D65?X?l-DK&j>{F_GxwY1K5wcLla zQ&UVGv7Hbfj+N4hd;!R5K~q61wSSJV18@6Wc)EEU59>BoNlKpljlP>b!+(6)#5i)# z|4%Wh$UKWWh6<+CK&U#kAz||2;|jCoVIdxyrL!;I>0fLYO3JsxHU-}to4*Qde#1&) zl7#LvXq>s7E5PeGC*dOb&oRA+qp!n+6Cc%pFBG4;(>^e=7lkS{mngwR?B=t6B;Nu>++jCh})f9YJ%yLTed z9A&-Faj-o~ydW8TbGzSZFbSt;k7LyV^{g}+RccSbc*s;#?-rIPS6+?SLqn6Kob)$> z!uDL_{lhD9PCZ1uuHN5%9{Kn@X`DUZUVip+#`V?E=l6=O4_>@Z74&ZZ$UwwSO_$`- zsn=UjdgvEVE>nG=cI|82aR+-P@xy3*Xl(AMJ{NcMQ_P4AaQNf0_crHKa+i&Spj1de z$nTpBxwO7tYXTzC{7c<988XEM9x6(@l!{*pemQwX{P@Q3WBcdwdafI`Z@wjaYN4ID zzi)pr&^q*T#V{hhxzJ(v(qS*)%y+&?aFUIa{9eNE_4U>5KdEX8qHK&0tL`xOhER8L zj1FYa%ao$lySwuwm~gJJ%pC95Kg?o(y8PRd=h2`d;b7@=^ZeLG_sb};xrQSL>C^J< z=VDD01&O8VKdEVQQ^SHT59a-BA7UbrKgh5h%O+BlN1jGfciL(jDE@7Y-JkVpt~yHi zQ!%Brw%YxE)1{a8Ebw_WHDXd(H?*JIA*VZjAk&~ehk{JBtYnuz#E8xcMNj>OZJ6IE zDLVLo?tM0e=bdt|48NGn(>?L+7dl@0+xd$f^4=u(`}1FOP<;6y<@Ukp(ej9Og3@hq zV}iIO{1;L`ObB`huy#fboAHR#wbM7<cC**va+djvhiB5p#zxP(?eA0NdG|HwzMx7$Q?Dph^Y~Ps(p;ksJXQM{6y)22~b(Z14j(kH|6@%=EC`u$s9 zPfxE>v!{$PP|zr(fO8)C`jMeiuD*RDKMT%7Hn~Ea7s8I4EDG2KVJ|h!K-^*+fdGd| z2qcl$MX5Ng_B)YfcqEkpS;r4c4g56k9eK zGMB+(uWYZC@C*OcUi5+~il3t>lw+MMV4UV8(ajQ%DO!@C_kn83D4{u4nM1p)K%Zi$ zf8|Ry{D6MSv*LhwdhIs$%78m;>*m^OmDp**a(%I$L+1X%a_0v*6{xmr%bDiR_5u9#iWQOt4uq5W zn9=xlg9L9-M5GqO9|R4U{4&ETcWkdsQIsEe@I{#>exiAxYt8g~W}zB0(c+sbtK~}1 zR(Ae;YR+A9KTd_UiK)C%XtKV%$)HjbNl?txJ52EAPo+4j2y2n{CmSM}P0VQMwn$@@ zL3{7*O1qPp>N0{%%#+E^*=IKSQ_@9=QNq{*qO)bWi#vM&Pt$z9StwM1o$(M;nLJ`Fl z@)e9=qt~}@GOY0W?d0Ok{eGZQhv`l7fG&`d&aY~ zc;9D92deBeL}igg8=8C}f2atdVkd<)EI7*q@y#O1Z+sueyPJXs@LwENDu`NIb@}VP zTLLUouN1^^sXOjwV-aWUf9G~ofO_MlD}vx=*=!3%XUVF?nS4>sIgF*(@ozT5gHoAO zitP}~Omh2U@b1r^&PJDqDqD+>U%F#8Qf}C5GRE<`$ZYBaBtOL(?9oWPZakE{MfSsf z$~@z(&cyyTm+qljcbt?T{qIwzwcC~4eY2>s9{YZjOrpYwe)AeX8{3+*B}67lbV-Vz zu(gY)^6}17r3xE-)_vi-)wD`w7XF ztb}QzDni?LsFaCBB5yMM>n4h(cg%p}M;jt9?z#3p)oYHKD>9tcQGO68C*|q3+SZ=h zfgju3LlPv)Bb~IO;6~8=G@6s{^v_>^Bq{c*-aB@2B_!-HnU*Q~X}emW|Gm8U#uzub zD1!~(R+`$oNzvNBFWhp(@%j8DKiS~P#O{a4R9a)7>4_-FkSYBRHnJ6vSyAecWbb7O!B+B^YMDag8Bg`cifCh0Fu19pg7CfjOqZpCqHH z?=7VED8v|EznW}WUrn~2slZV0TqHWHz98d>qS*TpQ3#M`WCxGSnaWI`u)(~o={uLx zTSw)5??R%AGi{9<`6b`EiWuq|jP9yXCUj>j<>#MYO*ISh=OTFiFA@WY|hf7 zG6jFiQj#)q&-A4$uoW~QtRUG4s-PaW%_*OU}Mc6Im4j>pI2Fy%&7V4-RMB;{Iyj4_g z{i`+;k$Di6r%azJcLQM#&K)1Gg|QpV)bv?*1X%Xdml{8$C8wpOl{h&&S36fXkX{?4 zzT-=2l0+1uVpm)3=jT^gSeTrg{L3ZKz=j#UOs7(4z98d~Uj-@YM2!`OjslJ9R%t;& z+(ohGFGCF&N?-Glq*kJj1CGmoW_vIxQO&%}Qljgyef#z;kYcXBV(d}fqUBuF!*npFAou!Pi%U9vDEBkWJ$FEkayrHg_B&9$r5_N62VjFQf# zmck;}P*VCdxaQCC`en7ojJI1O4CF`q`dD~)#&*p<(7$qn8L#Z%gD6S(3;Ut2Svh4Hj(iI zUyq)>TxCfqfHutk2hOV5(}95j3|~nZnQ~E$y_LRnPE#du`DBbg)e}iNp$eJ)B7c@{ zqWhVmkQf`UNX0xNBqG9Mu^juH0XzuCZa#i~p!x_23hIn`9jp)U?AQc|&TVV}J98VT zKtZ(fE4`_DnspMHR2UDKnM;NiNcr6MO^l3;#N`X=g@n>5-viq;-ZOvMu?cvv5%&cF zBb}X}j}~b>v}kZYvXYd<=q>}}x^bH2Yy(0+b3ld3n7m3yq_#=~u5_agoXb^$GnJ zsl)M(L1zE)UZ1e{<@pgC8=I(5%p+N>+qa3Des>#{xtVqY8S~DaJ3zU7xix6Ev%7o6 z5!2V#XHwbm1{&0jTcn)!%Rhlarc-P8dgu2y6ciNTjeuUCrcki{H_RB4ZunTC!&w*inZ_23d1e9;c?FEdAbj*K>2hboe0Oz{<@_%eDu7} zRb1-qSLA#t=NA^}f+s#3_|a+T>ZbYmwF398+H&$yoU|-<@~%L7a`F8Qs4AjWu=2@luL4i%{dF0J+v7Zs*OQBH zDp8wYodaj3@P&Hm2Cx+IgBCZdXP;QA(Gmcmjo)SGV`kICDjKJWm~fVFzqR*@(w-DHsUs&H?PgXt9EPgC zV1V8sA|j%oC{-==^ziUMa@v|Y0ovYD4yupytY?D3htlSYx*@M8m331@ag$DaRj{yZ zoSgd|6yBjhL2JW>qd%e^o?sCb(;k$R=~$c?U#Idn)?_i}8=U81e)6OWXm?;JAJ|?= z*x188Dk=jh{Mxl^KuT6qWH$(Gj}X3a0cM(z=jqwuq&0*< z@cZl~v>;&JyagiyF5*&<>|;K7!E=9A5x&FD&d$bW2Uz<7EXS|Ufi62UGm|r1BO{20 z)M_&eNJ>fqVbR{x0BBws8X7=4hcISHk~8&_q4a6(-FW(n*iWB60R!7=s>T)^5IzlY zHQ&ba*V)ZJeOBezT`)n2u`g)}7`uCuxmlh(85tg~R4(kcRHRWMv>~eorkK3Eytnry ztS8D-TU*;&S=vHrolso%cX!wrD-Bu$BO`ISxw+>wfyAz7XIG|M$}KzwrrhVSzdZ(I z{%~?g3e9#)U0|JZ$xn(uUH23(s0dBgElub&0#;aPM8qY~*Ma-oHX-*AbDs$BQp3=+k^CI-$E-*P-`qoSf>oUut#uto_S1&C>oNbVzU zhZ}c^hz{X^x9>eE`uOo{>?2`ey<*jbWIN#90zX=`41flZ){KBto~@~)t*u=pzN9)V z@GVC+d8AaY8BX}({2wtPVd4#)nfZ3hKjzs@KYzY}T>>H7mg_ zC@J+JW&&CNE^zE%k-Bn<&TcT-1K+Q~@lU*THhZcKEddrFjLB+iHRZ`NLe^6I+PkVV zXJ=;~dpeNbIEqy#;f#&@(l8Sykl(<@wVbG+JJA0%Yi#B~Y!HY;!Tj@|uAGr^#bH6g4WxFY? zR=VX5n*pVnl#GlF4-d$0!>1*;MOA!63QvVjS2IhSPR+pUKu&Y{+YbkL+8%Q)XqB3R zP8(xtdY+Cm4X%Q2`!1jr04z}fu9QSj0VH1_H3b9)>Na_dpRpm3827heZU2O_UzezsJLVun0o~%zaa+0*W|7VvtTEd9e;u@e;Y3BI*>sx?GC&$MPz?i-gm}pry zisQDyG6LQCir9apxZ!s`Jw1(2E}L?SGjptEqOaauwloFL1^fCA%j~#5F9g{y8{RPc2j;QzDZWj80u$9U?RiIiJyqimd?7oVmNT{_REu}7zxCEgF}Y}5{w44h z3!H(6{_UGVXEe>L@1gnY#ag&!k3fmWrGqLWnLPmfcmP^WhZE)pj#_m7jPI_>^7o%; z0}Vg}7SF6>)`>NN!G(KO=G(Oy{)H4y#Hg;&kDrObrT(8MK|18`?E3r(*7p0i3Ne0o z*yGIqE1jD`V-Sfz-@Pyi;5xIC_}}<1awi~I;B~7OqKoF)L4z7Jc?9B$ZjEURJO)x2 zXT-#27)8Vtp%M$QSK!#MEAjC}hg-`w3RDVDkG3J%ltPL3OFWwTJCpKu5orayMQ9NP`hgZvIUM%Y3W}_aLx|H}yPc1(V1Y*bYmF#L z>ZzU1Q!X*tJ0L|=FvTWj_Bh=)6)yz`KLq)Q^i@F&KcXa!dg?MB-S$&0=Jmet0G2x- zFp#(XT`w&40%rPw&O;duUHeJPSJS4k|EM`RD|S&~H~*`&2erU)J)16-*vMO%m?6?i z&07f6@x`iz$_1w-a)j!-dlP2h|Nm9fP4IG(RZVVlDJm#XPEoe%v;J<@xVpLmF0;xc z1Cmxspm8{S! za@jAxK-4QtqU!7GwVAg^iYMDch*2on*uH=a13##3c2@6rjh~4rjPgB%Oy`X;+bi!K zgs29KrJA8|75T_IfSnC%!Uwec>;aIkr3iUW?0*Lp;K=BxTCuA2*ylOO=8#N4UO?e> zoS_g?)*e>?Qm3QlOB#%Mzz}iOc*uPLmtUoN!a-tCfI&(n!`31vBfE(}9Yl#?2-yN6 z($+wBh&tnK42&@l?N#2}-KyV))cqoN5ap?dM}scOK05L-O`0;ZWQ;;@+ zoMEioI3hE10pc1cL~8+;LH0GT)P&L)Y(@z1%kOV~SON2p9P{(@k>6`;wE)nejK!9} ze(hS7)%5AK^DHP8z+6@x>Q44n(m`a%%xnd6Lx3HSjOd^*1hpSO)I0I7A)_3?`#r$| zQAMU2-K(eOrH4~iPN1Yn`jiK%Ew!2NDJf*c#0hzMc_1nQnU@J<38E^#hYznKa_K^; zadB~piEFE?$KP&H0ET@9)gq+l-KFkf^B>S|NKH)@zBqoBwgTx;`1tqrw3UD&sIlw+ zOJ&6n*9D<}X=NpWslu)CrPDT-8*+v1q5lBz3mcS?(1^-e4OkzAiVBb=xWd|Li6nW; zM-{a$K@acaxp$!JSbpdMMGHU&E*ahLZDFK{Eirqi(%Z@ry?safV&ZP}Tqvps)Uw zw0~Rl2cJ!s`}F*!TkRF*1`r~Aw$logiV%cYc;nrhfuBEr2CSYu@R?ByV1iVdsKpov zp5Z#dW&4}GFQKxri^y136m!wGvK_W0+Yzwl+e(Kq?yx^xd$@%u7cKIK0 z+%px5{rE+Yqe7l|qW=}~rQ``O|ExO9T%rx<7!ejmn1qf9^l6QmCFNlH;u4kL#=;5? z3E|+Pf(jTEmBUs)puYj#3c#>@kc(45TxQ(>Ju47GcQI8Ex!<5te$Hw#Oo)Sn!_8fN zdA1@9s411zpan#9h(086@D6!N$r`6kP0+l8Rlv)#(MHP3L*wPfAnmMxGlxv39_V6z7;ubaWVl znSc56rL~e`S?z=xgX*-?Dg_rhsqk_7@Xd$>k2ux`D+7P2s<$Z z^nE>$fS{g*9?35sy}kd=1pwFXyF8!01dS(b526lYP{3uP&z^0=dqIo+;ll^eoR(_W z+5xQ7sdLEJefFj|1rT31+pF)tIL|>_ZUwa!AD^Zq#hp8Y&EDQrjL^4}WKMvnQsrh0 zPFwG^31zy=f36XaMUZtk9t5KxA1@?)7fH#))YIL~z{NE@I5@~hb9#JyrNIQD^9Wt< zDTXh&?$vHW&hxh}opmf|ek&(KWdu6WfRhxA49zR3 z26sc)UxT=rV{d-bzvyOeL#9M8d`A*-_SM34dmD;js1U*@|4(-zs9c7~jw&MHwx0+7 z8GkmI=XC%rD!APE(b`Y385tQZVW>Tg4w;sl4E_qOZ$%16#^w!uhIKF4Kr&l7stY3K zARG6uXxKzoiqhS%s+v+<(OW3!#hR}|LQquQPo4un1wU>AQ8Ng8;no45@U?C_$yvH* zX{$%&PYOb_%A6rXpQRE(^{uWjg_#Mz?5dmh1;!liS`OtYULJP{`*(O>{+pfkzVxKE zmTFZpC%pC*Itn1*d=w?g=eSPIxT7AkL;+pZJ?QhnrNCZ9+~bz3!LARum%Q8(gprZd z`ccJP>_I=66{0^}paLO=@QD{-EC7Z&P(b}%=uj)xL_^?uuky!>7cWY68zn$IymvO! z=ni_ydU)gN>MD5OBO02xX?%QqNR@=hZ=NkMyymmr5Y{k$aBc8Hh{|x0U^rLF6zayu zFFsIGqx%4u!hudHY!N`vxw$zZA)zae<}#1BDIF_wBqBngb9eSrqDvQaCxT*k3iTWk zCG-G~z{~3}Cq2aY1hE@owVfCHPiIX>M+dHj!SN{umk3Ekk(j!> zf_m-WN$X|+=XGA^bvx0Y53fpBSo{gd6_8XDrXS!3T~*4E@&I+U1 ze`dY@6~i9rkMjk?Za_B7$rYHfqM$PJgUDN~WR4SG|KC;^#Rt$lNY`uin!T<>p^}n6 zKuH|%QDrMSFxw2Dknmt{(ppb0!nzL8MtpRuNMB~4HRH>FO{*QIP z9=*@#JS;{flqjkLcXsG5gAVU?AG-ujMwL7A@_o>`BI9$A{NOMHFwKs^Vs1C0@;0%Y z7>33*;MD-s6_x{?8u&zrOE?@@r}-e3l_+9fd)|#*YCMR{o^7l5$Lwo&=OY3&TwGs^}>A z+1o3}NHoY9wm_Dx=!{s|Tp^uO89Vrb$&GX@`S2JF68AE9J);kjy=Rl8bFP`={LO7k)@PwNx2np#tqq^k>i=2gm~>&eGD-;exlgW!e%Ffu@m3%Qb_XJq=G4 z34nS#V$}GN#$!Y(+K41@fd{}Rgvq7B9$+0Z9}p>&jFTUilTAsjqXlA7K*(y&PMAlqwXkljX?LRsj{%Ds58F_T>11T-fB`(odAd9l9RC!7#nSZ@>=Y{+E9Ls&C=(oBP?U<|Gk)k zP9pSM;v}grE-oPQ(yB1by^#AsfdEc~gpP^8_`uHDIiY(A0@j`0y{_e$_;?B2HYnAg zr^y4Fc&c}hE)KV*wH`!altKVyqosuzaVQyYSyooor|`t}f| zC?JwRhZZ6rB$sZ;0ids+%T|H3w+&5FxNXc`a&ke3)n8C>q{^u&3dKB@zYn+mfX@fy zLQEs)G>N1ugtE#F5E;NYeu#Z=N?pY^^fdS!n)|-gOUJo~68Qn%n%JO5i}UmaHE)_uEG#7zo_uu-~GLRzFj8tD|IrKF@&M35GckQ4zyx%p4n`)%;m85UkYl~ z^0cnC5^8=AtutS zbVTvbC8bB1M_O*SN?;E)|6*o*?x2{tLF1R?X*20OjKLy1h z;2!iL)rKV}O;EExoN8%KsVJttwE>jl4dZ&E%}>CX;o;&^U$=qm+}hgO!QmHpb+Ay^ z>Ga1M->CcQ1+yEnTp+mZ@nR(Ym^i{5)YVNw2d$CYOr56;&b^2zxC42iU0?2JfIFei zzINpbL*{kJN*AEmgxc6#vCi^hmlA z>QpG)!COQOU=V5P58$0)H{1)RMMdwNzFG2pw&V*tH^%+7C9klj?~_#DdBgzs7XAzX z8`jNEn@|)(VUMwl2{jwy(AfhVu++H0{G=?!n)0i1JY%Stbrl8O01&SbB8s%xSEb^m zv&+{&+d&^vEMyL{XY3lz_&Ih>WCGL!AVWaP>dvDL$oec_9Umd;ze~VD9+Be=wBfuDsN- zH=tg4c*o$A18?~U04S85m=FH^mJM4HhHU%(TCR2i`_iRL*w|nSwFDL$Sx_s(hl!;D zJvjSS)eFz(3duH1jrNWXD2@t`0*It3C@8?M2B4!f$GKDid$mk43<|OVE+UYmxueQY zW`7wKQ-y|x0vB-ZVUL%Ba|VP6cny&2HSk~~Bn4J1Sr)v-0T+{tBpV`YHj#j!8H2f2 zt3PqpZQw1)^{1#+IvF z%Jfe)ttACDHStG3??U|!6x(tkNCpRkGOB$5Km!h?+ROK%G%E{BJ_tMj1qA;S_j*!% zbTPMZOk_-q2}CkH?xy>Wb$0l37xOQjLrg0^s2PPRs_o`e^%W=hE=+A8S|M4#BP)vm z&;(wps2ROW3xZXFP!FCHF45&UmHqO$tGgR&ww)m(C&;;h4JfFt9ysK!$Nf`}LTsV{ zdxRX%&7CyU3}WDH?5hlrO3;SD!W4q=Szlit5`q=aC=%oLweQ}=crwH^FIbTQkn5*+ z=N#n~rx6JtdZAVX>I$aN`pUBvaz-vho5jKLaWBlRK@EPB!Lts@{%Ts}5Cd1f+yXm6 zAi+6=`sdzWb-zC(&f$geNz&P`Y<-}#(|V&q9LUQxdHd)1&vXL#T?j7}5Ea0)0d5SB zsplhT@#K;s9s(hTwg}80L}8Wy!t)LhmYfExKVyhZfc+JrU6>RM5s2v_!2KY)VuI1t zs|zdo#+Nadtgk69=1l$*GIIO<6I;|)A!5({$B#WYz}RE~g?AHwZfQx#>_Z4-OCirk96s_mz{Hy6 zhplQ!_#vdRH=;@ZUPki5K45d0q864NKwKx=-P`~cV~pvvwJmS;!1!MRBNo6CfI@Cy zSG#*Q>R8-dT&Z3iWs}=i+8Cc&y?QsImV8)~j1sCBtgF-oP z-n=O$)}*O=1Z3&tJ9lNj+rU4<%7=2l(uM5&o-k%qqT>YQo`!`fGsXi_(;!HFSH@Wt z4{_H9jEE*m4z!@|v9^Bm*|PHXu#&nu05s&Ly(zqCOFkxMX3!eA!#$RUlMJB#ZY7d{ z0fI#szzg@N6s#a65zTlA$-z((RPO@zL-^PDKaKJ^Pq#rN3It3AAjr~Z!~^FsKbzSF zpe)#grM-AzDVV0poKS>*3pOf#ASQwGs;8$1qYyDMnFN%M6jv^9=P`iyVzAGbo1plB1l!B+)gMkP18*V0gUU*) zXt1`Q2&)1JNMOxi3IBab&X_HUDsu?}%x$2O0kGQMc9kMBrushD4DMG>On{VSM`W_+Wjpa{Pv_TC*v3>I%PxXygQgc~ByKHyUebxqko8c^Z5 z45ak%_39sGLr)HR7Z+PR6(z)mv<*N_9FChkAfD}^9+ttHk4?sqv>IZbm-80_)ZJ#j<$nH|- z7R1-QygZBw18PUjx1^mjX;{KIeghDs3c!2|z)vMLHEIwzbyp^4<`PQ!W7eS;JB%`9 zaV>s_+kh#8wy_9(j(`%@pCr|@hzP@GA50=pbm3orm*GU5j2Jaj7XhR)`2J=UnR^*$Z04$F)BH@2GF&z!A$@d467P~w#AgN z#YcR5J;?AtSzu7>Ffw&^>RG`AQZzn3SHMnTp^zGZ)(JvPC-Bpd7_pSGA=>8J;>ZeO zF`!IEiZV1bJSn{jZFLRDaYO$j$k~!Y4#s!CE$FhuA09EWKahkX>GCv3;N=bv z4+~BC?Z9QGz;6!pb1<#IT<_O)$41=Cm$Tr;0F85VbHj)dl9MrFCD2JO`%S|oFuGEZ z%0bcDZONBk3h^PQej{!O9tpfFzXR9=xSvdZzZ7(J{}tJ2e*tr@H3rD>ZiJeF@25RL z;BLLQ>)>?}To()p3%lh|JTR6Ig)Vrofgm6cpdkp$(Xp`-pc2WedWIzhzb?DJ-U|xq zuSV*Kpi8ce3|#+*0IHF*^8t@rc>EX4PmDRLq6HH~Q%p$JcniYdu{`90i;jLJJ4!%8v-Gk@pX_6 zu_u z!VT7rv)YC`Q_}E1xC@yp#F@-rf!JId2OJc&(S^t7~Ev+p^nzv+aXCs@%}F#Dnv)G}`u@8P4NYR$*A05EmB z@T=b9;z1byd9e?vjX5eQF>w$`hPp|g?wa=l?9?o>06&gq%UlXI0~Ft&?snst^(E^3 z^76Z&IjJ$M8o`$k!u~oxo13RwL@fO;H3EBb`>P+Ty!effaF)!v2rt0453!)Pve>XB z0IDPVgGcwr5Sm(Ay!f}gw;82>_6SXZV~9ppE1ZJS3rV~lGvUWn6G#Nbq)0IBp5-45?nuj;erIH!$z^c0hbKnUjEJ< zhLEpW5@)b~bar$Ax%6pNpJm*!>O!mq52{!_Ff8|C`sZCm~i|5qzKTB`2A(i%Ugo>lp+ljDQ((ILNsuqCdjU z0J4B5geq#>f_drH%m@J`@&0o3X0PP_YQSFw8tq8jEIOiyZkr6HjQx|B#7YE&2DvV)$?q$K9k z{l<+JyvvvC0vLeTdI$vu;dOUWD(Rn)FQ`{}fMC!BRNQN0fk(^mjG_i-$B>$+z2wW& zET}#A?2sAyDTuIwX+dseKBkXevlz=pBQmX3&q*Sip>+VD8$csi8M*5EQ1MB=6Wf@j z1K3HZS^Jqe>f_EIpdU|&F?-625^xWm%K!A5WAmD>s zVK3VKD$r4T(R#3r!%C8r3_6(F-rioy3T)9V5ZK2W%TI;vplz;Rua5-leX52mq&)C~ z4q>$&gRvx_MLXO+QbZ1cdEBQz%q%iwsi~=fNO;JDg8nAQ#u110k*a(z7v**y6>smu z1g-^~+E{oVhh->@U2n7y=fr~{HmG-?pD5z&Di%iB@ehdA>ZPr*Qwo3_Ky>#r7J%-B zjt2K(=mQHiQ#a5T0Y%X3*HACElskU;^a*Bg$3l0q4!cep1Mbt;eN6(FU;UlCsaSCP znID?tho(;Vgr2wls$n<8J-aCsv+&i zv;vgWO}fHwwgikg`pwNvOjQVA6eKO`U|s_`0JxU# zb#at|lP7NH79c(#kYQzFT9}KKFo7&H&%5aIG>nF080-sy4hUp6z``KoE-NqJh;>WGKA71A%O_#uzH*X%Pm6E!wWquH7}*cE~6xITMwDYBYK~1_0c(+wX49R_GS%e=pSGNPVNjKKPtY z28=^Fk<$MYsalh%!7jJ_OrpITjS`p=ueG-1|2BXzXCC@Z;iM9D>${HzY^&? z@l4~2x3@Qh#96J!yZ_b#)Ht01IaLMx`+5y@2(w z!lDI=BJ7R@0x_NiS375H4tX7<=I-vKfTno;S2Tt6u^-8-#i*OBYo>$8(u>qbhR@-W z{H5Hg!Q*AuW-KCE#q(Hh2!xA}(9mp*(w;-K*&6%p-=8a%KWNWmR!Vs3%@A51}A~3=53(;cYH@PqtuN5Gbp9_{e78#kzPgysYCv1`#79 zl9~OP%^C^x&k2>Ujp~V4JGtoFZ~ZV0+}+wzR8wn#N)1#Tpxm~Zn&r_RqmKZbZO@&d zq}L4QJm~Ax6%~mKOZHK~i9)vxBxb2rBbd&k@}l?pXw8J7kAN6{NfqcmG7anCS`|}3 z$Rerd=1Z@kEz%*gq^#t!ePDX{t);{4EgCchAa@>KSKH$v93j(qq%WlTCus^K%gM<< zAQkASD}1}35*J4&h|vha3M`(HFObj6UC5LQ4#ka68neS!>~4PO4MHkdLjXboEkCRE z6hM3b1({=Rt@7VXza%2~{gcN;MQsYTCNp#R28W@@iS@4c#$Uvj(|X zBnd>SOkXChFeZXE4w}(A;o=%$z#PF{{#REb2Ox;eE_3q6#_9f%XdZ`1R*#?G-nTr= zb?<^Ir^ab20Q48k32&Kqsf#q>-RZdKk5_(u5+WUkKqR}BmjtQAli(+F686uD`D_=EMFlR3)ommab)je^*Krrf5IkQQ5>Qq zjle?GKatOTzymFU+r3Zs^5m|TljIdLzJFSYt&Yll$f@{zOcvHT{yS` z+ubPE3X5wZma zb^>wL5qS-O8$g!F(qd9&9bI+4>^camsNuW_7}}CP|X-NdAWez8Eyoat6kWwA~sGzfY06hyALRgz~eQcr|{AR+UP)CKR*Q1YMLLc#3m#Wz+qkyujo;d1{5DoP-j#gH9aBmrd;K3jGlU;;X42 z|5LUJGTiqweH`J?Py{VvC1+5iF96T|3YXPcON}E~6GEK1Sw27tfJg)Q5^a_OOdCKg zN2X^-Ge@H7(0K-|QY@v& zHoQ|Hn~8*i9rYk5voIJkZ2kgC`S#u(X8(p92Sfy_%vyOk?MxAY1Q^ZKj3MAMC1~5m zpGs7s66p$`h+heP=w@Lxg00tdWnwJ_e{gwO7tt} zSy>DEtgLLCItc&+<{97Q<>^!n0phjltlOWBj<&c} zhECUbdnGKb!>l)ULU)fwEbJV}PPG1}r<#L(YYDm@9)h7}vt(&EZPWlty33_t_^^nV zk;}7bA>%b`2<&v7F6p$$3XMOme*e566X=?Qbu~L%T*#K7AGXrykb~ZJ7ybe1+nSIx z6Z923oI#QgT{W^AFQl`70$T==pGu86xx2_<)lN`OrQfy!dkq3yE-U z05tzXtCKU}nUEYnsB*|l0;Cb{1e(cO03(7eVwOBfx2PsS$3VnbHlr-{uH(o@Apha} z0f0p5bO5CDa4hYlN~yy&HF2np#N|dXtwQCySShe@|Ld?1+K}2cv0mtnD|z)wvY6-d zP7|@)+56Z8AU%wIc5x1yqb>_k&Z;NtrQ(U{23MX(R$PFAu40w*n zhpn#Og|ro@?&7nWvtVTvm0~a)hrHS0`b|DQC^9q+4S^Iis6cMRngJHP@XTWBUz6N` zAnbalhe@7hI8o(f_PQe@ zm&_Db|G6CzI5`GT;;+D+Lfs3>Phr0UXPwCM*!G@oK~ZDVFp!-5cWSsrsp$WaO*??B z6kn z5`|P(S3~FS-$sj3h!BuvLY!`e$peC+x7tv}b=>C}uKT%r1cT{D3B7(*Ko&EEK%inr zet?E>5@O;RU=m?3w#oAM0N#|4+>B-g61Hd}pr^`aR`lKx%MgY9w+vpXC=iZ$fMb0q zZdwaR<@bg9Ko%!D{`^fF_1IS?j-+gUsn5RiV!8Z@+ zJv2K&8UAFAA3$~-Lc-c+W>ed6!XNJnKSGubm7;Q+}f=GT0wP)VXioVEUqNB!farXK>q+#(gHxlkzxkZ0Z@E8c^;RA?Hn@po2c7O-SS$MTro`_hBZ3oX->vOt8ogp@04mFG5n#FUxN`{GfXZ z?hNGrz~xU35}%ts53)rlV1WwqaHmCx$y=x!0+&P_*f0*|EC6E9$V<;XZ~(FNbxO+d z3X3~>m(I6Y@bBf;cy5^Jg&~4gn|FtdHd#HViN$`cOF>l_P4R!@QU68W3|xzu8)hht zxyXOxtFqzd4EC${w2E-Vfc=Uq&|<_Gnx{bE?_mM$msT1@$-dPKZ*+E;i`+SQcHtWW zoIF6|7HUT8bdJT%uCMFUr%ylwf}Faihl`W52@b>Z@bCaT0VoIhKOhhT#?a~vUw1KI zL@{&V3Y4b_%|_5u5B5$jlyVxgiU2F(Uc&=eakZ0*jxkXQnmPaz{cIT#69Y%Ky!?9^ z%1#%t-pE^Qp=KCgA(Fo>IIxR>@*t(!n2PyLCDe=*_vwfO*(xPA z78V9w|NCPL3kwFGnya_h!7DtL2URO`9Vi-L;k z-()83ZU`J`+(xY9duUESoW^B`SNWew@c&@m>^w2Gb1<#G!=_m8PSb&|V{*y&2T)P~ zwaLj@yw;>UyKAHE!AJ5k^;VRWDl4@pjL_5WVe|m=h1mb=|&YA}QmfO`AOK z2b0s+6qF6lmL9mkMD@b3+Y+32ZhsXqZZt&`4b-uqrVNeyEA=_`ZYqy^ z!&zO+3u$#k(zBNjI-&?J8MVnYoKY`P-WV@4L#Dr~oimDTyu?M;-P5?>PGsTV)g=UZ z0-OUOTQr#3V=W}Mj4w&x-3~ucI*R#$9Njsi##|j;GryJ16}^{3+WPth-yeX}4NR1> zzHsc0)-8fR z|D_@>3?UMd9Q&{l{P^XjLJk5nQ`jjWI6&}g|AQYyjSlt~ayeZ*S!r_)`S!T#klZP0 zxJ97Wv>+>@BFm~H#%Mvb3hlXZb@|8HOC6{8=ch-CeWPiS*jEDgAFqEq436K<_kTE9 z9oIu7eJgh1&64(^B!fg^2>sTjJnSbGTc=;!-<@!gnb?=li+{^%*6=I4yWxBdXppA6 zh7UzObLO{8`cgd!pBTj|Do=B5sVQYmHeAmiVX`l0{Z17ndEpQ}%OnJgp;9ljG%!YU7f> z?IcJa5-?f4@T=odyftwsLx={yk+#0&tV^U~EYav@_g+=9(+uNNB6@{US_N-B96j@M z&I&|cA>UGKTGQV>r=H%p8SrJA`Te>6kM0M@!4jmTGIyhBH=8=X7)71$wLryOVN|<| zQln+uY9!2+6f&V@++2`0WT^Ofcm8Ejy2_qk)>nTY4{D)< zku1;L6jrk2LdvB1@)Uxmj%P$iE^BXIRM|>Ny`SrB8fFx}zZeoyaWjx!0Y~QOK5Et* z&q)xciGnU1QYkZ*_GLYLDUShR%bSaGDE z(Rv#af6yZh_gCSWc$~)J8B`CXStkb5ezNs1BWyWkXWlpAm730e zFCLiLD+GU0K{mPn)}ukF|!H!k!G5|W8#Q`eski_P?Nxygpdg;vPyEe4;Y z7il=DM%(=&5js2}Z=HP4p3%xR3^_9t&9^6B5@yN}ea-YV4TkSK?+tpfcHi#Pq7gY^ zhMk8z`RbZw6XJe1^fKkvL|Yo}G+t2_4=6G1J)jDFWBj!xYz7O4_uJs_^n@QuOq3qW zANRy>-@1ZDm#=D=U_#{8IkgeHUZ~vcbNuP4*o`aQ97k)7fh{Ba>@O-`q~b5|=aYKj zvAx*0UA<1t$j$v@>Jz(YQmT4|U2r@rs>Sn8OkP*wOK2}%wqKIp2ssrA|0e`Gb=n!gL>zQ zuv#qexbe|@EHth09*#rRLPz`SUcb7hR6pWselTERKq}GexZp~p$BX?*!()l^i9YDW zmQq5!yj-*?hl-iQQmnvYvQ4+4#-s~z-TwOV<|+mH4&QA#kWbn4nK;wORrBtbXi?wt zw;Eb%)YnN!;Tg1X+KpE}TVmgCz2gMshV^R$t0#(vSWIz+GEL_#kip^^ar;X%JN}JzRO9(4pSeAF`@)v?dE(<0 zbfUDWu2V!2u@AmjxccD5d(>V%S~cE0w~#2$DfdvLhApEuO7MD*{JrvItM6m}16{3O z)iZ@X=W?7b*Ex{YKCttoV5wMJ8d$!W8myv5G2ARuJ*r&0UU$LDuvtTo-`DRrB~W z+;F-5%cm0SO&^WE%*GA1f4^YLK4^v&PV|B2PkUZdvyA}3vdVdcd(2Xgb&lG!YB+a0 zT}WT)1Yatz3XlJ$(sgc;owcs$u$Zjvt-Vv{(e_fR%w<%mqI*ve?U%%gIWqO;A43cZpW@;Z-{qZ4O)eqp?2VyrgYv5=dakyZYWwXaos(79#~ z;TT8q>v?P(XPXHdMDN3w#tS#n$vMdqCLK68rb=#0|Lhukl=HNWXS|sE@Mlc``^EjW zuH8Fo>3T(^G-|#jZjII%7HZAnrOi5X3Q=*0;o?y_(T>`T}O*Q%M0w&}NMuhUub zYp48vG`84+v-LeMwNhQeH2W>jqm^&gzsmz?xD0pp>}7&4_)DbhEHBu5qMk}T&QYKA zo>YGC-{^{-nX_pgG@(&T8(?aYTS{T@jWxNd{)Yss#dm6^$xqe8gjaf{lX=eYZCNf; z4p&LqrIp>#Tm~B)gZoD%>2KobGR8tozj(K?UHG1}s;NP2rXlFI{K(M%@PZT(5eboW z+zefu;D+^hgA<>fV)%eaVl8B`7Gb;8!*|H9{epjN_|lR&r#^i0ALZXA+|v_h<=opd z4{IGJ?^KL_LwPZ~KIQo=iK@aYLoMCYm9!l;Ih52@_m$^YQPWU^wi#h%l6%T$)*ZZM zL}t&%$7i%|bPfk;{ot3_Xekg-z{k${Y$=7bn(7~Cd##_4>vo*-?SAW}%MpM4nnEwH zvmm8Gz^ULYnYQ?Nwr-xItl5`c;0MR629*ifl?4Wqx`%gp@X^k}=E@w^NIIvQE{B0| z@@-;iE{oSjhAgrL_f}KzJWB`X?H&IVkY6Gweul(*NHTBFmnL!CtDepm^op^BzMlE< z0Rr-RrmjxSv-=4N(DM^1gVnS0{yHJ0d&WRBlSY}V9tkCq;qeY{_&?Q(PxO zuk;(D6YQZrHIgSSBJZ#m@QL4hfC>VvT-Do2}rl5rn@E=+y7m3x(8hnW?-v#7Xa{W}l7 z1qzo=Th}DUfqH*)uyQa=vs3|PyGk~*(ZcI7J526|`@8eu3#}+hhQeFXLzQw`TNitG zELhZ(d-x9SJYrN3<Ef+L@?=-8l3p3a3eU=$dE#*EdAH>dr~Vrj zo%g+;n$fJ`#b3*o2lV<@Mnw^HjLC{wtCJ(6!}&JJvB?klR=SFlYaUvW8-M2OZnDB+ zd+!{aE273Wm^681syV^Pl`GbAo*kbq{ndl7k#SwqJa|ENeury?Zplu5`y1Yroh`zf z4P}D&=oM0#U7#MB&v9GiPAq)FCLco=wZye>^&RmOSd$A^4RMNc4e}tLn)$R&Di%89 z-LXg!S=TDq}nai=yWV`SyN3 z2T%O$aoiCcB!(oDk~|ZtJo`k~+{IDN*z-M|yobFaERaUKB_Q;Qwv@=}UT4vyU!HoT zHF1fNeCQQEpm_rH3|CAwwkXlr5zsgDfDUG6=0uc}8k2L3S6|NX& zrZ`>_fgX08_`;p7lH^>brBC%Kw2@C~b_35OvF`9`w(p#!-pW&SzTYW0qqiY(e=F>g z3f5=mE<}T;uoN#Uln(z+aL`;xebNirH=8;xT?qWnI4_{V_@`rq5$cy@kPBolvXr^A zcFG2oms$_EpNms?b`zSgf9C!T?S1yw689xnJIeW(5okrqn>X2ao@g>iv$D3u)}?Tg z<+Sb0Ccil0Rz^m=OVK#(iW1<{{89Mwth4{D*Jy39M73?`$&Yr4ii_`Ip*D5+wZ0;y z8{iZzysO-0U>ZN`75S3LE3|`Vy%{8OSVOd)Hs^4fUJ70Rg3YD;;_~|7MY`&pSTxDo zh}_|@x)cr0rK_}BLfKtb=jCZb6wcpF!xt2ayp@!o=OtQlf3nX#a;Ztst{v~khM2JP z={bh|&)kee(mkp2C88r)_W zy`~I_qctnn@VD+&L5Caad%8vMs<)Op-pd6a_=f+HCXg;Lcr@Q^UZqKIV=ECw>ufng znk#G5PbGg}*d#2S?GFM$ZmcLPrA;$u#O*nIdrVrD=%67|Pc474`5tG_+s*FMO%az< zLFs}sx84>0L+c#fadQ;k&Ku3D(zaqQ9-Vok-%sV6L+eu7d%|agrTVa#=oE0Il$BalFwmLhqgWXQ_o!_yI zez%p0LZG+DBN}h9+s$0&u8KCTw8EdfucjxsAD&J2EVh0xu}x8bOP0~(C3H&&nw15Y zxtLF$&=W|rz78)_bK{ncj!A!KUaK!1d_g>5$w@Qm0}tu-h2!1y8T3(&$7ewo=E#l$ z4vsd0{YJsgvzyKN$~HWse5VeyS_;l>P0JQ}oH2BCf?fM#-V7FL zPoF&*D{@m-32e1`o@Y=m1dpJ|;8C zH_Ydv;1cP%^G*#Z8hd=wCz-3;pM}O$LUCQX?tLBbq|*sK|rl` zH%eRX?&B#((G!)O^4gMAZXc(gMO6WR0#4qu%^gKu%M`ZrdOhMmr+A+3h2Oz^&zR?B zN|7J=zQ<0dhZ47PM$Dld5?vk^$Yze}O?y`x`E&GPgqqDO)_BHn-^0tN3gVrgQge;# zPaKSQV&AAAP2Z!IDNt>#YcbyH{w*X=5SZP$N$#Uh?m0g%x3!@`$-yeURn51h2c}P>C`=dSFL2b$s~rIM!6_7~Atf|D&&8gv9+~?;tyNeOFg? z#wHJs%j39ruODhktX2}zNZ1cZ_>qx@UMZOTSht>#^n%Xl4m+_lOYc;Cp)_N9L@u1N z(^na(VjU?>x7ydTkaz22;f)@O-&QEK`@-Q6>B%~A>YrVd?70tn-6GQPxifoaY)bQ^ z)py))4!k(Xn}vR_^?Hmq81a}(j*+RN8A;gZJoX-=Z9LW_&}@GsjspVUzIkmLyGJSq zN5C<)O|kHaRp+WiUg1})nk7_F%)O+wUY@%!qDz-ecPiR^`Y9YwIr)se{5@{gv$Z>w z{!N6t8^oP1ziQG9_;ZHKS-5U_j{Hpa5$hMzq|fGixWq`+TF-L0&4Ko12w~NgN*TsJ zOjD-p^v>ii`!?B4kL;yZj1ER+1LdMKk$vtG2DKiQYSv~ZY#FP({=j} z!TM08K|7it;Mbo_v|cS`y~dlt{q@lZ`p<6U{pU~o=|7~ACO<$1Gb@DG3#aNlmtW3e zd8mZ6NcK=gr(*s5eU!t(9Z+M<-HY{0IB9j&zfk=LDkjmm+2s;?Gt1MotD%hIP&z-U z=R-$t-+P874=Uk1Rk*wy2_$T0Z&voy_FHQeda@z_ay^(UU1jOBC+(cD|BR$-P*2gO zP`HaP{mEg#4)#JV$Oo6)%x4IK{OlICr3 zZ}x=Z$}e_%U%Zd(2O~%^-aBJvsXg}5`Mr}F*=axO91uZPWfNf>)R`Q;VT$`{tAM3440ZNH|h$_|FS zPzTxZDQc9YWU)mfpV%uimqLDpE3VKhQqj~TB;m#QtruF@Lz_Y!fJ@uo!rH&7mb5rU zigk`fA^63&;^4SLXQjZ@oewx&0Bmx6;q-OxEZ|1-;CCtpTQj96mj!ONXK#^3F3)Ji zonG<~-CbWUy3EO(QTpqgl+wKTOnM&;+F3@<#bAaigE{LJV>;h+^Lb|fmuha9(c`iX z&SlcP%CWynNp9%Oy)^K4&Pr}S)0VpSG%Qe2q;@PwPt~|OIZ!Jh+t|n_HaCyY9T|E> zw8y`MdCH+c68XJ2SPR<~H!wXH4#C)d?cncC7s{hM)5-qFE7CwO$%&PlFYULm+TG&d z#N%l!$ALsc6>QxZ7Ok+mQB9$N$pDqFuC7bz+{MGEi-?zX=8U#6uMFyAE=sBry=ZS#ydH1P8cvGF2{solqm76& z`huOF%N9=BsTi*9xJP)E=Bfhb=m#+;?+U%{uP(g|zsY*>W<`poJbrNxX|H;<8SsRZL~DRr8a&OCrDCofa)D6<_`06=T$wupQjWlt;m| z(^ztvoN9gNovahjp+2z6hYHl+nn(=zUi^WEPRg$Z@Iz!O;K5#(C^|mPPtL#()}=hB zy)XPY0q&;7&cnHkOlQ}?`1kbih`BCBTi1zm@N3jdugEvDg&!0R3vuO(^Bx)Tlkn8V{^C1GxyrH)U2&gqOyn)oaa zS1oC-DK>Ds0*xeHLX(YG*STrc}AH&PU(~m2O_VV zQc4Q%ZqQ6v7PZ8aaqLJTM<#Cv&tP?YV%^fr^-}3#i<@wnL{DKU2ygKk6KrFhN9>+s zVbz@pGdn=DOqvTT-o#Rj3?z?OGvD(W4(16lWA;BB;SgytiKh*`sv7swV88av#cOS^ zzbnY*fv#d7ry}op66*CRtfvY5T*&hz?ISen*{Iiyn(^k2V>t25wz@=Kj7U@{13sEZ zg$PTDUIuw0;B-Af;U4ma5T8EI|S4Jjf8oc&F+ z#$69eV^hWMnW?*|&@pNOowGPcFPaC4mFqEjqdC04zF$_LKB190b9*aAtKhR#(Rke> zeIW6wK1@_tj!IVtQnj$3^K-0MosG#XBo!txE#@iR;@dm8^vjzEpM^)eXC~(fK`;@C zbKy9xK%bP@n}A2kN2a%x6jI|A6vMxq;&|)@4(YAXuzb4Lxh=RZTEg{NV5R#x?Q;?} zs!@}btBf;E)E>`9+^2sw)-h*=goPJoc9LhN5K3Bv&OpTSS2Seb!MVa|jnASG!NjEL zC0x^gAEl<0i`6rl?`8dtK$ zWS^55-W3P$LM9g>&b@2!Wu!xo^fUGr^FphLr0+)8`ia;!rq@gx!4E19n>zs zi3f@PC9r_d3i?Z6$qasrPjY-0D$#{_fV$uMaQGk2ZFYAgAGZew>zw%1E3?fg3_Vy4 z^q{b!<^^C8<_^V60 z`^h~jQiS@alYQYO=a~QknQ5#VswqQ8I0QmCMm4I6wICutrfQu+`R3O6J#Yh=oSejc z8f;|{&**~b)De4;wgDvKD3FM+T5i8T7@F)$3xY;twft9a$zoUMg*LwNYfq%=o z>g7_0`Itb7tEL_g{uQq)*59wZIZvUn22M3&4Z(U_Tgyr%l{bs~anhFm-gQ6zA#^;4> zxJ5Ak2P>!9E-liF`{poWx|pFD3z$HLy)qg6d@yvwmKQ%q8MibE+;+>Fzzo{#ddH@38LV9WkL)vrp=le|)ah!b1z}23&rJ^;#gS zjn;ldyCGL}96R^l{_R$uR&l-~{C_3v|5LMj<`Q@|p+YB`y!!_q4rTNAJ<13it&&`XITO-{?2EXta?#gq4Rg3X}%L0 z`*)ywHKGh;N`25Tv9*h_1BEWseDAtNWhvys;vyJ@LcbEUGK0jo|2~iY67*2nKX_1} z!ki}NU0LIQxV->25BO+kl7VJ!J%{4L1Q6dtr}h)85oqK%*~vO1?fnG3Bll6z9?zy` zaa&7IM`E_23nV~yLAq zK@2r$w+qI5MXC4jPeHN@M%rL`hH+%Kv$F%~f1ai-C|*HxYSg1!v*4^+RaFIU31HC* zM+dwC8P$(f`<8Zg5J^vef<{~H%ha66^SwJD)Mr$L1~}vg&?<|AJ@nrZBgV>xMi~&P zgHRgWs=bVj$HC}_2`MclbxxWUO#ehg4xz^al#Lia1uTr;D!2lIt4BR(|DgSJ!4(Al z-Z~lJLkiX^XUDB)-CbRVU^fZ=7{e7gw8*~=LeS6^NqobgQr4kNJO#!W?4Xtw7eO}~ z{ywE=5!9fdXf|!~+5%lFsE9apiwne4pdkm$_uN5y%&!`W$93ZdS%STR!ArvuP-^-> zgFkq?LA$E}+%p`k=i?9JIPgY>VGa1RbtlOR#A-Ld*l!Pdo5 zEO>aTrDy(iYK>e=14Ey3GZ~!>hq2loL5e6Ei55-FOqztISUZRyIGD)@MhRb|>F?_UfipD5w(ljI z22jvJpO0%;Ms8l-2zEbc^{cD-$MTMkk6Xb75o2r$4#XgB_kr#-n@n$PI!yaJh>u+< zS=5H$00yv??ibaAhR)rpNVsY{IS8wcz>RHdAP-$)cRdX)aIgHnl!{7}zO)wp7xM~G z{X!qNgh%)wMzHRzLLc`V4y=LY0zGSBIYAbQInV|gAYfXMx6)@VPF%au;UE|m8VZgl zQyxjos;Ms$6T^2RxEK{9&a4dfK*J2HcoMYg;5CK0Wta%sgE*Zo&@DSEt?}aEb)Q>j zk9F0qoWOg18?c+gR`0JqVfa$ClG|s=uc~MFxx0H66oO#WYHp5csPcmK37YW8)f;5{ zmG`O>-Re0~6I~Re$@fShE;|5HBR}L!0{fS;Hyy9pSB8Yu=V@iUJZ5bW} zJQm;!%(Ea&T?2cr{#GM&9zSSuSy;l%QeXkV7g$6+A>aPl+Jw3S zyA(LW(2w2|c!pm|+lSt}Ly*HMs(_`KB2^xTM@9cSO*;RT%tb0u)bEWU`4>{e7Z)06 zS@r3D-Tm1yG4#6qNpeJlWY?&F*v#h~=ON4@u^3yd`{e4{cnEkC+~xU&&M;m`MoXvRRrq9Gm73FRA^#ya!#RIes^#0 zlJNS*Mrm1@latd~?ZWY~FW6ui85yyXib_b->2s3GDN1;n!wF2_r4Ao4GBjjnVPR6l z#X1j`1#qm_me=^kz6Hv|)s_B7IBSG%QQ~V>=w|oy?7ze-tHF=Lzmd#ANeJz}&|mxL zwlB|eHZQ(~V48-6s;Vle1;u)4XlUS+n!!Px1~u?Ofc@h^Y6}e6$Os!d`%!s*O-+p) z5jZ05@9&FeUG8-Goutif#e)J_y~gz#!+UyqEGOgr5%M{#>& zM@O!<-fnJSkP02aO3KQx8oqrLKblF)$S5f(8MLcmU|?8g!0ztuhEv&4^6~SdqlQUp zus}M@3Bw>+U0k%T8_N)9@_O{hlJBK6rKd0@2;Y}i4T2BTs~HvFwX}GMW?)Ha>*{{} z^2PG<;>t?b!#Z$0|M>AEEXm7?t7~gLYaZZcLP}2V`REZ3z6=qTs4WFy7`zf*{3^o| z>ex>63Wcorr13eCH_xN0j#lq{5$*$hWq$u#&9_}$UC<_+@Oge|`KUklV&Hk$V=>zw z#{9t7_x=6N#tVlx!Q`GKe0a@6J0mq!^!DxB!omlW1ucZXAlVNJZiYK_cR%!N(dVpf zZSCd3SM8so8Xynue**3J_kEfJpdre~XLN20;!qTrZC<_{=y}%^nFM}K2v%&)%ivIS u_aFBl(tm7&I{tRcNdDt5^zU!YU7{}ENt#f5Z4W6LLQzglwp99l@c#ln)`xWf diff --git a/docs/images/flows/02 - AEF API Provider registration.png b/docs/images/flows/02 - AEF API Provider registration.png deleted file mode 100644 index 3b42e21185ae36c97f3c6504624757a80e282ef6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49215 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 mWiR6Y|L1M)|L-L4fYppywbA9Gtg-NFa} diff --git a/docs/images/flows/03 - AEF Publish.png b/docs/images/flows/03 - AEF Publish.png deleted file mode 100644 index dfe3a3e09ff3183fa93eab95e1df107a2dfa9638..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43268 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

;+2huQDFFW8~?y}*!p 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

F3fCiTMO;Dogq@HPo_g3_<>El|cFy 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 P85lfW{an^LB{Ts5-B6Fs diff --git a/docs/images/flows/05 - Invoker Onboarding.png b/docs/images/flows/05 - Invoker Onboarding.png deleted file mode 100644 index 9cd4b2d9fc99daa0f285a0f9e5cf77d9c51ea17c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64059 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

Ti(31Oz7P%yeFy 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 diff --git a/docs/images/flows/08 - Invoker Get Token.png b/docs/images/flows/08 - Invoker Get Token.png deleted file mode 100644 index 2e39f52a3d12efa8c19ea3032957e9f48d890d0e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55546 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 CaPF-D diff --git a/docs/images/flows/09 - Invoker Send Request to AEF Service API.png b/docs/images/flows/09 - Invoker Send Request to AEF Service API.png deleted file mode 100644 index 1e4a87c54b04d2d6524da2617fa615dab7318dc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26214 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 G9{mp%Cp}jH diff --git a/docs/images/robot_log_example.png b/docs/images/robot_log_example.png deleted file mode 100644 index 6c15a031e26eae47fed53b21a1e69e2f7bfa89db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 158079 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 De;ua! diff --git a/docs/images/robot_report_example.png b/docs/images/robot_report_example.png deleted file mode 100644 index 1cf36d8766753d08938be73c87e958f4c8d56068..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183065 zcmaHT1z1~6_BWK43PmbFaVRac6nB@>;!bgQf+V;@kQONJ7Tn!~YoS;O6oM2BP~4s1 zeCaE@`|b08H+hmfnVECXotZOdj{M-Ol7b}eBhp707#O(HQXf<>Ft7(PFtEBGV54gu zF;a?QU_1r@#Ko1Q#l@+W96;s(8#4?Hsju-G_dlujlcwv$L`D6$^Car2$FmO}(NEup zQd6r{K8X>1|CNoV@MrYLdy~xBnpH1D%FBHj8H`y%jrT(D5t`{Sz6lwCJpbWrz_%x2 zvFmldn$d2yiX_3P^=y19&0LHnn$BxLO7-L$IV7d$mC(Hh9e%>BclCAC<-Uc4hGHvW zU$nz6$&|!y2rTx^S_Fe3m>rWxuSUHvf*&ERtlB>Kt-UX0 z`GWbrMaHk@lUw2)i$c3t6?(RG+ygTV&O+UCP8^If&nOF`{pw7LvSEVekn@*p7}H(U zc*_qPy@Cm=@L@1WX$K*}jJWs1ZdS^L*n>29Y5Uahi`wB8b6jIJeVLI5g#E7?XE$yw z05|o^D+Bkvetwf6l%ZWBJnte!4kVN3b$kkL?j~WY3egsyefO(m6Awe?$pCHar;Eae ziamiRSdWAC&S!tT36`hEBVOzIP;wq7r_6ibxIy~|AlpmfWp^L}%k#c(PsjKnd9h35 zZ9kgi-a_(LhaK`{+8-ISnG^{rmNJq zbvI?T|J@T^K|U&Gg?Mj4)flS8!jNYygR2oCs^0@xutt1w-(x=OGlF5nig6P_UB+EI z4T_`fI_{}N-MibN1%Es!r5q8w$btFXUxel<#?(R!!Ac> z#~u{4!!}D^QSdIl??jeasvjAa#Vpo^(W2Oo8hX>I%?3g>qZ`9Z5Q!HGRCv-?ZW<@} znOv^!);>QWCt)p<@MBqfMI^&SSf@wLeMz6=;hXXcub-0Fg=iWzUrN=RM1J7(>A2{=9n(cp8Ea_L;J9T~nU0VySmft}bD>_=-HhHZpu3eD5nY zq1-NpU`n%ze2bMf6f)xlhhiSS7Gjbb>}F%4(T- znbm=uFQ}HLJ~~K>{t)~8v+*0kaY=b)9Vgz+fa)J_IG?2XnrEs0cwhTM&tE-LZJn|f zPbflPv?6C`UAFc6gh6^m zdY0E9TAmn|Xn{WO=XxO&uYF^r`tt{KJ}FtfEqH@^1LET1V&d}V>a`5vT(eTQ+?^CG zIafNORg2t|GE?nUT^V~lmNsU-X}hUI8g4F&lb<(o2#~cLuYFc~zgC~ADIt_dGPHtA zvP7jsVJl?foo#hpIJc~4wo8^vh>sC6#yIgX#W*i5xv_-gOJ1&rgN{SdMTesnl6wYW1_X$B}DUQ`6*48by5;yk7 zLF0SeKREVTrMQ#Kl>m0#nQ4A(HYDT{%qDM_FqW{F5CjOCQW}4n(7-XgM)u`^MWrr7 zaX+J?Md7Zkz>KgY2_p$U30U}M_%7K}PehMNSOF=BQ`l^l>l3)}RtfoIH0ypKl1)sW zT>h85d6ErRqvfcHl{u*q1Dl?;$G2Kom=Tdpf^FTvcA-`Q-<;qNUDLLKtE6+Be9YMI zgJ$ZiitNoya*QvTmWgy2wV5s$cNk+CLm5$8$og!}b4@etvTCcU%{uOSN?RdBQ_n>N z?o>~&gk??7Y8d>!j=;?GnR|67b;CQ4cX(aCxNy0kwgIPNr>ff}+ZB8S6kg;Gbu>xJlU>gnUw zQ>Jr!o$qsWwKsV*tu_H2TLs>Ox(lCtq$QPXv3GP@YE)~|b1Y~KY96)+*@1T!mudEY zuG_<6clo9r{VYP)h*>}^MH*!q1@%zZCX~k>MEFA3UO3cy=;EoDxYybl_qosU(IE^a zuz*TzuR$N3z}q$29f?O0K(ce*4t8I%%Ga4fQ6x!}4SbbCeJ_K_J`oKl?I}q>td)oK1;yPW&-(tQ^d=Yw3|9JRO#lyXb{jj7b zBjnP-VIjJd?y)8Vi}uBQb2%-Na8hUX5Yl)8DD$Rrtw}9slRYQD$Jly&&ffQ*-N7cC zCSr{AE@s=;yv+@7?c2j?BEFKT@~r}{g3poNvJn5teveB_^tNg z8Xc+PV49n0%;~G$+<=YG$8pDV8ySsNvkX8*YWEFDm?tCPdj=1?c=8jIL#kyM)*gzw)?i_r>Go^9bHzB*YcNlVTeB2VFAX8XKTTV z3fvGuTL(zDw8~3B#xEYTlQZAl^~c86EBf;;{|^5Fe`Ha=D!u|D zjV|5AzG>oPK_;GSQS7ptlKiT4Mjjeo##Y3rN`0&T7uK`@R&PqcKrtT+X0( z0zAnbUmFk_;3>;0jjjMx1eaTCTDQ7PO%HE<-YVA!{IshBs`eSVz$ZH+E4O)!)NJl| zQk}|IgnWd6Y`^|g(J<3dYm0Ay8g3zM`)7u1P*eJ?U2c+75iA1V1!i2o@9}Hx=;^i2 zn4(5WYbEp%jTTqwFi&w5_Za^qaH{vGMjh35;vV=w_JV~6?*oM;-@0SvsRqPgk}8{u zp-g-;f6{ppXKRK`g=`uxl7Dt|eco-ICvo0|TZ>bTl8v|e_|1yt!N`rtmtK#Q*Kobw zng|hK@|Xjv`{%Psg$TYW4~EULDa2xdVyaOZp9}Yb4T7)TF=+TQwW58&Icqn2?Ynrp z=Z#O{cnNej29*~GYfB`Xk*K3+ed@GwHvXfepu)afuYuCBbw^?}T!*K3-lqL~`^lBd z?#`s^qut}VG|x!8isQDS>%;qW_{BGNXC?;C8kf;QS@*Csy(6$S%IV&5P zN!t@dLXW#o1l=-FZF}N?3gx}>NMWjBFPZb>QqZQw=voL1o&jmQgm>#?#^gN9qDldl2|mM-@XV9K$R66os-~-@C2G#r7ogU42_D zQ20K0f2fjA)UTY3PPPL-yD7sbaVb(r>B9o1&-U%TY4u3Fx?d=7wC|B%rYUVMFOR{1 zEPuc1@p~ceePgj1OYJa{?$htef|B4LVwWI ze_gR+0x|BRe?3KiK4;$fyEpb=Cf46&tZsB0hM20jv^4svYT{sK26VIpIf(|4)S@dM z+DU0SVqg%x{{6v}R(Z9LfpKRR@JZ82Q(lhW1Z2x>WC}7iV|KH(`+Xk_K{tMM(bmk# zh}zB82I$D|CiL=;9{lL??`oEp)PHnwvKD%&DX&B=4stM~=4NJPW_>CAh?<&O(81K4 zU*&_uU*zatLN6_yob32nSX^CQnO!-UK@JuyY%^)IsjZu&n-wf;@Y$Ibbll>gE4AC$kxz^`KF2(oehJ%?&Q zfRix0Aj|(%{ohoY|0WY=;IcZ>mO+Tv;Mzn6deHQX*Bx%c*1}4_|Lk( z`U|rBUjP4)hd+hwk6N^}2|p5K`IqDhKjMuVpu@lr#gP6W_Q?%%d+uT8Cj?c;fx%_| z$I#Q(Pj5zxv)??=QZg;r{*)kfx*hVqIxjvq%3F4%T|OjAs^rw0eiM2%yw@`1?YatY z;n{0Vu7{&o=dNcxg;7H-$(PM*se(cBPoLg-K<$h1&tLWoQ9KGT`Kl+P7?^hn{_(5w z&fUQVf`4xRP54XO*H=q7!{<@t|KRt4TC^j~`hW5gr7pksAnE(bM#297e)e-3Q4GJ* zC%kt1ndJOVQh1btuY&NY>Xw2AqdxzSM@W7r+QI19I;E3}^NO68`aT}{bBdV%F*It= z2L!Az$~C{7zVCOmtr=++eYX{*D!KX{?3Le={`1uGsi5a0LeQ4nNrB*pIS~pXI@NZ4 z>G*BYA?%;W)m?^xS<@hN@?0?C&h+xHHt)+X!u`fYa>#!s`5=kr<3!$n4^wer{`Pc4 zGP$7h!9PFNlmWW?sfZ-G5#AGCr}d~S3#)hvUN_bta<-ta|6|%1&{@6(bOANZv-Fvr)va`zE@%6Tvl_fI|GYM$EHlhFyOJQotY+f>BwoSyF;h&#f@IxsVD9vBZH>S9zHW0;6W_=9|_13ABFVR8m|~sdwrKFQiq0&3Dg10 z>3=pkLm1D_iO&I@!_}^P?bRM|Qk_*RwGx?;?2pUKJ@nE`u?lh3X0TjKHsj?N8_sj% zZ_A%;eF>^{nYtM)(>vX3za6Mvftf83zjWpt7pjlDj0lJ%2;}x<6WUQKk`ql2e# z$*Zd0$ES?*ixi>}aNT9+dI=lALZK1`-4Ao$;;T#iDI|XTcP_kFQx(>x37Ol@UN)Z8 z>2y_)2fS_t?a|^8GsiKg756pN_w*RzkMygwTT}2mz0phFK-FBGbL?0cHbt<}l5KE3 zXWl^7efxqFq?7t?^%gY`9xYnpt79bAwQDf0%;>Y)3kBO;+%vr6*EJ+0D4je6i z<+|qS;`tb|@Wj6k(pMno{!0cvyoq|-9_NomS*DZrGEpzZAw2Lwq=;mBKAe@3*G^Qo zauC;1{D;jRvh{EjRzX*ymcOQJ9i>*H{=GyjU!J2}zjcCpyilII>G&kBC>zdu+H%T3 zB4VT>TK1t{LH+TwK|Wa6qh!+Ki$hq&oe<4_&eosVS{#l?`PH_8%GR+P_!_q9_B+$g z?5;>bEl>~F#?`?ReIFje%cM8L;{I|h$ukJIZ32CUN6*YShoVTdEl_To2M(Z15?<%L zwg7VGaRHPx0?bnCQnbzA$sZL%t;Xf7xl8{(1FA_#(w{tgWAum$Vqn zZtJ0JV5^;BjAcqI5q?lNelx_$n}yl19fL>C`{GFaAzlK7-i2l3k1-jc<&}}2$Btwb8<8S6QO$ds7NKA`mxe5lDu=N4 z+auCtMW60iI)#{#oZg@J0y4;6;gWu+2V6)|0uiD)ZNeApCP#Db)sLxes?kEtaNfB( z%D2&2=Vp+}`f7F}Y(WJCD=i(?4BReYwpz+}nEx;pFrd)ME<{;oL7?Tdf5B3~V^S zo`O1BR8tHkv8no;(Ht}IvY@a+|j0C)rxx?KFOB7x0B5;h`9MNtU^rb+EgKT-oC6SF>j8_5 zS-X1|c?GMny7C1~ze_n}W?W@0hikKHYF#)14tMaLG((>ULD}0t5#076}^9L>o84{K69?icfK4eUPeTG6d_RVr|Yrtng-@^#8lr0=b5t! za4k_Ui`hWj((5(p5;)AcW%OUNBP%cl{@%n7x)Vj^?wH?&jk-K(Tu8!6Xv~6S)Xi3# z@8FBvjLlSWcfA0>*+Dt9v>rQW)-CMV5bK+Uxp#tQcI zklDn6!fWA|k{%~5>ggoMskZ6L0rn|ib51xfY>dEZ<0-=FM|1uZg}mdc7fYG|y^HW} z!ex*)$Id+X8I~;gCe1EiN|1z7nYl2vXSDW-IAZ~SN-kPLWv*^^>G>97u|EWq()wzf(mYS#zliB)Qi>2n%vb+CR}N6lY`ux&J*A|xxq!R^>30)a*gvs zfcD+u*E413Hk$37K8lDG(Dy;tSgBZ_jHyba)}7G8PenO!*YFS{YYu?O$4k^sBKYQ} z7p4!}%|9aU+w;ENSMTcx<_FGJDs$V+8eVFaD`oj_u+&$h^cbO~C$pv6P;G9=9@F$B zuwhTxgBdSn6+Y)a<^`22*ZmUu9Ish%&gHy%<+Rx$JcTR!Fiol=5q)wpOzO=bi0soh z@Okrmlbf-*fwQl^z+K^w2;T*3e@q!w!FI(>N*Gsd8A=7@PJV_>CT~?qI@o@^^e&oj zKDn-iA8YAln@W4O#xtm~Gny5m+RIDZF^{Jad&?ehN)zOKG#ylS%Oc<=pWoC6>#P;Z z*bea_#-|WA4M1G@41}R7+UC_uHQ#c{IUX#Qch#ovDXpj$d<-R;y3b-gUL0R)U?s$} zHBlCOvRb1&e~CT@7sZ04Sl&Lbu(;4J_DSJcmh*?r>%B%#+*Jj1-v9K!v!v&hwq*NSC!5 z_Ia9R&JTP0-WJqG;mgkx@Pl0#Fl)G0C6643aiKGHB1=w1S~YBu;b z!>@iWy0;@#IR;(_@p6}^I?_@PY#3_JlqLI%3hJV3hJ_J2BD29rjHLbD4)EV%9Hhu-kk>QI!dpFwgq_0hW!#UAp?9qu3&9g zlT~!RHY##^MH`E}6eUU$zVeSvkPV2q=ob3Z?Dk82vp;D7P9|&N`bK%-e-Ed{?B)h0 z>~@Rgesw^z47Kk|LrNH4uGI@->&NYc=SL)`?73a=sMT!;1@FcT}1=Kln=0Z#|3nr=8B}z zfV53tNmm3}r2DAX;?sdg_r6Td+qLR?pQzDL2_9BJM$e?_)0Uaig{(2M&_+ZKI{`9JkxP2gh+@6(p0xGd!vVQjHEPcMhGNrSYemL`qvf zi69jkh2sgx?Li60Q7iiz%V`IDuLLB#MNT(ndhhC$ywV}Mx?zbDX&1ac<~ZD{_&Q%O zhYg4D@J!ClIiLs9q}3lta&BV@mbib(_oGmI&~n=iR`-1UOf9Hjpinu-8#Xxqvngjm~@Dq(^dvU7T(^NL{kXPJTxje}U6`D`XVI5Oj6BUWk z`BVbLj5xX}yj+0M%fs*K1HtTM*UL2f zVM4*a{E_Sh4#W@SclxYA?7h5BJ&>!ywk=OO$75+{l2+^^?jS%_jCrF9kK#h5&oH?> zXT_uU_!;loWh3xe;GZY=@*6*#p}nW<9lsv2fstMU#XNe}6QvvGFr%XkSp%PgFM_vM zQNEB`U^3o`SekI_W#Hq4jYC41hE#j=W&h-==Lf5e^R;vy{B6NxM$P=0Rx|%MSoej^ zseV0-kDSnNY@5WT2{3DFLk3Sw%TH&hi-s(^D%|?4Cei)iGR(WK#-_&&1o%39`|KJ( zJhhPlcfBpwV}9((EW(a2r{}czz1kd33cAF)kMFjWB@r`Z_e%x$v999@wBO&X=~KG~ z9tHa+V9O$%&O7E$KGB0m^=~pK=Vilzt+%`J0(&j*s5Mu2Skiisr*Ff&4iD&0kEBF&uB55RfA}_GpJJ)-M}D-9 zKWV;9N8bSM8GOD?Ve9V?3ip)7e%v53wN}~2W5A97uH|&o8Z+qI-M<~+sMx#N>RpVy z3u2Xw-idxC%BxmMzm1DFU6bZxjHd7T8{0FYUA4n% z+Y!dAXT`ugJ2-38%1stFe4;`JWU=9sO&E5x*A+gT$_ERVLFCJCyM-pe%@P`EasYLJ z@~S$(04=oE?I6P?o*`c=<5`3Vk~=MqUXc=1^LAeD8Ao;y*0(k&pWu#oTa8Ye!r_R46S)1(Y?U}s z3K@I*P16r%h@rpvYniyH?W<%%{5bHR(YFUQtd>6v0VY=dm$!9WR@2LACGa0j&Lx#x zLAqsC<-{(8Wi+|?R*mMNCwm)MD3OJ^zKLnmwKUiCYyE6E`_TD~XR=dnqa|)iypQ8& zPTL92ZsicmA5sCvq%F<+**JO5CF(A%Q;w^Bm-!RP$QL);h~eDyBKT8X4y&N^nKGVT zJzUh zV7CHlW|6y;k(WO_1(CbeV+PnZCNSy9&>$Yy`UH)T0=3%;(!FFDAm$SaLOY-8iz)C( z_{W$hAC}pUUdQaNHcLx84&s<)1878H`99hlCAZ$!JKV!Us8nxX>(N|k_VYee9wrJu z>X}@7YYM{!CF#|S;H%|A=uR?4iRIy&or%YfE338lF`==<{ zjgm$(qpb*dxJQ?w@c9RQ@Q$-U8Pv_=xM576g>)?0>`uD&r4j;na{diE#G^0jn31f8 z?TPW^CN|yr9qFE|5Qx-w{3x$TEC72n&zhL;=4{EP#^1o}rqQ-d4@=DTxK&)=^Lo4z z4zaG(MQjw*$DUG5&ybdoc3cmRxs0X+U!-p#3Mz^=xY#o{MlF4~*KasC)?|BYhU->F zd^3GS6eFwPjQ-$2$h5l$#p(7q!s|56avuLei~SJZJ^dv|j=IFQxo)(<-UOoK5&3;Q z1(A}ymg-q<>wgR>Qf*Cf)P6Ufhs;kXo2Z;c%?qCorb?oa6DH%s-_d4nvl_|EzoXxL z64VTs96Rj;bI)C0Pi`!@1GPK?iBspbnvyhJ@m|5lC6`~;hjy0Z{%w45XVjlix zm9(|AcE=^LSWOlg7N|o6EQAu&c*=Lz3U82BWulZ1B230=qZTopL?7Esr(=tb zPjEk2C9;eSyfB%0eSOd{+Cml77Ih}B0J2I03G;m0QN7ec+lP5GvsdfoK+t2r4ySzd zL%q$N%O{-_w3D}`Tq{=-+WTeQF3SO>40Eb^2 z*$M#N!O{6&v+2JfxI9 z^3^2ncZED~*?5MPvV5MOTazUgkf&qGt)8f8P&jliPEeVNXoaYovo%B2MmYD=L|;vAxzW5AB@3 z=?Z-9<2vG#bPNT}gP8>&^|zN1_0$x!q`d1hYj_}-UnK7HR<_-k1T|1Y^WmH)2%OAk z9q4*R`oIwri)m)=de;Rqz4{Wu33w^wdHS@>`(#|h049`QB=-JvHo@QT)XMIqo4*W|n3LnHJRwR7S zP_NK8@vp%5sNoD=YBwL|U2^hF=VZ<7{BCg{m%Z$n;9gk1U#dG7K#%#_tnWhP@=3$tR5%d)g~Gvl8`Vn8+gQG|-smtRm{pHT_$SmlaotlT z)@NwzAo}m*hhHikK|CFa2s>ttX2IZ3Ne>tS%rIFXfAD9}jD~g)vr_hq8b?}Y>?mEP z7Z@YdX5BP-jtHPqG8UOR zGHp@yr(m> z-52S0+h`B#!VM*2GEOJ2upqXs37e^VeOmHsVv^LZ{OaOvG4`5niU3dlUJseay}Phk zehU6n8fQ6-PqEsY_{M+PhJ4Zsi3VUI?{h5t-LvcKR~uNYi=(gexm63%c2osaPIq+S zi-_jxs=@4zG2hm9&2}y}zt8?PuFF=#B2)b=IRT^N5mZDOOeGX5U*?j?Ib0#1iFRl4 z-M0^VTMT1$B&{a_R8pj4h}>)6FIFK#WmNF22~F3|mZM=27PN73nzPcp+ho0yn_z*~ z3czR$YXytvZnpZSrXRpLmSrDrWwDAuEo|?HME34f^u%JnWNthj^@X>euabeZ146ROwOX-jsW{}B zgV(u3)Jb%Gw79~0E)Nm3_+9`KeYeGXESjqWx%hQL0_$2lfQt|M>*4n!Ei2dCXrV;c zun-D0xxmNrk2JASAflb&z#uB0H(Hg0V8N;v?LBuf(4n$=e6Z*_>f^|{cjomwYvhY8!w`Jr+_<;$NagPE8Q+n>=w7;ye)Mom>AH8Y#$@jNGA} zV&*t~(r;g>aD4Chp+@2{oj7N#h{#Saqk6vq zFV)C-+nYOW$`3lokZoCgp>ysZLMr7b&$cO_7Z(8oW7$6xc~8M5{zno>mzouf6x4Zy zt8a;$ZCB(Tnmet3<9xhVWGy3iWaszzmLJ%XwwJ$q2SKmWkPC-=k)8gKDGR>1z`Q&f z?#|D1PM(-}H2FDu;t^ow5?N_E0%C4X!UG?AivUvIt1jTmJ5Dk)X;qJfrv$lfx-F4< zd8~H^Eu$mt_%n%+dIwU8E2v7k+m$rgf;Q-)LEd_l4g=J>Qse}{~6>UXT-YukE184y1oW$Jhr2pZdGZg8S|g99@0!2(f!Npox7(3B4hG@ zYN`4a?Ze<$`d7(ssphUhH6$yYhYa%6;iSs8%=ROD;NY!hIcJ7s?rUeNDK51#?eEqR zAn3*{MB5x7pgLXGj5A=V1fyD((NXB1JiXsIJLybZyaOuSH>XJd+Odn9Wp`cM#958{)o|O#fWcq;nnRh6v}_1((&>1Fo5&5-pCZ zuVQIGe4}O2Tf;F&1@$v`A%+LRlP&+W={YRFW=_~?opN-<9@hmI?fXc^zP>%UG8%B5 zh~DGWUMfLH15AI^1Z;Ctc^_A;%VH^(&D7Sx+3P@HiE8Z+{Vq}89t(ldX&hF#GX2Ei zC`Mu%=ZkE4<2nvMxgAG&YdW)60NiGzw@1)ppHhSNPT0j}$+z4569GVFt!`uY7Dvs% z0)^>giR{BR&g=8WT=jx_fi8JEGJe@IOp;xczlb4(5sM?o=lY@!J8w15`=J%<+vyD4 zcm}g=9;w2m2vhxW)=r5z^#HADT`NwN^G`mJ>%z6|T}p33NUtn&4*b6pqkYPENG{_S z-xY5K42*N4&+G_2QWh}|s*iyZ;`d+?BIRO%hhZl@7oSUIOEfCr@t8F&s}DytPfYT! z(Z+P$>{F|nujNxh7AetL?Ib;xsL0g_>(gjdlY%vZ2Zig_!SskPRlqg9@!`{(UWdN$ zh(RYzQCyjgceHO6k%A;sg3EQ<;-!u5+Bzclp;{8xRpp9^3rNlwJ6j6Utm6?-{jx>!7pT)nw(q~gIV$y2D*=3lSZu(Yop zJSw%1#ltKWt#aB4*Dom8~4(NP`j1yDeKo&jT_RPm@^; zgxy!C3Rd7xKm(s2?s>pPt*FgLyQNR_)(RpYx;Sr4zt-i>zK)r5@ias@RJ5KpO`7IU zWZPqwFUTExrpiXY*l0rMx?CVX9VBxe>j!GFK8;7^?HTbfz0XZR$9{3oef7SCyuDw- zaGp{>nOT56h*kzs_X@u4s(>w`{#N9eca2%1ssx6_dL@4_K6n)#%g3gf|50%2m}8Ou z^Mv_Yt&0(747MKE{*Rx_;ECj&oM^MAqaRdnf(8-8RZ3)U*v403NQgyOQ$yQk_b6Er-Z<_w$^q#GxaPsv5Ww|bjymF0m1WaO7f)J zCB@b09qLfx+YPSXbx@r$$~$huvGesUoP=jsWGtnHxp8Q8j`I&qj(NAb+4uCd^A6W! zzYZu8$MVp+(?6qC9OUI-0up4Ry`k%#NFu^lks;%Gbpp~Jm`qIuleJc5sKCLvc7P}; zRVIU@?bouexOVD+dMYB&o0hqT_T)~L*RqXMY!nHM@wFK`pk}oQ*~QM5>&s`khMjkQ z;j=acY>gGgu3K-6=GC!Oo*#kZ0ICIz-w!8rKr_=e0bbs!?l`nBMy5@8kWM+e;I5PW zBKW<-rK|J906?ANP0P89wU7wD$W<^oIz?J*(4-{Yeu+IPwK-ECNz95c*`8>Hd@h#O zoZuYZ8QQBfVv%mRUxt^QKv)Zq(4(9k*5Vu>DAon0`w&N%h4fsqH%N~r2I1p)_U(Eg z2)~X08by6VhYh~1R`pE2%JS4?MQ>;$*P%QA0w(zB(h!XMCWjDk%Fa7Ss|J{9Ywy%- zrn%Bj`CDg-Le?2|xcB=PYl|%n7vRb!+vcR7VVl0mHv|!CRzLUMDoC=a2IYNSb`(OV z%Y9CqQ_3#Cx@e!xyd-OJIM?^lG7Q=Pw&BfFigYSi9lg;$LOUFp;MTYv**HccEc2!z z?W2H3mon|QW$CYB$GwfVH{atN(*X&}Smqx3T!00ry%H7ZhV0=5r>%@MRxxutL$Z_j z5ZomAkJP=W3$Z5DD*pVES8>`^%HxGq+rz+yT>0WW`uM|p{x_^}NI_geGN~SqDX`0X%LwxN9ukjq3&55x6FajW2C%-(TQ=CeP?QH;TQVMjB`Yjbyg}%B z3#=Z$Sv__>UrC&yYl6D>bOvD02tw<$&@m>u66K@>icME~+$glFwnQs3Hm(^7nCD(A zsD?zqMfWE4M>R=u@v$UIowF$g)}u`cMOB|h7vFb$}DQd$q znRFUzEl}xE(1lt)SK=RzTN0ix!clqcU?yBc^J9;rc8mA%C=~6Uw-ESsq?bm-cMI`C z?|BjxFb@K3 zK{^5F8Mn8xahjXd%uZAHS`TW&&NBQ@dG3*<_O2Oty;T%G2@^c%MpWCII87hnBPC@J zWszgmV?OU|Cw{WDUs-8@#`f5b^7!2hx}z(+$KG8EFwGixpW@^CD>*?q6bll zuB%}mP)aj9^X&%QNpw5MD=FO*C!9(()|1R+_)=3b`e!|9V#tDMekc$1xav1lJRPey8V1>&{}8Xrt8m{E30@h&~!4!>YPr@^!J zdD;J!2G4#)Q1b>utfp^BNdZ5hQN^l^J<*39zUd|OdPCwfgla>nUT^=11-bdBFl38` zYv#TD7!vHFzdO2&FM`*=wzyhgR>;H6NtMV6o>SzEMW$+2zdGm#>?krqJIrJ0q~#gm zX#$hh*{n>xxtM&3wT)d$Jgl|nT@`I9#+T7Q&43bU+|Yz=44{ih68Lx>uQ$ST=AHEx zcbPb4KyzmSj$}BgEwKExbJQ;P*)n2yTXmej3^~;b5_g?_{3lfL0g>eb4#V|{hDrx* z>vdw|)$op2Ia$2V^=>rNYG?ccr^oS&WMS{7v@$OJl>8qS(`YD7i}UrlWt3TJ(bRIx ztskYgRR6uO{b{tLL`t%=q^oUf?lJhJNyFhli6A}Lj+54`&~>{CNX!lx^+Yz1jbyGI4ddF! z0w^WWnaU&Ysq>cH`LFf!NF7dAqSORNt(O+5NTl}Zrn~pPF-ZHe%5ynd@-?q!3Fd^r z1#+>>RYfTjZ{)^i-{raatEVP&pCT{EHT0oo4tnID``PDeXoV7-_Iz>wMu__9(nX0R ztYo?O#5p_4R?5k0&5yAPputh>E0FFsoZ5tBZj-{J(HKt6)rzeA8Ed3g+99MM!6bPa zDx}tS32gGVa=onEVfVRe9<5p9qH#U{_G9h+5NeqGVD-(27w1Me8C+xU^@wCx9_Z*%_XSZ-Z&gnLxJ!{1E-8NVN&7VGw)@yWRdKBxG z$Zbo9z+>;|1&P0~K3Qn>jsv)UO%d3;9+xGID~(q0Hj24IL>?FZl% zr@F5u=Wl%DuM32a0T3-1Vc1@jZu^+^MLc8qSbC${eBPJGT;3fnQC8z6(m4|0v)P>Z z&^owmzGC_AT+@v0eSDvxI$$RL-g=c~yVYI`hZs?^c|Dg}k;+E}k!vS`@>|P=#S>b; z>O=!dAl0GO2)q3ZF4+XE!5-8fb_=iEvSzJLJ>OkbR4e&(LI=gK^3JD&Oo)!=^FmLP z@-pzTH7FL z;^DEjeUU^s>EU{Z!#P_pZS7^Q4-QYzdi!Q0Q?4|2JU2-CXVVp|&P=s!=HZ;UCed;w~UxvX` zx!zxB<9K=g%WBa68SS+fz%hgcmyEg9Lm!f#Vx(Oo7l1|>F`@9#e{#kmuS$swED9Us z9rcPSpBSCAk(Yr$ZKh^%t_C@03b+jO-RoH|M@cmeQm9}l$ANKe{;}92{I!ZY9o)Q; zVRGnP^#JyuE+rE@ywJ{japNj|QqKg4(fY=E&{Iml{uar3n^Upiw8N!g25Xz~kBkNU zX&1hKD2Tc;e5+ksSTBX8%gn`F?uT}%m2T68E&Uaz$*Zw#kFlF3p5nyoQwlSF-=mqn zKl+`{E=P<8H|jjVl(`)^*F5DsTT4diJf_T!4lRGga`AXf>g9m>ral4>8aQKJ(Ykp8 zT{g&vlUXT=S^EJ-wQVm(g)@(Ta>UfC`A+JC<_bC9*$eqivH{Hi&@@=fZZscneBvGtoqX!6i>9R^f#nqI^12e&ZMQm0$0Y3;KNL3XIP0;c@YE@oq9?OH>s|GAdkO*H9dXn$8 z2`HmEb1t~fdtYOjTjW0WxPDA>*fMM1o5rnZdxMz2TR0@Rma1@gjdE=D)$15iO%7h3 z4!O7-y_*27Ku+b?s`<}Q%C2Ke5LEj%z9QVTD0_GyL9IN3#(QBG+0l-Q8-@- z&T(;BcvwbCoC9XoVnAb5j|a5-of^_~QZ%-_x8yP9j{q2@2Dg_hd5H$`S~Y7)zS^Ua zcW`3>JJdV#3X^Eu-^pI$8SO~Gz^`aDZ0P&(1MQ?$?2}%oEa|a_%l^?56d+%WTW_9< z5phL&KE3biZsFA6xTfIAA#*g6V!~xT!D>=e@JyZn_PPvrHKn~DLcfvIC$c+%OX;%d zRA-rC`5lh?Wg#~$)Rl{ex4iju(^Mn@N|Yk|maC>n(V+QxOZrx@5C|y+6G^VQFO&J>U1X5UvSt@O`m!l~blaLhVVZqB< zT$}UB@jc=CS%;cgYpa+qvf4A2%Y{6)JLo{8rF@xkj-;X!4*eaUc1hQrlS2oyN~WI)a$o@2S9pOYiz;VTPMy&tG^U)K!pHJE^ttF=@GK0&ijt_dJ;mOU;$NZzzr8>j#$*hDEG{�b7X;9Qeq&}J;TV~ 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� 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 Ee{B@pxc~qF diff --git a/docs/test_plan/README.md b/docs/test_plan/README.md deleted file mode 100644 index d250997..0000000 --- a/docs/test_plan/README.md +++ /dev/null @@ -1,16 +0,0 @@ -[**[Return To Main]**] - -# Testplans -List of Common API Services implemented: -* [Api Invoker Management](./api_invoker_management/README.md) -* [Api Provider Management](./api_provider_management/README.md) -* [Api Publish Service](./api_publish_service/README.md) -* [Api Discover Service](./api_discover_service/README.md) -* [Api Events Service](./api_events_service/README.md) -* [Api Security Service](./api_security_service/README.md) -* [Api Logging Service](./api_logging_service/README.md) -* [Api Auditing Service](./api_auditing_service/README.md) -* [Api Access Control Policy](./api_access_control_policy/README.md) - - - [Return To Main]: ../../README.md#test-plan-documentation \ No newline at end of file diff --git a/docs/test_plan/api_access_control_policy/README.md b/docs/test_plan/api_access_control_policy/README.md deleted file mode 100644 index 05a9e63..0000000 --- a/docs/test_plan/api_access_control_policy/README.md +++ /dev/null @@ -1,813 +0,0 @@ -[**[Return To All Test Plans]**] - -- [Test Plan for CAPIF Api Access Control Policy](#test-plan-for-capif-api-access-control-policy) -- [Tests](#tests) - - [Test Case 1: Retrieve ACL](#test-case-1-retrieve-acl) - - [Test Case 2: Retrieve ACL with 2 Service APIs published](#test-case-2-retrieve-acl-with-2-service-apis-published) - - [Test Case 3: Retrieve ACL with security context created by two different Invokers](#test-case-3-retrieve-acl-with-security-context-created-by-two-different-invokers) - - [Test Case 4: Retrieve ACL filtered by api-invoker-id](#test-case-4-retrieve-acl-filtered-by-api-invoker-id) - - [Test Case 5: Retrieve ACL filtered by supported-features](#test-case-5-retrieve-acl-filtered-by-supported-features) - - [Test Case 6: Retrieve ACL with aef-id not valid](#test-case-6-retrieve-acl-with-aef-id-not-valid) - - [Test Case 7: Retrieve ACL with service-id not valid](#test-case-7-retrieve-acl-with-service-id-not-valid) - - [Test Case 8: Retrieve ACL with service-api-id and aef-id not valid](#test-case-8-retrieve-acl-with-service-api-id-and-aef-id-not-valid) - - [Test Case 9: Retrieve ACL without SecurityContext created previously by Invoker](#test-case-9-retrieve-acl-without-securitycontext-created-previously-by-invoker) - - [Test Case 10: Retrieve ACL filtered by api-invoker-id not present](#test-case-10-retrieve-acl-filtered-by-api-invoker-id-not-present) - - [Test Case 11: Retrieve ACL with APF Certificate](#test-case-11-retrieve-acl-with-apf-certificate) - - [Test Case 12: Retrieve ACL with AMF Certificate](#test-case-12-retrieve-acl-with-amf-certificate) - - [Test Case 13: Retrieve ACL with Invoker Certificate](#test-case-13-retrieve-acl-with-invoker-certificate) - - [Test Case 14: No ACL for invoker after be removed](#test-case-14-no-acl-for-invoker-after-be-removed) - - - - -# Test Plan for CAPIF Api Access Control Policy -At this documentation you will have all information and related files and examples of test plan for this API. - -# Tests - -## Test Case 1: Retrieve ACL -* **Test ID**: ***capif_api_acl-1*** -* **Description**: - - This test case will check that an API Provider can retrieve ACL from CAPIF -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * API Invoker had a Security Context for Service API published. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${aef_id}* - * Use *serviceApiId* and *aefId* - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information. - -* **Expected Result**: - - 1. ACL Response: - 1. **200 OK** Response. - 2. body returned must accomplish **AccessControlPolicyList** data structure. - 3. apiInvokerPolicies must: - 1. contain only one object. - 2. apiInvokerId must match apiInvokerId registered previously. - - -## Test Case 2: Retrieve ACL with 2 Service APIs published -* **Test ID**: ***capif_api_acl-2*** -* **Description**: - - This test case will check that an API Provider can retrieve ACL from CAPIF for 2 different serviceApis published. -* **Pre-Conditions**: - - * API Provider had two Service API Published on CAPIF - * API Invoker had a Security Context for both Service APIs published. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_2 - * Store *serviceApiId* - * Use APF Certificate - - 4. Perform [Invoker Onboarding] store apiInvokerId - 5. Discover published APIs - 6. Create Security Context for this Invoker for both published APIs - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 7. Provider Retrieve ACL for serviceApiId1 - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId1}?aef-id=${aef_id}* - * Use *serviceApiId* and *aefId* - * Use AEF Provider Certificate - - 8. Provider Retrieve ACL for serviceApiId2 - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId2}?aef-id=${aef_id}* - * Use *serviceApiId* and *aefId* - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 and service_2 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information for service_1. - 7. Provider Get ACL information for service_2. - -* **Expected Result**: - - 1. ACL Response: - 1. **200 OK** Response. - 2. body returned must accomplish **AccessControlPolicyList** data structure. - 3. apiInvokerPolicies must: - 1. contain one object. - 2. apiInvokerId must match apiInvokerId registered previously. - -## Test Case 3: Retrieve ACL with security context created by two different Invokers -* **Test ID**: ***capif_api_acl-3*** -* **Description**: - - This test case will check that an API Provider can retrieve ACL from CAPIF containing 2 objects. -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * Two API Invokers had a Security Context for same Service API published by provider. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker for both published APIs - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Repeat previous 3 steps in order to have a new Invoker. - - 7. Provider Retrieve ACL for serviceApiId - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId1}?aef-id=${aef_id}* - * Use *serviceApiId* and *aefId* - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 and service_2 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information. - -* **Expected Result**: - - 1. ACL Response: - 1. **200 OK** Response. - 2. body returned must accomplish **AccessControlPolicyList** data structure. - 3. apiInvokerPolicies must: - 1. Contain two objects. - 2. One object must match with apiInvokerId1 and the other one with apiInvokerId2 an registered previously. - -## Test Case 4: Retrieve ACL filtered by api-invoker-id -* **Test ID**: ***capif_api_acl-4*** -* **Description**: - - This test case will check that an API Provider can retrieve ACL filtering by apiInvokerId from CAPIF containing 1 objects. -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * Two API Invokers had a Security Context for same Service API published by provider. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 4. Perform [Invoker Onboarding] store apiInvokerId - 6. Discover published APIs - 7. Create Security Context for this Invoker for both published APIs - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 8. Repeat previous 3 steps in order to have a new Invoker. - - 9. Provider Retrieve ACL for serviceApiId - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId1}?aef-id=${aef_id}&api-invoker-id={apiInvokerId1}* - * Use *serviceApiId*, *aefId* and apiInvokerId1 - * Use AEF Provider Certificate - - 10. Provider Retrieve ACL for serviceApiId - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId1}?aef-id=${aef_id}&api-invoker-id={apiInvokerId2}* - * Use *serviceApiId*, *aefId* and apiInvokerId2 - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 and service_2 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information with query parameter indicating first api-invoker-id. - 7. Provider Get ACL information with query parameter indicating second api-invoker-id. - -* **Expected Result**: - - 1. ACL Response: - 1. **200 OK** Response. - 2. body returned must accomplish **AccessControlPolicyList** data structure. - 3. apiInvokerPolicies must: - 1. Contain one objects. - 2. Object must match with apiInvokerId1. - - 2. ACL Response: - 1. **200 OK** Response. - 2. body returned must accomplish **AccessControlPolicyList** data structure. - 3. apiInvokerPolicies must: - 1. Contain one objects. - 2. Object must match with apiInvokerId2. - -## Test Case 5: Retrieve ACL filtered by supported-features -* **Test ID**: ***capif_api_acl-5*** -* **Description**: - **CURRENTLY NOT SUPPORTED FEATURE** - - This test case will check that an API Provider can retrieve ACL filtering by supportedFeatures from CAPIF containing 1 objects. - -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * Two API Invokers had a Security Context for same Service API published by provider. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker for both published APIs - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Repeat previous 3 steps in order to have a new Invoker. - - 7. Provider Retrieve ACL for serviceApiId - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId1}?aef-id=${aef_id}&supported-features={apiInvokerId1}* - * Use *serviceApiId*, *aefId* and apiInvokerId1 - * Use AEF Provider Certificate - - 8. Provider Retrieve ACL for serviceApiId - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId1}?aef-id=${aef_id}&supported-features={apiInvokerId2}* - * Use *serviceApiId*, *aefId* and apiInvokerId2 - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 and service_2 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information with query parameter indicating first supported-features. - 7. Provider Get ACL information with query parameter indicating second supported-features. - -* **Expected Result**: - - 1. ACL Response: - 1. **200 OK** Response. - 2. body returned must accomplish **AccessControlPolicyList** data structure. - 3. apiInvokerPolicies must: - 1. Contain one objects. - 2. Object must match with supportedFeatures1. - - 2. ACL Response: - 1. **200 OK** Response. - 2. body returned must accomplish **AccessControlPolicyList** data structure. - 3. apiInvokerPolicies must: - 1. Contain one objects. - 2. Object must match with supportedFeatures1. - - -## Test Case 6: Retrieve ACL with aef-id not valid -* **Test ID**: ***capif_api_acl-6*** -* **Description**: - - This test case will check that an API Provider can't retrieve ACL from CAPIF if aef-id is not valid -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * API Invoker had a Security Context for Service API published. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${AEF_ID_NOT_VALID}* - * Use *serviceApiId* and *AEF_ID_NOT_VALID* - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information. - -* **Expected Result**: - - 1. ACL Response: - 1. **404 Not Found** Response. - 2. body returned must accomplish **Problem Details** data structure. - 3. apiInvokerPolicies must: - * status **404** - * title with message "Not Found" - * detail with message "No ACLs found for the requested service: {service_api_id}, aef_id: {aef_id}, invoker: {api_invoker_id} and supportedFeatures: {supported_features}". - * cause with message "Wrong id". - - -## Test Case 7: Retrieve ACL with service-id not valid -* **Test ID**: ***capif_api_acl-7*** -* **Description**: - - This test case will check that an API Provider can't retrieve ACL from CAPIF if service-api-id is not valid -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * API Invoker had a Security Context for Service API published. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${NOT_VALID_SERVICE_API_ID}?aef-id=${aef_id}* - * Use *NOT_VALID_SERVICE_API_ID* and *aef_id* - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information. - -* **Expected Result**: - - 1. ACL Response: - 1. **404 Not Found** Response. - 2. body returned must accomplish **Problem Details** data structure. - 3. apiInvokerPolicies must: - * status **404** - * title with message "Not Found" - * detail with message "No ACLs found for the requested service: {service_api_id}, aef_id: {aef_id}, invoker: {api_invoker_id} and supportedFeatures: {supported_features}". - * cause with message "Wrong id". - -## Test Case 8: Retrieve ACL with service-api-id and aef-id not valid -* **Test ID**: ***capif_api_acl-8*** -* **Description**: - - This test case will check that an API Provider can't retrieve ACL from CAPIF if service-api-id and aef-id are not valid -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * API Invoker had a Security Context for Service API published. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${NOT_VALID_SERVICE_API_ID}?aef-id=${AEF_ID_NOT_VALID}* - * Use *NOT_VALID_SERVICE_API_ID* and *aef_id* - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information. - -* **Expected Result**: - - 1. ACL Response: - 1. **404 Not Found** Response. - 2. body returned must accomplish **Problem Details** data structure. - 3. apiInvokerPolicies must: - * status **404** - * title with message "Not Found" - * detail with message "No ACLs found for the requested service: {NOT_VALID_SERVICE_API_ID}, aef_id: {AEF_ID_NOT_VALID}, invoker: {api_invoker_id} and supportedFeatures: {supported_features}". - * cause with message "Wrong id". - - -## Test Case 9: Retrieve ACL without SecurityContext created previously by Invoker -* **Test ID**: ***capif_api_acl-9*** -* **Description**: - - This test case will check that an API Provider can't retrieve ACL if no invoker had requested Security Context to CAPIF -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * API Invoker created but no Security Context for Service API published had been requested. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - - 5. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${aef_id}* - * Use *serviceApiId* and *aefId* - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information. - -* **Expected Result**: - - 1. ACL Response: - 1. **404 Not Found** Response. - 2. body returned must accomplish **Problem Details** data structure. - 3. apiInvokerPolicies must: - * status **404** - * title with message "Not Found" - * detail with message "No ACLs found for the requested service: {NOT_VALID_SERVICE_API_ID}, aef_id: {AEF_ID_NOT_VALID}, invoker: {api_invoker_id} and supportedFeatures: {supported_features}". - * cause with message "Wrong id". - -## Test Case 10: Retrieve ACL filtered by api-invoker-id not present -* **Test ID**: ***capif_api_acl-10*** -* **Description**: - - This test case will check that an API Provider get not found response if filter by not valid api-invoker-id doesn't match any registered ACL. -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * API Invoker had a Security Context for Service API published. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${aef_id}&api-invoker-id={NOT_VALID_API_INVOKER_ID}* - * Use *serviceApiId*, *aefId* and *NOT_VALID_API_INVOKER_ID* - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information. - -* **Expected Result**: - - 1. ACL Response: - 1. **404 Not Found** Response. - 2. body returned must accomplish **Problem Details** data structure. - 3. apiInvokerPolicies must: - * status **404** - * title with message "Not Found" - * detail with message "No ACLs found for the requested service: {NOT_VALID_SERVICE_API_ID}, aef_id: {AEF_ID_NOT_VALID}, invoker: {api_invoker_id} and supportedFeatures: {supported_features}". - * cause with message "Wrong id". - -## Test Case 11: Retrieve ACL with APF Certificate -* **Test ID**: ***capif_api_acl-11*** -* **Description**: - - This test case will check that an API Provider can't retrieve ACL from CAPIF using APF Certificate -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * API Invoker had a Security Context for Service API published. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${aef_id}* - * Use *serviceApiId* and *aefId* - * Use APF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information. - -* **Expected Result**: - - 1. Response to Logging Service must accomplish: - 1. **401 Unauthorized** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 401 - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "Certificate not authorized". - -## Test Case 12: Retrieve ACL with AMF Certificate -* **Test ID**: ***capif_api_acl-12*** -* **Description**: - - This test case will check that an API Provider can't retrieve ACL from CAPIF using AMF Certificate -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * API Invoker had a Security Context for Service API published. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${aef_id}* - * Use *serviceApiId* and *aefId* - * Use AMF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information. - -* **Expected Result**: - - 1. Response to Logging Service must accomplish: - 1. **401 Unauthorized** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 401 - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "Certificate not authorized". - -## Test Case 13: Retrieve ACL with Invoker Certificate -* **Test ID**: ***capif_api_acl-13*** -* **Description**: - - This test case will check that an API Provider can't retrieve ACL from CAPIF using Invoker Certificate -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * API Invoker had a Security Context for Service API published. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${aef_id}* - * Use *serviceApiId* and *aefId* - * Use Invoker Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information. - -* **Expected Result**: - - 1. Response to Logging Service must accomplish: - 1. **401 Unauthorized** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 401 - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "Certificate not authorized". - -## Test Case 14: No ACL for invoker after be removed -* **Test ID**: ***capif_api_acl-14*** -* **Description**: - - This test case will check that ACLs are removed after invoker is removed. -* **Pre-Conditions**: - - * API Provider had a Service API Published on CAPIF - * API Invoker had a Security Context for Service API published and ACL is present - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Store *serviceApiId* - * Use APF Certificate - - 3. Perform [Invoker Onboarding] store apiInvokerId - 4. Discover published APIs - 5. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - - 6. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${aef_id}&api-invoker-id={api-invoker-id}* - * Use *serviceApiId*, *aefId* and *api-invoker-id* - * Use AEF Provider Certificate - 7. Remove Invoker from CAPIF - 8. Provider Retrieve ACL - * Send GET *https://{CAPIF_HOSTNAME}/access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${aef_id}&api-invoker-id={api-invoker-id}* - * Use *serviceApiId*, *aefId* and *api-invoker-id* - * Use AEF Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Provider at CCF. - 2. Publish a provider API with name service_1 - 3. Register and onboard Invoker at CCF - 4. Store signed Certificate - 5. Create Security Context - 6. Provider Get ACL information of invoker. - 7. Remove Invoker from CAPIF. - 8. Provider Get ACL information of invoker. - -* **Expected Result**: - 1. ACL Response: - 1. **200 OK** Response. - 2. body returned must accomplish **AccessControlPolicyList** data structure. - 3. apiInvokerPolicies must: - 1. contain only one object. - 2. apiInvokerId must match apiInvokerId registered previously. - - 2. ACL Response: - 1. **404 Not Found** Response. - 2. body returned must accomplish **Problem Details** data structure. - 3. apiInvokerPolicies must: - * status **404** - * title with message "Not Found" - * detail with message "No ACLs found for the requested service: {NOT_VALID_SERVICE_API_ID}, aef_id: {AEF_ID_NOT_VALID}, invoker: None and supportedFeatures: None". - * cause with message "Wrong id". - - - -[Return To All Test Plans]: ../README.md - -[service api description]: ../api_publish_service/service_api_description_post_example.json "Service API Description Request" -[publisher register body]: ../api_publish_service/publisher_register_body.json "Publish register Body" -[service security body]: ./service_security.json "Service Security Request" -[security notification body]: ./security_notification.json "Security Notification Request" -[access token req body]: ./access_token_req.json "Access Token Request" -[example]: ./access_token_req.json "Access Token Request Example" -[invoker onboarding]: ../common_operations/README.md#register-an-invoker "Invoker Onboarding" -[provider registration]: ../common_operations/README.md#register-a-provider "Provider Registration" diff --git a/docs/test_plan/api_access_control_policy/service_api_description_post_example.json b/docs/test_plan/api_access_control_policy/service_api_description_post_example.json deleted file mode 100644 index b725b42..0000000 --- a/docs/test_plan/api_access_control_policy/service_api_description_post_example.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "apiName": "service_1", - "aefProfiles": [ - { - "aefId": "string", - "versions": [ - { - "apiVersion": "v1", - "expiry": "2021-11-30T10:32:02.004Z", - "resources": [ - { - "resourceName": "string", - "commType": "REQUEST_RESPONSE", - "uri": "string", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ], - "custOperations": [ - { - "commType": "REQUEST_RESPONSE", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ] - } - ], - "protocol": "HTTP_1_1", - "dataFormat": "JSON", - "securityMethods": ["PSK"], - "interfaceDescriptions": [ - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - }, - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - } - ] - }, - { - "aefId": "string", - "versions": [ - { - "apiVersion": "v1", - "expiry": "2021-11-30T10:32:02.004Z", - "resources": [ - { - "resourceName": "string", - "commType": "REQUEST_RESPONSE", - "uri": "string", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ], - "custOperations": [ - { - "commType": "REQUEST_RESPONSE", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ] - } - ], - "protocol": "HTTP_1_1", - "dataFormat": "JSON", - "securityMethods": ["PSK"], - "interfaceDescriptions": [ - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - }, - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - } - ] - } - ], - "description": "string", - "supportedFeatures": "fffff", - "shareableInfo": { - "isShareable": true, - "capifProvDoms": [ - "string" - ] - }, - "serviceAPICategory": "string", - "apiSuppFeats": "fffff", - "pubApiPath": { - "ccfIds": [ - "string" - ] - }, - "ccfId": "string" -} \ No newline at end of file diff --git a/docs/test_plan/api_auditing_service/README.md b/docs/test_plan/api_auditing_service/README.md deleted file mode 100644 index bd3204c..0000000 --- a/docs/test_plan/api_auditing_service/README.md +++ /dev/null @@ -1,244 +0,0 @@ -[**[Return To All Test Plans]**] - -- [Test Plan for CAPIF Api Auditing Service](#test-plan-for-capif-api-auditing-service) -- [Tests](#tests) - - [Test Case 1: Get a CAPIF Log Entry.](#test-case-1-creates-a-new-individual-capif-log-entry) - - -# Test Plan for CAPIF Api Auditing Service -At this documentation you will have all information and related files and examples of test plan for this API. - -# Tests - -## Test Case 1: Get CAPIF Log Entry. -* Test ID: ***capif_api_auditing-1*** -* Description: - - This test case will check that a CAPIF AMF can get log entry to Logging Service -* Pre-Conditions: - - * CAPIF provider is pre-authorised (has valid AMF cert from CAPIF Authority) - * Service exist in CAPIF - * Invoker exist in CAPIF - * Log Entry exist in CAPIF - -* Information of Test: - - 1. Perform [provider onboarding], [invoker onboarding] - - 2. Publish Service API at CCF: - - Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - - body [service api description] with apiName service_1 - - Use APF Certificate - - 3. Create Log Entry: - - Send POST to *https://{CAPIF_HOSTNAME}/api-invocation-logs/v1/{aefId}/logs* - - body [log entry request body] - - Use AEF Certificate - - 4. Get Log: - 1. Send GET to *https://{CAPIF_HOSTNAME}/logs/v1/apiInvocationLogs?aef-id={aefId}&api-invoker-id={api-invoker-id}* - 2. Use AMF Certificate - -* Execution Steps: - 1. Register Provider and Invoker CCF - 2. Publish Service - 3. Create Log Entry - 4. Get Log Entry - -* Expected Result: - - 1. Response to Logging Service must accomplish: - 1. **200 OK** - 2. Response Body must follow **InvocationLog** data structure with: - * aefId - * apiInvokerId - * logs - -## Test Case 2: Get CAPIF Log Entry With no Log entry in CAPIF. -* Test ID: ***capif_api_auditing-2*** -* Description: - - This test case will check that a CAPIF AEF can create log entry to Logging Service -* Pre-Conditions: - - * CAPIF provider is pre-authorised (has valid AMF cert from CAPIF Authority) - * Service exist in CAPIF - * Invoker exist in CAPIF - - -* Information of Test: - - 1. Perform [provider onboarding], [invoker onboarding] - - 2. Publish Service API at CCF: - - Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - - body [service api description] with apiName service_1 - - Use APF Certificate - - 4. Get Log: - 1. Send GET to *https://{CAPIF_HOSTNAME}/logs/v1/apiInvocationLogs?aef-id={aefId}&api-invoker-id={api-invoker-id}* - 2. Use AMF Certificate - -* Execution Steps: - 1. Register Provider and Invoker CCF - 2. Publish Service - 3. Get Log Entry - -* Expected Result: - - 1. Response to Logging Service must accomplish: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found Log Entry in CAPIF". - * cause with message "Not Exist Logs with the filters applied". - - -## Test Case 3: Get CAPIF Log Entry without aef-id and api-invoker-id. -* Test ID: ***capif_api_auditing-3*** -* Description: - - This test case will check that a CAPIF AEF can create log entry to Logging Service -* Pre-Conditions: - - * CAPIF provider is no pre-authorised (has no valid AMF cert from CAPIF Authority) - * Service exist in CAPIF - * Invoker exist in CAPIF - * Log Entry exist in CAPIF - -* Information of Test: - - 1. Perform [provider onboarding], [invoker onboarding] - - 2. Publish Service API at CCF: - - Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - - body [service api description] with apiName service_1 - - Use APF Certificate - - 3. Create Log Entry: - - Send POST to *https://{CAPIF_HOSTNAME}/api-invocation-logs/v1/{aefId}/logs* - - body [log entry request body] - - Use AEF Certificate - - 4. Get Log: - 1. Send GET to *https://{CAPIF_HOSTNAME}/logs/v1/apiInvocationLogs - 2. Use AMF Certificate - -* Execution Steps: - 1. Register Provider and Invoker CCF - 2. Publish Service - 3. Create Log Entry - 4. Get Log Entry - -* Expected Result: - - 1. Response to Logging Service must accomplish: - 1. **400 Bad Request** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 400 - * title with message "Bad Request" - * detail with message "aef_id and api_invoker_id parameters are mandatory". - * cause with message "Mandatory parameters missing". - - -## Test Case 4: Get CAPIF Log Entry with filtter api-version. -* Test ID: ***capif_api_auditing-4*** -* Description: - - This test case will check that a CAPIF AMF can get log entry to Logging Service -* Pre-Conditions: - - * CAPIF provider is pre-authorised (has valid AMF cert from CAPIF Authority) - * Service exist in CAPIF - * Invoker exist in CAPIF - * Log Entry exist in CAPIF - -* Information of Test: - - 1. Perform [provider onboarding], [invoker onboarding] - - 2. Publish Service API at CCF: - - Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - - body [service api description] with apiName service_1 - - Use APF Certificate - - 3. Create Log Entry: - - Send POST to *https://{CAPIF_HOSTNAME}/api-invocation-logs/v1/{aefId}/logs* - - body [log entry request body] - - Use AEF Certificate - - 4. Get Log: - 1. Send GET to *https://{CAPIF_HOSTNAME}/logs/v1/apiInvocationLogs?aef-id={aefId}&api-invoker-id={api-invoker-id}&api-version={v1}* - 2. Use AMF Certificate - -* Execution Steps: - 1. Register Provider and Invoker CCF - 2. Publish Service - 3. Create Log Entry - 4. Get Log Entry - -* Expected Result: - - 1. Response to Logging Service must accomplish: - 1. **200 OK** - 2. Response Body must follow **InvocationLog** data structure with: - * aefId - * apiInvokerId - * logs - - -## Test Case 5: Get CAPIF Log Entry with filter api-version but not exist in log entry. -* Test ID: ***capif_api_auditing-4*** -* Description: - - This test case will check that a CAPIF AMF can get log entry to Logging Service -* Pre-Conditions: - - * CAPIF provider is pre-authorised (has valid AMF cert from CAPIF Authority) - * Service exist in CAPIF - * Invoker exist in CAPIF - * Log Entry exist in CAPIF - -* Information of Test: - - 1. Perform [provider onboarding], [invoker onboarding] - - 2. Publish Service API at CCF: - - Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - - body [service api description] with apiName service_1 - - Use APF Certificate - - 3. Create Log Entry: - - Send POST to *https://{CAPIF_HOSTNAME}/api-invocation-logs/v1/{aefId}/logs* - - body [log entry request body] - - Use AEF Certificate - - 4. Get Log: - 1. Send GET to *https://{CAPIF_HOSTNAME}/logs/v1/apiInvocationLogs?aef-id={aefId}&api-invoker-id={api-invoker-id}&api-version={v58}* - 2. Use AMF Certificate - -* Execution Steps: - 1. Register Provider and Invoker CCF - 2. Publish Service - 3. Create Log Entry - 4. Get Log Entry - -* Expected Result: - - 1. Response to Logging Service must accomplish: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * detail with message "Parameters do not match any log entry" - * cause with message "No logs found". - - - -[log entry request body]: ../api_logging_service/invocation_log.json "Log Request Body" - -[invoker onboarding]: ../common_operations/README.md#register-an-invoker "Invoker Onboarding" - -[provider onboarding]: ../common_operations/README.md#register-a-provider "Provider Onboarding" - -[Return To All Test Plans]: ../README.md \ No newline at end of file diff --git a/docs/test_plan/api_discover_service/README.md b/docs/test_plan/api_discover_service/README.md deleted file mode 100644 index 3125c88..0000000 --- a/docs/test_plan/api_discover_service/README.md +++ /dev/null @@ -1,336 +0,0 @@ -[**[Return To All Test Plans]**] - -- [Test Plan for CAPIF Discover Service](#test-plan-for-capif-discover-service) -- [Tests](#tests) - - [Test Case 1: Discover Published service APIs by Authorised API Invoker](#test-case-1-discover-published-service-apis-by-authorised-api-invoker) - - [Test Case 2: Discover Published service APIs by Non Authorised API Invoker](#test-case-2-discover-published-service-apis-by-non-authorised-api-invoker) - - [Test Case 3: Discover Published service APIs by not registered API Invoker](#test-case-3-discover-published-service-apis-by-not-registered-api-invoker) - - [Test Case 4: Discover Published service APIs by registered API Invoker with 1 result filtered](#test-case-4-discover-published-service-apis-by-registered-api-invoker-with-1-result-filtered) - - [Test Case 5: Discover Published service APIs by registered API Invoker filtered with no match](#test-case-5-discover-published-service-apis-by-registered-api-invoker-filtered-with-no-match) - - [Test Case 6: Discover Published service APIs by registered API Invoker not filtered](#test-case-6-discover-published-service-apis-by-registered-api-invoker-not-filtered) - - -# Test Plan for CAPIF Discover Service -At this documentation you will have all information and related files and examples of test plan for this API. - -# Tests - -## Test Case 1: Discover Published service APIs by Authorised API Invoker -* **Test ID**: ***capif_api_discover_service-1*** -* **Description**: - - This test case will check if NetApp (Invoker) can discover published service APIs. -* **Pre-Conditions**: - * Service APIs are published. - * NetApp was registered previously - * NetApp was onboarded previously with {onboardingId} - -* **Information of Test**: - 1. Perform [Provider Registration] and [Invoker Onboarding] - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Use APF Certificate - 3. Request Discover Published APIs: - * Send GET to *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Use Invoker Certificate - -* **Execution Steps**: - - 1. Register Provider at CCF, store certificates and Publish Service API at CCF - 2. Register Invoker and Onboard Invoker at CCF - 3. Discover Service APIs by Invoker - -* **Expected Result**: - - 1. Response to Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - - 2. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - 3. Response to Discover Request By Invoker: - 1. **200 OK** response. - 2. Response body must follow **DiscoveredAPIs** data structure: - * Check if DiscoveredAPIs contains the API Published previously - - -## Test Case 2: Discover Published service APIs by Non Authorised API Invoker -* **Test ID**: ***capif_api_discover_service-2*** -* **Description**: - - This test case will check that an API Publisher can't discover published APIs because is not authorized. - -* **Pre-Conditions**: - * Service APIs are published. - -* **Information of Test**: - 1. Perform [Provider Registration] and [Invoker Onboarding] - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Use APF Certificate - 3. Request Discover Published APIs by no invoker entity: - * Send GET to *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Use not Invoker Certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API at CCF - 2. Register Invoker and Onboard Invoker at CCF - 3. Discover Service APIs by no invoker entity - -* **Expected Result**: - - 1. Response to Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - - 2. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - - 3. Response to Discover Request By no invoker entity: - 1. **401 Unauthorized** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 401 - * title with message "Unauthorized" - * detail with message "User not authorized". - * cause with message "Certificate not authorized". - - -## Test Case 3: Discover Published service APIs by not registered API Invoker -* **Test ID**: ***capif_api_discover_service-3*** -* **Description**: - - This test case will check that a not registered invoker is forbidden to discover published APIs. - -* **Pre-Conditions**: - * Service APIs are published. - -* **Information of Test**: - 1. Perform [Provider Registration] and [Invoker Onboarding] - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Use APF Certificate - 3. Request Discover Published APIs with not valid apiInvoker: - * Send GET to *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={INVOKER_NOT_REGISTERED}* - * Param api-invoker-id is mandatory - * Using invoker certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API at CCF - 2. Register Invoker and Onboard Invoker at CCF - 3. Discover Service APIs by Publisher - -* **Expected Result**: - 1. Response to Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - - 2. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - - 3. Response to Discover Request By Invoker: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "API Invoker does not exist". - * cause with message "API Invoker id not found". - - -## Test Case 4: Discover Published service APIs by registered API Invoker with 1 result filtered -* **Test ID**: ***capif_api_discover_service-4*** -* **Description**: - - This test case will check if NetApp (Invoker) can discover published service APIs. -* **Pre-Conditions**: - * At least 2 Service APIs are published. - * NetApp was registered previously - * NetApp was onboarded previously with {onboardingId} - -* **Information of Test**: - 1. Perform [Provider Registration] and [Invoker Onboarding] - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Use APF Certificate - 3. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_2 - * Use APF Certificate - 4. Request Discover Published APIs filtering by api-name: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}&api-name=service_1* - * Param api-invoker-id is mandatory - * Using invoker certificate - * filter by api-name service_1 - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 and service_2 at CCF - 2. Register Invoker and Onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Discover filtered by api-name service_1 Service APIs by Invoker - -* **Expected Result**: - 1. Response to Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - 2. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - 3. Response to Discover Request By Invoker: - 1. **200 OK** response. - 2. Response body must follow **DiscoveredAPIs** data structure: - * Check if DiscoveredAPIs contains previously registered Service APIs published. - 4. Response to Discover Request By Invoker: - 1. **200 OK** response. - 2. Response body must follow **DiscoveredAPIs** data structure: - * Check if DiscoveredAPIs contains only Service API published with api-name service_1 - - -## Test Case 5: Discover Published service APIs by registered API Invoker filtered with no match -* **Test ID**: ***capif_api_discover_service-5*** -* **Description**: - This test case will check if NetApp (Invoker) can discover published service APIs. -* **Pre-Conditions**: - * At least 2 Service APIs are published. - * NetApp was registered previously - * NetApp was onboarded previously with {onboardingId} - -* **Information of Test**: - 1. Perform [Provider Registration] and [Invoker Onboarding] - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Use APF Certificate - 3. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_2 - * Use APF Certificate - 4. Request Discover Published APIs filtering by api-name not published: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}&api-name=NOT_VALID_NAME* - * Param api-invoker-id is mandatory - * Using invoker certificate - * filter by api-name NOT_VALID_NAME - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 and service_2 at CCF - 2. Register Invoker and Onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Discover filtered by api-name not published Service APIs by Invoker - -* **Expected Result**: - 1. Response to Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - 2. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - 3. Response to Discover Request By Invoker: - 1. **200 OK** response. - 2. Response body must follow **DiscoveredAPIs** data structure: - * Check if DiscoveredAPIs contains previously registered Service APIs published. - 4. Response to Discover Request By Invoker: - 1. **404 Not Found** response. - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "API Invoker {api_invoker_id} has no API Published that accomplish filter conditions". - * cause with message "No API Published accomplish filter conditions". - - -## Test Case 6: Discover Published service APIs by registered API Invoker not filtered -* **Test ID**: ***capif_api_discover_service-6*** -* **Description**: - - This test case will check if NetApp (Invoker) can discover published service APIs. -* **Pre-Conditions**: - * 2 Service APIs are published. - * NetApp was registered previously - * NetApp was onboarded previously with {onboardingId} - -* **Information of Test**: - 1. Perform [Provider Registration] and [Invoker Onboarding] - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Use APF Certificate - 3. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_2 - * Use APF Certificate - 4. Request Discover Published APIs not filtered: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Using invoker certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 and service_2 at CCF - 2. Register Invoker and Onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Discover without filter by Invoker - -* **Expected Result**: - - 1. Response to Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - - 2. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - - 3. Response to Discover Request By Invoker: - 1. **200 OK** response. - 2. Response body must follow **DiscoveredAPIs** data structure: - * Check if DiscoveredAPIs contains the 2 previously registered Service APIs published. - - - - [service api description]: ./api_publish_service/service_api_description_post_example.json "Service API **Description** Request" - [publisher register body]: ./api_publish_service/publisher_register_body.json "Publish register Body" - [invoker onboarding body]: ../api_invoker_management/invoker_details_post_example.json "API Invoker Request" - [invoker register body]: ../api_invoker_management/invoker_register_body.json "Invoker Register Body" - [provider request body]: ../api_provider_management/provider_details_post_example.json "API Provider Enrolment Request" - [provider request patch body]: ../api_provider_management/provider_details_enrolment_details_patch_example.json "API Provider Enrolment Patch Request" - [provider getauth body]: ../api_provider_management/provider_getauth_example.json "Get Auth Example" - [invoker onboarding]: ../common_operations/README.md#register-an-invoker "Invoker Onboarding" - [provider registration]: ../common_operations/README.md#register-a-provider "Provider Registration" - - -[Return To All Test Plans]: ../README.md diff --git a/docs/test_plan/api_events_service/README.md b/docs/test_plan/api_events_service/README.md deleted file mode 100644 index 417c1aa..0000000 --- a/docs/test_plan/api_events_service/README.md +++ /dev/null @@ -1,265 +0,0 @@ -[**[Return To All Test Plans]**] - -- [Test Plan for CAPIF Api Events Service](#test-plan-for-capif-api-events-service) -- [Tests](#tests) - - [Test Case 1: Creates a new individual CAPIF Event Subscription.](#test-case-1-creates-a-new-individual-capif-event-subscription) - - [Test Case 2: Creates a new individual CAPIF Event Subscription with Invalid SubscriberId](#test-case-2-creates-a-new-individual-capif-event-subscription-with-invalid-subscriberid) - - [Test Case 3: Deletes an individual CAPIF Event Subscription](#test-case-3-deletes-an-individual-capif-event-subscription) - - [Test Case 4: Deletes an individual CAPIF Event Subscription with invalid SubscriberId](#test-case-4-deletes-an-individual-capif-event-subscription-with-invalid-subscriberid) - - [Test Case 5: Deletes an individual CAPIF Event Subscription with invalid SubscriptionId](#test-case-5-deletes-an-individual-capif-event-subscription-with-invalid-subscriptionid) - - - -# Test Plan for CAPIF Api Events Service -At this documentation you will have all information and related files and examples of test plan for this API. - -# Tests - -## Test Case 1: Creates a new individual CAPIF Event Subscription. -* Test ID: ***capif_api_events-1*** -* Description: - - This test case will check that a CAPIF subscriber (Invoker or Publisher) can Subscribe to Events -* Pre-Conditions: - - * CAPIF subscriber is pre-authorised (has valid InvokerId or apfId from CAPIF Authority) - -* Information of Test: - - 1. Perform [Invoker Onboarding] - - 2. Event Subscription: - 1. Send POST to *https://{CAPIF_HOSTNAME}/capif-events/v1/{subscriberId}/subscriptions* - 2. body [event subscription request body] - 3. Use Invoker Certificate - -* Execution Steps: - - 1. Register Invoker and Onboard Invoker at CCF - 2. Subscribe to Events - 3. Retrieve {subscriberId} and {subscriptionId} from Location Header - -* Expected Result: - - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - - 2. Response to Event Subscription must accomplish: - 1. **201 Created** - 2. The URI of the created resource shall be returned in the "Location" HTTP header, following this structure: *{apiRoot}/capif-events/{apiVersion}/{subscriberId}/subscriptions/{subscriptionId} - 3. Response Body must follow **EventSubscription** data structure. - - 3. Event Subscriptions are stored in CAPIF Database - - -## Test Case 2: Creates a new individual CAPIF Event Subscription with Invalid SubscriberId -* Test ID: ***capif_api_events-2*** -* Description: - - This test case will check that a CAPIF subscriber (Invoker or Publisher) cannot Subscribe to Events without valid SubcriberId -* Pre-Conditions: - - * CAPIF subscriber is not pre-authorised (has invalid InvokerId or apfId) - -* Information of Test: - - 1. Perform [Invoker Onboarding] - - 2. Event Subscription: - 1. Send POST to *https://{CAPIF_HOSTNAME}/capif-events/v1/{SUBSCRIBER_NOT_REGISTERED}/subscriptions* - 2. body [event subscription request body] - 3. Use Invoker Certificate - -* Execution Steps: - - 1. Register Invoker and Onboard Invoker at CCF - 2. Subscribe to Events - -* Expected Result: - - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - - 2. Response to Event Subscription must accomplish: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "Invoker or APF or AEF or AMF Not found". - * cause with message "Subscriber Not Found". - - 3. Event Subscriptions are not stored in CAPIF Database - - -## Test Case 3: Deletes an individual CAPIF Event Subscription -* Test ID: ***capif_api_events-3*** -* Description: - - This test case will check that a CAPIF subscriber (Invoker or Publisher) can Delete an Event Subscription -* Pre-Conditions: - - * CAPIF subscriber is pre-authorised (has valid InvokerId or apfId from CAPIF Authority) - -* Information of Test: - - 1. Perform [Invoker Onboarding] - - 2. Event Subscription: - 1. Send POST to *https://{CAPIF_HOSTNAME}/capif-events/v1/{subscriberId}/subscriptions* - 2. body [event subscription request body] - 3. Use Invoker Certificate - - 3. Remove Event Subscription: - 1. Send DELETE to *https://{CAPIF_HOSTNAME}/capif-events/v1/{subscriberId}/subscriptions* - 2. Use Invoker Certificate - -* Execution Steps: - - 1. Register Invoker and Onboard Invoker at CCF - 2. Subscribe to Events - 3. Retrieve {subscriberId} and {subscriptionId} from Location Header - 4. Remove Event Subscription - -* Expected Result: - - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - - 2. Response to Event Subscription must accomplish: - 1. **201 Created** - 2. The URI of the created resource shall be returned in the "Location" HTTP header, following this structure: *{apiRoot}/capif-events/{apiVersion}/{subscriberId}/subscriptions/{subscriptionId} - 3. Response Body must follow **EventSubscription** data structure. - - 3. Event Subscriptions are stored in CAPIF Database - 4. Remove Event Subscription: - 1. **204 No Content** - - 5. Event Subscription is not present at CAPIF Database. - - -## Test Case 4: Deletes an individual CAPIF Event Subscription with invalid SubscriberId -* Test ID: ***capif_api_events-4*** -* Description: - - This test case will check that a CAPIF subscriber (Invoker or Publisher) cannot Delete to Events without valid SubcriberId -* Pre-Conditions: - - * CAPIF subscriber is pre-authorised (has valid InvokerId or apfId). - * CAPIF subscriber is subscribed to Events. - -* Information of Test: - - 1. Perform [Invoker Onboarding] - - 2. Event Subscription: - 1. Send POST to https://{CAPIF_HOSTNAME}/capif-events/v1/{subscriberId}/subscriptions - 2. body [event subscription request body] - 3. Use Invoker Certificate - - 3. Remove Event Subcription with not valid subscriber: - 1. Send DELETE to to https://{CAPIF_HOSTNAME}/capif-events/v1/{SUBSCRIBER_ID_NOT_VALID}/subscriptions/{subcriptionId} - 2. Use Invoker Certificate - -* Execution Steps: - - 1. Register Invoker and Onboard Invoker at CCF - 2. Subscribe to Events - 3. Retrieve Location Header with subscriptionId. - 4. Remove Event Subscribed with not valid Subscriber. - -* Expected Result: - - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - - 2. Response to Event Subscription must accomplish: - 1. 201 Created - 2. The URI of the created resource shall be returned in the "Location" HTTP header, following this structure: *{apiRoot}/capif-events/{apiVersion}/{subscriberId}/subscriptions/{subscriptionId} - 3. Response Body must follow **EventSubscription** data structure. - - 3. Event Subscriptions are stored in CAPIF Database - 4. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "Invoker or APF or AEF or AMF Not found". - * cause with message "Subscriber Not Found". - - -## Test Case 5: Deletes an individual CAPIF Event Subscription with invalid SubscriptionId -* Test ID: ***capif_api_events-5*** -* Description: - - This test case will check that a CAPIF subscriber (Invoker or Publisher) cannot Delete an Event Subscription without valid SubscriptionId -* Pre-Conditions: - - * CAPIF subscriber is pre-authorised (has invalid InvokerId or apfId). - * CAPIF subscriber is subscribed to Events. - -* Information of Test: - - 1. Perform [Invoker Onboarding] - - 2. Event Subscription: - 1. Send POST to https://{CAPIF_HOSTNAME}/capif-events/v1/{subscriberId}/subscriptions - 2. body [event subscription request body] - 3. Use Invoker Certificate - - 3. Remove Event Subcription with not valid subscriber: - 1. Send DELETE to to https://{CAPIF_HOSTNAME}/capif-events/v1/{subcriberId}/subscriptions/{SUBSCRIPTION_ID_NOT_VALID} - 2. Use Invoker Certificate - -* Execution Steps: - - 1. Register Invoker and Onboard Invoker at CCF - 2. Subscribe to Events - 3. Retrieve Location Header with subscriptionId. - 4. Remove Event Subscribed with not valid Subscriber. - -* Expected Result: - - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - - 2. Response to Event Subscription must accomplish: - 1. **201 Created** - 2. The URI of the created resource shall be returned in the "Location" HTTP header, following this structure: *{apiRoot}/capif-events/{apiVersion}/{subscriberId}/subscriptions/{subscriptionId} - 3. Response Body must follow **EventSubscription** data structure. - - 3. Event Subscriptions are stored in CAPIF Database - 4. Remove Event Subscription with not valid subscriber: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * detail with message "Service API not existing". - * cause with message "Event API subscription id not found". - - - - -[invoker register body]: ../api_invoker_management/invoker_register_body.json "Invoker Register Body" -[invoker onboard request body]: ../api_invoker_management/invoker_details_post_example.json "API Invoker Request" -[event subscription request body]: ./event_subscription.json "Event Subscription Request" -[invoker onboarding]: ../common_operations/README.md#register-an-invoker "Invoker Onboarding" - - -[Return To All Test Plans]: ../README.md diff --git a/docs/test_plan/api_events_service/event_subscription.json b/docs/test_plan/api_events_service/event_subscription.json deleted file mode 100644 index 40dc09b..0000000 --- a/docs/test_plan/api_events_service/event_subscription.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "eventFilters": [ - { - "aefIds": ["aefIds", "aefIds"], - "apiIds": ["apiIds", "apiIds"], - "apiInvokerIds": ["apiInvokerIds", "apiInvokerIds"] - }, - { - "aefIds": ["aefIds", "aefIds"], - "apiIds": ["apiIds", "apiIds"], - "apiInvokerIds": ["apiInvokerIds", "apiInvokerIds"] - } - ], - "eventReq": { - "grpRepTime": 5, - "immRep": true, - "maxReportNbr": 0, - "monDur": "2000-01-23T04:56:07+00:00", - "partitionCriteria": ["string1", "string2"], - "repPeriod": 6, - "sampRatio": 15 - }, - "events": ["SERVICE_API_AVAILABLE", "API_INVOKER_ONBOARDED"], - "notificationDestination": "http://robot.testing", - "requestTestNotification": true, - "supportedFeatures": "aaa", - "websockNotifConfig": { - "requestWebsocketUri": true, - "websocketUri": "websocketUri" - } -} diff --git a/docs/test_plan/api_invoker_management/README.md b/docs/test_plan/api_invoker_management/README.md deleted file mode 100644 index 9846c96..0000000 --- a/docs/test_plan/api_invoker_management/README.md +++ /dev/null @@ -1,306 +0,0 @@ -[**[Return To All Test Plans]**] - -- [Test Plan for CAPIF Api Invoker Management](#test-plan-for-capif-api-invoker-management) -- [Tests](#tests) - - [Test Case 1: Onboard NetApp](#test-case-1-onboard-netapp) - - [Test Case 2: Onboard NetApp Already onboarded](#test-case-2-onboard-netapp-already-onboarded) - - [Test Case 3: Update Onboarded NetApp](#test-case-3-update-onboarded-netapp) - - [Test Case 4: Update Not Onboarded NetApp](#test-case-4-update-not-onboarded-netapp) - - [Test Case 5: Offboard NetApp](#test-case-5-offboard-netapp) - - [Test Case 6: Offboard Not previsouly Onboarded NetApp](#test-case-6-offboard-not-previsouly-onboarded-netapp) - - [Test Case 7: Update Onboarded NetApp Certificate](#test-case-7-update-onboarded-netapp-certificate) - - -# Test Plan for CAPIF Api Invoker Management -At this documentation you will have all information and related files and examples of test plan for this API. - -# Tests - -## Test Case 1: Onboard NetApp -* **Test ID**: ***capif_api_invoker_management-1*** -* **Description**: - - This test will try to register new NetApp at CAPIF Core. -* **Pre-Conditions**: - - * NetApp was not registered previously - * NetApp was not onboarded previously - -* **Information of Test**: - - 1. Create public and private key at invoker - - 2. Register of Invoker at CCF: - * Send POST to *http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register* - * body [invoker register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [invoker getauth body] - - 4. Onboard Invoker: - * Send POST to *https://{CAPIF_HOSTNAME}/api-invoker-management/v1/onboardedInvokers* - * Reference Request Body: [invoker onboarding body] - * "onboardingInformation"->"apiInvokerPublicKey": must contain public key generated by Invoker. - * Send at Authorization Header the Bearer access_token obtained previously (Authorization:Bearer ${access_token}) - -* **Execution Steps**: - 1. Register Invoker at CCF - 2. Onboard Invoker at CCF - 3. Store signed Certificate - -* **Expected Result**: - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - - -## Test Case 2: Onboard NetApp Already onboarded - -* **Test ID**: ***capif_api_invoker_management-2*** -* **Description**: - - This test will check second onboard of same NetApp is not allowed. - -* **Pre-Conditions**: - - * NetApp was registered previously - * NetApp was onboarded previously - -* **Information of Test**: - - 1. Perform [Invoker Onboarding] - - 2. Repeat Onboard Invoker: - * Send POST to *https://{CAPIF_HOSTNAME}/api-invoker-management/v1/onboardedInvokers* - * Reference Request Body: [invoker onboarding body] - * "onboardingInformation"->"apiInvokerPublicKey": must contain public key generated by Invoker. - * Send at Authorization Header the Bearer access_token obtained previously (Authorization:Bearer ${access_token}) - -* **Execution Steps**: - 1. Register NetApp at CCF - 2. Onboard NetApp at CCF - 3. Store signed Certificate at NetApp - 4. Onboard Again the NetApp at CCF - -* **Expected Result**: - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - 2. Response to Second Onboard of NetApp must accomplish: - 1. **403 Forbidden** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 403 - * title with message "Forbidden" - * detail with message "Invoker Already registered". - * cause with message "Identical invoker public key". - - -## Test Case 3: Update Onboarded NetApp -* **Test ID**: ***capif_api_invoker_management-3*** -* **Description**: - - This test will try to update information of previous onboard NetApp at CAPIF Core. -* **Pre-Conditions**: - - * NetApp was registered previously - * NetApp was onboarded previously with {onboardingId} - -* **Information of Test**: - - 1. Perform [Invoker Onboarding] - - 2. Update information of previously onboarded Invoker: - * Send PUT to *https://{CAPIF_HOSTNAME}/api-invoker-management/v1/onboardedInvokers/{onboardingId}* - * Reference Request Body is: [put invoker onboarding body] - * "notificationDestination": "*http://host.docker.internal:8086/netapp_new_callback*", - -* **Execution Steps**: - - 1. Register Invoker at CCF - 2. Onboard Invoker at CCF - 3. Store signed Certificate - 4. Update Onboarding Information at CCF with a minor change on "notificationDestination" - -* **Expected Result**: - - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - 2. Response to Update Request (PUT) with minor change must contain: - 1. **200 OK** response. - 2. notificationDestination on response must contain the new value - - -## Test Case 4: Update Not Onboarded NetApp -* **Test ID**: ***capif_api_invoker_management-4*** -* **Description**: - - This test will try to update information of not onboarded NetApp at CAPIF Core. -* **Pre-Conditions**: - - * NetApp was registered previously - * NetApp was not onboarded previously - -* **Information of Test**: - - 1. Perform [Invoker Onboarding] - - 2. Update information of not onboarded Invoker: - * Send PUT to *https://{CAPIF_HOSTNAME}/api-invoker-management/v1/onboardedInvokers/{INVOKER_NOT_REGISTERED}* - * Reference Request Body is: [put invoker onboarding body] - -* **Execution Steps**: - - 1. Register Invoker at CCF - 2. Onboard Invoker at CCF - 3. Update Onboarding Information at CCF of not onboarded - -* **Expected Result**: - - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response to Update Request (PUT) must contain: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "Please provide an existing Netapp ID". - * cause with message "Not exist NetappID". - - - -## Test Case 5: Offboard NetApp -* **Test ID**: ***capif_api_invoker_management-5*** -* **Description**: - - This test case will check that a Registered NetApp can be deleted. -* **Pre-Conditions**: - - * NetApp was registered previously - * NetApp was onboarded previously - -* **Information of Test**: - - 1. Perform [Invoker Onboarding] - - 2. Offboard: - * Send Delete to *https://{CAPIF_HOSTNAME}/api-invoker-management/v1/onboardedInvokers/{onboardingId}* - -* **Execution Steps**: - - 1. Register Invoker at CCF - 2. Onboard Invoker at CCF - 3. Offboard Invoker at CCF - -* **Expected Result**: - - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response to Offboard Request (DELETE) must contain: - 1. **204 No Content** - - -## Test Case 6: Offboard Not previsouly Onboarded NetApp -* **Test ID**: ***capif_api_invoker_management-6*** -* **Description**: - - This test case will check that a Non-Registered NetApp cannot be deleted -* **Pre-Conditions**: - - * NetApp was registered previously - * NetApp was not onboarded previously - -* **Information of Test**: - - 1. Perform [Invoker Onboarding] - - 2. Offboard: - * Send Delete to *https://{CAPIF_HOSTNAME}/api-invoker-management/v1/onboardedInvokers/{INVOKER_NOT_REGISTERED}* - -* **Execution Steps**: - - 1. Register Invoker at CCF - 2. Offboard Invoker at CCF - -* **Expected Result**: - - 1. Response to Offboard Request (DELETE) must contain: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "Please provide an existing Netapp ID". - * cause with message "Not exist NetappID". - -## Test Case 7: Update Onboarded NetApp Certificate -* **Test ID**: ***capif_api_invoker_management-7*** -* **Description**: - - This test will try to update public key and get a new signed certificate by CAPIF Core. -* **Pre-Conditions**: - - * NetApp was registered previously - * NetApp was onboarded previously with {onboardingId} and {public_key_1} - -* **Information of Test**: - - 1. Perform [Invoker Onboarding] with public_key_1. - - 2. Create {public_key_2} - - 3. Update information of previously onboarded Invoker: - * Send PUT to *https://{CAPIF_HOSTNAME}/api-invoker-management/v1/onboardedInvokers/{onboardingId}* - * Reference Request Body is: [put invoker onboarding body] - * ["onboardingInformation"]["apiInvokerPublicKey"]: {public_key_2}, - * Store new certificate. - - 4. Update information of previously onboarded Invoker Using new certificate: - * Send PUT to *https://{CAPIF_HOSTNAME}/api-invoker-management/v1/onboardedInvokers/{onboardingId}* - * Reference Request Body is: [put invoker onboarding body] - * "notificationDestination": "*http://host.docker.internal:8086/netapp_new_callback*", - * Use new invoker certificate - -* **Execution Steps**: - - 1. Register Invoker at CCF - 2. Onboard Invoker at CCF - 3. Store signed Certificate - 4. Update Onboarding Information at CCF with new public key - 5. Update Onboarding Information at CCF with minor change - -* **Expected Result**: - - 1. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - 2. Response to Update Request (PUT) with new public key: - 1. **200 OK** response. - 2. apiInvokerCertificate with new certificate on response -> store to use. - 3. Response to Update Request (PUT) with minor change must contain: - 1. **200 OK** response. - 2. notificationDestination on response must contain the new value - - - - -[invoker onboarding body]: ./invoker_details_post_example.json "API Invoker Request" -[invoker register body]: ./invoker_register_body.json "Invoker Register Body" -[put register body]: ./invoker_details_put_example.json "API Invoker Update Request" -[invoker getauth body]: ./invoker_getauth_example.json "Get Auth Example" - -[invoker onboarding]: ../common_operations/README.md#register-an-invoker "Invoker Onboarding" - -[Return To All Test Plans]: ../README.md \ No newline at end of file diff --git a/docs/test_plan/api_invoker_management/invoker_details_post_example.json b/docs/test_plan/api_invoker_management/invoker_details_post_example.json deleted file mode 100644 index c306a17..0000000 --- a/docs/test_plan/api_invoker_management/invoker_details_post_example.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "notificationDestination": "http://host.docker.internal:8086/netapp_callback", - "supportedFeatures": "fffffff", - "apiInvokerInformation": "ROBOT_TESTING_INVOKER", - "websockNotifConfig": { - "requestWebsocketUri": true, - "websocketUri": "websocketUri" - }, - "onboardingInformation": { - "apiInvokerPublicKey": "{PUBLIC_KEY}", - "onboardingSecret": "onboardingSecret", - "apiInvokerCertificate": "apiInvokerCertificate" - }, - "requestTestNotification": true -} diff --git a/docs/test_plan/api_invoker_management/invoker_details_put_example.json b/docs/test_plan/api_invoker_management/invoker_details_put_example.json deleted file mode 100644 index 37a1eef..0000000 --- a/docs/test_plan/api_invoker_management/invoker_details_put_example.json +++ /dev/null @@ -1,393 +0,0 @@ -{ - "notificationDestination": "http://host.docker.internal:8086/netapp_new_callback", - "supportedFeatures": "fffffff", - "apiInvokerInformation": "ROBOT_TESTING_INVOKER", - "websockNotifConfig": { - "requestWebsocketUri": true, - "websocketUri": "websocketUri" - }, - "onboardingInformation": { - "apiInvokerPublicKey": "{PUBLIC_KEY}", - "onboardingSecret": "onboardingSecret", - "apiInvokerCertificate": "apiInvokerCertificate" - }, - "requestTestNotification": true, - "apiList": [ - { - "serviceAPICategory": "serviceAPICategory", - "ccfId": "ccfId", - "apiName": "apiName", - "shareableInfo": { - "capifProvDoms": ["capifProvDoms", "capifProvDoms"], - "isShareable": true - }, - "supportedFeatures": "fffffff", - "description": "description", - "apiSuppFeats": "fffffff", - "apiId": "apiId", - "aefProfiles": [ - { - "securityMethods": ["PSK"], - "versions": [ - { - "apiVersion": "apiVersion", - "resources": [ - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - } - ], - "custOperations": [ - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - } - ], - "expiry": "2000-01-23T04:56:07.000+00:00" - }, - { - "apiVersion": "apiVersion", - "resources": [ - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - } - ], - "custOperations": [ - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - } - ], - "expiry": "2000-01-23T04:56:07.000+00:00" - } - ], - "aefId": "aefId", - "interfaceDescriptions": [ - { - "securityMethods": ["PSK"], - "port": 5248, - "ipv4Addr": "ipv4Addr" - }, - { "securityMethods": ["PSK"], "port": 5248, "ipv4Addr": "ipv4Addr" } - ] - }, - { - "securityMethods": ["PSK"], - "versions": [ - { - "apiVersion": "apiVersion", - "resources": [ - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - } - ], - "custOperations": [ - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - } - ], - "expiry": "2000-01-23T04:56:07.000+00:00" - }, - { - "apiVersion": "apiVersion", - "resources": [ - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - } - ], - "custOperations": [ - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - } - ], - "expiry": "2000-01-23T04:56:07.000+00:00" - } - ], - "aefId": "aefId", - "interfaceDescriptions": [ - { - "securityMethods": ["PSK"], - "port": 5248, - "ipv4Addr": "ipv4Addr" - }, - { "securityMethods": ["PSK"], "port": 5248, "ipv4Addr": "ipv4Addr" } - ] - } - ], - "pubApiPath": { "ccfIds": ["ccfIds", "ccfIds"] } - }, - { - "serviceAPICategory": "serviceAPICategory", - "ccfId": "ccfId", - "apiName": "apiName2", - "shareableInfo": { - "capifProvDoms": ["capifProvDoms", "capifProvDoms"], - "isShareable": true - }, - "supportedFeatures": "fffffff", - "description": "description", - "apiSuppFeats": "fffffff", - "apiId": "apiId", - "aefProfiles": [ - { - "securityMethods": ["PSK"], - "versions": [ - { - "apiVersion": "apiVersion", - "resources": [ - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - } - ], - "custOperations": [ - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - } - ], - "expiry": "2000-01-23T04:56:07.000+00:00" - }, - { - "apiVersion": "apiVersion", - "resources": [ - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - } - ], - "custOperations": [ - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - } - ], - "expiry": "2000-01-23T04:56:07.000+00:00" - } - ], - "aefId": "aefId", - "interfaceDescriptions": [ - { - "securityMethods": ["PSK"], - "port": 5248, - "ipv4Addr": "ipv4Addr" - }, - { "securityMethods": ["PSK"], "port": 5248, "ipv4Addr": "ipv4Addr" } - ] - }, - { - "securityMethods": ["PSK"], - "versions": [ - { - "apiVersion": "apiVersion", - "resources": [ - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - } - ], - "custOperations": [ - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - } - ], - "expiry": "2000-01-23T04:56:07.000+00:00" - }, - { - "apiVersion": "apiVersion", - "resources": [ - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "resourceName": "resourceName", - "custOpName": "custOpName", - "uri": "uri", - "commType": "REQUEST_RESPONSE" - } - ], - "custOperations": [ - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - }, - { - "operations": ["GET"], - "description": "description", - "custOpName": "custOpName", - "commType": "REQUEST_RESPONSE" - } - ], - "expiry": "2000-01-23T04:56:07.000+00:00" - } - ], - "aefId": "aefId", - "interfaceDescriptions": [ - { - "securityMethods": ["PSK"], - "port": 5248, - "ipv4Addr": "ipv4Addr" - }, - { "securityMethods": ["PSK"], "port": 5248, "ipv4Addr": "ipv4Addr" } - ] - } - ], - "pubApiPath": { "ccfIds": ["ccfIds", "ccfIds"] } - } - ] -} diff --git a/docs/test_plan/api_invoker_management/invoker_getauth_example.json b/docs/test_plan/api_invoker_management/invoker_getauth_example.json deleted file mode 100644 index a66dad5..0000000 --- a/docs/test_plan/api_invoker_management/invoker_getauth_example.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "username": "ROBOT_TESTING_INVOKER", - "password": "password" -} diff --git a/docs/test_plan/api_invoker_management/invoker_register_body.json b/docs/test_plan/api_invoker_management/invoker_register_body.json deleted file mode 100644 index e5bf1fc..0000000 --- a/docs/test_plan/api_invoker_management/invoker_register_body.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "password": "password", - "username": "ROBOT_TESTING_INVOKER", - "role": "invoker", - "description": "Testing", - "cn": "ROBOT_TESTING_INVOKER" -} diff --git a/docs/test_plan/api_logging_service/README.md b/docs/test_plan/api_logging_service/README.md deleted file mode 100644 index 913a652..0000000 --- a/docs/test_plan/api_logging_service/README.md +++ /dev/null @@ -1,241 +0,0 @@ -[**[Return To All Test Plans]**] - -- [Test Plan for CAPIF Api Logging Service](#test-plan-for-capif-api-logging-service) -- [Tests](#tests) - - [Test Case 1: Creates a new individual CAPIF Log Entry.](#test-case-1-creates-a-new-individual-capif-log-entry) - - [Test Case 2: Creates a new individual CAPIF Log Entry with Invalid aefID](#test-case-2-creates-a-new-individual-capif-log-entry-with-invalid-aefid) - - [Test Case 3: Creates a new individual CAPIF Log Entry with Invalid serviceAPI](#test-case-3-creates-a-new-individual-capif-log-entry-with-invalid-serviceapi) - - [Test Case 4: Creates a new individual CAPIF Log Entry with Invalid apiInvokerId](#test-case-4-creates-a-new-individual-capif-log-entry-with-invalid-apiinvokerid) - - - [Test Case 5: Creates a new individual CAPIF Log Entry with differnted aef_id in body and request](#test-case-5-creates-a-new-individual-capif-log-entry-with-invalid-aefid-in-body) - - -# Test Plan for CAPIF Api Logging Service -At this documentation you will have all information and related files and examples of test plan for this API. - -# Tests - -## Test Case 1: Creates a new individual CAPIF Log Entry. -* Test ID: ***capif_api_logging-1*** -* Description: - - This test case will check that a CAPIF AEF can create log entry to Logging Service -* Pre-Conditions: - - * CAPIF provider is pre-authorised (has valid aefId from CAPIF Authority) - * Service exist in CAPIF - * Invoker exist in CAPIF - -* Information of Test: - - 1. Perform [provider onboarding] and [invoker onboarding] - - 2. Publish Service API at CCF: - - Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - - body [service api description] with apiName service_1 - - Use APF Certificate - - 3. Log Entry: - 1. Send POST to *https://{CAPIF_HOSTNAME}/api-invocation-logs/v1/{aefId}/logs* - 2. body [log entry request body] - 3. Use AEF Certificate - -* Execution Steps: - 1. Register Provider and Invoker CCF - 2. Publish Service - 3. Create Log Entry - -* Expected Result: - - 1. Response to Logging Service must accomplish: - 1. **201 Created** - 2. Response Body must follow **InvocationLog** data structure with: - * aefId - * apiInvokerId - * logs - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invocation-logs/v1/{aefId}/logs/{logId}* - - - - -## Test Case 2: Creates a new individual CAPIF Log Entry with Invalid aefId -* Test ID: ***capif_api_logging-2*** -* Description: - - This test case will check that a CAPIF subscriber (AEF) cannot create Log Entry without valid aefId -* Pre-Conditions: - - * CAPIF provider is not pre-authorised (has not valid aefId from CAPIF Authority) - * Service exist in CAPIF - * Invoker exist in CAPIF - -* Information of Test: - - 1. Perform [provider onboarding] and [invoker onboarding] - - 2. Publish Service API at CCF: - - Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - - body [service api description] with apiName service_1 - - Use APF Certificate - - 3. Log Entry: - 1. Send POST to *https://{CAPIF_HOSTNAME}/api-invocation-logs/v1/{not-valid-aefId}/logs* - 2. body [log entry request body] - 3. Use AEF Certificate - -* Execution Steps: - 1. Register Provider and Invoker CCF - 2. Publish Service - 3. Create Log Entry - -* Expected Result: - - 1. Response to Logging Service must accomplish: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "Exposer not exist". - * cause with message "Exposer id not found". - -## Test Case 3: Creates a new individual CAPIF Log Entry with Invalid serviceAPI -* Test ID: ***capif_api_logging-3*** -* Description: - - This test case will check that a CAPIF subscriber (AEF) cannot create Log Entry without valid aefId -* Pre-Conditions: - - * CAPIF subscriber is pre-authorised (has valid aefId from CAPIF Authority) - -* Information of Test: - - 1. Perform [provider onboarding] and [invoker onboarding] - - 2. Publish Service API at CCF: - - Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - - body [service api description] with apiName service_1 - - Use APF Certificate - - 3. Log Entry: - 1. Send POST to *https://{CAPIF_HOSTNAME}/api-invocation-logs/v1/{aefId}/logs* - 2. body [log entry request body with serviceAPI apiName apiId not valid] - 3. Use AEF Certificate - -* Execution Steps: - 1. Register Provider and Invoker CCF - 2. Publish Service - 3. Create Log Entry - -* Expected Result: - - 1. Response to Logging Service must accomplish: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "Invoker not exist". - * cause with message "Invoker id not found". - - - -## Test Case 4: Creates a new individual CAPIF Log Entry with Invalid apiInvokerId -* Test ID: ***capif_api_logging-4*** -* Description: - - This test case will check that a CAPIF subscriber (AEF) cannot create Log Entry without valid aefId -* Pre-Conditions: - - * CAPIF subscriber is pre-authorised (has valid aefId from CAPIF Authority) - -* Information of Test: - - 1. Perform [provider onboarding] and [invoker onboarding] - - 2. Publish Service API at CCF: - - Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - - body [service api description] with apiName service_1 - - Use APF Certificate - - 3. Log Entry: - 1. Send POST to *https://{CAPIF_HOSTNAME}/api-invocation-logs/v1/{aefId}/logs* - 2. body [log entry request body with invokerId not valid] - 3. Use AEF Certificate - -* Execution Steps: - 1. Register Provider and Invoker CCF - 2. Publish Service - 3. Create Log Entry - -* Expected Result: - - 1. Response to Onboard request must accomplish: - 1. **201 Created** response. - 2. body returned must accomplish **APIProviderEnrolmentDetails** data structure. - 3. For each **apiProvFuncs**, we must check: - 1. **apiProvFuncId** is set - 2. **apiProvCert** under **regInfo** is set properly - 5. Location Header must contain the new resource URL *{apiRoot}/api-provider-management/v1/registrations/{registrationId}* - - 2. Response to Logging Service must accomplish: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "Invoker not exist". - * cause with message "Invoker id not found". - - 3. Log Entry are not stored in CAPIF Database - - -## Test Case 5: Creates a new individual CAPIF Log Entry with Invalid aefId in body -* Test ID: ***capif_api_logging-5*** -* Description: - - This test case will check that a CAPIF subscriber (AEF) cannot create Log Entry without valid aefId in body -* Pre-Conditions: - - * CAPIF provider is pre-authorised (has valid apfId from CAPIF Authority) - * Service exist in CAPIF - * Invoker exist in CAPIF - -* Information of Test: - - 1. Perform [provider onboarding] and [invoker onboarding] - - 2. Publish Service API at CCF: - - Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - - body [service api description] with apiName service_1 - - Use APF Certificate - - 3. Log Entry: - 1. Send POST to *https://{CAPIF_HOSTNAME}/api-invocation-logs/v1/{aefId}/logs* - 2. body [log entry request body with bad aefId] - 3. Use AEF Certificate - -* Execution Steps: - 1. Register Provider and Invoker CCF - 2. Publish Service - 3. Create Log Entry - -* Expected Result: - - 1. Response to Logging Service must accomplish: - 1. **401 Unauthorized** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 401 - * title with message "Unauthorized" - * detail with message "AEF id not matching in request and body". - * cause with message "Not identical AEF id". - - - - - - -[log entry request body]: ./invocation_log.json "Log Request Body" - -[invoker onboarding]: ../common_operations/README.md#register-an-invoker "Invoker Onboarding" - -[provider onboarding]: ../common_operations/README.md#register-a-provider "Provider Onboarding" - -[Return To All Test Plans]: ../README.md diff --git a/docs/test_plan/api_logging_service/invocation_log.json b/docs/test_plan/api_logging_service/invocation_log.json deleted file mode 100644 index ceabcf0..0000000 --- a/docs/test_plan/api_logging_service/invocation_log.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "aefId": "aefId", - "apiInvokerId": "apiInvokerId", - "logs": [ - { - "apiId": "apiId", - "apiName": "apiName", - "apiVersion": "string", - "resourceName": "string", - "uri": "string", - "protocol": "HTTP_1_1", - "operation": "GET", - "result": "string", - "invocationTime": "2023-03-30T10:30:21.404Z", - "invocationLatency": 0, - "inputParameters": "string", - "outputParameters": "string", - "srcInterface": { - "ipv4Addr": "string", - "ipv6Addr": "string", - "fqdn": "string", - "port": 65535, - "apiPrefix": "string", - "securityMethods": [ - "PSK", - "Oauth" - ] - }, - "destInterface": { - "ipv4Addr": "string", - "ipv6Addr": "string", - "fqdn": "string", - "port": 65535, - "apiPrefix": "string", - "securityMethods": [ - "PSK", - "string" - ] - }, - "fwdInterface": "string" - } - ], - "supportedFeatures": "string" - } - \ No newline at end of file diff --git a/docs/test_plan/api_provider_management/README.md b/docs/test_plan/api_provider_management/README.md deleted file mode 100644 index 547d654..0000000 --- a/docs/test_plan/api_provider_management/README.md +++ /dev/null @@ -1,398 +0,0 @@ -[**[Return To All Test Plans]**] - -- [Test Plan for CAPIF Api Provider Management](#test-plan-for-capif-api-provider-management) -- [Tests](#tests) - - [Test Case 1: Register Api Provider](#test-case-1-register-api-provider) - - [Test Case 2: Register Api Provider Already registered](#test-case-2-register-api-provider-already-registered) - - [Test Case 3: Update Registered Api Provider](#test-case-3-update-registered-api-provider) - - [Test Case 4: Update Not Registered Api Provider](#test-case-4-update-not-registered-api-provider) - - [Test Case 5: Partially Update Registered Api Provider](#test-case-5-partially-update-registered-api-provider) - - [Test Case 6: Partially Update Not Registered Api Provider](#test-case-6-partially-update-not-registered-api-provider) - - [Test Case 7: Delete Registered Api Provider](#test-case-7-delete-registered-api-provider) - - [Test Case 8: Delete Not Registered Api Provider](#test-case-8-delete-not-registered-api-provider) - - -# Test Plan for CAPIF Api Provider Management -At this documentation you will have all information and related files and examples of test plan for this API. - -# Tests - -## Test Case 1: Register Api Provider -* **Test ID**: ***capif_api_provider_management-1*** -* **Description**: - - This test case will check that Api Provider can be registered con CCF -* **Pre-Conditions**: - - * Provider is pre-authorised (has valid certificate from CAPIF Authority) - -* **Information of Test**: - - 1. Create public and private key at provider for provider itself and each function (apf, aef and amf) - - 2. Register of Provider at CCF: - * Send POST to *http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register* - * body [provider register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [provider getauth body] - - 4. Register Provider: - * Send POST *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations* - * body [provider request body] - * Authentication Bearer with access_token - * Store each cert in a file with according name. - -* **Execution Steps**: - - 1. Create private and public key for provider and each function to register. - 2. Register Provider. - -* **Expected Result**: - - 1. Register Provider at Provider Management: - 1. **201 Created** response. - 2. body returned must accomplish **APIProviderEnrolmentDetails** data structure. - 3. For each **apiProvFuncs**, we must check: - 1. **apiProvFuncId** is set - 2. **apiProvCert** under **regInfo** is set properly - 5. Location Header must contain the new resource URL *{apiRoot}/api-provider-management/v1/registrations/{registrationId}* - -## Test Case 2: Register Api Provider Already registered -* **Test ID**: ***capif_api_provider_management-2*** -* **Description**: - - This test case will check that a Api Provider previously registered cannot be re-registered -* **Pre-Conditions**: - - * Api Provider was registered previously and there is a {registerId} for his Api Provider in the DB - -* **Information of Test**: - - 1. Create public and private key at provider for provider itself and each function (apf, aef and amf) - - 2. Register of Provider at CCF: - * Send POST to *http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register* - * body [provider register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [provider getauth body] - - 4. Register Provider: - * Send POST *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations* - * body [provider request body] - * Authentication Bearer with access_token - * Store each cert in a file with according name. - - 5. Re-Register Provider: - * Same regSec than Previous registration - -* **Execution Steps**: - - 1. Create private and public key for provider and each function to register. - 2. Register Provider. - 3. Re-Register Provider. - -* **Expected Result**: - - 1. Re-Register Provider: - 1. **403 Forbidden** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status 403 - * title with message "Forbidden" - * detail with message "Provider already registered". - * cause with message "Identical provider reg sec". - -## Test Case 3: Update Registered Api Provider -* **Test ID**: ***capif_api_provider_management-3*** -* **Description**: - - This test case will check that a Registered Api Provider can be updated -* **Pre-Conditions**: - - * Api Provider was registered previously and there is a {registerId} for his Api Provider in the DB - -* **Information of Test**: - - 1. Create public and private key at provider for provider itself and each function (apf, aef and amf) - - 2. Register of Provider at CCF: - * Send POST to *http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register* - * body [provider register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [provider getauth body] - - 4. Register Provider: - * Send POST *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations* - * body [provider request body] - * Authentication Bearer with access_token - * Get Resource URL from Location - - 5. Update Provider: - * Send PUT to Resource URL returned at registration *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations/{registrationId}* - * body [provider request body] with apiProvDomInfo set to ROBOT_TESTING_MOD - * Use AMF Certificate. - - -* **Execution Steps**: - - 1. Create private and public key for provider and each function to register. - 2. Register Provider - 3. Update Provider - -* **Expected Result**: - 1. Register Provider: - 1. **201 Created** response. - 2. body returned must accomplish **APIProviderEnrolmentDetails** data structure. - 3. Location Header must contain the new resource URL *{apiRoot}/api-provider-management/v1/registrations/{registrationId}* - - - 2. Update Provider: - 1. **200 OK** response. - 2. body returned must accomplish **APIProviderEnrolmentDetails** data structure, with: - * apiProvDomInfo set to ROBOT_TESTING_MOD - - -## Test Case 4: Update Not Registered Api Provider -* **Test ID**: ***capif_api_provider_management-4*** -* **Description**: - - This test case will check that a Non-Registered Api Provider cannot be updated -* **Pre-Conditions**: - - * Api Provider was not registered previously - -* **Information of Test**: - - 1. Create public and private key at provider for provider itself and each function (apf, aef and amf) - - 2. Register of Provider at CCF: - * Send POST to *http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register* - * body [provider register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [provider getauth body] - - 4. Register Provider: - * Send POST *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations* - * body [provider request body] - * Authentication Bearer with access_token - * Store each cert in a file with according name. - - 5. Update Not Registered Provider: - * Send PUT *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations/{API_PROVIDER_NOT_REGISTERED}* - * body [provider request body] - * Use AMF Certificate. - -* **Execution Steps**: - - 1. Register Provider at CCF - 3. Update Not Registered Provider - -* **Expected Result**: - - 1. Update Not Registered Provider: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status 404 - * title with message "Not Found" - * detail with message "Not Exist Provider Enrolment Details". - * cause with message "Not found registrations to send this api provider details". - -## Test Case 5: Partially Update Registered Api Provider -* **Test ID**: ***capif_api_provider_management-5*** -* **Description**: - - This test case will check that a Registered Api Provider can be partially updated -* **Pre-Conditions**: - - * Api Provider was registered previously and there is a {registerId} for his Api Provider in the DB - -* **Information of Test**: - - 1. Create public and private key at provider for provider itself and each function (apf, aef and amf) - - 2. Register of Provider at CCF: - * Send POST to *http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register* - * body [provider register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [provider getauth body] - - 4. Register Provider: - * Send POST *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations* - * body [provider request body] - * Authentication Bearer with access_token - * Store each cert in a file with according name. - - 5. Partial update provider: - * Send PATCH *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations/{registrationId}* - * body [provider request patch body] - * Use AMF Certificate. - -* **Execution Steps**: - - 1. Register Provider at CCF - 2. Register Provider - 3. Partial update provider - -* **Expected Result**: - - 1. Partial update provider at Provider Management: - 1. **200 OK** response. - 2. body returned must accomplish **APIProviderEnrolmentDetails** data structure, with: - * apiProvDomInfo with "ROBOT_TESTING_MOD" - -## Test Case 6: Partially Update Not Registered Api Provider -* **Test ID**: ***capif_api_provider_management-6*** -* **Description**: - - This test case will check that a Non-Registered Api Provider cannot be partially updated - -* **Pre-Conditions**: - - * Api Provider was not registered previously - -* **Information of Test**: - - 1. Create public and private key at provider for provider itself and each function (apf, aef and amf) - - 2. Register of Provider at CCF: - * Send POST to *http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register* - * body [provider register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [provider getauth body] - - 4. Register Provider: - * Send POST *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations* - * body [provider request body] - * Authentication Bearer with access_token - * Store each cert in a file with according name. - - 5. Partial update Provider: - * Send PATCH *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations/{API_API_PROVIDER_NOT_REGISTERED}* - * body [provider request patch body] - * Use AMF Certificate. - - -* **Execution Steps**: - - 1. Register Provider at CCF - 2. Register Provider - 3. Partial update provider - -* **Expected Result**: - - 1. Partial update provider: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status 404 - * title with message "Not Found" - * detail with message "Not Exist Provider Enrolment Details". - * cause with message "Not found registrations to send this api provider details". - -## Test Case 7: Delete Registered Api Provider -* **Test ID**: ***capif_api_provider_management-7*** -* **Description**: - - This test case will check that a Registered Api Provider can be deleted -* **Pre-Conditions**: - - * Api Provider was registered previously - -* **Information of Test**: - - 1. Create public and private key at provider for provider itself and each function (apf, aef and amf) - - 2. Register of Provider at CCF: - * Send POST to *http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register* - * body [provider register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [provider getauth body] - - 4. Register Provider: - * Send POST *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations* - * body [provider request body] - * Authentication Bearer with access_token - * Store each cert in a file with according name. - - 5. Delete registered provider: - * Send DELETE *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations/{registrationId}* - * Use AMF Certificate. - -* **Execution Steps**: - - 1. Register Provider at CCF - 2. Register Provider - 3. Delete Provider - -* **Expected Result**: - - 1. Delete Provider: - 1. **204 No Content** response. - -## Test Case 8: Delete Not Registered Api Provider -* **Test ID**: ***capif_api_provider_management-8*** -* **Description**: - - This test case will check that a Non-Registered Api Provider cannot be deleted -* **Pre-Conditions**: - - * Api Provider was not registered previously - -* **Information of Test**: - - 1. Create public and private key at provider for provider itself and each function (apf, aef and amf) - - 2. Register of Provider at CCF: - * Send POST to *http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register* - * body [provider register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [provider getauth body] - - 4. Register Provider: - * Send POST *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations* - * body [provider request body] - * Authentication Bearer with access_token - * Store each cert in a file with according name. - - 5. Delete registered provider at Provider Management: - * Send DELETE *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations/{API_PROVIDER_NOT_REGISTERED}* - * Use AMF Certificate. - -* **Execution Steps**: - - 1. Register Provider at CCF - 2. Delete Provider - -* **Expected Result**: - - 1. Delete Provider: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status 404 - * title with message "Not Found" - * detail with message "Not Exist Provider Enrolment Details". - * cause with message "Not found registrations to send this api provider details". - -[provider register body]: ./provider_details_post_example.json "API Provider Enrolment Request" - -[provider request body]: ./provider_details_post_example.json "API Provider Enrolment Request" - -[provider request patch body]: ./provider_details_enrolment_details_patch_example.json "API Provider Enrolment Patch Request" - -[provider getauth body]: ./provider_getauth_example.json "Get Auth Example" - -[Return To All Test Plans]: ../README.md diff --git a/docs/test_plan/api_provider_management/provider_details_enrolment_details_patch_example.json b/docs/test_plan/api_provider_management/provider_details_enrolment_details_patch_example.json deleted file mode 100644 index 4dac4f4..0000000 --- a/docs/test_plan/api_provider_management/provider_details_enrolment_details_patch_example.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "regSec": "", - "apiProvFuncs": [ - { - "regInfo": { - "apiProvPubKey": "" - }, - "apiProvFuncRole": "APF", - "apiProvFuncInfo": "APF_ROBOT_TESTING_PROVIDER" - }, - { - "regInfo": { - "apiProvPubKey": "" - }, - "apiProvFuncRole": "AEF", - "apiProvFuncInfo": "AEF_ROBOT_TESTING_PROVIDER" - }, - { - "regInfo": { - "apiProvPubKey": "" - }, - "apiProvFuncRole": "AMF", - "apiProvFuncInfo": "AMF_ROBOT_TESTING_PROVIDER" - } - ], - "apiProvDomInfo": "ROBOT_TESTING", - "suppFeat": "string", - "failReason": "string" -} \ No newline at end of file diff --git a/docs/test_plan/api_provider_management/provider_details_post_example.json b/docs/test_plan/api_provider_management/provider_details_post_example.json deleted file mode 100644 index 48e91ba..0000000 --- a/docs/test_plan/api_provider_management/provider_details_post_example.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "regSec": "string", - "apiProvFuncs": [ - { - "apiProvFuncId": "string", - "regInfo": { - "apiProvPubKey": "string", - "apiProvCert": "string" - }, - "apiProvFuncRole": "AEF", - "apiProvFuncInfo": "string" - } - ], - "apiProvDomInfo": "string", - "suppFeat": "string", - "failReason": "string" -} \ No newline at end of file diff --git a/docs/test_plan/api_provider_management/provider_getauth_example.json b/docs/test_plan/api_provider_management/provider_getauth_example.json deleted file mode 100644 index 8fc82ae..0000000 --- a/docs/test_plan/api_provider_management/provider_getauth_example.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "username": "ROBOT_TESTING_PROVIDER", - "password": "password" -} diff --git a/docs/test_plan/api_provider_management/provider_register_body.json b/docs/test_plan/api_provider_management/provider_register_body.json deleted file mode 100644 index fc26db2..0000000 --- a/docs/test_plan/api_provider_management/provider_register_body.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "password": "password", - "username": "ROBOT_TESTING_PUBLISHER", - "role": "provider", - "description": "Testing", - "cn": "ROBOT_TESTING_PUBLISHER" -} diff --git a/docs/test_plan/api_publish_service/README.md b/docs/test_plan/api_publish_service/README.md deleted file mode 100644 index 8487f5d..0000000 --- a/docs/test_plan/api_publish_service/README.md +++ /dev/null @@ -1,599 +0,0 @@ -[**[Return To All Test Plans]**] - -- [Test Plan for CAPIF Api Publish Service](#test-plan-for-capif-api-publish-service) -- [Tests](#tests) - - [Test Case 1: Publish API by Authorised API Publisher](#test-case-1-publish-api-by-authorised-api-publisher) - - [Test Case 2: Publish API by NON Authorised API Publisher](#test-case-2-publish-api-by-non-authorised-api-publisher) - - [Test Case 3: Retrieve all APIs Published by Authorised apfId](#test-case-3-retrieve-all-apis-published-by-authorised-apfid) - - [Test Case 4: Retrieve all APIs Published by NON Authorised apfId](#test-case-4-retrieve-all-apis-published-by-non-authorised-apfid) - - [Test Case 5: Retrieve single APIs Published by Authorised apfId](#test-case-5-retrieve-single-apis-published-by-authorised-apfid) - - [Test Case 6: Retrieve single APIs non Published by Authorised apfId](#test-case-6-retrieve-single-apis-non-published-by-authorised-apfid) - - [Test Case 7: Retrieve single APIs Published by NON Authorised apfId](#test-case-7-retrieve-single-apis-published-by-non-authorised-apfid) - - [Test Case 8: Update API Published by Authorised apfId with valid serviceApiId](#test-case-8-update-api-published-by-authorised-apfid-with-valid-serviceapiid) - - [Test Case 9: Update APIs Published by Authorised apfId with invalid serviceApiId](#test-case-9-update-apis-published-by-authorised-apfid-with-invalid-serviceapiid) - - [Test Case 10: Update APIs Published by NON Authorised apfId](#test-case-10-update-apis-published-by-non-authorised-apfid) - - [Test Case 11: Delete API Published by Authorised apfId with valid serviceApiId](#test-case-11-delete-api-published-by-authorised-apfid-with-valid-serviceapiid) - - [Test Case 12: Delete APIs Published by Authorised apfId with invalid serviceApiId](#test-case-12-delete-apis-published-by-authorised-apfid-with-invalid-serviceapiid) - - [Test Case 13: Delete APIs Published by NON Authorised apfId](#test-case-13-delete-apis-published-by-non-authorised-apfid) - - -# Test Plan for CAPIF Api Publish Service -At this documentation you will have all information and related files and examples of test plan for this API. - -# Tests - -## Test Case 1: Publish API by Authorised API Publisher -* **Test ID**: ***capif_api_publish_service-1*** -* **Description**: - - This test case will check that an API Publisher can Publish an API -* **Pre-Conditions**: - - * CAPIF subscriber is pre-authorised (has valid apfId from CAPIF Authority) - -* **Information of Test**: - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Publish Service API - 3. Retrieve {apiId} from body and Location header with new resource created from response - -* **Expected Result**: - 1. Response to Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - - 2. Published Service API is stored in CAPIF Database - -## Test Case 2: Publish API by NON Authorised API Publisher -* **Test ID**: ***capif_api_publish_service-2*** -* **Description**: - - This test case will check that an API Publisher cannot Publish an API withot valid apfId -* **Pre-Conditions**: - - * CAPIF subscriber is NOT pre-authorised (has invalid apfId from CAPIF Authority) - -* **Information of Test**: - 1. Perform [Provider Registration] - - 2. Publish Service API with invalid APF ID at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{APF_ID_NOT_VALID}/service-apis* - * body [service api description] with apiName service_1 - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Publish Service API with invalid APF ID - -* **Expected Result**: - 1. Response to Publish request must accomplish: - 1. **401 Unauthorized** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status **401** - * title with message "Unauthorized" - * detail with message "Publisher not existing". - * cause with message "Publisher id not found". - - 2. Service API is NOT stored in CAPIF Database - - -## Test Case 3: Retrieve all APIs Published by Authorised apfId -* **Test ID**: ***capif_api_publish_service-3*** -* **Description**: - - This test case will check that an API Publisher can Retrieve all API published -* **Pre-Conditions**: - - * CAPIF subscriber is pre-authorised (has valid apfId from CAPIF Authority) - * At least 2 service APIs are published. - -* **Information of Test**: - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Get apiId - * Use APF Certificate - - 3. Publish Other Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_2 - * Get apiId - * Use APF Certificate - - 4. Retrieve all published APIs: - * Send Get to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Publish Service API service_1 - 3. Retrieve {apiId1} from body and Location header with new resource created from response - 4. Publish Service API service_2 - 5. Retrieve {apiId2} from body and Location header with new resource created from response - 6. Retrieve All published APIs and check if both are present. - -* **Expected Result**: - 1. Response to service 1 Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId1}* - - 2. Response to service 2 Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId2}* - - 3. Published Service APIs are stored in CAPIF Database - - 4. Response to Retrieve all published APIs: - 1. **200 OK** - 2. Response body must return an array of **ServiceAPIDescription** data. - 3. Array must contain all previously published APIs. - -## Test Case 4: Retrieve all APIs Published by NON Authorised apfId -* **Test ID**: ***capif_api_publish_service-4*** -* **Description**: - - This test case will check that an API Publisher cannot Retrieve API published when apfId is not authorised -* **Pre-Conditions**: - - * CAPIF subscriber is NOT pre-authorised (has invalid apfId from CAPIF Authority) - -* **Information of Test**: - 1. Perform [Provider Registration] - - 2. Retrieve all published APIs: - * Send Get to *https://{CAPIF_HOSTNAME}/published-apis/v1/{APF_ID_NOT_VALID}/service-apis* - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Retrieve All published APIs - -* **Expected Result**: - 1. Response to Publish request must accomplish: - 1. **401 Non Authorized** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status **401** - * title with message "Unauthorized" - * detail with message "Provider not existing". - * cause with message "Provider id not found". - - 2. Service API is NOT stored in CAPIF Database - -## Test Case 5: Retrieve single APIs Published by Authorised apfId -* **Test ID**: ***capif_api_publish_service-5*** -* **Description**: - - This test case will check that an API Publisher can Retrieve API published one by one -* **Pre-Conditions**: - - * CAPIF subscriber is pre-authorised (has valid apfId from CAPIF Authority) - * At least 2 service APIs are published. - -* **Information of Test**: - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Get apiId - * Use APF Certificate - - 3. Publish Other Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_2 - * Get apiId - * Use APF Certificate - - 4. Retrieve service_1 published APIs detail: - * Send Get to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{apiId1}* - * Use APF Certificate - - 5. Retrieve service_2 published APIs detail: - * Send Get to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{apiId2}* - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Publish Service API service_1. - 3. Retrieve {apiId1} from body and Location header with new resource created from response. - 4. Publish Service API service_2. - 5. Retrieve {apiId2} from body and Location header with new resource created from response. - 6. Retrieve service_1 API Detail. - 7. Retrieve service_2 API Detail. - -* **Expected Result**: - 1. Response to service 1 Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId1}* - - 2. Response to service 2 Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId2}* - - 3. Published Service APIs are stored in CAPIF Database - - 4. Response to Retrieve service_1 published API using apiId1: - 1. **200 OK** - 2. Response body must return a **ServiceAPIDescription** data. - 3. Array must contain same information than service_1 published registration response. - - 5. Response to Retrieve service_2 published API using apiId2: - 1. **200 OK** - 2. Response body must return a **ServiceAPIDescription** data. - 3. Array must contain same information than service_2 published registration response. - - -## Test Case 6: Retrieve single APIs non Published by Authorised apfId -* **Test ID**: ***capif_api_publish_service-6*** -* **Description**: - - This test case will check that an API Publisher try to get detail of not published api. -* **Pre-Conditions**: - - * CAPIF subscriber is pre-authorised (has valid apfId from CAPIF Authority) - * No published api - -* **Information of Test**: - 1. Perform [Provider Registration] - 2. Retrieve not published APIs detail: - * Send Get to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{SERVICE_API_ID_NOT_VALID}* - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Retrieve not published API Detail. - -* **Expected Result**: - 1. Response to Retrieve for NOT published API must accomplish: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status **404** - * title with message "Not Found" - * detail with message "Service API not found". - * cause with message "No Service with specific credentials exists". - - -## Test Case 7: Retrieve single APIs Published by NON Authorised apfId -* **Test ID**: ***capif_api_publish_service-7*** -* **Description**: - - This test case will check that an API Publisher cannot Retrieve detailed API published when apfId is not authorised -* **Pre-Conditions**: - - * CAPIF subscriber is NOT pre-authorised (has invalid apfId from CAPIF Authority) - -* **Information of Test**: - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Get apiId - * Use APF Certificate - - 3. Retrieve detailed published APIs: - * Send Get to *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/${apiId}* - * Use Invoker certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Publish Service API at CCF - 3. Retrieve {apiId} from body and Location header with new resource created from response. - 4. Register and onboard Invoker at CCF - 5. Store signed Invoker Certificate - 6. Retrieve detailed published API acting as Invoker - -* **Expected Result**: - 1. Response to Retrieve Detailed published API acting as Invoker must accomplish: - 1. **401 Unauthorized** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status **401** - * title with message "Unauthorized" - * detail with message "User not authorized". - * cause with message "Certificate not authorized". - - 2. Service API is NOT stored in CAPIF Database - - -## Test Case 8: Update API Published by Authorised apfId with valid serviceApiId -* **Test ID**: ***capif_api_publish_service-8*** -* **Description**: - - This test case will check that an API Publisher can Update published API with a valid serviceApiId -* **Pre-Conditions**: - - * CAPIF subscriber is pre-authorised (has valid apfId from CAPIF Authority) - * A service APIs is published. - -* **Information of Test**: - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Get apiId - * get resource url from location Header. - * Use APF Certificate - - 3. Update published API at CCF: - * Send PUT to resource URL *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{serivceApiId}* - * body [service api description] with overrided apiName to service_1_modified - * Use APF Certificate - - 4. Retrieve detail of service API: - * Send Get to resource URL *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{serivceApiId}* - * check apiName is service_1_modified - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Publish Service API - 3. Retrieve {apiId} from body and Location header with new resource url created from response - 4. Update published Service API. - 5. Retrieve detail of Service API - -* **Expected Result**: - 1. Response to Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - - 2. Response to Update Published Service API: - 1. **200 OK** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiName service_1_modified - - 3. Response to Retrieve detail of Service API: - 1. **200 OK** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiName service_1_modified. - - -## Test Case 9: Update APIs Published by Authorised apfId with invalid serviceApiId -* **Test ID**: ***capif_api_publish_service-9*** -* **Description**: - - This test case will check that an API Publisher cannot Update published API with a invalid serviceApiId -* **Pre-Conditions**: - - * CAPIF subscriber is pre-authorised (has valid apfId from CAPIF Authority) - -* **Information of Test**: - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Get apiId - * Use APF Certificate - - 3. Update published API at CCF: - * Send PUT to resource URL *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{SERVICE_API_ID_NOT_VALID}* - * body [service api description] with overrided apiName to ***service_1_modified*** - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Update published Service API. - -* **Expected Result**: - 1. Response to Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - - 2. Response to Update Published Service API: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status **404** - * title with message "Not Found" - * detail with message "Service API not found". - * cause with message "Service API id not found". - - ## Test Case 10: Update APIs Published by NON Authorised apfId -* **Test ID**: ***capif_api_publish_service-10*** -* **Description**: - - This test case will check that an API Publisher cannot Update API published when apfId is not authorised -* **Pre-Conditions**: - - * CAPIF subscriber is NOT pre-authorised (has invalid apfId from CAPIF Authority) - -* **Information of Test**: - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Get apiId - * Use APF Certificate - - 3. Update published API at CCF: - * Send PUT to resource URL *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - * body [service api description] with overrided apiName to ***service_1_modified*** - * Use invoker certificate - - 4. Retrieve detail of service API: - * Send Get to resource URL *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{serivceApiId}* - * check apiName is service_1 - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Publish Service API at CCF - 3. Retrieve {apiId} from body and Location header with new resource created from response. - 4. Register and onboard Invoker at CCF - 5. Store signed Invoker Certificate - 6. Update published API at CCF as Invoker - 7. Retrieve detail of Service API as publisher - -* **Expected Result**: - 1. Response to Update published API acting as Invoker must accomplish: - 1. **401 Unauthorized** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status **401** - * title with message "Unauthorized" - * detail with message "User not authorized". - * cause with message "Certificate not authorized". - - 2. Response to Retrieve Detail of Service API: - 1. **200 OK** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiName service_1. - - -## Test Case 11: Delete API Published by Authorised apfId with valid serviceApiId -* **Test ID**: ***capif_api_publish_service-11*** -* **Description**: - - This test case will check that an API Publisher can Delete published API with a valid serviceApiId -* **Pre-Conditions**: - - * CAPIF subscriber is pre-authorised (has valid apfId from CAPIF Authority). - * A service APIs is published. - -* **Information of Test**: - 1. Perform [Provider Registration] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Get apiId - * Use APF Certificate - - 3. Remove published Service API at CCF: - * Send DELETE to resource URL *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - * Use APF Certificate - 4. Retrieve detail of service API: - * Send Get to resource URL *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{serivceApiId}* - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Publish Service API - 3. Retrieve {apiId} from body and Location header with new resource created from response - 4. Remove published API at CCF - 5. Try to retreive deleted service API from CCF - -* **Expected Result**: - 1. Response to Publish request must accomplish: - 1. **201 Created** - 2. Response Body must follow **ServiceAPIDescription** data structure with: - * apiId - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/published-apis/v1/{apfId}/service-apis/{serviceApiId}* - - 2. Published Service API is stored in CAPIF Database - - 3. Response to Remove published Service API at CCF: - 1. **204 No Content** - - 4. Response to Retrieve for DELETED published API must accomplish: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "Service API not found". - * cause with message "No Service with specific credentials exists". - - -## Test Case 12: Delete APIs Published by Authorised apfId with invalid serviceApiId -* **Test ID**: ***capif_api_publish_service-12*** -* **Description**: - - This test case will check that an API Publisher cannot Delete with invalid serviceApiId -* **Pre-Conditions**: - - * CAPIF subscriber is pre-authorised (has valid apfId from CAPIF Authority). - -* **Information of Test**: - 1. Perform [Provider Registration] - - 2. Remove published Service API at CCF with invalid serviceId: - * Send DELETE to resource URL *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{SERVICE_API_ID_NOT_VALID}* - * Use APF Certificate - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Remove published API at CCF with invalid serviceId - -* **Expected Result**: - 1. Response to Remove published Service API at CCF: - 1. **404 Not Found** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status 404 - * title with message "Not Found" - * detail with message "Service API not found". - * cause with message "Service API id not found". - - -## Test Case 13: Delete APIs Published by NON Authorised apfId -* **Test ID**: ***capif_api_publish_service-12*** -* **Description**: - - This test case will check that an API Publisher cannot Delete API published when apfId is not authorised -* **Pre-Conditions**: - - * CAPIF subscriber is pre-authorised (has valid apfId from CAPIF Authority). - -* **Information of Test**: - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis* - * body [service api description] with apiName service_1 - * Get apiId - * Use APF Certificate - - 3. Remove published Service API at CCF with invalid serviceId as Invoker: - * Send DELETE to resource URL *https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis/{SERVICE_API_ID_NOT_VALID}* - * Use invoker certificate. - -* **Execution Steps**: - 1. Register Provider at CCF and store certificates. - 2. Register Invoker and onboard Invoker at CCF - 3. Remove published API at CCF with invalid serviceId as Invoker - -* **Expected Result**: - 1. Response to Remove published Service API at CCF: - 1. **401 Unauthorized** - 2. Error Response Body must accomplish with **ProblemDetails** data structure with: - * status **401** - * title with message "Unauthorized" - * detail with message "User not authorized". - * cause with message "Certificate not authorized". - - - [service api description]: ./service_api_description_post_example.json "Service API Description Request" - [publisher register body]: ./publisher_register_body.json "Publish register Body" - [invoker onboarding body]: ../api_invoker_management/invoker_details_post_example.json "API Invoker Request" - [invoker register body]: ../api_invoker_management/invoker_register_body.json "Invoker Register Body" - [provider request body]: ../api_provider_management/provider_details_post_example.json "API Provider Enrolment Request" - [provider request patch body]: ../api_provider_management/provider_details_enrolment_details_patch_example.json "API Provider Enrolment Patch Request" - [provider getauth body]: ../api_provider_management/provider_getauth_example.json "Get Auth Example" - - [invoker onboarding]: ../common_operations/README.md#register-an-invoker "Invoker Onboarding" - [provider registration]: ../common_operations/README.md#register-a-provider "Provider Registration" - - - [Return To All Test Plans]: ../README.md \ No newline at end of file diff --git a/docs/test_plan/api_publish_service/publisher_register_body.json b/docs/test_plan/api_publish_service/publisher_register_body.json deleted file mode 100644 index fc26db2..0000000 --- a/docs/test_plan/api_publish_service/publisher_register_body.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "password": "password", - "username": "ROBOT_TESTING_PUBLISHER", - "role": "provider", - "description": "Testing", - "cn": "ROBOT_TESTING_PUBLISHER" -} diff --git a/docs/test_plan/api_publish_service/service_api_description_post_example.json b/docs/test_plan/api_publish_service/service_api_description_post_example.json deleted file mode 100644 index b725b42..0000000 --- a/docs/test_plan/api_publish_service/service_api_description_post_example.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "apiName": "service_1", - "aefProfiles": [ - { - "aefId": "string", - "versions": [ - { - "apiVersion": "v1", - "expiry": "2021-11-30T10:32:02.004Z", - "resources": [ - { - "resourceName": "string", - "commType": "REQUEST_RESPONSE", - "uri": "string", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ], - "custOperations": [ - { - "commType": "REQUEST_RESPONSE", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ] - } - ], - "protocol": "HTTP_1_1", - "dataFormat": "JSON", - "securityMethods": ["PSK"], - "interfaceDescriptions": [ - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - }, - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - } - ] - }, - { - "aefId": "string", - "versions": [ - { - "apiVersion": "v1", - "expiry": "2021-11-30T10:32:02.004Z", - "resources": [ - { - "resourceName": "string", - "commType": "REQUEST_RESPONSE", - "uri": "string", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ], - "custOperations": [ - { - "commType": "REQUEST_RESPONSE", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ] - } - ], - "protocol": "HTTP_1_1", - "dataFormat": "JSON", - "securityMethods": ["PSK"], - "interfaceDescriptions": [ - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - }, - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - } - ] - } - ], - "description": "string", - "supportedFeatures": "fffff", - "shareableInfo": { - "isShareable": true, - "capifProvDoms": [ - "string" - ] - }, - "serviceAPICategory": "string", - "apiSuppFeats": "fffff", - "pubApiPath": { - "ccfIds": [ - "string" - ] - }, - "ccfId": "string" -} \ No newline at end of file diff --git a/docs/test_plan/api_security_service/README.md b/docs/test_plan/api_security_service/README.md deleted file mode 100644 index c0d3e71..0000000 --- a/docs/test_plan/api_security_service/README.md +++ /dev/null @@ -1,1244 +0,0 @@ -[**[Return To All Test Plans]**] - -- [Test Plan for CAPIF Api Security Service](#test-plan-for-capif-api-security-service) -- [Tests](#tests) - - [Test Case 1: Create a security context for an API invoker](#test-case-1-create-a-security-context-for-an-api-invoker) - - [Test Case 2: Create a security context for an API invoker with Provider role](#test-case-2-create-a-security-context-for-an-api-invoker-with-provider-role) - - [Test Case 3: Create a security context for an API invoker with Provider entity role and invalid apiInvokerId](#test-case-3-create-a-security-context-for-an-api-invoker-with-provider-entity-role-and-invalid-apiinvokerid) - - [Test Case 4: Create a security context for an API invoker with Invoker entity role and invalid apiInvokerId](#test-case-4-create-a-security-context-for-an-api-invoker-with-invoker-entity-role-and-invalid-apiinvokerid) - - [Test Case 5: Retrieve the Security Context of an API Invoker](#test-case-5-retrieve-the-security-context-of-an-api-invoker) - - [Test Case 6: Retrieve the Security Context of an API Invoker with invalid apiInvokerID](#test-case-6-retrieve-the-security-context-of-an-api-invoker-with-invalid-apiinvokerid) - - [Test Case 7: Retrieve the Security Context of an API Invoker with invalid apfId](#test-case-7-retrieve-the-security-context-of-an-api-invoker-with-invalid-apfid) - - [Test Case 8: Delete the Security Context of an API Invoker](#test-case-8-delete-the-security-context-of-an-api-invoker) - - [Test Case 9: Delete the Security Context of an API Invoker with Invoker entity role](#test-case-9-delete-the-security-context-of-an-api-invoker-with-invoker-entity-role) - - [Test Case 10: Delete the Security Context of an API Invoker with Invoker entity role and invalid apiInvokerID](#test-case-10-delete-the-security-context-of-an-api-invoker-with-invoker-entity-role-and-invalid-apiinvokerid) - - [Test Case 11: Delete the Security Context of an API Invoker with invalid apiInvokerID](#test-case-11-delete-the-security-context-of-an-api-invoker-with-invalid-apiinvokerid) - - [Test Case 12: Update the Security Context of an API Invoker](#test-case-12-update-the-security-context-of-an-api-invoker) - - [Test Case 13: Update the Security Context of an API Invoker with Provider entity role](#test-case-13-update-the-security-context-of-an-api-invoker-with-provider-entity-role) - - [Test Case 14: Update the Security Context of an API Invoker with AEF entity role and invalid apiInvokerId](#test-case-14-update-the-security-context-of-an-api-invoker-with-aef-entity-role-and-invalid-apiinvokerid) - - [Test Case 15: Update the Security Context of an API Invoker with invalid apiInvokerID](#test-case-15-update-the-security-context-of-an-api-invoker-with-invalid-apiinvokerid) - - [Test Case 16: Revoke the authorization of the API invoker for APIs.](#test-case-16-revoke-the-authorization-of-the-api-invoker-for-apis) - - [Test Case 17: Revoke the authorization of the API invoker for APIs without valid apfID.](#test-case-17-revoke-the-authorization-of-the-api-invoker-for-apis-without-valid-apfid) - - [Test Case 18: Revoke the authorization of the API invoker for APIs with invalid apiInvokerId.](#test-case-18-revoke-the-authorization-of-the-api-invoker-for-apis-with-invalid-apiinvokerid) - - [Test Case 19: Retrieve access token](#test-case-19-retrieve-access-token) - - [Test Case 20: Retrieve access token by Provider](#test-case-20-retrieve-access-token-by-provider) - - [Test Case 21: Retrieve access token by Provider with invalid apiInvokerId](#test-case-21-retrieve-access-token-by-provider-with-invalid-apiinvokerid) - - [Test Case 22: Retrieve access token with invalid apiInvokerId](#test-case-22-retrieve-access-token-with-invalid-apiinvokerid) - - [Test Case 23: Retrieve access token with invalid client\_id](#test-case-23-retrieve-access-token-with-invalid-client_id) - - [Test Case 24: Retrieve access token with unsupported grant\_type](#test-case-24-retrieve-access-token-with-unsupported-grant_type) - - [Test Case 25: Retrieve access token with invalid scope](#test-case-25-retrieve-access-token-with-invalid-scope) - - [Test Case 26: Retrieve access token with invalid aefid at scope](#test-case-26-retrieve-access-token-with-invalid-aefid-at-scope) - - [Test Case 27: Retrieve access token with invalid apiName at scope](#test-case-27-retrieve-access-token-with-invalid-apiname-at-scope) - - - -# Test Plan for CAPIF Api Security Service -At this documentation you will have all information and related files and examples of test plan for this API. - -# Tests - -## Test Case 1: Create a security context for an API invoker -* **Test ID**: ***capif_security_api-1*** -* **Description**: - - This test case will check that an API Invoker can create a Security context -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID from CAPIF Authority) - -* **Information of Test**: - 1. Perform [Invoker Onboarding] - 2. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Use Invoker Certificate - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Store signed Certificate - 3. Create Security Context - -* **Expected Result**: - - 1. Create security context: - 1. **201 Created** response. - 2. body returned must accomplish **ServiceSecurity** data structure. - 3. Location Header must contain the new resource URL *{apiRoot}/capif-security/v1/trustedInvokers/{apiInvokerId}* - - -## Test Case 2: Create a security context for an API invoker with Provider role -* **Test ID**: ***capif_security_api-2*** -* **Description**: - - This test case will check that an Provider cannot create a Security context with valid apiInvokerId. -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID), but user that create Security Context with Provider role - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Create Security Context for this Invoker but using Provider certificate. - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using AEF certificate - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Register Provider at CCF - 3. Create Security Context using Provider certificate - -* **Expected Result**: - - 1. Create security context using Provider certificate: - 1. **401 Unauthorized** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **401** - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "User role must be invoker". - - 2. No context stored at DB - -## Test Case 3: Create a security context for an API invoker with Provider entity role and invalid apiInvokerId -* **Test ID**: ***capif_security_api-3*** -* **Description**: - - This test case will check that an Provider cannot create a Security context with invalid apiInvokerID. -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID), but user that create Security Context with Provider role - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Create Security Context for this not valid apiInvokerId and using Provider certificate. - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{API_INVOKER_NOT_VALID}* - * body [service security body] - * Using AEF certificate - -* **Execution Steps**: - - 1. Register Provider at CCF - 2. Create Security Context using Provider certificate - -* **Expected Result**: - - 1. Create security context using Provider certificate: - 1. **401 Unauthorized** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **401** - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "User role must be invoker". - 2. No context stored at DB - -## Test Case 4: Create a security context for an API invoker with Invoker entity role and invalid apiInvokerId -* **Test ID**: ***capif_security_api-4*** -* **Description**: - - This test case will check that an Invoker cannot create a Security context with valid apiInvokerId. -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID), but user that create Security Context with invalid apiInvokerId - -* **Information of Test**: - 1. Perform [Invoker Onboarding] - - 2. Create Security Context for this Invoker: - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{API_INVOKER_NOT_VALID}* - * body [service security body] - * Use Invoker Certificate - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Create Security Context using Provider certificate - -* **Expected Result**: - - 1. Create security context using Provider certificate: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **404** - * title with message "Not Found" - * detail with message "Invoker not found". - * cause with message "API Invoker not exists or invalid ID". - - 2. No context stored at DB - - -## Test Case 5: Retrieve the Security Context of an API Invoker -* **Test ID**: ***capif_security_api-5*** -* **Description**: - - This test case will check that an provider can retrieve the Security context of an API Invoker -* **Pre-Conditions**: - - * Provider is pre-authorised (has valid apfId from CAPIF Authority) and API Invoker has created a valid Security Context - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Create Security Context for this Invoker. - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker certificate - - 3. Retrieve Security Context of Invoker by Provider: - * Send GET *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * Using AEF Certificate - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Register Provider at CCF - 3. Create Security Context using Provider certificate - 4. Retrieve Security Context by Provider - -* **Expected Result**: - 1. Retrieve security context: - 1. **200 OK** response. - 2. body returned must accomplish **ServiceSecurity** data structure. - - -## Test Case 6: Retrieve the Security Context of an API Invoker with invalid apiInvokerID -* **Test ID**: ***capif_security_api-6*** -* **Description**: - - This test case will check that an provider can retrieve the Security context of an API Invoker -* **Pre-Conditions**: - - * Provider is pre-authorised (has valid apfId from CAPIF Authority) and API Invoker has created a valid Security Context - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Retrieve Security Context of invalid Invoker by Provider: - * Send GET *https://{CAPIF_HOSTNAME}/trustedInvokers/{API_INVOKER_NOT_VALID}* - * Using AEF Certificate. - -* **Execution Steps**: - - 2. Register Provider at CCF - 3. Create Security Context using Provider certificate - 4. Retrieve Security Context by Provider of invalid invoker - -* **Expected Result**: - 1. Retrieve security context: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **404** - * title with message "Not Found" - * detail with message "Invoker not found". - * cause with message "API Invoker not exists or invalid ID". - - -## Test Case 7: Retrieve the Security Context of an API Invoker with invalid apfId -* **Test ID**: ***capif_security_api-7*** -* **Description**: - - This test case will check that an Provider cannot retrieve the Security context of an API Invoker without valid apfId -* **Pre-Conditions**: - - * API Exposure Function is not pre-authorised (has invalid apfId) - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate - - 3. Retrieve Security Context as Invoker role: - * Send GET *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * Using Invoker Certificate - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Store signed Certificate - 3. Create Security Context - 4. Retrieve Security Context as Provider. - -* **Expected Result**: - - 1. Create security context: - 1. **401 Unauthorized** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **401** - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "User role must be aef". - - -## Test Case 8: Delete the Security Context of an API Invoker -* **Test ID**: ***capif_security_api-8*** -* **Description**: - - This test case will check that an Provider can delete a Security context -* **Pre-Conditions**: - - * Provider is pre-authorised (has valid apfId from CAPIF Authority) and API Invoker has created a valid Security Context - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Create Security Context for this Invoker but using Provider certificate. - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using AEF certificate - - 3. Delete Security Context of Invoker by Provider: - * Send DELETE *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * Use AEF certificate - - 4. Retrieve Security Context of Invoker by Provider: - * Send GET *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * Using AEF Certificate - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Register Provider at CCF - 3. Create Security Context using Provider certificate - 4. Delete Security Context by Provider - -* **Expected Result**: - - 1. Delete security context: - 1. **204 No Content** response. - - 2. Retrieve security context: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **404** - * title with message "Not Found" - * detail with message "Security context not found". - * cause with message "API Invoker not exists or invalid ID". - - -## Test Case 9: Delete the Security Context of an API Invoker with Invoker entity role -* **Test ID**: ***capif_security_api-9*** -* **Description**: - - This test case will check that an Invoker cannot delete a Security context -* **Pre-Conditions**: - - * Provider is pre-authorised (has valid apfId from CAPIF Authority) and API Invoker has created a valid Security Context - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Create Security Context for this Invoker: - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker certificate - - 3. Delete Security Context of Invoker: - * Send DELETE *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * Use Invoker certificate - -* **Execution Steps**: - - 1. Register Provider at CCF - 2. Create Security Context using Provider certificate - 3. Delete Security Context by Invoker - -* **Expected Result**: - - 1. Delete security context: - 1. **401 Unauthorized** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **401** - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "User role must be aef". - - -## Test Case 10: Delete the Security Context of an API Invoker with Invoker entity role and invalid apiInvokerID -* **Test ID**: ***capif_security_api-10*** -* **Description**: - - This test case will check that an Invoker cannot delete a Security context with invalid -* **Pre-Conditions**: - - * Invoker is pre-authorised. - -* **Information of Test**: - - 1. Perform [Invoker Onboarding] - - 2. Delete Security Context of Invoker: - * Send DELETE *https://{CAPIF_HOSTNAME}/trustedInvokers/{API_INVOKER_NOT_VALID}* - * Use Invoker certificate - -* **Execution Steps**: - - 1. Register Provider at CCF - 2. Delete Security Context by invoker - -* **Expected Result**: - - 1. Delete security context: - 1. **401 Unauthorized** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **401** - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "User role must be aef". - - -## Test Case 11: Delete the Security Context of an API Invoker with invalid apiInvokerID -* **Test ID**: ***capif_security_api-11*** -* **Description**: - - This test case will check that an Provider cannot delete a Security context of invalid apiInvokerId -* **Pre-Conditions**: - - * Provider is pre-authorised (has valid apfId from CAPIF Authority). - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 2. Delete Security Context of Invoker by Provider: - * Send DELETE *https://{CAPIF_HOSTNAME}/trustedInvokers/{API_INVOKER_NOT_VALID}* - * Use AEF certificate - -* **Execution Steps**: - - 1. Register Provider at CCF - 2. Delete Security Context by provider - -* **Expected Result**: - - 1. Retrieve security context: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **404** - * title with message "Not Found" - * detail with message "Invoker not found". - * cause with message "API Invoker not exists or invalid ID". - - -## Test Case 12: Update the Security Context of an API Invoker -* **Test ID**: ***capif_security_api-12*** -* **Description**: - - This test case will check that an API Invoker can update a Security context -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID from CAPIF Authority) and Provider is also authorized - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Create Security Context for this Invoker: - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - - 3. Update Security Context of Invoker: - * Send POST *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}/update* - * body [service security body] but with notification destination modified to http://robot.testing2 - * Using Invoker Certificate. - - 4. Retrieve Security Context of Invoker by Provider: - * Send GET *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * Using AEF Certificate. - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Register Provider at CCF - 3. Create Security Context By Invoker - 4. Update Security Context By Invoker - 5. Retrieve Security Context By Provider - -* **Expected Result**: - - 1. Update security context: - 1. **200 OK** response. - 2. body returned must accomplish **ServiceSecurity** data structure. - - 2. Retrieve security context: - 1. **200 OK** response. - 2. body returned must accomplish **ServiceSecurity** data structure. - 1. Check is this returned object match with modified one. - - -## Test Case 13: Update the Security Context of an API Invoker with Provider entity role -* **Test ID**: ***capif_security_api-13*** -* **Description**: - - This test case will check that an Provider cannot update a Security context - -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID from CAPIF Authority) and Provider is also authorized. - * Invoker has created the Security Context previously. - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Create Security Context for this Invoker: - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - - 3. Update Security Context of Invoker by Provider: - * Send POST *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}/update* - * body [service security body] but with notification destination modified to http://robot.testing2 - * Using AEF Certificate - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Register Provider at CCF - 3. Create Security Context - 4. Update Security Context as Provider - -* **Expected Result**: - - 1. Update security context: - 1. **401 Unauthorized** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **401** - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "User role must be invoker". - - -## Test Case 14: Update the Security Context of an API Invoker with AEF entity role and invalid apiInvokerId -* **Test ID**: ***capif_security_api-14*** -* **Description**: - - This test case will check that an Provider cannot update a Security context of invalid apiInvokerId - -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID from CAPIF Authority) and Provider is also authorized. - * Invoker has created the Security Context previously. - -* **Information of Test**: - - 1. Perform [Provider Registration] - - 4. Update Security Context of Invoker by Provider: - * Send POST *https://{CAPIF_HOSTNAME}/trustedInvokers/{API_INVOKER_NOT_VALID}/update* - * body [service security body] - * Using AEF Certificate - -* **Execution Steps**: - - 1. Register Provider at CCF - 2. Update Security Context as Provider - -* **Expected Result**: - - 1. Update security context: - 1. **401 Unauthorized** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **401** - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "User role must be invoker". - - -## Test Case 15: Update the Security Context of an API Invoker with invalid apiInvokerID -* **Test ID**: ***capif_security_api-15*** -* **Description**: - - This test case will check that an API Invoker cannot update a Security context not valid apiInvokerId -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID from CAPIF Authority) - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Update Security Context of Invoker: - * Send POST *https://{CAPIF_HOSTNAME}/trustedInvokers/{API_INVOKER_NOT_VALID}/update* - * body [service security body] - * Using Invoker Certificate. - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Update Security Context - -* **Expected Result**: - -1. Retrieve security context: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **404** - * title with message "Not Found" - * detail with message "Invoker not found". - * cause with message "API Invoker not exists or invalid ID". - - -## Test Case 16: Revoke the authorization of the API invoker for APIs. -* **Test ID**: ***capif_security_api-16*** -* **Description**: - - This test case will check that a Provider can revoke the authorization for APIs - -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID from CAPIF Authority) and Provider is also authorized - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Create Security Context By Invoker: - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate - - 3. Revoke Authorization by Provider: - * Send POST *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}/delete* - * body [security notification body] - * Using AEF Certificate. - - 4. Retrieve Security Context by Provider: - * Send GET *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * Using AEF Certificate. - - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Register Provider at CCF - 3. Create Security Context by Invoker - 4. Revoke Security Context by Provider - 5. Retrieve Security Context by Provider - -* **Expected Result**: - - 1. Revoke Authorization: - 1. **204 No Content** response. - - 2. Retrieve security context: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **404** - * title with message "Not Found" - * detail with message "Security context not found". - * cause with message "API Invoker has no security context". - - -## Test Case 17: Revoke the authorization of the API invoker for APIs without valid apfID. -* **Test ID**: ***capif_security_api-17*** -* **Description**: - - This test case will check that an Invoker can't revoke the authorization for APIs - -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID from CAPIF Authority) and Provider is also authorized - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Create Security Context for this Invoker: - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - - 3. Revoke Authorization by invoker: - * Send POST *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}/delete* - * body [security notification body] - * Using Invoker Certificate - - 4. Retrieve Security Context of Invoker by Provider: - * Send GET *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * Using Provider Certificate - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Register Provider at CCF - 3. Create Security Context - 4. Revoke Security Context by invoker - 5. Retrieve Security Context - -* **Expected Result**: - - 1. Revoke Security Context by invoker: - 1. **401 Unauthorized** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **401** - * title with message "Unauthorized" - * detail with message "Role not authorized for this API route". - * cause with message "User role must be provider". - - 3. Retrieve security context: - 1. **200 OK** response. - 2. body returned must accomplish **ServiceSecurity** data structure. - 1. Check is this returned object match with created one. - - -## Test Case 18: Revoke the authorization of the API invoker for APIs with invalid apiInvokerId. -* **Test ID**: ***capif_security_api-18*** -* **Description**: - - This test case will check that an API Exposure Function cannot revoke the authorization for APIs for invalid apiInvokerId - -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID from CAPIF Authority) and Provider is also authorized - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Create Security Context for this Invoker: - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - - 3. Revoke Authorization by Provider: - * Send POST *https://{CAPIF_HOSTNAME}/trustedInvokers/{API_INVOKER_NOT_VALID}/delete* - * body [security notification body] - * Using AEF Certificate. - - 4. Retrieve Security Context of Invoker by Provider: - * Send GET *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}?authenticationInfo=true&authorizationInfo=true* - * This request will ask with parameter to retrieve authenticationInfo and authorizationInfo - * Using AEF Certificate. - -* **Execution Steps**: - - 1. Register and onboard Invoker at CCF - 2. Register Provider at CCF - 3. Create Security Context - 4. Revoke Security Context by Provider - 5. Retrieve Security Context - -* **Expected Result**: - - 1. Revoke Security Context by invoker: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails** data structure, with: - * status **404** - * title with message "Not Found" - * detail with message "Invoker not found". - * cause with message "API Invoker not exists or invalid ID". - - 3. Retrieve security context: - 1. **200 OK** response. - 2. body returned must accomplish **ServiceSecurity** data structure. - 1. Check is this return one object that match with created one. - - -## Test Case 19: Retrieve access token -* **Test ID**: ***capif_security_api-19*** -* **Description**: - - This test case will check that an API Invoker can retrieve a security access token OAuth 2.0. -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerId) - * Service API of Provider is published - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - * body [service api description] with apiName service_1 - * Use APF Certificate - - 3. Request Discover Published APIs not filtered: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Using invoker certificate - - 4. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - * Create Security Information Body with one **securityInfo** for each aef present at each serviceAPIDescription present at Discover. - - 5. Request Access Token by invoker: - * Sent POST *https://{CAPIF_HOSTNAME}/securities/{securityId}/token*: - * body [access token req body] and example [example] - * ***securityId*** is apiInvokerId. - * ***grant_type=client_credentials***. - * Create Scope properly for request: ***3gpp#{aef_id}:{api_name}*** - * Using Invoker Certificate. - -* **Execution Steps**: - - 1. Register Provider at CCF, store certificates and Publish Service API service_1 at CCF - 2. Register and onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Create Security Context According to Service APIs discovered. - 5. Request Access Token - -* **Expected Result**: - - 1. Response to Request of Access Token: - 1. **200 OK** - 2. body must follow **AccessTokenRsp** with: - 1. access_token present - 2. token_type=Bearer - -## Test Case 20: Retrieve access token by Provider -* **Test ID**: ***capif_security_api-20*** -* **Description**: - - This test case will check that an API Exposure Function cannot revoke the authorization for APIs for invalid apiInvokerId - -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerID from CAPIF Authority) and Provider is also authorized - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - * body [service api description] with apiName service_1 - * Use APF Certificate - - 3. Request Discover Published APIs not filtered: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Using invoker certificate - - 4. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - * Create Security Information Body with one **securityInfo** for each aef present at each serviceAPIDescription present at Discover. - - 5. Request Access Token by provider: - * Sent POST *https://{CAPIF_HOSTNAME}/securities/{securityId}/token*: - * body [access token req body] - * ***securityId*** is apiInvokerId - * ***grant_type=client_credentials*** - * Using AEF certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 at CCF - 2. Register and onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Create Security Context According to Service APIs discovered. - 5. Request Access Token by Provider - -* **Expected Result**: - - 1. Response to Request of Access Token: - 1. **401 Unauthorized** response. - 2. body returned must accomplish **AccessTokenErr** data structure, with: - * error unauthorized_client - * error_description=Role not authorized for this API route - -## Test Case 21: Retrieve access token by Provider with invalid apiInvokerId -* **Test ID**: ***capif_security_api-21*** -* **Description**: - - This test case will check that an API Exposure Function cannot retrieve a security access token without valid apiInvokerId - -* **Pre-Conditions**: - - * API Invoker is pre-authorised and Provider is also authorized - - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - * body [service api description] with apiName service_1 - * Use APF Certificate - - 3. Request Discover Published APIs not filtered: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Using invoker certificate - - 4. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - * Create Security Information Body with one **securityInfo** for each aef present at each serviceAPIDescription present at Discover. - - 5. Request Access Token by provider: - * Sent POST *https://{CAPIF_HOSTNAME}/securities/{API_INVOKER_NOT_VALID}/token*. - * body [access token req body] - * ***securityId*** is apiInvokerId - * ***grant_type=client_credentials*** - * Using AEF certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 at CCF - 2. Register and onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Create Security Context According to Service APIs discovered. - 5. Request Access Token by Provider - -* **Expected Result**: - - 1. Response to Request of Access Token: - 1. **401 Unauthorized** response. - 2. body returned must accomplish **AccessTokenErr** data structure, with: - * error unauthorized_client - * error_description=Role not authorized for this API route - - -## Test Case 22: Retrieve access token with invalid apiInvokerId -* **Test ID**: ***capif_security_api-22*** -* **Description**: - - This test case will check that an API Invoker can't retrieve a security access token without valid apiInvokerId - -* **Pre-Conditions**: - - * API Invoker is pre-authorised (has valid apiInvokerId) - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - * body [service api description] with apiName service_1 - * Use APF Certificate - 3. Request Discover Published APIs not filtered: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Using invoker certificate - 4. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - * Create Security Information Body with one **securityInfo** for each aef present at each serviceAPIDescription present at Discover. - 5. Request Access Token by invoker: - * Sent POST *https://{CAPIF_HOSTNAME}/securities/{API_INVOKER_NOT_VALID}/token*. - * body [access token req body] - * ***securityId*** is apiInvokerId - * ***grant_type=client_credentials*** - * Using Invoker certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 at CCF - 2. Register and onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Create Security Context According to Service APIs discovered. - 5. Request Access Token by Invoker - -* **Expected Result**: - - 1. Response to Request of Access Token: - 1. **404 Not Found** response. - 2. body returned must accomplish **ProblemDetails29571** data structure, with: - * status 404 - * title Not Found - * detail Security context not found - * cause API Invoker has no security context - - -**NOTE: ProblemDetails29571 is the definition present for this request at swagger of ProblemDetails, and this is different from definition of ProblemDetails across other CAPIF Services** - -## Test Case 23: Retrieve access token with invalid client_id -* **Test ID**: ***capif_security_api-23*** -* **Description**: - - This test case will check that an API Exposure Function cannot retrieve a security access token without valid client_id at body - -* **Pre-Conditions**: - - * API Invoker is pre-authorised and Provider is also authorized - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - * body [service api description] with apiName service_1 - * Use APF Certificate - - 3. Request Discover Published APIs not filtered: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Using invoker certificate - - 4. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - * Create Security Information Body with one **securityInfo** for each aef present at each serviceAPIDescription present at Discover. - - 5. Request Access Token by invoker: - * Sent POST *https://{CAPIF_HOSTNAME}/securities/{securityId}/token*. - * body [access token req body] - * ***securityId*** is apiInvokerId - * ***grant_type=client_credentials*** - * **client_id is not-valid** - * Using Invoker certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 at CCF - 2. Register and onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Create Security Context According to Service APIs discovered. - 5. Request Access Token by Invoker - -* **Expected Result**: - - 1. Response to Request of Access Token: - 1. **400 Bad Request** response. - 2. body returned must accomplish **AccessTokenErr** data structure, with: - * error invalid_client - * error_description=Client Id not found - - -## Test Case 24: Retrieve access token with unsupported grant_type -* **Test ID**: ***capif_security_api-24*** -* **Description**: - - This test case will check that an API Exposure Function cannot retrieve a security access token with unsupported grant_type - -* **Pre-Conditions**: - - * API Invoker is pre-authorised and Provider is also authorized - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - * body [service api description] with apiName service_1 - * Use APF Certificate - - 3. Request Discover Published APIs not filtered: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Using invoker certificate - - 4. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - * Create Security Information Body with one **securityInfo** for each aef present at each serviceAPIDescription present at Discover. - - 5. Request Access Token by invoker: - * Sent POST *https://{CAPIF_HOSTNAME}/securities/{securityId}/token*. - * body [access token req body] - * ***securityId*** is apiInvokerId - * ***grant_type=not_valid*** - * Using Invoker certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 at CCF - 2. Register and onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Create Security Context According to Service APIs discovered. - 5. Request Access Token by Invoker - -* **Expected Result**: - - 1. Response to Request of Access Token: - 1. **400 Bad Request** response. - 2. body returned must accomplish **AccessTokenErr** data structure, with: - * error unsupported_grant_type - * error_description=Invalid value for `grant_type` \\(${grant_type}\\), must be one of \\['client_credentials'\\] - 'grant_type' - -## Test Case 25: Retrieve access token with invalid scope -* **Test ID**: ***capif_security_api-25*** -* **Description**: - - This test case will check that an API Exposure Function cannot retrieve a security access token with complete invalid scope - -* **Pre-Conditions**: - - * API Invoker is pre-authorised and Provider is also authorized - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - * body [service api description] with apiName service_1 - * Use APF Certificate - - 3. Request Discover Published APIs not filtered: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Using invoker certificate - - 4. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - * Create Security Information Body with one **securityInfo** for each aef present at each serviceAPIDescription present at Discover. - - 5. Request Access Token by invoker: - * Sent POST *https://{CAPIF_HOSTNAME}/securities/{securityId}/token*. - * body [access token req body] - * ***securityId*** is apiInvokerId - * ***grant_type=client_credentials*** - * ***scope=not-valid-scope*** - * Using Invoker certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 at CCF - 2. Register and onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Create Security Context According to Service APIs discovered. - 5. Request Access Token by Invoker - -* **Expected Result**: - - 1. Response to Request of Access Token: - 1. **400 Bad Request** response. - 2. body returned must accomplish **AccessTokenErr** data structure, with: - * error invalid_scope - * error_description=The first characters must be '3gpp' - - -## Test Case 26: Retrieve access token with invalid aefid at scope -* **Test ID**: ***capif_security_api-26*** -* **Description**: - - This test case will check that an API Exposure Function cannot retrieve a security access token with invalid aefId at scope - -* **Pre-Conditions**: - - * API Invoker is pre-authorised and Provider is also authorized - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - * body [service api description] with apiName service_1 - * Use APF Certificate - - 3. Request Discover Published APIs not filtered: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Using invoker certificate - - 4. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - * Create Security Information Body with one **securityInfo** for each aef present at each serviceAPIDescription present at Discover. - - 5. Request Access Token by invoker: - * Sent POST *https://{CAPIF_HOSTNAME}/securities/{securityId}/token*. - * body [access token req body] - * ***securityId*** is apiInvokerId - * ***grant_type=client_credentials*** - * ***scope=3gpp#1234:service_1*** - * Using Invoker certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 at CCF - 2. Register and onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Create Security Context According to Service APIs discovered. - 5. Request Access Token by Invoker - -* **Expected Result**: - - 1. Response to Request of Access Token: - 1. **400 Bad Request** response. - 2. body returned must accomplish **AccessTokenErr** data structure, with: - * error invalid_scope - * error_description=One of aef_id not belongs of your security context - - -## Test Case 27: Retrieve access token with invalid apiName at scope -* **Test ID**: ***capif_security_api-27*** -* **Description**: - - This test case will check that an API Exposure Function cannot retrieve a security access token with invalid apiName at scope - -* **Pre-Conditions**: - - * API Invoker is pre-authorised and Provider is also authorized - -* **Information of Test**: - - 1. Perform [Provider Registration] and [Invoker Onboarding] - - 2. Publish Service API at CCF: - * Send Post to ccf_publish_url https://{CAPIF_HOSTNAME}/published-apis/v1/{apfId}/service-apis - * body [service api description] with apiName service_1 - * Use APF Certificate - - 3. Request Discover Published APIs not filtered: - * Send GET to ccf_discover_url *https://{CAPIF_HOSTNAME}/service-apis/v1/allServiceAPIs?api-invoker-id={apiInvokerId}* - * Param api-invoker-id is mandatory - * Using invoker certificate - - 4. Create Security Context for this Invoker - * Send PUT *https://{CAPIF_HOSTNAME}/trustedInvokers/{apiInvokerId}* - * body [service security body] - * Using Invoker Certificate. - * Create Security Information Body with one **securityInfo** for each aef present at each serviceAPIDescription present at Discover. - - 5. Request Access Token by invoker: - * Sent POST *https://{CAPIF_HOSTNAME}/securities/{securityId}/token*. - * body [access token req body] - * ***securityId*** is apiInvokerId - * ***grant_type=client_credentials*** - * ***scope=3gpp#{aef_id}:not-valid*** - * Using Invoker certificate - -* **Execution Steps**: - 1. Register Provider at CCF, store certificates and Publish Service API service_1 at CCF - 2. Register and onboard Invoker at CCF - 3. Discover Service APIs by Invoker. - 4. Create Security Context According to Service APIs discovered. - 5. Request Access Token by Invoker - -* **Expected Result**: - - 1. Response to Request of Access Token: - 1. **400 Bad Request** response. - 2. body returned must accomplish **AccessTokenErr** data structure, with: - * error invalid_scope - * error_description=One of the api names does not exist or is not associated with the aef id provided - - - [Return To All Test Plans]: ../README.md - - - - [service security body]: ./service_security.json "Service Security Request" - [security notification body]: ./security_notification.json "Security Notification Request" - [access token req body]: ./access_token_req.json "Access Token Request" - [example]: ./access_token_req.json "Access Token Request Example" - - [invoker onboarding]: ../common_operations/README.md#register-an-invoker "Invoker Onboarding" - [provider registration]: ../common_operations/README.md#register-a-provider "Provider Registration" - - diff --git a/docs/test_plan/api_security_service/access_token_req.json b/docs/test_plan/api_security_service/access_token_req.json deleted file mode 100644 index 8504736..0000000 --- a/docs/test_plan/api_security_service/access_token_req.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "client_id", - "client_secret": "client_secret", - "grant_type": "client_credentials", - "scope": "scope" -} \ No newline at end of file diff --git a/docs/test_plan/api_security_service/access_token_req_example.json b/docs/test_plan/api_security_service/access_token_req_example.json deleted file mode 100644 index 070a717..0000000 --- a/docs/test_plan/api_security_service/access_token_req_example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "client_id": "bb260b4d0b3a0f954fa23f42d979ca", - "grant_type": "client_credentials", - "scope": "3gpp#af7e4cf70063814770e7b00b87273e:service_1" -} diff --git a/docs/test_plan/api_security_service/security_notification.json b/docs/test_plan/api_security_service/security_notification.json deleted file mode 100644 index 6b94eb5..0000000 --- a/docs/test_plan/api_security_service/security_notification.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "aefId": "aefId", - "apiIds": [ - "apiIds", - "apiIds" - ], - "apiInvokerId": "api_invoker_id", - "cause": "OVERLIMIT_USAGE" -} \ No newline at end of file diff --git a/docs/test_plan/api_security_service/service_security.json b/docs/test_plan/api_security_service/service_security.json deleted file mode 100644 index ad7bc1a..0000000 --- a/docs/test_plan/api_security_service/service_security.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "notificationDestination": "http://robot.testing", - "supportedFeatures": "fffffff", - "securityInfo": [{ - "authenticationInfo": "authenticationInfo", - "authorizationInfo": "authorizationInfo", - "interfaceDetails": { - "ipv4Addr": "127.0.0.1", - "securityMethods": ["PSK"], - "port": 5248 - }, - "prefSecurityMethods": ["PSK", "PKI", "OAUTH"], - }, - { - "authenticationInfo": "authenticationInfo", - "authorizationInfo": "authorizationInfo", - "prefSecurityMethods": ["PSK", "PKI", "OAUTH"], - "aefId": "aefId" - }], - "websockNotifConfig": { - "requestWebsocketUri": true, - "websocketUri": "websocketUri" - }, - "requestTestNotification": true -} diff --git a/docs/test_plan/common_operations/README.md b/docs/test_plan/common_operations/README.md deleted file mode 100644 index ff39d94..0000000 --- a/docs/test_plan/common_operations/README.md +++ /dev/null @@ -1,86 +0,0 @@ - -# Register an Invoker - -## Steps to perform operation - 1. Create public and private key at invoker - 2. Register of Invoker at CCF: - * Send POST to http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register - * Body [invoker register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [invoker getauth body] - - 4. Onboard Invoker: - * Send POST to https://{CAPIF_HOSTNAME}/api-invoker-management/v1/onboardedInvokers - * Reference Request Body: [invoker onboarding body] - * "onboardingInformation"->"apiInvokerPublicKey": must contain public key generated by Invoker. - * Send at Authorization Header the Bearer access_token obtained previously (Authorization:Bearer ${access_token}) - -## Checks to ensure onboarding - 1. Response to Register: - 1. **201 Created** - - 2. Response to Get Auth: - 1. **200 OK** - 2. ***access_token*** returned. - - 3. Response to Onboard request must accomplish: - 1. **201 Created** - 2. Response Body must follow **APIInvokerEnrolmentDetails** data structure with: - * apiInvokerId - * onboardingInformation->apiInvokerCertificate must contain the public key signed. - 3. Response Header **Location** must be received with URI to new resource created, following this structure: *{apiRoot}/api-invoker-management/{apiVersion}/onboardedInvokers/{onboardingId}* - - -# Register a Provider - -## Steps to Perform operation - 1. Create public and private key at provider for provider itself and each function (apf, aef and amf) - 2. Register of Provider at CCF: - * Send POST to *http://{CAPIF_HOSTNAME}:{CAPIF_HTTP_PORT}/register* - * body [provider register body] - - 3. Obtain Access Token: - * Send POST to *http://{CAPIF_HOSTNAME}/getauth* - * Body [provider getauth body] - - 4. Register Provider: - * Send POST *https://{CAPIF_HOSTNAME}/api-provider-management/v1/registrations* - * body [provider request body] - * Send at Authorization Header the Bearer access_token obtained previously (Authorization:Bearer ${access_token}) - * Store each cert in a file with according name. - -## Checks to ensure provider registration - 1. Response to Register: - 1. **201 Created** - - 2. Response to Get Auth: - 1. **200 OK** - 2. ***access_token*** returned. - - 3. Register Provider at Provider Management: - 1. **201 Created** response. - 2. body returned must accomplish **APIProviderEnrolmentDetails** data structure. - 3. For each **apiProvFuncs**, we must check: - 1. **apiProvFuncId** is set - 2. **apiProvCert** under **regInfo** is set properly - 4. Location Header must contain the new resource URL *{apiRoot}/api-provider-management/v1/registrations/{registrationId}* - - - - - -[invoker register body]: ../api_invoker_management/invoker_register_body.json "Invoker Register Body" -[invoker onboarding body]: ../api_invoker_management/invoker_details_post_example.json "API Invoker Request" -[invoker getauth body]: ../api_invoker_management/invoker_getauth_example.json "Get Auth Example" - -[provider register body]: ../api_provider_management/provider_register_body.json "Provider Register Body" -[provider request body]: ../api_provider_management/provider_details_post_example.json "API Provider Enrolment Request" -[provider getauth body]: ../api_provider_management/provider_getauth_example.json "Get Auth Example" - - - - - -[Return To All Test Plans]: ../README.md diff --git a/docs/testing_with_curl/README.md b/docs/testing_with_curl/README.md deleted file mode 100644 index 0903b8b..0000000 --- a/docs/testing_with_curl/README.md +++ /dev/null @@ -1,369 +0,0 @@ -[**[Return To Main]**] -# Testing Using Curl - -- [Testing Using Curl](#testing-using-curl) - - [cURL scripts (TLS supported)](#curl-scripts-tls-supported) - - [cURL manual execution](#curl-manual-execution) - - [Authentication](#authentication) - - [Invoker](#invoker) - - [Provider](#provider) - - [JWT Authentication APIs](#jwt-authentication-apis) - - [Register an entity](#register-an-entity) - - [Get access token for an existing entity](#get-access-token-for-an-existing-entity) - - [Retrieve and store CA certificate](#retrieve-and-store-ca-certificate) - - [Sign provider certificate](#sign-provider-certificate) - - [Invoker Management APIs](#invoker-management-apis) - - [Onboard an Invoker](#onboard-an-invoker) - - [Update Invoker Details](#update-invoker-details) - - [Offboard an Invoker](#offboard-an-invoker) - - [Publish APIs](#publish-apis) - - [Publish a new API.](#publish-a-new-api) - - [Update a published service API.](#update-a-published-service-api) - - [Unpublish a published service API.](#unpublish-a-published-service-api) - - [Retrieve all published APIs](#retrieve-all-published-apis) - - [Retrieve a published service API.](#retrieve-a-published-service-api) - - [Discover API](#discover-api) - - [Discover published service APIs and retrieve a collection of APIs according to certain filter criteria.](#discover-published-service-apis-and-retrieve-a-collection-of-apis-according-to-certain-filter-criteria) - -## cURL scripts (TLS supported) -Also you can follow the instructions and run the commands of the bash scripts: -* [provider](./capif_tls_curls_exposer.sh) to test CAPIF as provider with TLS support. -* [invoker](./capif_tls_curls_invoker.sh) to test CAPIF as invoker with TLS support. - -## cURL manual execution - -### Authentication -This version will use TLS communication, for that purpose we have 2 different scenarios, according to role: -* Invoker -* Provider - -#### Invoker -To authenticate an invoker user, we must perform next steps: -- Retrieve CA certificate from platform. [Retrieve and store CA certificate](#retrieve-and-store-ca-certificate) -- Register on the CAPIF with invoker role. [Register an entity](#register-an-entity) -- Get a Json Web Token (JWT) in order to request onboarding [Get access token for an existing entity](#get-access-token-for-an-existing-entity) -- Request onboarding adding public key to request. [Onboard an Invoker](#onboard-an-invoker) -- Store certificate signed by CAPIF platform to allow TLS onwards. - -**Flow:** - -![Flow](../images/flows/04%20-%20Invoker%20Register.png) -![Flow](../images/flows/05%20-%20Invoker%20Onboarding.png) - -#### Provider -To authenticate an provider user, we must perform next steps: -- Retrieve CA certificate from platform. [Retrieve and store CA certificate](#retrieve-and-store-ca-certificate) -- Register on the CAPIF with provider role. [Register an entity](#register-an-entity) -- Request sign the public key to CAPIF including beared with JWT. [Sign provider certificate](#sign-provider-certificate) -- Store certificate signed by CAPIF platform to allow TLS onwards. - -**Flow:** - -![Flow](../images/flows/01%20-%20Register%20of%20AEF.png) -![Flow](../images/flows/02%20-%20AEF%20API%20Provider%20registration.png) -![Flow](../images/flows/03%20-%20AEF%20Publish.png) - -### JWT Authentication APIs -These APIs are triggered by an entity (Invoker or Provider for release 1.0) to: -- register on the CAPIF Framework -- get a Json Web Token (JWT) in order to be authorized to call CAPIF APIs - -#### Register an entity -Request -```shell -curl --request POST 'http://:/register' --header 'Content-Type: application/json' --data '{ - "username":"...", - "password":"...", - "role":"...", - "description":"...", - "cn":"..." -}' -``` - -* Role: invoker or publisher -* cn: common name - -Response body -```json -{ - "id": "Entity ID", - "message": "Informative message" -} -``` - -#### Get access token for an existing entity -Request -```shell -curl --request POST 'http://:/gettoken' --header 'Content-Type: application/json' --data '{ - "username":"...", - "password":"...", - "role":"..." -}' -``` - -Response body -```json -{ - "access_token": "JSON Web Token for CAPIF APIs", - "message": "Informative message" -} -``` - -#### Retrieve and store CA certificate -```shell -curl --request GET 'http://:/ca-root' 2>/dev/null | jq -r '.certificate' -j > -``` - -#### Sign provider certificate -```shell -curl --request POST 'http:///sign-csr' --header 'Authorization: Bearer ' --header 'Content-Type: application/json' --data-raw '{ - "csr": "RAW PUBLIC KEY CREATED BY PUBLISHER", - "mode": "client", - "filename": provider -}' -``` -Response -``` json -{ - "certificate": "PUBLISHER CERTIFICATE" -} -``` -PUBLISHER CERTIFICATE value must be stored by Provider entity to next request to CAPIF (provider.crt for example) - -### Invoker Management APIs - -These APIs are triggered by a NetApp (i.e. Invoker) - -#### Onboard an Invoker - -```shell -curl --cacert --request POST 'https:///api-invoker-management/v1/onboardedInvokers' --header 'Authorization: Bearer ' --header 'Content-Type: application/json' --data-raw '{ - "notificationDestination" : "http://X:Y/netapp_callback", - "supportedFeatures" : "fffffff", - "apiInvokerInformation" : , - "websockNotifConfig" : { - "requestWebsocketUri" : true, - "websocketUri" : "websocketUri" - }, - "onboardingInformation" : { - "apiInvokerPublicKey" : - }, - "requestTestNotification" : true -}' -``` - -Response Body - -``` json -{ - "apiInvokerId": "7da0a8d4172d7d86c536c0fbc9c372", - "onboardingInformation": { - "apiInvokerPublicKey": "RAW PUBLIC KEY CREATED BY INVOKER", - "apiInvokerCertificate": "INVOKER CERTIFICATE", - "onboardingSecret": "onboardingSecret" - }, - "notificationDestination": "http://host.docker.internal:8086/netapp_callback", - "requestTestNotification": true, - ... -} -``` - -INVOKER CERTIFICATE value must be stored by Invoker entity to next request to CAPIF (invoker.crt for example) - -#### Update Invoker Details - -```shell -curl --location --request PUT 'https:///api-invoker-management/v1/onboardedInvokers/' --cert --key --cacert --header 'Content-Type: application/json' --data '{ - "notificationDestination" : "http://X:Y/netapp_callback2", - "supportedFeatures" : "fffffff", - "apiInvokerInformation" : , - "websockNotifConfig" : { - "requestWebsocketUri" : true, - "websocketUri" : "websocketUri2" - }, - "onboardingInformation" : { - "apiInvokerPublicKey" : - }, - "requestTestNotification" : true -}' -``` - -#### Offboard an Invoker - -```shell -curl --cert --key --cacert --request DELETE 'https:///api-invoker-management/v1/onboardedInvokers/' -``` - -### Publish APIs - -These APIs are triggered by the API Publishing Function (APF) of an Provider - -#### Publish a new API. -```shell -curl --cert --key --cacert --request POST 'https:///published-apis/v1//service-apis' --header 'Content-Type: application/json' --data '{ - "apiName": "3gpp-monitoring-event", - "aefProfiles": [ - { - "aefId": "string", - "versions": [ - { - "apiVersion": "v1", - "expiry": "2021-11-30T10:32:02.004Z", - "resources": [ - { - "resourceName": "string", - "commType": "REQUEST_RESPONSE", - "uri": "string", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ], - "custOperations": [ - { - "commType": "REQUEST_RESPONSE", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ] - } - ], - "protocol": "HTTP_1_1", - "dataFormat": "JSON", - "securityMethods": ["PSK"], - "interfaceDescriptions": [ - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - }, - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - } - ] - } - ], - "description": "string", - "supportedFeatures": "fffff", - "shareableInfo": { - "isShareable": true, - "capifProvDoms": [ - "string" - ] - }, - "serviceAPICategory": "string", - "apiSuppFeats": "fffff", - "pubApiPath": { - "ccfIds": [ - "string" - ] - }, - "ccfId": "string" -}' -``` - -#### Update a published service API. -```shell -curl --cert --key --cacert --request PUT 'https:///published-apis/v1//service-apis/' --header 'Content-Type: application/json' --data '{ - "apiName": "3gpp-monitoring-event", - "aefProfiles": [ - { - "aefId": "string1", - "versions": [ - { - "apiVersion": "v1", - "expiry": "2021-11-30T10:32:02.004Z", - "resources": [ - { - "resourceName": "string", - "commType": "REQUEST_RESPONSE", - "uri": "string", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ], - "custOperations": [ - { - "commType": "REQUEST_RESPONSE", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ] - } - ], - "protocol": "HTTP_1_1", - "dataFormat": "JSON", - "securityMethods": ["PSK"], - "interfaceDescriptions": [ - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - }, - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - } - ] - } - ], - "description": "string", - "supportedFeatures": "fffff", - "shareableInfo": { - "isShareable": true, - "capifProvDoms": [ - "string" - ] - }, - "serviceAPICategory": "string", - "apiSuppFeats": "fffff", - "pubApiPath": { - "ccfIds": [ - "string" - ] - }, - "ccfId": "string" -}' -``` - -#### Unpublish a published service API. -```shell -curl --cert --key --cacert --request DELETE 'https:///published-apis/v1//service-apis/' -``` - -#### Retrieve all published APIs -```shell -curl --cert --key --cacert --request GET 'https:///published-apis/v1//service-apis' -``` - -#### Retrieve a published service API. -```shell -curl --cert --key --cacert --request GET 'https:///published-apis/v1//service-apis/' -``` - -### Discover API - -This API is triggered by a NetApp (or Invoker) - -#### Discover published service APIs and retrieve a collection of APIs according to certain filter criteria. -```shell -curl --cert --key --cacert --request GET 'https:///service-apis/v1/allServiceAPIs?api-invoker-id=&api-name=&api-version=&aef-id=&api-cat=&supported-features=&api-supported-features=' -``` - - - -[Return To Main]: ../../README.md#using-curl \ No newline at end of file diff --git a/docs/testing_with_curl/capif_tls_curls_exposer.sh b/docs/testing_with_curl/capif_tls_curls_exposer.sh deleted file mode 100755 index 5b81712..0000000 --- a/docs/testing_with_curl/capif_tls_curls_exposer.sh +++ /dev/null @@ -1,205 +0,0 @@ -##### Execute Exposer curls locally - -##### Configure machine - -##### Add in /etc/hosts: 127.0.0.1 capifcore - - -##### Set environment variables -capifhost="capifcore" -capifhttpport="8080" - -exposerpk="-----BEGIN CERTIFICATE REQUEST-----\nMIIC0TCCAbkCAQAwgYsxEDAOBgNVBAMMB2V4cG9zZXIxFzAVBgNVBAoMDlRlbGVm\nb25pY2EgSStEMRMwEQYDVQQLDApJbm5vdmF0aW9uMQ8wDQYDVQQHDAZNYWRyaWQx\nDzANBgNVBAgMBk1hZHJpZDELMAkGA1UEBhMCRVMxGjAYBgkqhkiG9w0BCQEWC2lu\nbm9AdGlkLmVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkpJ7FzAI\nkzFYxLKbW54lIsQBNIQz5zQIvRZDFcrO4QLR2jQUps9giBWEDih++47JiBJyM+z1\nWkEh7b+moZhQThj7L9PKgJHRhU1oeHpSE1x/r7479J5F+CFRqFo5v9dC+2zGfP4E\nsSrNfp3MK/KQHsHhMzSt881xAHs+p2/bcM+sd/BlXC4J6E1y6Hk3ogI7kq443fcY\noUHZx9ClUSboOvXa1ZSPVxdCV6xKRraUdAKfhMGn+pYtJDsNp8Gg/BN8NXmYUzl9\ntDhjeuIxr4N38LgW3gRHLNIa8acO9eBctWw9AD20JWzFAXvvmsboBPc2wsOVcsml\ncCbisMRKX4JyKQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAIxZ1Sec9ATbqjhi\nRz4rvhX8+myXhyfEw2MQ62jz5tpH4qIVZFtn+cZvU/ULySY10WHaBijGgx8fTaMh\nvjQbc+p3PXmgtnmt1QmoOGjDTFa6vghqpxPLSUjjCUe8yj5y24gkOImY6Cv5rzzQ\nlnTMkNvnGgpDgUeiqWcQNbwwge3zkzp9bVRgogTT+EDxiFnjTTF6iUG80sRtXMGr\nD6sygLsF2zijGGfWoKRo/7aZTQxuCiCixceVFXegMfr+eACkOjV25Kso7hYBoEdP\nkgUf5PNpl5uK3/rmPIrl/TeE0SnGGfCYP7QajE9ELRsBVmVDZJb7ZxUl1A4YydFY\ni0QOM3Y=\n-----END CERTIFICATE REQUEST-----\n" - - -##### Retrieve and store CA certificate - -curl --request GET "http://$capifhost:$capifhttpport/ca-root" 2>/dev/null | jq -r '.certificate' -j > ca.crt - - -##### Register an entity - -exposerid=$(curl --request POST "http://$capifhost:$capifhttpport/register" --header 'Content-Type: application/json' --data '{ - "username":"exposer", - "password":"exposer", - "role":"exposer", - "description":"Exposer", - "cn":"exposer" -}' 2>/dev/null | jq -r '.id' -j) - - -##### Get access token - -exposertoken=$(curl --request POST "http://$capifhost:$capifhttpport/gettoken" --header 'Content-Type: application/json' --data '{ - "username":"exposer", - "password":"exposer", - "role":"exposer" -}' 2>/dev/null | jq -r '.access_token' -j) - - -##### Sign exposer certificate - -curl --request POST "http://$capifhost:$capifhttpport/sign-csr" --header "Authorization: Bearer $exposertoken" --header 'Content-Type: application/json' --data-raw "{ - \"csr\": \"$exposerpk\", - \"mode\": \"client\", - \"filename\": \"exposer\" -}" 2>/dev/null | jq -r '.certificate' -j > exposer.crt - - -##### Publish service -curl --cert exposer.crt --key exposer.key --cacert ca.crt --request POST "https://$capifhost/published-apis/v1/$exposerid/service-apis" --header 'Content-Type: application/json' --data '{ - "apiName": "3gpp-monitoring-event", - "aefProfiles": [ - { - "aefId": "string", - "versions": [ - { - "apiVersion": "v1", - "expiry": "2021-11-30T10:32:02.004Z", - "resources": [ - { - "resourceName": "string", - "commType": "REQUEST_RESPONSE", - "uri": "string", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ], - "custOperations": [ - { - "commType": "REQUEST_RESPONSE", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ] - } - ], - "protocol": "HTTP_1_1", - "dataFormat": "JSON", - "securityMethods": ["PSK"], - "interfaceDescriptions": [ - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - }, - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - } - ] - } - ], - "description": "string", - "supportedFeatures": "fffff", - "shareableInfo": { - "isShareable": true, - "capifProvDoms": [ - "string" - ] - }, - "serviceAPICategory": "string", - "apiSuppFeats": "fffff", - "pubApiPath": { - "ccfIds": [ - "string" - ] - }, - "ccfId": "string" -}' > response.json - -apiserviceid=$(cat response.json | jq -r '.apiId' -j) - - -##### Update a published service API -curl --cert exposer.crt --key exposer.key --cacert ca.crt --request PUT "https://$capifhost/published-apis/v1/$exposerid/service-apis/$apiserviceid" --header 'Content-Type: application/json' --data '{ - "apiName": "3gpp-monitoring-event", - "aefProfiles": [ - { - "aefId": "string1", - "versions": [ - { - "apiVersion": "v1", - "expiry": "2021-11-30T10:32:02.004Z", - "resources": [ - { - "resourceName": "string", - "commType": "REQUEST_RESPONSE", - "uri": "string", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ], - "custOperations": [ - { - "commType": "REQUEST_RESPONSE", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ] - } - ], - "protocol": "HTTP_1_1", - "dataFormat": "JSON", - "securityMethods": ["PSK"], - "interfaceDescriptions": [ - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - }, - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - } - ] - } - ], - "description": "string", - "supportedFeatures": "fffff", - "shareableInfo": { - "isShareable": true, - "capifProvDoms": [ - "string" - ] - }, - "serviceAPICategory": "string", - "apiSuppFeats": "fffff", - "pubApiPath": { - "ccfIds": [ - "string" - ] - }, - "ccfId": "string" -}' - - -##### Retrieve all published APIs - -curl --cert exposer.crt --key exposer.key --cacert ca.crt --request GET "https://$capifhost/published-apis/v1/$exposerid/service-apis" - - -##### Retrieve a published service API - -curl --cert exposer.crt --key exposer.key --cacert ca.crt --request GET "https://$capifhost/published-apis/v1/$exposerid/service-apis/$apiserviceid" - - -##### Unpublish a published service API - -curl --cert exposer.crt --key exposer.key --cacert ca.crt --request DELETE "https://$capifhost/published-apis/v1/$exposerid/service-apis/$apiserviceid" - - diff --git a/docs/testing_with_curl/capif_tls_curls_invoker.sh b/docs/testing_with_curl/capif_tls_curls_invoker.sh deleted file mode 100755 index d6c287a..0000000 --- a/docs/testing_with_curl/capif_tls_curls_invoker.sh +++ /dev/null @@ -1,86 +0,0 @@ -##### Execute Invoker curls locally - -##### Configure machine - -##### Add in /etc/hosts: 127.0.0.1 capifcore - - -##### Set environment variables - -capifhost="capifcore" -capifhttpport="8080" - -invokerpk="-----BEGIN CERTIFICATE REQUEST-----\nMIIC0TCCAbkCAQAwgYsxEDAOBgNVBAMMB2ludm9rZXIxFzAVBgNVBAoMDlRlbGVm\nb25pY2EgSStEMRMwEQYDVQQLDApJbm5vdmF0aW9uMQ8wDQYDVQQHDAZNYWRyaWQx\nDzANBgNVBAgMBk1hZHJpZDELMAkGA1UEBhMCRVMxGjAYBgkqhkiG9w0BCQEWC2lu\nbm9AdGlkLmVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArfITEb3/\nJ5KDt7ia2WsQrd8iSrlH8kh6D9YNPEF+KaIGQ9w8QhmOW416uvIAASzOaCKMNqgb\nCI0NqsbVF9lfaiBgB71vcwX0yKatjACn3Nl3Lnubi+tH4Jb5zGQQXOuxpMHMmgyn\nNTsSc/MeMzX3iUWqLmmhnTC31Mu1ESUPTBa+CitQAj2wYMvBS970WICKrDlxWkR8\nZZBkRBZaxMfqY21VWmREtR+Kl6GCMBtUCUBH6uWjFiOpxYbCxdygxxrA4a3IzmiO\ntXOyLs7iuOP/CLSYfk71MHX2qKlpAyjdRK2W0w0GioV90Hk4uT/YUYy9zjWWN+mm\nrQ9GBy8iRZm7YwIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAI0btA7KDMvkY4Ib\n0eMteeeT40bm11Yw8/6V48IaIPi9EpZMI+jWyCebw8PBFUs3l3ImWeO8Gma96gyf\np0WB/64MRkUSdOxUWOWGMPIMEF+BH3eiHthx+EbAETtJ0D4KzmH6raxl14qvwLS5\nwxtxPGxu/R5ue5RVJpAzzJ6OX36p05GYSzL+pTotVPpowSdoeNsV+xPgPA0diV8a\nB7Zn/ujwMpsh7IjQPKpOEkhQdxc478Si8dmRbzXkVar1Oa8/QSJ8ZAaFI4VGowjR\nmtxps7AvS5OG9iMPtFQHpqxHVO50CJU5cbsXsYdu9EipGhgIKJDKewBX7tCKk0Ot\nBLU03CY=\n-----END CERTIFICATE REQUEST-----\n" - - -##### Retrieve and store CA certificate - -curl --request GET "http://$capifhost:$capifhttpport/ca-root" 2>/dev/null | jq -r '.certificate' -j > ca.crt - - -##### Register an entity - -invokerid=$(curl --request POST "http://$capifhost:$capifhttpport/register" --header 'Content-Type: application/json' --data '{ - "username":"invoker", - "password":"invoker", - "role":"invoker", - "description":"Invoker", - "cn":"invoker" -}' 2>/dev/null | jq -r '.id' -j) - - -##### Get access token - -invokertoken=$(curl --request POST "http://$capifhost:$capifhttpport/gettoken" --header 'Content-Type: application/json' --data '{ - "username":"invoker", - "password":"invoker", - "role":"invoker" -}' 2>/dev/null | jq -r '.access_token' -j) - - -##### Onboard an Invoker - -curl --cacert ca.crt --request POST "https://$capifhost/api-invoker-management/v1/onboardedInvokers" --header "Authorization: Bearer $invokertoken" --header 'Content-Type: application/json' --data-raw "{ - \"notificationDestination\" : \"http://X:Y/netapp_callback\", - \"supportedFeatures\" : \"fffffff\", - \"apiInvokerInformation\" : \"invoker\", - \"websockNotifConfig\" : { - \"requestWebsocketUri\" : true, - \"websocketUri\" : \"websocketUri\" - }, - \"onboardingInformation\" : { - \"apiInvokerPublicKey\" : \"$invokerpk\" - }, - \"requestTestNotification\" : true -}" > response.json - -cat response.json | jq -r '.onboardingInformation.apiInvokerCertificate' -j > invoker.crt -apiinvokerid=$(cat response.json | jq -r '.apiInvokerId' -j) - - -##### Update Invoker Details - -curl --location --request PUT "https://$capifhost/api-invoker-management/v1/onboardedInvokers/$apiinvokerid" --cert invoker.crt --key invoker.key --cacert ca.crt --header 'Content-Type: application/json' --data "{ - \"notificationDestination\" : \"http://X:Y/netapp_callback2\", - \"supportedFeatures\" : \"fffffff\", - \"apiInvokerInformation\" : \"test\", - \"websockNotifConfig\" : { - \"requestWebsocketUri\" : true, - \"websocketUri\" : \"websocketUri2\" - }, - \"onboardingInformation\" : { - \"apiInvokerPublicKey\" : \"$invokerpk\" - }, - \"requestTestNotification\" : true -}" - - -##### Discover API - -curl --cert invoker.crt --key invoker.key --cacert ca.crt --request GET "https://$capifhost/service-apis/v1/allServiceAPIs?api-invoker-id=$apiinvokerid" - - -##### Offboard an Invoker - -curl --cert invoker.crt --key invoker.key --cacert ca.crt --request DELETE "https://$capifhost/api-invoker-management/v1/onboardedInvokers/$apiinvokerid" - diff --git a/docs/testing_with_curl/exposer.key b/docs/testing_with_curl/exposer.key deleted file mode 100644 index e84c8c4..0000000 --- a/docs/testing_with_curl/exposer.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCSknsXMAiTMVjE -sptbniUixAE0hDPnNAi9FkMVys7hAtHaNBSmz2CIFYQOKH77jsmIEnIz7PVaQSHt -v6ahmFBOGPsv08qAkdGFTWh4elITXH+vvjv0nkX4IVGoWjm/10L7bMZ8/gSxKs1+ -ncwr8pAeweEzNK3zzXEAez6nb9twz6x38GVcLgnoTXLoeTeiAjuSrjjd9xihQdnH -0KVRJug69drVlI9XF0JXrEpGtpR0Ap+Ewaf6li0kOw2nwaD8E3w1eZhTOX20OGN6 -4jGvg3fwuBbeBEcs0hrxpw714Fy1bD0APbQlbMUBe++axugE9zbCw5VyyaVwJuKw -xEpfgnIpAgMBAAECggEACs11TqlcIG5qd/N1Ts8ni9noACpe4ZiXV578lRkW8++E -xEZtX+P4iIm+wK+3DYGhvyp430naGsD30rF62FMaVr8xmCijC/nIoutTGqS38t8G -Ns+C/2Lrjj+fvemJyGasSaKOjdIc9L/OWG7MiE/+05LU2bTKvfrIwXvT4NGg2ei1 -NDO8vS5fRHYZ1LyCyrCDetP2aYrTlPao20hmU4IDyh4N17wLuPgijC+AuqR2Xic0 -Mk4ofZ/6Y3oN0rrov2yG7IXjMJQI469IQ6TJLlyFc8tQIF5Y3CMMCMuVMq5m33bq -/6bow4/VYFG8mPzy7lQLQ8YeEPsgDKL0pB4zqDr7ZwKBgQDJRJoG2PSaEOt6DIKV -84to73oD9x9lOSrmaH2/NzL3mwLXP2Is4nmLzEDQvA0UhTZe9c0n6OoE3uRZ1gAu -JIe3zXTJSK4/ysmePUZL1js5bKtuHBrcSCOupWRuJXbaXK5uqISDHUgHiRw3bq8y -g8SZY/JOBPyJhVlKhmhNCYMi9wKBgQC6bjJ//tLpH6EG4ux0O2StzUoHrvV2cyUj -RRxGvAt92sdsZaVKmIW/SlLy8tv5HJqblfn6m7aY/vUYbN3AfMJ4teLZz5Y//CH3 -jPchHyk/uhh7gxufiD65i5bfVyRt54tDbyVDc2/1prUyD5W4q4UNOmvhXym5saIc -U5WNCnSr3wKBgQCs8MaM5bVgAPPlfoRixs9ejo/AgoK2nqWvL9AFEzA3NDn/rJX2 -TW/1YL+83Ck9Ha33cKwlA+y53LBIRSsIexknJWKZZltbsysFTk9t8JoZILg5N+sY -puAKPFGMl6KFxSeZLDIY23s+BmF5fCEMfc7botbclUpN/IgaEl3i/C5zRwKBgHsx -lKdmEaNBZlwxmgTYtpfvH2tiXwwN3M2ovp2zZ3icGMn1hTt8/GzCxXuLpnbAQx5r -BcxoF0qUuAuS7RpklvHDZ4t9FJFloGCAQ1Ic0FovNDxyD8/k7WYY6vLdF9KUfj9q -c9pVrvdKWVQiXlKw7PQn1eAQzXbK/g/v39Raw2xLAoGBAILTLY3sGBNkFCVhJlyZ -DaIwkbtnpCBT2T7DUupw51aLhh4rnuJ5wA3uGdRqoKVYSc9DuOwB/yNFGuQDElxQ -jfKlX0X5xItaxZ5FR4EvGCnqBJl6JM3QekzhXtq5VdY5zIf/HHqFYebcMFrkEicZ -uuAZd4wa+jn9SR9mUYtS+Lq+ ------END PRIVATE KEY----- diff --git a/docs/testing_with_curl/invoker.key b/docs/testing_with_curl/invoker.key deleted file mode 100644 index 15b96bb..0000000 --- a/docs/testing_with_curl/invoker.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCt8hMRvf8nkoO3 -uJrZaxCt3yJKuUfySHoP1g08QX4pogZD3DxCGY5bjXq68gABLM5oIow2qBsIjQ2q -xtUX2V9qIGAHvW9zBfTIpq2MAKfc2Xcue5uL60fglvnMZBBc67GkwcyaDKc1OxJz -8x4zNfeJRaouaaGdMLfUy7URJQ9MFr4KK1ACPbBgy8FL3vRYgIqsOXFaRHxlkGRE -FlrEx+pjbVVaZES1H4qXoYIwG1QJQEfq5aMWI6nFhsLF3KDHGsDhrcjOaI61c7Iu -zuK44/8ItJh+TvUwdfaoqWkDKN1ErZbTDQaKhX3QeTi5P9hRjL3ONZY36aatD0YH -LyJFmbtjAgMBAAECggEAAyR5OxdJ1W5jnSD9kBCvO6jDMIUuIcU+SAZUfGaxYybn -EeNCtBiPGV8tWWLHJJ0bL6iKpAv+gOKeSpKOmwU7XkHZEWVlRAfpiNfen2bcTCiw -fg3D4bgRMmDwwyMH368QFlJ56UFMCuqb0x+oCeMRIdNjwfbcPVCpZDYNGwTDBzoy -72Aj5TssEu+Ft5VVGwhsvq0v6bd6OWmW34PI9SHzXzRlRw4b4ZtZekW8o/QpO1gO -F+ARbCGE2qjqHWRU/vzINMmAucqhDM6/f7Un5XXr+Zm+8u4PGa5eLWkebJHhfwKX -Ag0WToD/FmDPRqlnjZdzraJlhuXLGdhRAlzdnIQNRQKBgQDvhiVewu7CTzgB66dA -cdrJkXVJPZUGvUYmXkwPaSju7hjDc87pNz+szH2QP+Qm+pD1mV9OswIim4Oi7C1l -lEe423QGjtsn5txzcRk+ZzyX/Z2ltcnXi8N/MNeOZ2qFAgP/IIOTcgowKftuUT6w -2A1DQFj6xxu6vrzxOqIL6tXy7wKBgQC56SM80udTqyb9+wk/KuDSgym3bSaZ8i5q -dNVV5wOxCotLGG9Any61TVOIP/SUjar4f4+FznLZjJYXIZvpbS32PUOtlnKtOmp6 -OBKIpEXq2zq0u/o/i8EyOb6laNqehfffRYqqYU9mJXVjiTUNcOVqfLljeeui1r1P -txSRBlTuzQKBgQDUgB/hbXHjw+J9mbM9soUXtUvn2ZHAc+Wrnpc+SN6+80/W/4R/ -VbvRM27mrjhc+InoytRKfvgS+gOUZJJ1/1KOR2wtcUovoVrNtHZf7blNYv0dCiXz -bBTaX9uthER1km83RoJVKqStTGG74qqKvHMvygPnIQSR7iy0m38usX500wKBgGeM -koLzWcOBhhNa+tiDMnwucFLpaeG/QdkrwBO7u5OlstYeAwF0aFi1fDxcmwcPLVaB -/lfiGJhRtNunbacDl+EaWJLcRH12Fw6CItiW3xakCzvVo9o3JmGqRiTtlS9MoTZs -DoM99jKH1K2fI7yb0DySwdPFedjWUNWQvNTWOQJVAoGAYr9Kuo7s83Qe9CaHQW/Y -PPL0dYBA63guuw2mNQjBL5LuqMZPz6vVB0hIVlYb5Xgw48OWUThHksJ0qltJK7kR -OPRyOxiWpJVo5rZPVzS0Ofbmau9z1VYr358RqR2N2EqG5KDr5QZT9nQq7k8EJvrF -NM/zMhxmgtNYez417Q/3U+M= ------END PRIVATE KEY----- diff --git a/docs/testing_with_postman/CAPIF.postman_collection.json b/docs/testing_with_postman/CAPIF.postman_collection.json deleted file mode 100644 index e65c826..0000000 --- a/docs/testing_with_postman/CAPIF.postman_collection.json +++ /dev/null @@ -1,982 +0,0 @@ -{ - "info": { - "_postman_id": "5cfdf0d7-3b3c-4961-9cb9-84c2bf85056c", - "name": "CAPIF", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "31608242", - "_collection_link": "https://red-comet-993867.postman.co/workspace/Team-Workspace~bfc7c442-a60c-4bb1-8730-fdabc2df89b9/collection/31608242-5cfdf0d7-3b3c-4961-9cb9-84c2bf85056c?action=share&source=collection_link&creator=31608242" - }, - "item": [ - { - "name": "01-register_user_provider", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "pm.environment.set('ONBOARDING_URL', res.ccf_api_onboarding_url);", - "pm.environment.set('PUBLISH_URL', res.ccf_publish_url);", - "pm.environment.set('USER_ID', res.id);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME}}\",\n\"description\": \"provider\",\n\"role\": \"provider\",\n\"cn\": \"provider\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/register", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "register" - ] - } - }, - "response": [] - }, - { - "name": "02-getauth_provider", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "", - "pm.environment.set('CA_ROOT', res.ca_root);", - "pm.environment.set('ACCESS_TOKEN', res.access_token);", - "", - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_ca',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: res", - " }", - " }, function (err, res) {", - " console.log(res);", - " });", - " }, 5000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/getauth", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "getauth" - ] - } - }, - "response": [] - }, - { - "name": "03-onboard_provider", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "if (pm.response.code == 201){", - " ", - " pm.environment.set('PROVIDER_ID', res.apiProvDomId);", - "", - " const roleVariableMapping = {", - " \"AEF\": { id: 'AEF_ID', cert: 'AEF_CERT' },", - " \"APF\": { id: 'APF_ID', cert: 'APF_CERT' },", - " \"AMF\": { id: 'AMF_ID', cert: 'AMF_CERT' }", - " };", - "", - " res.apiProvFuncs.forEach(function(elemento) {", - " const role = elemento.apiProvFuncRole;", - " if (roleVariableMapping.hasOwnProperty(role)) {", - " const variables = roleVariableMapping[role];", - " pm.environment.set(variables.id, elemento.apiProvFuncId);", - " pm.environment.set(variables.cert, elemento.regInfo.apiProvCert);", - "", - " }", - " });", - "", - "}", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "", - "var res = JSON.parse(pm.request.body.raw);", - "", - "res.apiProvFuncs.forEach(function(elemento) {", - "", - " setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/generate_csr',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: elemento", - " }", - " }, function (err, response) {", - " j_file = JSON.parse(response.text());", - " elemento.regInfo.apiProvPubKey = j_file.csr;", - " pm.environment.set(elemento.apiProvFuncRole+'_KEY', j_file.key);", - " });", - " }, 5000);", - "", - "});", - "", - "pm.request.body.raw = res;" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{ACCESS_TOKEN}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "{\n\"apiProvFuncs\": [\n {\n \"regInfo\": {\n \"apiProvPubKey\": \"\"\n },\n \"apiProvFuncRole\": \"AEF\",\n \"apiProvFuncInfo\": \"dummy_aef\"\n },\n {\n \"regInfo\": {\n \"apiProvPubKey\": \"\"\n },\n \"apiProvFuncRole\": \"APF\",\n \"apiProvFuncInfo\": \"dummy_apf\"\n },\n {\n \"regInfo\": {\n \"apiProvPubKey\": \"\"\n },\n \"apiProvFuncRole\": \"AMF\",\n \"apiProvFuncInfo\": \"dummy_amf\"\n }\n],\n\"apiProvDomInfo\": \"This is provider\",\n\"suppFeat\": \"fff\",\n\"failReason\": \"string\",\n\"regSec\": \"{{ACCESS_TOKEN}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/{{ONBOARDING_URL}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "{{ONBOARDING_URL}}" - ] - } - }, - "response": [] - }, - { - "name": "04-publish_api", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('APF_CERT'), key:pm.environment.get('APF_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "\n{\n \"apiName\": \"hello_api_demo_v2\",\n \"aefProfiles\": [\n {\n \"aefId\": \"{{AEF_ID}}\",\n \"versions\": [\n {\n \"apiVersion\": \"v1\",\n \"expiry\": \"2021-11-30T10:32:02.004Z\",\n \"resources\": [\n {\n \"resourceName\": \"hello-endpoint\",\n \"commType\": \"REQUEST_RESPONSE\",\n \"uri\": \"/hello\",\n \"custOpName\": \"string\",\n \"operations\": [\n \"POST\"\n ],\n \"description\": \"Endpoint to receive a welcome message\"\n }\n ],\n \"custOperations\": [\n {\n \"commType\": \"REQUEST_RESPONSE\",\n \"custOpName\": \"string\",\n \"operations\": [\n \"POST\"\n ],\n \"description\": \"string\"\n }\n ]\n }\n ],\n \"protocol\": \"HTTP_1_1\",\n \"dataFormat\": \"JSON\",\n \"securityMethods\": [\"Oauth\"],\n \"interfaceDescriptions\": [\n {\n \"ipv4Addr\": \"localhost\",\n \"port\": 8088,\n \"securityMethods\": [\"Oauth\"]\n }\n ]\n }\n ],\n \"description\": \"Hello api services\",\n \"supportedFeatures\": \"fffff\",\n \"shareableInfo\": {\n \"isShareable\": true,\n \"capifProvDoms\": [\n \"string\"\n ]\n },\n \"serviceAPICategory\": \"string\",\n \"apiSuppFeats\": \"fffff\",\n \"pubApiPath\": {\n \"ccfIds\": [\n \"string\"\n ]\n },\n \"ccfId\": \"string\"\n }", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/published-apis/v1/{{APF_ID}}/service-apis", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "published-apis", - "v1", - "{{APF_ID}}", - "service-apis" - ] - } - }, - "response": [] - }, - { - "name": "05-register_user_invoker", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "pm.environment.set('ONBOARDING_URL_INVOKER', res.ccf_onboarding_url);", - "pm.environment.set('DISCOVER_URL', res.ccf_discover_url);", - "pm.environment.set('USER_INVOKER_ID', res.id);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME_INVOKER}}\",\n\"description\": \"invoker\",\n\"role\": \"invoker\",\n\"cn\": \"invoker\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/register", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "register" - ] - } - }, - "response": [] - }, - { - "name": "06-getauth_invoker", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "", - "pm.environment.set('CA_ROOT', res.ca_root);", - "pm.environment.set('ACCESS_TOKEN_INVOKER', res.access_token);", - "", - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_ca',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: res", - " }", - " }, function (err, res) {", - " console.log(res);", - " });", - " }, 5000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME_INVOKER}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/getauth", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "getauth" - ] - } - }, - "response": [] - }, - { - "name": "07-onboard_invoker", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "if (pm.response.code == 201){", - " ", - " pm.environment.set('INVOKER_ID', res.apiInvokerId);", - " pm.environment.set('INVOKER_CERT', res.onboardingInformation.apiInvokerCertificate);", - "}", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "", - "var res = JSON.parse(pm.request.body.raw);", - "", - "", - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/generate_csr_invoker',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {}", - " }", - " }, function (err, response) {", - " j_file = JSON.parse(response.text());", - " res.onboardingInformation.apiInvokerPublicKey = j_file.csr;", - " pm.environment.set('INVOKER_KEY', j_file.key);", - " });", - " }, 5000);", - "", - "", - "pm.request.body.raw = res;" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{ACCESS_TOKEN_INVOKER}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"notificationDestination\" : \"http://host.docker.internal:8086/netapp_callback\",\n \"supportedFeatures\" : \"fffffff\",\n \"apiInvokerInformation\" : \"dummy\",\n \"websockNotifConfig\" : {\n \"requestWebsocketUri\" : true,\n \"websocketUri\" : \"websocketUri\"\n },\n \"onboardingInformation\" : {\n \"apiInvokerPublicKey\" : \"\"\n },\n \"requestTestNotification\" : true\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/{{ONBOARDING_URL_INVOKER}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "{{ONBOARDING_URL_INVOKER}}" - ] - } - }, - "response": [] - }, - { - "name": "08-discover", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('INVOKER_CERT'), key:pm.environment.get('INVOKER_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "if (pm.response.code == 200){", - "", - " res.serviceAPIDescriptions.forEach(function(api) {", - " pm.environment.set('API_SERVICE_ID', api.apiId);", - " pm.environment.set('API_NAME', api.apiName);", - " pm.environment.set('API_AEF_ID', api.aefProfiles[0].aefId);", - " pm.environment.set('IPV4ADDR', api.aefProfiles[0].interfaceDescriptions[0].ipv4Addr);", - " pm.environment.set('PORT', api.aefProfiles[0].interfaceDescriptions[0].port);", - " pm.environment.set('URI', api.aefProfiles[0].versions[0].resources[0].uri);", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "disableBodyPruning": true, - "strictSSL": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/{{DISCOVER_URL}}{{INVOKER_ID}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "{{DISCOVER_URL}}{{INVOKER_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "09-security_context", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('INVOKER_CERT'), key:pm.environment.get('INVOKER_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "PUT", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"securityInfo\": [\n {\n \"prefSecurityMethods\": [\n \"Oauth\"\n ],\n \"authenticationInfo\": \"string\",\n \"authorizationInfo\": \"string\",\n \"aefId\": \"{{API_AEF_ID}}\",\n \"apiId\": \"{{API_SERVICE_ID}}\"\n }\n ],\n \"notificationDestination\": \"https://mynotificationdest.com\",\n \"requestTestNotification\": true,\n \"websockNotifConfig\": {\n \"websocketUri\": \"string\",\n \"requestWebsocketUri\": true\n },\n \"supportedFeatures\": \"fff\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/capif-security/v1/trustedInvokers/{{INVOKER_ID}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "capif-security", - "v1", - "trustedInvokers", - "{{INVOKER_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "10-get_token", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('INVOKER_CERT'), key:pm.environment.get('INVOKER_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "if (pm.response.code == 200){", - " pm.environment.set('NETAPP_SERVICE_TOKEN', res.access_token);", - "}" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": true, - "disabledSystemHeaders": {} - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "client_id", - "value": "{{INVOKER_ID}}", - "type": "text" - }, - { - "key": "grant_type", - "value": "client_credentials", - "type": "text" - }, - { - "key": "client_secret", - "value": "string", - "type": "text" - }, - { - "key": "scope", - "value": "3gpp#{{API_AEF_ID}}:{{API_NAME}}", - "type": "text" - } - ] - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/capif-security/v1/securities/{{INVOKER_ID}}/token", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "capif-security", - "v1", - "securities", - "{{INVOKER_ID}}", - "token" - ] - } - }, - "response": [] - }, - { - "name": "11-call_service", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": false - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{NETAPP_SERVICE_TOKEN}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "{\n\"name\": {{USERNAME_INVOKER}}\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://{{IPV4ADDR}}:{{PORT}}{{URI}}", - "protocol": "http", - "host": [ - "{{IPV4ADDR}}" - ], - "port": "{{PORT}}{{URI}}" - } - }, - "response": [] - }, - { - "name": "offboard_provider", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('AMF_CERT'), key:pm.environment.get('AMF_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "DELETE", - "header": [], - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/{{ONBOARDING_URL}}/{{PROVIDER_ID}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "{{ONBOARDING_URL}}", - "{{PROVIDER_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "offboard_invoker", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('INVOKER_CERT'), key:pm.environment.get('INVOKER_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "DELETE", - "header": [], - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/{{ONBOARDING_URL_INVOKER}}/{{INVOKER_ID}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "{{ONBOARDING_URL_INVOKER}}", - "{{INVOKER_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "remove_user_invoker", - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME_INVOKER}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/remove", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "remove" - ] - } - }, - "response": [] - }, - { - "name": "remove_user_provider", - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/remove", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "remove" - ] - } - }, - "response": [] - } - ] -} \ No newline at end of file diff --git a/docs/testing_with_postman/CAPIF.postman_environment.json b/docs/testing_with_postman/CAPIF.postman_environment.json deleted file mode 100644 index ab3839e..0000000 --- a/docs/testing_with_postman/CAPIF.postman_environment.json +++ /dev/null @@ -1,237 +0,0 @@ -{ - "id": "f2daf431-63c4-4275-8755-4cc5de2e566d", - "name": "CAPIF", - "values": [ - { - "key": "CAPIF_HOSTNAME", - "value": "capifcore", - "type": "default", - "enabled": true - }, - { - "key": "CAPIF_PORT", - "value": "8080", - "type": "default", - "enabled": true - }, - { - "key": "REGISTER_HOSTNAME", - "value": "localhost", - "type": "default", - "enabled": true - }, - { - "key": "REGISTER_PORT", - "value": "8084", - "type": "default", - "enabled": true - }, - { - "key": "USERNAME", - "value": "ProviderONE", - "type": "default", - "enabled": true - }, - { - "key": "PASSWORD", - "value": "pass", - "type": "default", - "enabled": true - }, - { - "key": "CALLBACK_IP", - "value": "host.docker.internal", - "type": "default", - "enabled": true - }, - { - "key": "CALLBACK_PORT", - "value": "8087", - "type": "default", - "enabled": true - }, - { - "key": "ONBOARDING_URL", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "PUBLISH_URL", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "USER_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "CA_ROOT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "ACCESS_TOKEN", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "APF_KEY", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AMF_KEY", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AEF_KEY", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "PROVIDER_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AEF_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AEF_CERT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "APF_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "APF_CERT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AMF_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AMF_CERT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "ONBOARDING_URL_INVOKER", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "DISCOVER_URL", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "USER_INVOKER_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "ACCESS_TOKEN_INVOKER", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "INVOKER_KEY", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "INVOKER_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "INVOKER_CERT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "API_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "API_NAME", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "IPV4ADDR", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "PORT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "URI", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "API_SERVICE_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "API_AEF_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "NETAPP_SERVICE_TOKEN", - "value": "", - "type": "any", - "enabled": true - } - ], - "_postman_variable_scope": "environment", - "_postman_exported_at": "2023-12-20T10:47:32.128Z", - "_postman_exported_using": "Postman/10.21.4" -} \ No newline at end of file diff --git a/docs/testing_with_postman/README.md b/docs/testing_with_postman/README.md deleted file mode 100644 index b6c7583..0000000 --- a/docs/testing_with_postman/README.md +++ /dev/null @@ -1,160 +0,0 @@ -[**[Return To Main]**] - -- [CAPIF in Postman](#capif-in-postman) - - [Requisites](#requisites) - - [First steps](#first-steps) - - [Not Local CAPIF](#not-local-capif) -- [CAPIF Flows](#capif-flows) - - [Publication of an API](#publication-of-an-api) - - [**01-register\_user\_provider**](#01-register_user_provider) - - [**02-getauth\_provider**](#02-getauth_provider) - - [**03-onboard\_provider**](#03-onboard_provider) - - [**04-publish\_api**](#04-publish_api) - - [Calling the API](#calling-the-api) - - [**05-register\_user\_invoker**](#05-register_user_invoker) - - [**06-getauth\_invoker**](#06-getauth_invoker) - - [**07-onboard\_invoker**](#07-onboard_invoker) - - [**08-discover**](#08-discover) - - [**09-security\_context**](#09-security_context) - - [**10-get\_token**](#10-get_token) - - [**11-call\_service**](#11-call_service) - - [Other requests](#other-requests) -- [Notes](#notes) - - -# CAPIF in Postman -In this section we can use Postman to publish an API as a provider and use it as an invoker. - -## Requisites - -- We will need to have Node.js installed since we will use a small script to create the CSRs of the certificates. -- An instance of CAPIF (If it is not local, certain variables would have to be modified both in the Node.js script and in the Postman environment variables). - -## First steps - -1. Install the Node dependencies to run the script with: - -``` -npm i -``` - -2. Run the script.js with the following command: - -``` -node script.js -``` - -3. Import Postman collection and environment variables (CAPIF.postman_collection.json and CAPIF.postman_environment.json) -4. Select CAPIF Environment before start testing. - -## Not Local CAPIF - -If the CAPIF is not local, the host and port of both the CAPIF and the register would have to be specified in the variables, and the CAPIF_HOSTNAME in the script, necessary to obtain the server certificate. - -**Enviroments in Postman** -``` -CAPIF_HOSTNAME capifcore -CAPIF_PORT 8080 -REGISTER_HOSTNAME register -REGISTER_PORT 8084 -``` - -**Const in script.js** -``` -CAPIF_HOSTNAME capifcore -``` - -# CAPIF Flows -Once the first steps have been taken, we can now use Postman requests. These requests are numbered in the order that must be followed to obtain everything necessary from CAPIF. - -## Publication of an API - -### **01-register_user_provider** - -![Flow](../images/flows/01a%20-%20Register%20(Only)%20AEF.png) - -### **02-getauth_provider** - -![Flow](../images/flows/01b%20-%20Register%20of%20AEF%20GetAuth.png) - -### **03-onboard_provider** - -![Flow](../images/flows/02%20-%20AEF%20API%20Provider%20registration.png) - -At this point we move on to using certificate authentication in CAPIF. In Postman it is necessary to add the certificates manually and using more than one certificate for the same host as we do in CAPIF complicates things. For this reason, we use the script to overwrite a certificate and a key when it is necessary to have a specific one. - -To configure go to **settings** in Postman and open the **certificates** section. - -- Here, activate the **CA certificates** option and add the **ca_cert.pem** file found in the **Responses** folder. -- Adds a client certificate specifying the CAPIF host being used and the files **client_cert.crt** and **client_key.key** in the **Responses** folder. - - -Once this is done, the node script will be in charge of changing the certificate that is necessary in each request. - -### **04-publish_api** - -![Flow](../images/flows/03%20-%20AEF%20Publish.png) - -Once the api is published, we can start it. In this case we have a test one created in python that can be executed with the following command: - -``` -python3 hello_api.py -``` - -The API publication interface is set to localhost with port 8088, so the service must be set up locally. If you wanted to build it on another site, you would have to change the interface description in the body of publish_api. - -With this the provider part would be finished. - -## Calling the API - -### **05-register_user_invoker** - -![Flow](../images/flows/04a%20-%20Invoker%20(Only)%20Register.png) - -### **06-getauth_invoker** - -![Flow](../images/flows/04b%20-%20Invoker%20Register%20GetAuth.png) - -### **07-onboard_invoker** - -![Flow](../images/flows/05%20-%20Invoker%20Onboarding.png) - -At this point we move on to using certificate authentication in CAPIF. **If you did not configure the provider's certificates, you would have to do it now**. - -### **08-discover** - -![Flow](../images/flows/06%20-%20Invoker%20Discover%20AEF.png) - -### **09-security_context** - -![Flow](../images/flows/07%20-%20Invoker%20Create%20Security%20Context.png) - -### **10-get_token** - -![Flow](../images/flows/08%20-%20Invoker%20Get%20Token.png) - -### **11-call_service** - -![Flow](../images/flows/09%20-%20Invoker%20Send%20Request%20to%20AEF%20Service%20API.png) - -With this, we would have made the API call and finished the flow. - -## Other requests - -Other requests that we have added are the following: - -- **offboard_provider** Performs offboarding of the provider, thereby eliminating the published APIs. -- **offboard_invoker** Offboards the invoker, also eliminating access to the APIs of that invoker. -- **remove_user_invoker** Delete the user created for the invoker. -- **remove_user_provider** Delete the user created for the provider. - -# Notes - -- This process is designed to teach how requests are made in Postman and the flow that should be followed to publish and use an API. -- It is possible that if external CAPIFs are used (Public CAPIF) the test data may already be used or the API already registered. -- It is necessary to have the Node service running to make the certificate change for the requests, otherwise it will not work. -- We are working on adding more requests to the Postman collection. -- This collection is a testing guide and is recommended for testing purposes only. - -[Return To Main]: ../../README.md#using-postman - diff --git a/docs/testing_with_postman/hello_api.py b/docs/testing_with_postman/hello_api.py deleted file mode 100644 index 0b2a359..0000000 --- a/docs/testing_with_postman/hello_api.py +++ /dev/null @@ -1,38 +0,0 @@ -from flask import Flask, jsonify, request -from flask_jwt_extended import jwt_required, JWTManager, get_jwt_identity, get_jwt -import ssl -from werkzeug import serving -import socket, ssl -import OpenSSL -from OpenSSL import crypto -import jwt -import pyone - -app = Flask(__name__) - -jwt_flask = JWTManager(app) - - -with open("Responses/cert_server.pem", "rb") as cert_file: - cert= cert_file.read() - -crtObj = crypto.load_certificate(crypto.FILETYPE_PEM, cert) -pubKeyObject = crtObj.get_pubkey() -pubKeyString = crypto.dump_publickey(crypto.FILETYPE_PEM,pubKeyObject) - -app.config['JWT_ALGORITHM'] = 'RS256' -app.config['JWT_PUBLIC_KEY'] = pubKeyString - - -@app.route("/hello", methods=["POST"]) -@jwt_required() -def hello(): - - request_data = request.get_json() - - user_name = request_data['name'] - - return jsonify(f"Hello: {user_name}, welcome to CAPIF.") - -if __name__ == '__main__': - serving.run_simple("0.0.0.0", 8088, app) diff --git a/docs/testing_with_postman/package.json b/docs/testing_with_postman/package.json deleted file mode 100644 index 6d612a7..0000000 --- a/docs/testing_with_postman/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "node-server", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "body-parser": "^1.18.3", - "express": "^4.16.3", - "shelljs": "^0.8.2" - } - } \ No newline at end of file diff --git a/docs/testing_with_postman/script.js b/docs/testing_with_postman/script.js deleted file mode 100644 index 980f81f..0000000 --- a/docs/testing_with_postman/script.js +++ /dev/null @@ -1,199 +0,0 @@ -// Change this variable if another host is used for CAPIF -const CAPIF_HOSTNAME = 'capifcore'; - -const express = require('express'), - app = express(), - fs = require('fs'), - shell = require('shelljs'), - - - folderPath = './Responses/', - bodyParser = require('body-parser'), - path = require('path'); - -const { exec } = require('child_process'); - -// Create the folder path in case it doesn't exist -shell.mkdir('-p', folderPath); - - // Change the limits according to your response size -app.use(bodyParser.json({limit: '50mb', extended: true})); -app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); -var opensslCommand = '' - -if (CAPIF_HOSTNAME.includes(':')){ - opensslCommand = `openssl s_client -connect ${CAPIF_HOSTNAME} | openssl x509 -text > ./Responses/cert_server.pem`; -} -else{ - opensslCommand = `openssl s_client -connect ${CAPIF_HOSTNAME}:443 | openssl x509 -text > ./Responses/cert_server.pem`; -} - -exec(opensslCommand, (error, stdout, stderr) => { - if (error) { - console.error(`Error generating CSR: ${stderr}`); - } -}); - -fs.writeFileSync('./Responses/client_cert.crt', ''); -fs.writeFileSync('./Responses/client_key.key', ''); - -app.get('/', (req, res) => res.send('Hello, I write data to file. Send them requests!')); - -app.post('/generate_csr', (req, res) => { - - console.log(req.body); - const csrFilePath = 'Responses/'+req.body.apiProvFuncRole+'_csr.pem'; - const privateKeyFilePath = 'Responses/'+req.body.apiProvFuncRole+'_key.key'; - - const subjectInfo = { - country: 'ES', - state: 'Madrid', - locality: 'Madrid', - organization: 'Telefonica I+D', - organizationalUnit: 'IT Department', - emailAddress: 'admin@example.com', - }; - - const opensslCommand = `openssl req -newkey rsa:2048 -nodes -keyout ${privateKeyFilePath} -out ${csrFilePath} -subj "/C=${subjectInfo.country}/ST=${subjectInfo.state}/L=${subjectInfo.locality}/O=${subjectInfo.organization}/OU=${subjectInfo.organizationalUnit}/emailAddress=${subjectInfo.emailAddress}"`; - - exec(opensslCommand, (error, stdout, stderr) => { - if (error) { - console.error(`Error generating CSR: ${stderr}`); - } else { - console.log('CSR generated successfully:'); - fs.readFile(csrFilePath, 'utf8', (readError, csrContent) => { - if (readError) { - console.error(`Error reading CSR: ${readError}`); - res.status(500).send('Error reading CSR'); - } else { - console.log('CSR read successfully:'); - // Send the CSR content in the response - fs.readFile(privateKeyFilePath, 'utf8', (readError, keyContent) => { - if (readError) { - console.error(`Error reading KEY: ${readError}`); - res.status(500).send('Error reading KEY'); - } else { - console.log('KEY read successfully:'); - // Send the CSR content in the response - fs.unlink(csrFilePath, (err) => { - if (err) { - console.error(`Error deleting file: ${err.message}`); - } - }); - fs.unlink(privateKeyFilePath, (err) => { - if (err) { - console.error(`Error deleting file: ${err.message}`); - } - }); - res.send({csr: csrContent, key: keyContent}); - } - }); - } - }); - } - }); -}); - -app.post('/generate_csr_invoker', (req, res) => { - - console.log(req.body); - const csrFilePath = 'Responses/invoker_csr.pem'; - const privateKeyFilePath = 'Responses/invoker_key.key'; - - const subjectInfo = { - country: 'ES', - state: 'Madrid', - locality: 'Madrid', - organization: 'Telefonica I+D', - organizationalUnit: 'IT Department', - emailAddress: 'admin@example.com', - }; - - const opensslCommand = `openssl req -newkey rsa:2048 -nodes -keyout ${privateKeyFilePath} -out ${csrFilePath} -subj "/C=${subjectInfo.country}/ST=${subjectInfo.state}/L=${subjectInfo.locality}/O=${subjectInfo.organization}/OU=${subjectInfo.organizationalUnit}/emailAddress=${subjectInfo.emailAddress}"`; - - exec(opensslCommand, (error, stdout, stderr) => { - if (error) { - console.error(`Error generating CSR: ${stderr}`); - } else { - console.log('CSR generated successfully:'); - fs.readFile(csrFilePath, 'utf8', (readError, csrContent) => { - if (readError) { - console.error(`Error reading CSR: ${readError}`); - res.status(500).send('Error reading CSR'); - } else { - console.log('CSR read successfuly:'); - // Send the CSR content in the response - fs.readFile(privateKeyFilePath, 'utf8', (readError, keyContent) => { - if (readError) { - console.error(`Error reading KEY: ${readError}`); - res.status(500).send('Error reading KEY'); - } else { - console.log('KEY read successfully:'); - // Send the CSR content in the response - fs.unlink(csrFilePath, (err) => { - if (err) { - console.error(`Error deleting file: ${err.message}`); - } - }); - fs.unlink(privateKeyFilePath, (err) => { - if (err) { - console.error(`Error deleting file: ${err.message}`); - } - }); - res.send({csr: csrContent, key: keyContent}); - } - }); - } - }); - } - }); -}); - - -app.post('/write_cert', (req, res) => { - let extension = 'crt', - fsMode = 'writeFile', - filename = "client_cert", - filePath = `${path.join(folderPath, filename)}.${extension}`, - options = {encoding: 'binary'}; - fs[fsMode](filePath, req.body.cert, options, (err) => { - if (err) { - console.log(err); - res.send('Error'); - } - }); - extension = 'key'; - filename = "client_key"; - filePath = `${path.join(folderPath, filename)}.${extension}`; - fs[fsMode](filePath, req.body.key, options, (err) => { - if (err) { - console.log(err); - res.send('Error'); - } - else { - res.send('Success'); - } - }); -}); - -app.post('/write_ca', (req, res) => { - let extension = 'pem', - fsMode = 'writeFile', - filename = "ca_cert", - filePath = `${path.join(folderPath, filename)}.${extension}`, - options = {encoding: 'binary'}; - fs[fsMode](filePath, req.body.ca_root, options, (err) => { - if (err) { - console.log(err); - res.send('Error'); - } - else { - res.send('Success'); - } - }); -}); - -app.listen(3000, () => { - console.log('ResponsesToFile App is listening now! Send them requests my way!'); - console.log(`Data is being stored at location: ${path.join(process.cwd(), folderPath)}`); -}); \ No newline at end of file diff --git a/docs/testing_with_robot/README.md b/docs/testing_with_robot/README.md deleted file mode 100644 index 71504c1..0000000 --- a/docs/testing_with_robot/README.md +++ /dev/null @@ -1,74 +0,0 @@ -[**[Return To Main]**] -# Testing With Robot Framework - -- [Testing With Robot Framework](#testing-with-robot-framework) - - [Steps to Test](#steps-to-test) - - [Script Test Execution](#script-test-execution) - - [Manual Build And Test Execution](#manual-build-and-test-execution) - - [Test result review](#test-result-review) - -## Steps to Test - -To run any test locally you will need *docker* and *docker-compose* installed in order run services and execute test plan. Steps will be: -* **Run All Services**: See section [Run All CAPIF Services](../../README.md#run-all-capif-services-locally-with-docker-images) -* **Run desired tests**: At this point we have 2 options: - * **Using helper script**: [Script Test Execution](#script-test-execution) - * **Build robot docker image and execute manually robot docker**: [Manual Build And Test Execution](#manual-build-and-test-execution) - - -## Script Test Execution -This script will build robot docker image if it's need and execute tests selected by "include" option. Just go to service folder, execute and follow steps. -``` -./runCapifTests.sh --include -``` -Results will be stored at /results - -Please check parameters (include) under *Test Execution* at [Manual Build And Test Execution](#manual-build-and-test-execution). - -## Manual Build And Test Execution - -* **Build Robot docker image**: -``` -cd tools/robot -docker build . -t 5gnow-robot-test:latest -``` - -* **Tests Execution**: - -Execute all tests locally: -``` -=path in local machine to repository cloned. -=path to a folder on local machine to store results of Robot Framework execution. -=Is the hostname set when run.sh is executed, by default it will be capifcore. -=This is the port to reach when robot framework want to reach CAPIF deployment using http, this should be set to port without TLS set on Nginx, 8080 by default. - -To execute all tests run : -docker run -ti --rm --network="host" -v /tests:/opt/robot-tests/tests -v :/opt/robot-tests/results 5gnow-robot-test:latest --variable CAPIF_HOSTNAME:capifcore --variable CAPIF_HTTP_PORT:8080 --include all -``` - -Execute specific tests locally: -``` -To run more specific tests, for example, only one functionality: -=Select one from list: - "capif_api_discover_service", - "capif_api_invoker_management", - "capif_api_publish_service", - "capif_api_events", - "capif_security_api - -And Run: -docker run -ti --rm --network="host" -v /tests:/opt/robot-tests/tests -v :/opt/robot-tests/results 5gnow-robot-test:latest --variable CAPIF_HOSTNAME:capifcore --variable CAPIF_HTTP_PORT:8080 --include -``` - -## Test result review - -In order to Review results after tests, you can check general report at /report.html or if you need more detailed information /log.html, example: -* Report: -![Report](../images/robot_report_example.png) -* Detailed information: -![Log](../images/robot_log_example.png) - -**NOTE: If you need more detail at Robot Framework Logs you can set log level option just adding to command --loglevel DEBUG** - - -[Return To Main]: ../../README.md#robot-framework \ No newline at end of file -- GitLab From f2c00df73228d36c2eee2340d85697b2d7ccc5ad Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 8 Apr 2024 12:33:57 +0200 Subject: [PATCH 069/310] Fix url on README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3066edc..a43067e 100644 --- a/README.md +++ b/README.md @@ -154,4 +154,5 @@ Frequently asked questions can be found here: [FAQ Directory] [Testing Using Curl]: ./docs/testing_with_curl/README.md "Testing Using Curl" [Testing with Robot Framework]: ./docs/testing_with_robot/README.md "Testing with Robot Framework" [FAQ Directory]: https://ocf.etsi.org/documentation/latest/FAQ/ "FAQ Url" -[OCF Documentation]: (https://ocf.etsi.org/documentation/latest/) "OCF Documentation" \ No newline at end of file +[OCF Documentation]: https://ocf.etsi.org/documentation/latest/ "OCF Documentation" + -- GitLab From 794e34fa14ff5993580b4bccbd3f2d4eb0bc14ea Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Wed, 10 Apr 2024 14:30:02 +0200 Subject: [PATCH 070/310] New register Service with httpauth and uuid --- .../controllers/default_controller.py | 9 +-- .../core/apiinvokerenrolmentdetails.py | 9 +-- .../api_invoker_management/db/db.py | 28 +------- .../controllers/default_controller.py | 9 +-- .../core/provider_enrolment_details_api.py | 10 ++- .../api_provider_management/db/db.py | 27 -------- services/register/config.yaml | 4 ++ services/register/register_prepare.sh | 23 ++----- .../register/register_service/__main__.py | 56 ++++++++++++++-- .../register/register_service/auth_utils.py | 8 --- .../controllers/register_controller.py | 55 ++++++++++++---- .../core/register_operations.py | 65 +++++++++---------- services/register/requirements.txt | 1 + 13 files changed, 148 insertions(+), 156 deletions(-) delete mode 100644 services/register/register_service/auth_utils.py diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py index 456b4ec..27eb1c8 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py @@ -104,18 +104,13 @@ def onboarded_invokers_post(body): # noqa: E501 """ identity = get_jwt_identity() - username, role = identity.split() - - if role != "invoker": - prob = ProblemDetails(title="Unauthorized", status=401, detail="Role not authorized for this API route", - cause="User role must be invoker") - return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype='application/json') + username, uuid = identity.split() if connexion.request.is_json: body = APIInvokerEnrolmentDetails.from_dict(connexion.request.get_json()) # noqa: E501 current_app.logger.info("Creating Invoker") - res = invoker_operations.add_apiinvokerenrolmentdetail(body, username) + res = invoker_operations.add_apiinvokerenrolmentdetail(body, username, uuid) if res.status_code == 201: current_app.logger.info("Invoker Created") publisher_ops.publish_message("events", "API_INVOKER_ONBOARDED") diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py index 1d79405..dc186e4 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py @@ -52,13 +52,11 @@ class InvokerManagementOperations(Resource): self.config = Config().get_config() - def add_apiinvokerenrolmentdetail(self, apiinvokerenrolmentdetail, username): + def add_apiinvokerenrolmentdetail(self, apiinvokerenrolmentdetail, username, uuid): mycol = self.db.get_col_by_name(self.db.invoker_enrolment_details) - register = self.db.get_col_by_name_register(self.db.capif_users) #try: - current_app.logger.debug("Creating invoker resource") res = mycol.find_one({'onboarding_information.api_invoker_public_key': apiinvokerenrolmentdetail.onboarding_information.api_invoker_public_key}) @@ -83,9 +81,10 @@ class InvokerManagementOperations(Resource): # Onboarding Date Record invoker_dict = apiinvokerenrolmentdetail.to_dict() invoker_dict["onboarding_date"] = datetime.now() + invoker_dict["username"]=username + invoker_dict["uuid"]=uuid mycol.insert_one(apiinvokerenrolmentdetail.to_dict()) - register.update_one({'username':username}, {"$push":{'list_invokers':api_invoker_id}}) current_app.logger.debug("Invoker inserted in database") current_app.logger.debug("Netapp onboarded sucessfuly") @@ -141,7 +140,6 @@ class InvokerManagementOperations(Resource): def remove_apiinvokerenrolmentdetail(self, onboard_id): mycol = self.db.get_col_by_name(self.db.invoker_enrolment_details) - register = self.db.get_col_by_name_register(self.db.capif_users) try: current_app.logger.debug("Removing invoker resource") result = self.__check_api_invoker_id(onboard_id) @@ -150,7 +148,6 @@ class InvokerManagementOperations(Resource): return result mycol.delete_one({'api_invoker_id':onboard_id}) - register.update_one({'list_invokers':onboard_id}, {"$pull":{'list_invokers':onboard_id}}) self.auth_manager.remove_auth_invoker(onboard_id) current_app.logger.debug("Invoker resource removed from database") diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py index 32a34b8..5bfcd0b 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py @@ -15,7 +15,6 @@ class MongoDatabse(): def __init__(self): self.config = Config().get_config() self.db = self.__connect() - self.register = self.__connect_register() self.invoker_enrolment_details = self.config['mongo']['col'] self.capif_users = self.config['mongo']['capif_users_col'] self.service_col = self.config['mongo']["service_col"] @@ -25,10 +24,6 @@ class MongoDatabse(): def get_col_by_name(self, name): return self.db[name].with_options(codec_options=CodecOptions(tz_aware=True)) - def get_col_by_name_register(self, name): - return self.register[name].with_options(codec_options=CodecOptions(tz_aware=True)) - - def __connect(self, max_retries=3, retry_delay=1): retries = 0 @@ -48,29 +43,8 @@ class MongoDatabse(): print(f"Reconnecting... Retry {retries} of {max_retries}") time.sleep(retry_delay) return None - - def __connect_register(self, max_retries=3, retry_delay=1): - - retries = 0 - - while retries < max_retries: - try: - uri = f"mongodb://{self.config['mongo_register']['user']}:{self.config['mongo_register']['password']}@" \ - f"{self.config['mongo_register']['host']}:{self.config['mongo_register']['port']}" - - - client = MongoClient(uri) - mydb = client[self.config['mongo_register']['db']] - mydb.command("ping") - return mydb - except AutoReconnect: - retries += 1 - print(f"Reconnecting... Retry {retries} of {max_retries}") - time.sleep(retry_delay) - return None def close_connection(self): if self.db.client: self.db.client.close() - if self.register.client: - self.register.client.close() + diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py index 70e5373..f5bcf16 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py @@ -59,20 +59,15 @@ def registrations_post(body): # noqa: E501 """ identity = get_jwt_identity() - username, role = identity.split() + username, uuid = identity.split() current_app.logger.info("Registering Provider Domain") - if role != "provider": - prob = ProblemDetails(title="Unauthorized", status=401, detail="Role not authorized for this API route", - cause="User role must be provider") - return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype='application/json') - if connexion.request.is_json: body = APIProviderEnrolmentDetails.from_dict(connexion.request.get_json()) # noqa: E501 - res = provider_management_ops.register_api_provider_enrolment_details(body, username) + res = provider_management_ops.register_api_provider_enrolment_details(body, username, uuid) return res diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py index 18c5e5b..6ba9043 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py @@ -31,10 +31,9 @@ class ProviderManagementOperations(Resource): Resource.__init__(self) self.auth_manager = AuthManager() - def register_api_provider_enrolment_details(self, api_provider_enrolment_details, username): + def register_api_provider_enrolment_details(self, api_provider_enrolment_details, username, uuid): try: mycol = self.db.get_col_by_name(self.db.provider_enrolment_details) - register = self.db.get_col_by_name_register(self.db.capif_users) current_app.logger.debug("Creating api provider domain") search_filter = {'reg_sec': api_provider_enrolment_details.reg_sec} @@ -58,10 +57,11 @@ class ProviderManagementOperations(Resource): # Onboarding Date Record provider_dict = api_provider_enrolment_details.to_dict() provider_dict["onboarding_date"] = datetime.now() + provider_dict["username"] = username + provider_dict["uuid"] = uuid mycol.insert_one(provider_dict) - register.update_one({'username':username}, {"$push":{'list_providers':api_provider_enrolment_details.api_prov_dom_id}}) - + current_app.logger.debug("Provider inserted in database") res = make_response(object=api_provider_enrolment_details, status=201) @@ -76,7 +76,6 @@ class ProviderManagementOperations(Resource): def delete_api_provider_enrolment_details(self, api_prov_dom_id): try: mycol = self.db.get_col_by_name(self.db.provider_enrolment_details) - register = self.db.get_col_by_name_register(self.db.capif_users) current_app.logger.debug("Deleting provider domain") result = self.__check_api_provider_domain(api_prov_dom_id) @@ -89,7 +88,6 @@ class ProviderManagementOperations(Resource): amf_id = [ provider_func['api_prov_func_id'] for provider_func in result["api_prov_funcs"] if provider_func['api_prov_func_role'] == 'AMF' ] mycol.delete_one({'api_prov_dom_id': api_prov_dom_id}) - register.update_one({'list_providers':api_prov_dom_id}, {"$pull":{'list_providers':api_prov_dom_id}}) out = "The provider matching apiProvDomainId " + api_prov_dom_id + " was offboarded." current_app.logger.debug("Removed provider domain from database") diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py index a5d843a..bb08025 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py @@ -16,17 +16,12 @@ class MongoDatabse(): def __init__(self): self.config = Config().get_config() self.db = self.__connect() - self.register = self.__connect_register() self.provider_enrolment_details = self.config['mongo']['col'] self.capif_users = self.config['mongo']['capif_users'] self.certs_col = self.config['mongo']['certs_col'] def get_col_by_name(self, name): return self.db[name].with_options(codec_options=CodecOptions(tz_aware=True)) - def get_col_by_name_register(self, name): - return self.register[name].with_options(codec_options=CodecOptions(tz_aware=True)) - - def __connect(self, max_retries=3, retry_delay=1): retries = 0 @@ -44,30 +39,8 @@ class MongoDatabse(): time.sleep(retry_delay) return None - def __connect_register(self, max_retries=3, retry_delay=1): - - retries = 0 - - while retries < max_retries: - try: - uri = f"mongodb://{self.config['mongo_register']['user']}:{self.config['mongo_register']['password']}@" \ - f"{self.config['mongo_register']['host']}:{self.config['mongo_register']['port']}" - - - client = MongoClient(uri) - mydb = client[self.config['mongo_register']['db']] - mydb.command("ping") - return mydb - except AutoReconnect: - retries += 1 - print(f"Reconnecting... Retry {retries} of {max_retries}") - time.sleep(retry_delay) - return None - def close_connection(self): if self.db.client: self.db.client.close() - if self.register.client: - self.register.client.close() diff --git a/services/register/config.yaml b/services/register/config.yaml index 2ac53bd..3c33fa0 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -12,3 +12,7 @@ ca_factory: { "token": "dev-only-token" } +register: { + register_uuid: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + admin_users: {admin: "password123"} +} \ No newline at end of file diff --git a/services/register/register_prepare.sh b/services/register/register_prepare.sh index de69be7..4a49b87 100644 --- a/services/register/register_prepare.sh +++ b/services/register/register_prepare.sh @@ -1,27 +1,16 @@ #!/bin/bash -CERTS_FOLDER="/usr/src/app/register_service" +CERTS_FOLDER="/usr/src/app/register_service/certs" cd $CERTS_FOLDER -VAULT_ADDR="http://$VAULT_HOSTNAME:$VAULT_PORT" -VAULT_TOKEN=$VAULT_ACCESS_TOKEN - -curl -k -retry 30 \ - --retry-all-errors \ - --connect-timeout 5 \ - --max-time 10 \ - --retry-delay 10 \ - --retry-max-time 300 \ - --header "X-Vault-Token: $VAULT_TOKEN" \ - --request GET "$VAULT_ADDR/v1/secret/data/server_cert/private" 2>/dev/null | jq -r '.data.data.key' -j > $CERTS_FOLDER/server.key - openssl req -x509 \ -sha256 -days 356 \ -nodes \ -newkey rsa:2048 \ -subj "/CN=register/C=ES/L=Madrid" \ - -keyout /usr/src/app/register_service/registerCA.key -out /usr/src/app/register_service/registerCA.crt + -keyout /usr/src/app/register_service/certs/registerCA.key -out /usr/src/app/register_service/certs/registerCA.crt + -openssl genrsa -out /usr/src/app/register_service/register_key.key 2048 +openssl genrsa -out /usr/src/app/register_service/certs/register_key.key 2048 COUNTRY="ES" # 2 letter country-code STATE="Madrid" # state or province name @@ -37,7 +26,7 @@ COMPANY="" # company name # DAYS="-days 365" # create the certificate request -cat <<__EOF__ | openssl req -new $DAYS -key /usr/src/app/register_service/register_key.key -out /usr/src/app/register_service/register.csr +cat <<__EOF__ | openssl req -new $DAYS -key /usr/src/app/register_service/certs/register_key.key -out /usr/src/app/register_service/certs/register.csr $COUNTRY $STATE $LOCALITY @@ -49,7 +38,7 @@ $CHALLENGE $COMPANY __EOF__ -openssl x509 -req -in /usr/src/app/register_service/register.csr -CA /usr/src/app/register_service/registerCA.crt -CAkey /usr/src/app/register_service/registerCA.key -CAcreateserial -out /usr/src/app/register_service/register_cert.crt -days 365 -sha256 +openssl x509 -req -in /usr/src/app/register_service/certs/register.csr -CA /usr/src/app/register_service/certs/registerCA.crt -CAkey /usr/src/app/register_service/certs/registerCA.key -CAcreateserial -out /usr/src/app/register_service/certs/register_cert.crt -days 365 -sha256 cd /usr/src/app/ python3 -m register_service \ No newline at end of file diff --git a/services/register/register_service/__main__.py b/services/register/register_service/__main__.py index 5225f78..2548ebd 100644 --- a/services/register/register_service/__main__.py +++ b/services/register/register_service/__main__.py @@ -1,27 +1,73 @@ import os -import base64 from flask import Flask from .controllers.register_controller import register_routes from flask_jwt_extended import JWTManager +from OpenSSL.crypto import PKey, TYPE_RSA, X509Req, dump_certificate_request, FILETYPE_PEM, dump_privatekey +import requests +import json +from .config import Config app = Flask(__name__) jwt = JWTManager(app) -with open("/usr/src/app/register_service/server.key", "rb") as key_file: - key_data = key_file.read() +config = Config().get_config() +# Create a superadmin CSR and keys +key = PKey() +key.generate_key(TYPE_RSA, 2048) +req = X509Req() +req.get_subject().O = 'Telefonica I+D' +req.get_subject().OU = 'Innovation' +req.get_subject().L = 'Madrid' +req.get_subject().ST = 'Madrid' +req.get_subject().C = 'ES' +req.get_subject().emailAddress = 'inno@tid.es' +req.set_pubkey(key) +req.sign(key, 'sha256') + +csr_request = dump_certificate_request(FILETYPE_PEM, req) +private_key = dump_privatekey(FILETYPE_PEM, key) + +# Save superadmin private key +key_file = open("register_service/certs/superadmin.key", 'wb+') +key_file.write(bytes(private_key)) +key_file.close() + +# Request superadmin certificate +url = 'http://{}:{}/v1/pki_int/sign/my-ca'.format(config["ca_factory"]["url"], config["ca_factory"]["port"]) +headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} +data = { + 'format':'pem_bundle', + 'ttl': '43000h', + 'csr': csr_request, + 'common_name': "superadmin" +} + +response = requests.request("POST", url, headers=headers, data=data, verify = False) +superadmin_cert = json.loads(response.text)['data']['certificate'] + +# Svae the superadmin certificate +cert_file = open("register_service/certs/superadmin.crt", 'wb') +cert_file.write(bytes(superadmin_cert, 'utf-8')) +cert_file.close() + +# Request CAPIF private key to encode the token +url = 'http://{}:{}/v1/secret/data/server_cert/private'.format(config["ca_factory"]["url"], config["ca_factory"]["port"]) +headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} +response = requests.request("GET", url, headers=headers, verify = False) + +key_data = json.loads(response.text)["data"]["data"]["key"] app.config['JWT_ALGORITHM'] = 'RS256' app.config['JWT_PRIVATE_KEY'] = key_data app.register_blueprint(register_routes) - #---------------------------------------- # launch #---------------------------------------- if __name__ == "__main__": - app.run(debug=True, host = '0.0.0.0', port=8080, ssl_context= ("/usr/src/app/register_service/register_cert.crt", "/usr/src/app/register_service/register_key.key")) + app.run(debug=True, host = '0.0.0.0', port=8080, ssl_context= ("/usr/src/app/register_service/certs/register_cert.crt", "/usr/src/app/register_service/certs/register_key.key")) diff --git a/services/register/register_service/auth_utils.py b/services/register/register_service/auth_utils.py deleted file mode 100644 index f799772..0000000 --- a/services/register/register_service/auth_utils.py +++ /dev/null @@ -1,8 +0,0 @@ -import bcrypt - -def hash_password(password): - hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) - return hashed_password - -def check_password(input_password, stored_password): - return bcrypt.checkpw(input_password.encode('utf-8'), stored_password) \ No newline at end of file diff --git a/services/register/register_service/controllers/register_controller.py b/services/register/register_service/controllers/register_controller.py index 71af562..119e493 100644 --- a/services/register/register_service/controllers/register_controller.py +++ b/services/register/register_service/controllers/register_controller.py @@ -1,39 +1,68 @@ #!/usr/bin/env python3 -from flask import Flask, jsonify, request, Blueprint -from flask_jwt_extended import JWTManager, jwt_required, create_access_token -from pymongo import MongoClient +from flask import current_app, Flask, jsonify, request, Blueprint from ..core.register_operations import RegisterOperations -import secrets +from ..config import Config +from flask_httpauth import HTTPBasicAuth + +auth = HTTPBasicAuth() + +config = Config().get_config() + +@auth.verify_password +def verify_password(username, password): + users = register_operation.get_users()[0].json["users"] + if username in config["register"]["admin_users"] and password == config["register"]["admin_users"][username]: + return username, "admin" + for user in users: + if user["username"] == username and user["password"]==password: + return username, "client" + +def admin_required(fn): + def wrapper(*args, **kwargs): + username, role = auth.current_user() + if role == 'admin': + return fn(*args, **kwargs) + else: + return {"Access denied. Administrator privileges required."}, 403 + wrapper.__name__ = fn.__name__ + return wrapper register_routes = Blueprint("register_routes", __name__) register_operation = RegisterOperations() -@register_routes.route("/register", methods=["POST"]) +@register_routes.route("/createUser", methods=["POST"]) +@auth.login_required +@admin_required def register(): username = request.json["username"] password = request.json["password"] description = request.json["description"] - role = request.json["role"] - cn = request.json["cn"] - if role != "invoker" and role != "provider": - return jsonify(message="Role must be invoker or provider"), 400 - - - return register_operation.register_user(username, password, description, cn, role) + email = request.json["email"] + return register_operation.register_user(username, password, description, email) @register_routes.route("/getauth", methods=["POST"]) +@auth.login_required def getauth(): username = request.json["username"] password = request.json["password"] return register_operation.get_auth(username, password) -@register_routes.route("/remove", methods=["DELETE"]) +@register_routes.route("/deleteUser", methods=["DELETE"]) +@auth.login_required +@admin_required def remove(): username = request.json["username"] password = request.json["password"] return register_operation.remove_user(username, password) + + +@register_routes.route("/getUsers", methods=["GET"]) +@auth.login_required +@admin_required +def getUsers(): + return register_operation.get_users() diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index bef2f65..a6e8992 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -1,14 +1,14 @@ -from flask import Flask, jsonify, request, current_app +from flask import current_app, Flask, jsonify, request, Response from flask_jwt_extended import create_access_token from ..db.db import MongoDatabse from datetime import datetime from ..config import Config -from register_service import auth_utils import secrets import requests import json import sys - +import uuid + class RegisterOperations: def __init__(self): @@ -16,28 +16,20 @@ class RegisterOperations: self.mimetype = 'application/json' self.config = Config().get_config() - def register_user(self, username, password, description, cn, role): + def register_user(self, username, password, description, email): mycol = self.db.get_col_by_name(self.db.capif_users) exist_user = mycol.find_one({"username": username}) if exist_user: return jsonify("user already exists"), 409 + + name_space = uuid.UUID(self.config["register"]["register_uuid"]) + user_uuid = str(uuid.uuid5(name_space, username)) - hashed_password = auth_utils.hash_password(password) - user_info = dict(_id=secrets.token_hex(7), username=username, password=hashed_password, role=role, description=description, cn=cn, list_invokers=[], list_providers=[]) + user_info = dict(uuid=user_uuid, username=username, password=password, description=description, email=email, onboarding_date=datetime.now()) obj = mycol.insert_one(user_info) - if role == "invoker": - return jsonify(message="invoker registered successfully", - id=obj.inserted_id, - ccf_onboarding_url="api-invoker-management/v1/onboardedInvokers", - ccf_discover_url="service-apis/v1/allServiceAPIs?api-invoker-id="), 201 - else: - return jsonify(message="provider" + " registered successfully", - id=obj.inserted_id, - ccf_api_onboarding_url="api-provider-management/v1/registrations", - ccf_publish_url="published-apis/v1//service-apis"), 201 - + return jsonify(message="invoker registered successfully", uuid=user_uuid), 201 def get_auth(self, username, password): @@ -45,16 +37,12 @@ class RegisterOperations: try: - exist_user = mycol.find_one({"username": username}) + exist_user = mycol.find_one({"username": username, "password": password}) if exist_user is None: - return jsonify("No user with these credentials"), 400 + return jsonify("Not exister user with this credentials"), 400 - stored_password = exist_user["password"] - if not auth_utils.check_password(password, stored_password): - return jsonify("No user with these credentials"), 400 - - access_token = create_access_token(identity=(username + " " + exist_user["role"])) + access_token = create_access_token(identity=(username + " " + exist_user["uuid"])) url = f"http://{self.config['ca_factory']['url']}:{self.config['ca_factory']['port']}/v1/secret/data/ca" headers = { @@ -62,7 +50,13 @@ class RegisterOperations: } response = requests.request("GET", url, headers=headers, verify = False) response_payload = json.loads(response.text) - return jsonify(message="Token and CA root returned successfully", access_token=access_token, ca_root=response_payload['data']['data']['ca']), 200 + return jsonify(message="Token and CA root returned successfully", + access_token=access_token, + ca_root=response_payload['data']['data']['ca'], + ccf_api_onboarding_url="api-provider-management/v1/registrations", + ccf_publish_url="published-apis/v1//service-apis", + ccf_onboarding_url="api-invoker-management/v1/onboardedInvokers", + ccf_discover_url="service-apis/v1/allServiceAPIs?api-invoker-id="), 200 except Exception as e: return jsonify(message=f"Errors when try getting auth: {e}"), 500 @@ -71,17 +65,22 @@ class RegisterOperations: mycol = self.db.get_col_by_name(self.db.capif_users) try: - exist_user = mycol.find_one({"username": username}) + mycol.delete_one({"username": username, "password": password}) + + + # Request to the helper to delete invokers and providers - if exist_user is None: - return jsonify("No user with these credentials"), 400 - stored_password = exist_user["password"] - if not auth_utils.check_password(password, stored_password): - return jsonify("No user with these credentials"), 400 - - mycol.delete_one({"username": username}) return jsonify(message="User removed successfully"), 204 except Exception as e: return jsonify(message=f"Errors when try remove user: {e}"), 500 + + def get_users(self): + mycol = self.db.get_col_by_name(self.db.capif_users) + + try: + users=list(mycol.find({}, {"_id":0})) + return jsonify(message="Users successfully obtained", users=users), 200 + except Exception as e: + return jsonify(message=f"Error trying to get users: {e}"), 500 diff --git a/services/register/requirements.txt b/services/register/requirements.txt index 05b9f7d..c5446a6 100644 --- a/services/register/requirements.txt +++ b/services/register/requirements.txt @@ -7,3 +7,4 @@ pyopenssl pyyaml requests bcrypt +flask_httpauth \ No newline at end of file -- GitLab From a2ac3ccf377fa0a166827f5f5b33427134bd8703 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Thu, 11 Apr 2024 09:20:08 +0200 Subject: [PATCH 071/310] ca root caught in main and getauth GET method without body --- .../register/register_service/__main__.py | 14 ++++++++++- .../controllers/register_controller.py | 9 +++---- .../core/register_operations.py | 25 ++++++++----------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/services/register/register_service/__main__.py b/services/register/register_service/__main__.py index 2548ebd..373cfd2 100644 --- a/services/register/register_service/__main__.py +++ b/services/register/register_service/__main__.py @@ -48,11 +48,23 @@ data = { response = requests.request("POST", url, headers=headers, data=data, verify = False) superadmin_cert = json.loads(response.text)['data']['certificate'] -# Svae the superadmin certificate +# Save the superadmin certificate cert_file = open("register_service/certs/superadmin.crt", 'wb') cert_file.write(bytes(superadmin_cert, 'utf-8')) cert_file.close() +url = f"http://{config['ca_factory']['url']}:{config['ca_factory']['port']}/v1/secret/data/ca" +headers = { + + 'X-Vault-Token': config['ca_factory']['token'] +} +response = requests.request("GET", url, headers=headers, verify = False) + +ca_root = json.loads(response.text)['data']['data']['ca'] +cert_file = open("register_service/certs/ca_root.crt", 'wb') +cert_file.write(bytes(ca_root, 'utf-8')) +cert_file.close() + # Request CAPIF private key to encode the token url = 'http://{}:{}/v1/secret/data/server_cert/private'.format(config["ca_factory"]["url"], config["ca_factory"]["port"]) headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} diff --git a/services/register/register_service/controllers/register_controller.py b/services/register/register_service/controllers/register_controller.py index 119e493..5efd21e 100644 --- a/services/register/register_service/controllers/register_controller.py +++ b/services/register/register_service/controllers/register_controller.py @@ -43,13 +43,12 @@ def register(): return register_operation.register_user(username, password, description, email) -@register_routes.route("/getauth", methods=["POST"]) +@register_routes.route("/getauth", methods=["GET"]) @auth.login_required def getauth(): - username = request.json["username"] - password = request.json["password"] - - return register_operation.get_auth(username, password) + username, role = auth.current_user() + + return register_operation.get_auth(username) @register_routes.route("/deleteUser", methods=["DELETE"]) @auth.login_required diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index a6e8992..ab68c5e 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -1,12 +1,9 @@ -from flask import current_app, Flask, jsonify, request, Response +from flask import Flask, jsonify, request, Response from flask_jwt_extended import create_access_token from ..db.db import MongoDatabse from datetime import datetime from ..config import Config -import secrets -import requests -import json -import sys +import base64 import uuid class RegisterOperations: @@ -31,28 +28,26 @@ class RegisterOperations: return jsonify(message="invoker registered successfully", uuid=user_uuid), 201 - def get_auth(self, username, password): + def get_auth(self, username): mycol = self.db.get_col_by_name(self.db.capif_users) try: - exist_user = mycol.find_one({"username": username, "password": password}) + exist_user = mycol.find_one({"username": username}) if exist_user is None: return jsonify("Not exister user with this credentials"), 400 access_token = create_access_token(identity=(username + " " + exist_user["uuid"])) - url = f"http://{self.config['ca_factory']['url']}:{self.config['ca_factory']['port']}/v1/secret/data/ca" - headers = { + + cert_file = open("register_service/certs/ca_root.crt", 'rb') + ca_root = cert_file.read() + cert_file.close() - 'X-Vault-Token': self.config['ca_factory']['token'] - } - response = requests.request("GET", url, headers=headers, verify = False) - response_payload = json.loads(response.text) return jsonify(message="Token and CA root returned successfully", - access_token=access_token, - ca_root=response_payload['data']['data']['ca'], + access_token=access_token, + ca_root=ca_root.decode("utf-8"), ccf_api_onboarding_url="api-provider-management/v1/registrations", ccf_publish_url="published-apis/v1//service-apis", ccf_onboarding_url="api-invoker-management/v1/onboardedInvokers", -- GitLab From e22523bec105ca305b59c48413c41d0559eafce3 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Thu, 11 Apr 2024 09:50:36 +0200 Subject: [PATCH 072/310] jsonify denied message --- .../register_service/controllers/register_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/register/register_service/controllers/register_controller.py b/services/register/register_service/controllers/register_controller.py index 5efd21e..9537808 100644 --- a/services/register/register_service/controllers/register_controller.py +++ b/services/register/register_service/controllers/register_controller.py @@ -25,7 +25,7 @@ def admin_required(fn): if role == 'admin': return fn(*args, **kwargs) else: - return {"Access denied. Administrator privileges required."}, 403 + return jsonify(message="Access denied. Administrator privileges required."), 403 wrapper.__name__ = fn.__name__ return wrapper @@ -47,7 +47,7 @@ def register(): @auth.login_required def getauth(): username, role = auth.current_user() - + return register_operation.get_auth(username) @register_routes.route("/deleteUser", methods=["DELETE"]) -- GitLab From 4721c92b7db7ba002ed48dfd48f862bbfcccd67a Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Thu, 11 Apr 2024 11:08:04 +0200 Subject: [PATCH 073/310] Remove postman files --- .../CAPIF.postman_collection.json | 982 ------------------ .../CAPIF.postman_environment.json | 243 ----- 2 files changed, 1225 deletions(-) delete mode 100644 docs/testing_with_postman/CAPIF.postman_collection.json delete mode 100644 docs/testing_with_postman/CAPIF.postman_environment.json diff --git a/docs/testing_with_postman/CAPIF.postman_collection.json b/docs/testing_with_postman/CAPIF.postman_collection.json deleted file mode 100644 index dcbd5ad..0000000 --- a/docs/testing_with_postman/CAPIF.postman_collection.json +++ /dev/null @@ -1,982 +0,0 @@ -{ - "info": { - "_postman_id": "5cfdf0d7-3b3c-4961-9cb9-84c2bf85056c", - "name": "CAPIF", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "31608242", - "_collection_link": "https://red-comet-993867.postman.co/workspace/Team-Workspace~bfc7c442-a60c-4bb1-8730-fdabc2df89b9/collection/31608242-5cfdf0d7-3b3c-4961-9cb9-84c2bf85056c?action=share&source=collection_link&creator=31608242" - }, - "item": [ - { - "name": "01-register_user_provider", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "pm.environment.set('ONBOARDING_URL', res.ccf_api_onboarding_url);", - "pm.environment.set('PUBLISH_URL', res.ccf_publish_url);", - "pm.environment.set('USER_ID', res.id);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME}}\",\n\"description\": \"provider\",\n\"role\": \"provider\",\n\"cn\": \"provider\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/register", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "register" - ] - } - }, - "response": [] - }, - { - "name": "02-getauth_provider", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "", - "pm.environment.set('CA_ROOT', res.ca_root);", - "pm.environment.set('ACCESS_TOKEN', res.access_token);", - "", - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_ca',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: res", - " }", - " }, function (err, res) {", - " console.log(res);", - " });", - " }, 5000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/getauth", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "getauth" - ] - } - }, - "response": [] - }, - { - "name": "03-onboard_provider", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "if (pm.response.code == 201){", - " ", - " pm.environment.set('PROVIDER_ID', res.apiProvDomId);", - "", - " const roleVariableMapping = {", - " \"AEF\": { id: 'AEF_ID', cert: 'AEF_CERT' },", - " \"APF\": { id: 'APF_ID', cert: 'APF_CERT' },", - " \"AMF\": { id: 'AMF_ID', cert: 'AMF_CERT' }", - " };", - "", - " res.apiProvFuncs.forEach(function(elemento) {", - " const role = elemento.apiProvFuncRole;", - " if (roleVariableMapping.hasOwnProperty(role)) {", - " const variables = roleVariableMapping[role];", - " pm.environment.set(variables.id, elemento.apiProvFuncId);", - " pm.environment.set(variables.cert, elemento.regInfo.apiProvCert);", - "", - " }", - " });", - "", - "}", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "", - "var res = JSON.parse(pm.request.body.raw);", - "", - "res.apiProvFuncs.forEach(function(elemento) {", - "", - " setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/generate_csr',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: elemento", - " }", - " }, function (err, response) {", - " j_file = JSON.parse(response.text());", - " elemento.regInfo.apiProvPubKey = j_file.csr;", - " pm.environment.set(elemento.apiProvFuncRole+'_KEY', j_file.key);", - " });", - " }, 5000);", - "", - "});", - "", - "pm.request.body.raw = res;" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{ACCESS_TOKEN}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "{\n\"apiProvFuncs\": [\n {\n \"regInfo\": {\n \"apiProvPubKey\": \"\"\n },\n \"apiProvFuncRole\": \"AEF\",\n \"apiProvFuncInfo\": \"dummy_aef\"\n },\n {\n \"regInfo\": {\n \"apiProvPubKey\": \"\"\n },\n \"apiProvFuncRole\": \"APF\",\n \"apiProvFuncInfo\": \"dummy_apf\"\n },\n {\n \"regInfo\": {\n \"apiProvPubKey\": \"\"\n },\n \"apiProvFuncRole\": \"AMF\",\n \"apiProvFuncInfo\": \"dummy_amf\"\n }\n],\n\"apiProvDomInfo\": \"This is provider\",\n\"suppFeat\": \"fff\",\n\"failReason\": \"string\",\n\"regSec\": \"{{ACCESS_TOKEN}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/{{ONBOARDING_URL}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "{{ONBOARDING_URL}}" - ] - } - }, - "response": [] - }, - { - "name": "04-publish_api", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('APF_CERT'), key:pm.environment.get('APF_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "\n{\n \"apiName\": \"hello_api_demo_v2\",\n \"aefProfiles\": [\n {\n \"aefId\": \"{{AEF_ID}}\",\n \"versions\": [\n {\n \"apiVersion\": \"v1\",\n \"expiry\": \"2021-11-30T10:32:02.004Z\",\n \"resources\": [\n {\n \"resourceName\": \"hello-endpoint\",\n \"commType\": \"REQUEST_RESPONSE\",\n \"uri\": \"/hello\",\n \"custOpName\": \"string\",\n \"operations\": [\n \"POST\"\n ],\n \"description\": \"Endpoint to receive a welcome message\"\n }\n ],\n \"custOperations\": [\n {\n \"commType\": \"REQUEST_RESPONSE\",\n \"custOpName\": \"string\",\n \"operations\": [\n \"POST\"\n ],\n \"description\": \"string\"\n }\n ]\n }\n ],\n \"protocol\": \"HTTP_1_1\",\n \"dataFormat\": \"JSON\",\n \"securityMethods\": [\"Oauth\"],\n \"interfaceDescriptions\": [\n {\n \"ipv4Addr\": \"localhost\",\n \"port\": 8088,\n \"securityMethods\": [\"Oauth\"]\n }\n ]\n }\n ],\n \"description\": \"Hello api services\",\n \"supportedFeatures\": \"fffff\",\n \"shareableInfo\": {\n \"isShareable\": true,\n \"capifProvDoms\": [\n \"string\"\n ]\n },\n \"serviceAPICategory\": \"string\",\n \"apiSuppFeats\": \"fffff\",\n \"pubApiPath\": {\n \"ccfIds\": [\n \"string\"\n ]\n },\n \"ccfId\": \"string\"\n }", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/published-apis/v1/{{APF_ID}}/service-apis", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "published-apis", - "v1", - "{{APF_ID}}", - "service-apis" - ] - } - }, - "response": [] - }, - { - "name": "05-register_user_invoker", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "pm.environment.set('ONBOARDING_URL_INVOKER', res.ccf_onboarding_url);", - "pm.environment.set('DISCOVER_URL', res.ccf_discover_url);", - "pm.environment.set('USER_INVOKER_ID', res.id);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME_INVOKER}}\",\n\"description\": \"invoker\",\n\"role\": \"invoker\",\n\"cn\": \"invoker\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/register", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "register" - ] - } - }, - "response": [] - }, - { - "name": "06-getauth_invoker", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "", - "pm.environment.set('CA_ROOT', res.ca_root);", - "pm.environment.set('ACCESS_TOKEN_INVOKER', res.access_token);", - "", - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_ca',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: res", - " }", - " }, function (err, res) {", - " console.log(res);", - " });", - " }, 5000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME_INVOKER}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/getauth", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "getauth" - ] - } - }, - "response": [] - }, - { - "name": "07-onboard_invoker", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "if (pm.response.code == 201){", - " ", - " pm.environment.set('INVOKER_ID', res.apiInvokerId);", - " pm.environment.set('INVOKER_CERT', res.onboardingInformation.apiInvokerCertificate);", - "}", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "", - "var res = JSON.parse(pm.request.body.raw);", - "", - "", - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/generate_csr_invoker',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {}", - " }", - " }, function (err, response) {", - " j_file = JSON.parse(response.text());", - " res.onboardingInformation.apiInvokerPublicKey = j_file.csr;", - " pm.environment.set('INVOKER_KEY', j_file.key);", - " });", - " }, 5000);", - "", - "", - "pm.request.body.raw = res;" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{ACCESS_TOKEN_INVOKER}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"notificationDestination\" : \"http://host.docker.internal:8086/netapp_callback\",\n \"supportedFeatures\" : \"fffffff\",\n \"apiInvokerInformation\" : \"dummy\",\n \"websockNotifConfig\" : {\n \"requestWebsocketUri\" : true,\n \"websocketUri\" : \"websocketUri\"\n },\n \"onboardingInformation\" : {\n \"apiInvokerPublicKey\" : \"\"\n },\n \"requestTestNotification\" : true\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/{{ONBOARDING_URL_INVOKER}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "{{ONBOARDING_URL_INVOKER}}" - ] - } - }, - "response": [] - }, - { - "name": "08-discover", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('INVOKER_CERT'), key:pm.environment.get('INVOKER_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "if (pm.response.code == 200){", - "", - " res.serviceAPIDescriptions.forEach(function(api) {", - " pm.environment.set('API_SERVICE_ID', api.apiId);", - " pm.environment.set('API_NAME', api.apiName);", - " pm.environment.set('API_AEF_ID', api.aefProfiles[0].aefId);", - " pm.environment.set('IPV4ADDR', api.aefProfiles[0].interfaceDescriptions[0].ipv4Addr);", - " pm.environment.set('PORT', api.aefProfiles[0].interfaceDescriptions[0].port);", - " pm.environment.set('URI', api.aefProfiles[0].versions[0].resources[0].uri);", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "disableBodyPruning": true, - "strictSSL": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/{{DISCOVER_URL}}{{INVOKER_ID}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "{{DISCOVER_URL}}{{INVOKER_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "09-security_context", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('INVOKER_CERT'), key:pm.environment.get('INVOKER_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "PUT", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"securityInfo\": [\n {\n \"prefSecurityMethods\": [\n \"Oauth\"\n ],\n \"authenticationInfo\": \"string\",\n \"authorizationInfo\": \"string\",\n \"aefId\": \"{{API_AEF_ID}}\",\n \"apiId\": \"{{API_SERVICE_ID}}\"\n }\n ],\n \"notificationDestination\": \"https://mynotificationdest.com\",\n \"requestTestNotification\": true,\n \"websockNotifConfig\": {\n \"websocketUri\": \"string\",\n \"requestWebsocketUri\": true\n },\n \"supportedFeatures\": \"fff\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/capif-security/v1/trustedInvokers/{{INVOKER_ID}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "capif-security", - "v1", - "trustedInvokers", - "{{INVOKER_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "10-get_token", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('INVOKER_CERT'), key:pm.environment.get('INVOKER_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);", - "", - "", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "var res = JSON.parse(responseBody);", - "if (pm.response.code == 200){", - " pm.environment.set('NETAPP_SERVICE_TOKEN', res.access_token);", - "}" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": true, - "disabledSystemHeaders": {} - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "client_id", - "value": "{{INVOKER_ID}}", - "type": "text" - }, - { - "key": "grant_type", - "value": "client_credentials", - "type": "text" - }, - { - "key": "client_secret", - "value": "string", - "type": "text" - }, - { - "key": "scope", - "value": "3gpp#{{API_AEF_ID}}:{{API_NAME}}", - "type": "text" - } - ] - }, - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/capif-security/v1/securities/{{INVOKER_ID}}/token", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "capif-security", - "v1", - "securities", - "{{INVOKER_ID}}", - "token" - ] - } - }, - "response": [] - }, - { - "name": "11-call_service", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": false - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{NETAPP_SERVICE_TOKEN}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "raw": "{\n\"name\": \"{{USERNAME_INVOKER}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://{{IPV4ADDR}}:{{PORT}}{{URI}}", - "protocol": "http", - "host": [ - "{{IPV4ADDR}}" - ], - "port": "{{PORT}}{{URI}}" - } - }, - "response": [] - }, - { - "name": "offboard_provider", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('AMF_CERT'), key:pm.environment.get('AMF_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "DELETE", - "header": [], - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/{{ONBOARDING_URL}}/{{PROVIDER_ID}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "{{ONBOARDING_URL}}", - "{{PROVIDER_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "offboard_invoker", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(() => {", - " pm.sendRequest({", - " url: 'http://localhost:3000/write_cert',", - " method: 'POST',", - " header: 'Content-Type:application/json',", - " encoding: 'binary',", - " body: {", - " mode: 'raw',", - " raw: {cert: pm.environment.get('INVOKER_CERT'), key:pm.environment.get('INVOKER_KEY')}", - " }", - " }, function (err, response) {", - " console.log(response)", - " });", - " }, 5000);" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "strictSSL": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "DELETE", - "header": [], - "url": { - "raw": "https://{{CAPIF_HOSTNAME}}/{{ONBOARDING_URL_INVOKER}}/{{INVOKER_ID}}", - "protocol": "https", - "host": [ - "{{CAPIF_HOSTNAME}}" - ], - "path": [ - "{{ONBOARDING_URL_INVOKER}}", - "{{INVOKER_ID}}" - ] - } - }, - "response": [] - }, - { - "name": "remove_user_invoker", - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME_INVOKER}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/remove", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "remove" - ] - } - }, - "response": [] - }, - { - "name": "remove_user_provider", - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\"password\": \"{{PASSWORD}}\",\n\"username\": \"{{USERNAME}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "https://{{REGISTER_HOSTNAME}}:{{REGISTER_PORT}}/remove", - "protocol": "https", - "host": [ - "{{REGISTER_HOSTNAME}}" - ], - "port": "{{REGISTER_PORT}}", - "path": [ - "remove" - ] - } - }, - "response": [] - } - ] -} \ No newline at end of file diff --git a/docs/testing_with_postman/CAPIF.postman_environment.json b/docs/testing_with_postman/CAPIF.postman_environment.json deleted file mode 100644 index fd084b3..0000000 --- a/docs/testing_with_postman/CAPIF.postman_environment.json +++ /dev/null @@ -1,243 +0,0 @@ -{ - "id": "f2daf431-63c4-4275-8755-4cc5de2e566d", - "name": "CAPIF", - "values": [ - { - "key": "CAPIF_HOSTNAME", - "value": "capifcore", - "type": "default", - "enabled": true - }, - { - "key": "CAPIF_PORT", - "value": "8080", - "type": "default", - "enabled": true - }, - { - "key": "REGISTER_HOSTNAME", - "value": "localhost", - "type": "default", - "enabled": true - }, - { - "key": "REGISTER_PORT", - "value": "8084", - "type": "default", - "enabled": true - }, - { - "key": "USERNAME", - "value": "ProviderONE", - "type": "default", - "enabled": true - }, - { - "key": "USERNAME_INVOKER", - "value": "InvokerONE", - "type": "default", - "enabled": true - }, - { - "key": "PASSWORD", - "value": "pass", - "type": "default", - "enabled": true - }, - { - "key": "CALLBACK_IP", - "value": "host.docker.internal", - "type": "default", - "enabled": true - }, - { - "key": "CALLBACK_PORT", - "value": "8087", - "type": "default", - "enabled": true - }, - { - "key": "ONBOARDING_URL", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "PUBLISH_URL", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "USER_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "CA_ROOT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "ACCESS_TOKEN", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "APF_KEY", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AMF_KEY", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AEF_KEY", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "PROVIDER_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AEF_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AEF_CERT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "APF_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "APF_CERT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AMF_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "AMF_CERT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "ONBOARDING_URL_INVOKER", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "DISCOVER_URL", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "USER_INVOKER_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "ACCESS_TOKEN_INVOKER", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "INVOKER_KEY", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "INVOKER_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "INVOKER_CERT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "API_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "API_NAME", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "IPV4ADDR", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "PORT", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "URI", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "API_SERVICE_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "API_AEF_ID", - "value": "", - "type": "any", - "enabled": true - }, - { - "key": "NETAPP_SERVICE_TOKEN", - "value": "", - "type": "any", - "enabled": true - } - ], - "_postman_variable_scope": "environment", - "_postman_exported_at": "2023-12-20T10:47:32.128Z", - "_postman_exported_using": "Postman/10.21.4" -} \ No newline at end of file -- GitLab From 98014c12fc0c01ca1b03b32b1f40f6f740ba55eb Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 11 Apr 2024 15:55:20 +0200 Subject: [PATCH 074/310] container_scanning_register --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From b5b38b84c56e1063660921836e351f3fa9d2032a Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 11 Apr 2024 16:16:59 +0200 Subject: [PATCH 075/310] cvs_ocf_access_control_policy_api --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 9fb1d87b26dd71107aaea1758e7da60a29f731ca Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 11 Apr 2024 16:31:02 +0200 Subject: [PATCH 076/310] main_security --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 8f1dc75defa2f97149f8ba0617e0a8220860425d Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 11 Apr 2024 16:47:36 +0200 Subject: [PATCH 077/310] cvs_ocf_api_invoker_management_api --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 0d828bbcbae56c3b444628a2538aae6226520637 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 11 Apr 2024 17:12:40 +0200 Subject: [PATCH 078/310] cvs_ocf_api_provider_management_api --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 88138370f344b4a4bd43190222763f61d8f0d390 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 12 Apr 2024 12:35:55 +0200 Subject: [PATCH 079/310] cvs --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 484765a7fce380bcfb789aaebf48c7a34504b648 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 12 Apr 2024 13:12:56 +0200 Subject: [PATCH 080/310] secrets in repo and iac --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 42dedcd7e360efaa413aa81e156b06920056cdf7 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 12 Apr 2024 13:16:14 +0200 Subject: [PATCH 081/310] kubesec-sast --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 8eb88dfab16012b2013dfd83180c9b090e83ffd3 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 12 Apr 2024 13:58:12 +0200 Subject: [PATCH 082/310] main_cancel_previous_action --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 17f6d12a6f5118f03fd81bd591c2e93e89c98427 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 12 Apr 2024 14:00:04 +0200 Subject: [PATCH 083/310] needs: ["main_serect_detection"] --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 5e527b722d8cb6b12247b653e5929c5a4c5493f5 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 12 Apr 2024 14:13:24 +0200 Subject: [PATCH 084/310] main_sast --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 327af975e1d7a1c71e01816d519c084ae1e52eb3 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 12 Apr 2024 14:14:48 +0200 Subject: [PATCH 085/310] main_security --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From e86b9ad1141ec558112e4ae17f5853a627fc3b70 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 12 Apr 2024 14:15:46 +0200 Subject: [PATCH 086/310] main_sast --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 1a1c4b64cc8dc2c5a6e7e52ff2aba93804b744dc Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 12 Apr 2024 14:44:10 +0200 Subject: [PATCH 087/310] semgrep-sast --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From c0cb56aa53e50fa72304b701371e9b9181305760 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 12 Apr 2024 14:46:33 +0200 Subject: [PATCH 088/310] stage test never run --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 794e1767a4ec65cd1263f1fb5381b06e2e878fbb Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 11:25:42 +0200 Subject: [PATCH 089/310] deploy_ocf_main --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 7ba615accbab15ac3b9f456db289e2b6c0e465b6 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 11:26:42 +0200 Subject: [PATCH 090/310] environment ocf_main --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From e35d486e906ffd7f0d4ce574c31f45345fbbe878 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 11:27:57 +0200 Subject: [PATCH 091/310] name: review/main --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 9b95218d252f0383d4d9ad373758223b5f2eef81 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 11:31:14 +0200 Subject: [PATCH 092/310] main_build_and_push --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From af960e364a7e8532f4c830673f1a5ad695a33b83 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 11:36:51 +0200 Subject: [PATCH 093/310] main_common --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 8480199bdb8f56598ccf141d72134abf5a8aa043 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 11:52:14 +0200 Subject: [PATCH 094/310] test --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From c3ad02dd5cae4879fdf45c33ab87eecb9483d4a5 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 11:53:31 +0200 Subject: [PATCH 095/310] main_build_and_push --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From bee74c4a6e7fda5b53118c3367c17c19b2dc7d06 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 11:56:45 +0200 Subject: [PATCH 096/310] deploy_ocf_main --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 5b1924aee90b528857f63a6d22424231144af0fd Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 11:58:27 +0200 Subject: [PATCH 097/310] ocf_main --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 188d509fdbcfc28474eb1f710a29b81125c19a89 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:17:10 +0200 Subject: [PATCH 098/310] test --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 0cc13d4d4909e0131c05c65642788452ddf8c528 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:18:31 +0200 Subject: [PATCH 099/310] test --- helm/DELETE.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..706855a 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1,2 @@ -delete me \ No newline at end of file +delete me +t \ No newline at end of file -- GitLab From fd2fb659c9a0e0a884bae052aa1e9c5735f265e1 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:40:11 +0200 Subject: [PATCH 100/310] ocf_main --- helm/DELETE.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 706855a..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1,2 +1 @@ -delete me -t \ No newline at end of file +delete me \ No newline at end of file -- GitLab From 193a30bd4d5dfacd6264a03cb5b40e11a5a5021f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:41:24 +0200 Subject: [PATCH 101/310] robot_framework_testing --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 470892cf3c9bb5cf229464283f12bf21d975a9d6 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:42:25 +0200 Subject: [PATCH 102/310] test --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 322a3ded42c48b0a1d4d09408de223ee60e3b8ae Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:47:34 +0200 Subject: [PATCH 103/310] post_rf --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 5bdfd3563d32f1bbf9c33f4ebb8319175d301775 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:50:07 +0200 Subject: [PATCH 104/310] post_rf --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 34821ec2d9542241af8aa7d5e36013d3c5b70a9e Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:51:11 +0200 Subject: [PATCH 105/310] test --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From a86ab713cd9d4b14b557e3be42a4552e85261196 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:52:55 +0200 Subject: [PATCH 106/310] new_rf --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 6bf1280f7b86846cf0f7baa2a945385d713c4ad8 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:54:26 +0200 Subject: [PATCH 107/310] stages --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 9a25f36c9b3e82790e5633abce264f2061488370 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:56:52 +0200 Subject: [PATCH 108/310] - main_rf_testing --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 1fd0b4affd44a007c2bee8edf3414dda2bf5baf3 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 12:58:17 +0200 Subject: [PATCH 109/310] needs: ["deploy_ocf_main"] --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 9e0589b624a4dbe29a35d3ad9935d11c43a5f83c Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 13:35:52 +0200 Subject: [PATCH 110/310] before_script --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From b4616e619425aa02ccba4b2dbbae8eaf3a1d7d66 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 14:25:05 +0200 Subject: [PATCH 111/310] docker login --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From ee76301422f44627ad59a45a28638d402c28a09d Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 14:49:52 +0200 Subject: [PATCH 112/310] staging_semgrep_sast --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From f1157372040ff22188397a1c81359e1607c4f884 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 14:51:38 +0200 Subject: [PATCH 113/310] rules --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From f79a100fdb0e9d1375cccae9099a1994d93c2bb7 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 14:56:13 +0200 Subject: [PATCH 114/310] CI_MERGE_REQUEST_TARGET_BRANCH_NAME --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From d23723acd915540db4ed1d4a9b2dd8cee69bb68d Mon Sep 17 00:00:00 2001 From: Stavros Charismiadis Date: Mon, 15 Apr 2024 16:02:34 +0300 Subject: [PATCH 115/310] First draft for deloy script --- deploy.sh | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 deploy.sh diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..5b483bd --- /dev/null +++ b/deploy.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +dirlocation=`pwd`/. +# If no argument is provided, use "main" as the default value +default_branch="main" +default_mon="false" +branch="${1:-$default_branch}" +monitoring="${2:-default_mon}" + +echo "Selected branch: $branch" +echo "We're working with $dirlocation" +cd $dirlocation + + +updaterepo(){ + cd $dirlocation + echo "Build " $1 + if [ ! -d $1 ]; then + git clone https://labs.etsi.org/rep/ocf/$1.git + fi + + cd $1/ + git checkout $branch + git pull +} + +updaterepo capif + +cd $dirlocation +cd capif/services + +./run.sh -m $monitoring \ No newline at end of file -- GitLab From 6d1d470eef70b6922579f12cd7a12030265062aa Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 15:32:25 +0200 Subject: [PATCH 116/310] staging_gemnasium_python_sca --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 79077f7214cbcf0157733b61c9067c4568e6a4c1 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 15:33:50 +0200 Subject: [PATCH 117/310] staging_gemnasium_python_sca --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 614d40c7b492926a968a11f8fa610b2466329e2d Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Mon, 15 Apr 2024 16:47:06 +0300 Subject: [PATCH 118/310] Add instructions on README.md --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index a43067e..358015b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,23 @@ This repository has the python-flask Mockup servers created with openapi-generat Please refer to [OCF Documentation] for more detailed information. + +# Install and Run +### (to be added in Getting Started section of Documentation) +``` +mkdir OpenCAPIF + +cd OpenCAPIF + +# The link must be changed when finally merged with the default branch and be permanent +wget https://labs.etsi.org/rep/ocf/capif/-/raw/OCF19-local-installation-of-capif-downloading-script/deploy.sh + +chmod +x deploy.sh + +# ./deploy.sh [branch to fetch] [true or false (default) to install monitoring stack or not] +./deploy.sh staging +``` + ## How to run CAPIF services in this Repository Capif services are developed under /service/ folder. -- GitLab From e4e25aff1a5fc1f4da492ab7ba7a725d87acb454 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:00:12 +0200 Subject: [PATCH 119/310] dev_build_and_push --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 84129afa6fbe1d4de3de59e2cc88c050f5cad64f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:11:03 +0200 Subject: [PATCH 120/310] stages --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From dabb4e06c6b5b1575ee6d6f4ee81b3015259fa5b Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:12:11 +0200 Subject: [PATCH 121/310] staging_unit_tests --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 59603c525c5bc68b5cf92bb6e7000a5aefc71e4e Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:13:46 +0200 Subject: [PATCH 122/310] cancel_previous_action --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 2d5a9483679a57c29392c3c04059f73df01cc368 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:14:56 +0200 Subject: [PATCH 123/310] dev_cancel_previous_action --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 5978601f1a246340d8f447e8d33b82f265d49547 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:17:00 +0200 Subject: [PATCH 124/310] merge_request_staging_into_main --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From c1543c512bbcdc20990aef45d103c40577b9421c Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:17:45 +0200 Subject: [PATCH 125/310] main_rf_testing --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 5bf5c5e213560884ae96a651cc54038a03356321 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:29:51 +0200 Subject: [PATCH 126/310] pipeline dev --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From b5a88ff9a863b55ac09f85b244037ff699366c01 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:30:50 +0200 Subject: [PATCH 127/310] dev_pre_pipeline --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 47bbcd3f627a42d0ae33ae79103fc27e5e70246a Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:33:29 +0200 Subject: [PATCH 128/310] dev_cancel_previous_action --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 761b30cf92f4fd166a600bd88b2dac530ac37813 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 15 Apr 2024 16:37:15 +0200 Subject: [PATCH 129/310] needs dev --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From b9dd91d20723b172d65f2b847eed74dd3ccfdc89 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 16 Apr 2024 09:13:41 +0200 Subject: [PATCH 130/310] staging pipeline --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 448b37067e2bdd84ed64fc3b57cbb13f8dd6a6d5 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 16 Apr 2024 15:09:23 +0200 Subject: [PATCH 131/310] main pipeline --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 8067c2d832fce276402741739e6e999af554b220 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 16 Apr 2024 15:11:21 +0200 Subject: [PATCH 132/310] main_cancel_previous_action --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 4844d947103959306d6c93026c666eb0110ab77a Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 17 Apr 2024 10:09:29 +0200 Subject: [PATCH 133/310] test cd release --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 3fdf8cf942b6d3fdd718c993529aa627ff914636 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 17 Apr 2024 10:13:12 +0200 Subject: [PATCH 134/310] cd-deploy-release --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 6517da716a7826fbab7a7d0c6a6e9fa5a23cca7b Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 17 Apr 2024 10:14:03 +0200 Subject: [PATCH 135/310] deploy-release --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 3bbb84cd5fbd6956f0145400d4cf9cc5375afea6 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Wed, 17 Apr 2024 11:33:23 +0200 Subject: [PATCH 136/310] Admin security with tokens --- services/register/config.yaml | 2 + .../register/register_service/__main__.py | 7 +- .../controllers/register_controller.py | 98 +++++++++++++------ .../core/register_operations.py | 9 +- 4 files changed, 82 insertions(+), 34 deletions(-) diff --git a/services/register/config.yaml b/services/register/config.yaml index 3c33fa0..bc1370f 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -14,5 +14,7 @@ ca_factory: { register: { register_uuid: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + refresh_expiration: 30, #days + token_expiration: 10, #mins admin_users: {admin: "password123"} } \ No newline at end of file diff --git a/services/register/register_service/__main__.py b/services/register/register_service/__main__.py index 373cfd2..12f6ffd 100644 --- a/services/register/register_service/__main__.py +++ b/services/register/register_service/__main__.py @@ -6,11 +6,12 @@ from flask_jwt_extended import JWTManager from OpenSSL.crypto import PKey, TYPE_RSA, X509Req, dump_certificate_request, FILETYPE_PEM, dump_privatekey import requests import json +import jwt from .config import Config app = Flask(__name__) -jwt = JWTManager(app) +jwt_manager = JWTManager(app) config = Config().get_config() @@ -65,15 +66,17 @@ cert_file = open("register_service/certs/ca_root.crt", 'wb') cert_file.write(bytes(ca_root, 'utf-8')) cert_file.close() -# Request CAPIF private key to encode the token +# Request CAPIF private key to encode the CAPIF token url = 'http://{}:{}/v1/secret/data/server_cert/private'.format(config["ca_factory"]["url"], config["ca_factory"]["port"]) headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} response = requests.request("GET", url, headers=headers, verify = False) key_data = json.loads(response.text)["data"]["data"]["key"] + app.config['JWT_ALGORITHM'] = 'RS256' app.config['JWT_PRIVATE_KEY'] = key_data +app.config['REGISTRE_SECRET_KEY'] = config["register"]["register_uuid"] app.register_blueprint(register_routes) diff --git a/services/register/register_service/controllers/register_controller.py b/services/register/register_service/controllers/register_controller.py index 9537808..14877f8 100644 --- a/services/register/register_service/controllers/register_controller.py +++ b/services/register/register_service/controllers/register_controller.py @@ -3,13 +3,34 @@ from flask import current_app, Flask, jsonify, request, Blueprint from ..core.register_operations import RegisterOperations from ..config import Config +from functools import wraps +from datetime import datetime, timedelta from flask_httpauth import HTTPBasicAuth +import jwt auth = HTTPBasicAuth() config = Config().get_config() +register_routes = Blueprint("register_routes", __name__) +register_operation = RegisterOperations() + +# Function to generate access tokens and refresh tokens +def generate_tokens(username): + access_payload = { + 'username': username, + 'exp': datetime.now() + timedelta(minutes=config["register"]["token_expiration"]) + } + refresh_payload = { + 'username': username, + 'exp': datetime.now() + timedelta(days=config["register"]["refresh_expiration"]) + } + access_token = jwt.encode(access_payload, current_app.config['REGISTRE_SECRET_KEY'], algorithm='HS256') + refresh_token = jwt.encode(refresh_payload, current_app.config['REGISTRE_SECRET_KEY'], algorithm='HS256') + return access_token, refresh_token + +# Function in charge of verifying the basic auth @auth.verify_password def verify_password(username, password): users = register_operation.get_users()[0].json["users"] @@ -18,24 +39,51 @@ def verify_password(username, password): for user in users: if user["username"] == username and user["password"]==password: return username, "client" - -def admin_required(fn): - def wrapper(*args, **kwargs): - username, role = auth.current_user() - if role == 'admin': - return fn(*args, **kwargs) - else: - return jsonify(message="Access denied. Administrator privileges required."), 403 - wrapper.__name__ = fn.__name__ - return wrapper -register_routes = Blueprint("register_routes", __name__) -register_operation = RegisterOperations() +# Function responsible for verifying the token +def admin_required(): + def decorator(f): + @wraps(f) + def decorated(*args, **kwargs): + + token = request.headers.get('Authorization') + if not token: + return jsonify({'message': 'Token is missing'}), 401 + + if token.startswith('Bearer '): + token = token.split('Bearer ')[1] + + if not token: + return jsonify({'message': 'Token is missing'}), 401 -@register_routes.route("/createUser", methods=["POST"]) + try: + data = jwt.decode(token, current_app.config['REGISTRE_SECRET_KEY'], algorithms=['HS256'], options={'verify_exp': True}) + username = data['username'] + return f(username, *args, **kwargs) + except Exception as e: + return jsonify({'message': str(e)}), 401 + + return decorated + return decorator + +@register_routes.route('/login', methods=['POST']) @auth.login_required -@admin_required -def register(): +def login(): + username, rol = auth.current_user() + if rol != "admin": + return jsonify(message="Unauthorized. Administrator privileges required."), 401 + access_token, refresh_token = generate_tokens(username) + return jsonify({'access_token': access_token, 'refresh_token': refresh_token}) + +@register_routes.route('/refresh', methods=['POST']) +@admin_required() +def refresh_token(username): + access_token, _ = generate_tokens(username) + return jsonify({'access_token': access_token}) + +@register_routes.route("/createUser", methods=["POST"]) +@admin_required() +def register(username): username = request.json["username"] password = request.json["password"] description = request.json["description"] @@ -46,22 +94,16 @@ def register(): @register_routes.route("/getauth", methods=["GET"]) @auth.login_required def getauth(): - username, role = auth.current_user() - + username, _ = auth.current_user() return register_operation.get_auth(username) -@register_routes.route("/deleteUser", methods=["DELETE"]) -@auth.login_required -@admin_required -def remove(): - username = request.json["username"] - password = request.json["password"] - - return register_operation.remove_user(username, password) +@register_routes.route("/deleteUser/", methods=["DELETE"]) +@admin_required() +def remove(username, uuid): + return register_operation.remove_user(uuid) @register_routes.route("/getUsers", methods=["GET"]) -@auth.login_required -@admin_required -def getUsers(): +@admin_required() +def getUsers(username): return register_operation.get_users() diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index ab68c5e..a8fee91 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -26,7 +26,7 @@ class RegisterOperations: user_info = dict(uuid=user_uuid, username=username, password=password, description=description, email=email, onboarding_date=datetime.now()) obj = mycol.insert_one(user_info) - return jsonify(message="invoker registered successfully", uuid=user_uuid), 201 + return jsonify(message="User registered successfully", uuid=user_uuid), 201 def get_auth(self, username): @@ -51,16 +51,17 @@ class RegisterOperations: ccf_api_onboarding_url="api-provider-management/v1/registrations", ccf_publish_url="published-apis/v1//service-apis", ccf_onboarding_url="api-invoker-management/v1/onboardedInvokers", - ccf_discover_url="service-apis/v1/allServiceAPIs?api-invoker-id="), 200 + ccf_discover_url="service-apis/v1/allServiceAPIs?api-invoker-id=", + ccf_security_url="capif-security/v1/trustedInvokers/"), 200 except Exception as e: return jsonify(message=f"Errors when try getting auth: {e}"), 500 - def remove_user(self, username, password): + def remove_user(self, uuid): mycol = self.db.get_col_by_name(self.db.capif_users) try: - mycol.delete_one({"username": username, "password": password}) + mycol.delete_one({"uuid": uuid}) # Request to the helper to delete invokers and providers -- GitLab From 2d9f55e170b788365f89444bb04413c820723a7d Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 17 Apr 2024 12:11:37 +0200 Subject: [PATCH 137/310] Update Robot tests with latest changes --- .../capif_security_api.robot | 18 +-- tests/libraries/helpers.py | 13 ++ tests/requirements.txt | 3 +- tests/resources/common/basicRequests.robot | 68 ++++++++-- tests/tasks/Dummy Info/__init__.robot | 2 + tests/tasks/Dummy Info/populate.robot | 126 ++++++++++++++++++ tests/tasks/__init__.robot | 56 ++++++++ 7 files changed, 265 insertions(+), 21 deletions(-) create mode 100644 tests/tasks/Dummy Info/__init__.robot create mode 100644 tests/tasks/Dummy Info/populate.robot create mode 100644 tests/tasks/__init__.robot diff --git a/tests/features/CAPIF Security Api/capif_security_api.robot b/tests/features/CAPIF Security Api/capif_security_api.robot index 2a41e80..c920d01 100644 --- a/tests/features/CAPIF Security Api/capif_security_api.robot +++ b/tests/features/CAPIF Security Api/capif_security_api.robot @@ -670,7 +670,7 @@ Retrieve access token # Test ${discover_response}= Get Request Capif - ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} @@ -729,7 +729,7 @@ Retrieve access token by Provider # Test ${discover_response}= Get Request Capif - ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} @@ -787,7 +787,7 @@ Retrieve access token by Provider with invalid apiInvokerId # Test ${discover_response}= Get Request Capif - ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} @@ -846,7 +846,7 @@ Retrieve access token with invalid apiInvokerId # Test ${discover_response}= Get Request Capif - ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} @@ -907,7 +907,7 @@ Retrieve access token with invalid client_id # Test ${discover_response}= Get Request Capif - ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} @@ -966,7 +966,7 @@ Retrieve access token with unsupported grant_type # Test ${discover_response}= Get Request Capif - ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} @@ -1032,7 +1032,7 @@ Retrieve access token with invalid scope # Test ${discover_response}= Get Request Capif - ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} @@ -1093,7 +1093,7 @@ Retrieve access token with invalid aefid at scope # Test ${discover_response}= Get Request Capif - ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} @@ -1154,7 +1154,7 @@ Retrieve access token with invalid apiName at scope # Test ${discover_response}= Get Request Capif - ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} diff --git a/tests/libraries/helpers.py b/tests/libraries/helpers.py index ec1abe0..94c850f 100644 --- a/tests/libraries/helpers.py +++ b/tests/libraries/helpers.py @@ -7,6 +7,8 @@ from OpenSSL.crypto import (dump_certificate_request, dump_privatekey, from OpenSSL.SSL import FILETYPE_PEM import socket import copy +import json +import pickle def parse_url(input): @@ -139,3 +141,14 @@ def create_scope(aef_id, api_name): data = "3gpp#" + aef_id + ":" + api_name return data + +def read_dictionary(file_path): + with open(file_path, 'rb') as fp: + data = pickle.load(fp) + print('Dictionary loaded') + return data + +def write_dictionary(file_path, data): + with open(file_path, 'wb') as fp: + pickle.dump(data, fp) + print('dictionary saved successfully to file ' + file_path) diff --git a/tests/requirements.txt b/tests/requirements.txt index 71b55e2..c6d9032 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -4,4 +4,5 @@ requests==2.28.1 configparser==5.3.0 redis==4.3.4 rfc3987==1.3.8 -robotframework-httpctrl \ No newline at end of file +robotframework-httpctrl +robotframework-archivelibrary == 0.4.2 \ No newline at end of file diff --git a/tests/resources/common/basicRequests.robot b/tests/resources/common/basicRequests.robot index 1d8d695..580c7d0 100644 --- a/tests/resources/common/basicRequests.robot +++ b/tests/resources/common/basicRequests.robot @@ -332,12 +332,6 @@ Get Auth For User RETURN ${resp.json()} -# Clean Test Information By HTTP Requests -# Create Session jwtsession ${CAPIF_HTTP_URL} verify=True - -# ${resp}= DELETE On Session jwtsession /testdata -# Should Be Equal As Strings ${resp.status_code} 200 - Clean Test Information ${capif_users_dict}= Call Method ${CAPIF_USERS} get_capif_users_dict @@ -413,17 +407,38 @@ Remove entity Log Dictionary ${capif_users_dict} Log List ${register_users} +Remove Resource + [Arguments] ${resource_url} ${management_cert} ${username} + + ${resp}= Delete Request Capif + ... ${resource_url} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${management_cert} + + Status Should Be 204 ${resp} + + &{body}= Create Dictionary + ... password=password + ... username=${username} + + Create Session jwtsession ${CAPIF_HTTPS_REGISTER_URL} verify=False disable_warnings=1 + + ${resp}= DELETE On Session jwtsession /remove json=${body} + + Should Be Equal As Strings ${resp.status_code} 204 + Invoker Default Onboarding [Arguments] ${invoker_username}=${INVOKER_USERNAME} ${register_user_info}= Register User At Jwt Auth - ... username=${INVOKER_USERNAME} role=${INVOKER_ROLE} + ... username=${invoker_username} role=${INVOKER_ROLE} # Send Onboarding Request ${request_body}= Create Onboarding Notification Body ... http://${CAPIF_CALLBACK_IP}:${CAPIF_CALLBACK_PORT}/netapp_callback ... ${register_user_info['csr_request']} - ... ${INVOKER_USERNAME} + ... ${invoker_username} ${resp}= Post Request Capif ... ${register_user_info['ccf_onboarding_url']} ... json=${request_body} @@ -437,12 +452,15 @@ Invoker Default Onboarding # Assertions Status Should Be 201 ${resp} Check Variable ${resp.json()} APIInvokerEnrolmentDetails - Check Location Header ${resp} ${LOCATION_INVOKER_RESOURCE_REGEX} + ${resource_url}= Check Location Header ${resp} ${LOCATION_INVOKER_RESOURCE_REGEX} # Store dummy signede certificate - Store In File ${INVOKER_USERNAME}.crt ${resp.json()['onboardingInformation']['apiInvokerCertificate']} + Store In File ${invoker_username}.crt ${resp.json()['onboardingInformation']['apiInvokerCertificate']} ${url}= Parse Url ${resp.headers['Location']} + Set To Dictionary ${register_user_info} resource_url=${resource_url} + Set To Dictionary ${register_user_info} management_cert=${invoker_username} + RETURN ${register_user_info} ${url} ${request_body} Provider Registration @@ -500,13 +518,15 @@ Provider Registration ... provider_enrollment_details=${request_body} ... resource_url=${resource_url} ... provider_register_response=${resp} + ... management_cert=${register_user_info['amf_username']} RETURN ${register_user_info} Provider Default Registration + [Arguments] ${provider_username}=${PROVIDER_USERNAME} # Register Provider ${register_user_info}= Register User At Jwt Auth Provider - ... username=${PROVIDER_USERNAME} role=${PROVIDER_ROLE} + ... username=${provider_username} role=${PROVIDER_ROLE} ${register_user_info}= Provider Registration ${register_user_info} @@ -591,3 +611,29 @@ Basic ACL registration END RETURN ${register_user_info_invoker} ${register_user_info_provider} ${service_api_description_published} + +Create Security Context Between invoker and provider + [Arguments] ${register_user_info_invoker} ${register_user_info_provider} + + ${discover_response}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${register_user_info_invoker['management_cert']} + + Check Response Variable Type And Values ${discover_response} 200 DiscoveredAPIs + + # create Security Context + ${request_body}= Create Service Security From Discover Response + ... http://${CAPIF_HOSTNAME}:${CAPIF_HTTP_PORT}/test + ... ${discover_response} + + ${resp}= Put Request Capif + ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${register_user_info_invoker['management_cert']} + + Check Response Variable Type And Values ${resp} 201 ServiceSecurity + diff --git a/tests/tasks/Dummy Info/__init__.robot b/tests/tasks/Dummy Info/__init__.robot new file mode 100644 index 0000000..f6bbb18 --- /dev/null +++ b/tests/tasks/Dummy Info/__init__.robot @@ -0,0 +1,2 @@ +*** Settings *** +Force Tags populate \ No newline at end of file diff --git a/tests/tasks/Dummy Info/populate.robot b/tests/tasks/Dummy Info/populate.robot new file mode 100644 index 0000000..9a61a40 --- /dev/null +++ b/tests/tasks/Dummy Info/populate.robot @@ -0,0 +1,126 @@ +*** Settings *** +Resource /opt/robot-tests/tests/resources/common.resource +Resource /opt/robot-tests/tests/resources/api_invoker_management_requests/apiInvokerManagementRequests.robot +Resource ../../resources/common.resource +Resource ../../resources/common/basicRequests.robot +Library /opt/robot-tests/tests/libraries/bodyRequests.py +Library Process +Library Collections +Library ArchiveLibrary +Library OperatingSystem +Library DateTime + +Suite Teardown Reset Testing Environment +Test Setup Reset Testing Environment + + +*** Variables *** +${API_INVOKER_NOT_REGISTERED} not-valid +${TOTAL_INVOKERS} 10 +${TOTAL_PROVIDERS} 10 + +${BACKUP_DIRECTORY} backup +${RESULT_FOLDER} /opt/robot-tests/results +${OUTPUT_ZIP_FILE} entities_loaded.zip + +${INVOKER_USERNAME_POPULATE} ${INVOKER_USERNAME}_POPULATE +${PROVIDER_USERNAME_POPULATE} ${PROVIDER_USERNAME}_POPULATE + + +*** Test Cases *** +Create Dummy Invokers and Providers + [Tags] populate-create + ${entities_dictionary}= Create Dictionary + Create Directory ${BACKUP_DIRECTORY} + + FOR ${counter} IN RANGE ${TOTAL_PROVIDERS} + ${USERNAME}= Set Variable ${PROVIDER_USERNAME_POPULATE}_${counter} + ${register_user_info}= Run Keyword And Continue On Failure Provider Default Registration ${USERNAME} + + Set To Dictionary ${entities_dictionary} ${USERNAME}=${register_user_info} + Copy Files *${USERNAME}* ${BACKUP_DIRECTORY}/ + + ${service_api_description_published} + ... ${resource_url} + ... ${request_body}= + ... Run Keyword And Continue On Failure + ... Publish Service Api + ... ${register_user_info} + ... ROBOT_SERVICE_${counter} + END + + ${last_provider_used}= Evaluate -1 + FOR ${counter} IN RANGE ${TOTAL_INVOKERS} + ${USERNAME}= Set Variable ${INVOKER_USERNAME_POPULATE}_${counter} + ${register_user_info} ${url} ${request_body}= Run Keyword And Continue On Failure + ... Invoker Default Onboarding + ... ${USERNAME} + + IF ${TOTAL_PROVIDERS} > 0 + ${last_provider_used} ${register_user_info_provider}= Get Provider + ... ${last_provider_used} + ... ${entities_dictionary} + Log Dictionary ${register_user_info_provider} + + Run Keyword And Continue On Failure + ... Create Security Context Between invoker and provider + ... ${register_user_info} + ... ${register_user_info_provider} + END + + Set To Dictionary ${entities_dictionary} ${USERNAME}=${register_user_info} + Copy Files ${USERNAME}* ${BACKUP_DIRECTORY}/ + END + + Write Dictionary ${BACKUP_DIRECTORY}/registers.json ${entities_dictionary} + ${date}= Get Current Date result_format=%Y_%m_%d_%H_%M_%S + Create Zip From Files In Directory ${BACKUP_DIRECTORY} ${RESULT_FOLDER}/${date}_${OUTPUT_ZIP_FILE} + + ${result}= Run Process ls -l + + Log Many ${result.stdout} + +Remove Dummy Invokers and Providers + [Tags] populate-remove + ${files}= List Files In Directory ${RESULT_FOLDER} *${OUTPUT_ZIP_FILE} + ${sorted_list}= Copy List ${files} + + Sort List ${sorted_list} + ${last_backup}= Get From List ${sorted_list} -1 + + Copy File ${RESULT_FOLDER}/${last_backup} ./ + Extract Zip File ${last_backup} + + ${entities_dictionary}= Read Dictionary registers.json + + Log Dictionary ${entities_dictionary} + + FOR ${username} IN @{entities_dictionary} + Log ${username}=${entities_dictionary}[${username}] + ${resource_url}= Set Variable ${entities_dictionary}[${username}][resource_url] + ${management_cert}= Set Variable ${entities_dictionary}[${username}][management_cert] + Run Keyword And Ignore Error Remove Resource ${resource_url.path} ${management_cert} ${username} + END + + ${result}= Run Process ls -l + + Log Many ${result.stdout} + + +*** Keywords *** +Get Provider + [Arguments] ${index} ${entities_dictionary} + ${index}= Evaluate ${index} + 1 + IF ${index} == ${TOTAL_PROVIDERS} + ${index}= Evaluate 0 + END + + ${username}= Set Variable ${PROVIDER_USERNAME_POPULATE}_${index} + ${usernames}= Get Dictionary Keys ${entities_dictionary} + IF '${username}' in ${usernames} + log ${username} is in the list + ELSE + Log Dictionary not contain ${username}, no provider returned + END + + RETURN ${index} ${entities_dictionary}[${username}] diff --git a/tests/tasks/__init__.robot b/tests/tasks/__init__.robot new file mode 100644 index 0000000..a65a0e7 --- /dev/null +++ b/tests/tasks/__init__.robot @@ -0,0 +1,56 @@ +*** Settings *** +Resource /opt/robot-tests/tests/resources/common.resource +Resource ../resources/common.resource + +Suite Setup Prepare environment +# Suite Teardown Reset Testing Environment + +Force Tags tasks + + +*** Keywords *** +Prepare environment + Log ${CAPIF_HOSTNAME} + Log "${CAPIF_HTTP_PORT}" + Log "${CAPIF_HTTPS_PORT}" + + Set Global Variable ${CAPIF_HTTP_VAULT_URL} http://${CAPIF_VAULT}/ + IF "${CAPIF_VAULT_PORT}" != "" + Set Global Variable ${CAPIF_HTTP_VAULT_URL} http://${CAPIF_VAULT}:${CAPIF_VAULT_PORT}/ + END + + Set Global Variable ${CAPIF_HTTPS_REGISTER_URL} https://${CAPIF_REGISTER}/ + IF "${CAPIF_REGISTER_PORT}" != "" + Set Global Variable ${CAPIF_HTTPS_REGISTER_URL} https://${CAPIF_REGISTER}:${CAPIF_REGISTER_PORT}/ + END + + Set Global Variable ${CAPIF_HTTP_URL} http://${CAPIF_HOSTNAME}/ + IF "${CAPIF_HTTP_PORT}" != "" + Set Global Variable ${CAPIF_HTTP_URL} http://${CAPIF_HOSTNAME}:${CAPIF_HTTP_PORT}/ + END + + Set Global Variable ${CAPIF_HTTPS_URL} https://${CAPIF_HOSTNAME}/ + IF "${CAPIF_HTTPS_PORT}" != "" + Set Global Variable ${CAPIF_HTTPS_URL} https://${CAPIF_HOSTNAME}:${CAPIF_HTTPS_PORT}/ + END + + ${status} ${CAPIF_IP}= Run Keyword And Ignore Error Get Ip From Hostname ${CAPIF_HOSTNAME} + + IF "${status}" == "PASS" + Log We will use a remote deployment + Log ${CAPIF_IP} + ELSE + Log We will use a local deployment + Add Dns To Hosts 127.0.0.1 ${CAPIF_HOSTNAME} + END + # Obtain ca root certificate + Retrieve Ca Root + + Reset Testing Environment + +Retrieve Ca Root + [Documentation] This keyword retrieve ca.root from CAPIF and store it at ca.crt in order to use at TLS communications + ${resp}= Get CA Vault /v1/secret/data/ca ${CAPIF_HTTP_VAULT_URL} + Status Should Be 200 ${resp} + Log ${resp.json()['data']['data']['ca']} + Store In File ca.crt ${resp.json()['data']['data']['ca']} -- GitLab From 48ade4415db5e710a26c0e73c2d31f90e91bf64b Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Wed, 17 Apr 2024 16:01:56 +0200 Subject: [PATCH 138/310] gitKeep in register/certs --- services/register/register_service/certs/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/register/register_service/certs/.gitkeep diff --git a/services/register/register_service/certs/.gitkeep b/services/register/register_service/certs/.gitkeep new file mode 100644 index 0000000..e69de29 -- GitLab From a5173bdd04d73a4eab2fc8944e4b7e12f2df8da3 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 17 Apr 2024 17:14:53 +0200 Subject: [PATCH 139/310] Upgrade Robot to new Register flow --- .../capif_api_access_control_policy.robot | 15 +- .../capif_auditing_api.robot | 20 +- .../capif_api_service_discover.robot | 24 +- .../CAPIF Api Events/capif_events_api.robot | 10 +- .../capif_api_invoker_managenet.robot | 8 - .../capif_logging_api.robot | 20 +- .../capif_api_provider_management.robot | 86 ++++--- .../capif_api_publish_service.robot | 32 +-- .../capif_security_api.robot | 88 +++---- tests/libraries/environment.py | 22 +- tests/resources/common.resource | 15 +- tests/resources/common/basicRequests.robot | 227 +++++++++++++----- 12 files changed, 338 insertions(+), 229 deletions(-) diff --git a/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot b/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot index 1772f3b..905e4c7 100644 --- a/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot +++ b/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot @@ -28,7 +28,7 @@ Retrieve ACL ... update_capif_users_dicts ... ${register_user_info_provider['resource_url'].path} ... ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info_provider} @@ -51,7 +51,7 @@ Retrieve ACL ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${discover_response}= Get Request Capif @@ -102,7 +102,6 @@ Retrieve ACL with 2 Service APIs published ... update_capif_users_dicts ... ${register_user_info_provider['resource_url'].path} ... ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info_provider} @@ -119,7 +118,6 @@ Retrieve ACL with 2 Service APIs published ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} # Test ${discover_response}= Get Request Capif @@ -184,7 +182,6 @@ Retrieve ACL with security context created by two different Invokers ... update_capif_users_dicts ... ${register_user_info_provider['resource_url'].path} ... ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info_provider} @@ -207,7 +204,6 @@ Retrieve ACL with security context created by two different Invokers ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} ${INVOKER_USERNAME_2}= Set Variable ${INVOKER_USERNAME}_2 @@ -216,7 +212,6 @@ Retrieve ACL with security context created by two different Invokers ... ${INVOKER_USERNAME_2} Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME_2} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME_2} # Get Published APIs ${discover_response}= Get Request Capif @@ -301,7 +296,7 @@ Retrieve ACL filtered by api-invoker-id ... update_capif_users_dicts ... ${register_user_info_provider['resource_url'].path} ... ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info_provider} @@ -324,7 +319,7 @@ Retrieve ACL filtered by api-invoker-id ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${INVOKER_USERNAME_2}= Set Variable ${INVOKER_USERNAME}_2 @@ -333,7 +328,7 @@ Retrieve ACL filtered by api-invoker-id ... ${INVOKER_USERNAME_2} Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME_2} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME_2} + # Get Published APIs ${discover_response}= Get Request Capif diff --git a/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot b/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot index e4052ef..4c0a393 100644 --- a/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot +++ b/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot @@ -23,7 +23,7 @@ Get Log Entry ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -32,7 +32,7 @@ Get Log Entry ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} @@ -68,7 +68,7 @@ Get a log entry without entry created ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -77,7 +77,7 @@ Get a log entry without entry created ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${resp_1}= Get Request Capif @@ -100,7 +100,7 @@ Get a log entry withut aefid and apiInvokerId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -109,7 +109,7 @@ Get a log entry withut aefid and apiInvokerId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} @@ -149,7 +149,7 @@ Get Log Entry with apiVersion filter ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -158,7 +158,7 @@ Get Log Entry with apiVersion filter ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} @@ -194,7 +194,7 @@ Get Log Entry with no exist apiVersion filter ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -203,7 +203,7 @@ Get Log Entry with no exist apiVersion filter ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${discover_response}= Get Request Capif diff --git a/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot b/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot index 0e2a5f7..2b7ed5b 100644 --- a/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot +++ b/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot @@ -20,7 +20,7 @@ Discover Published service APIs by Authorised API Invoker ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api @@ -30,7 +30,7 @@ Discover Published service APIs by Authorised API Invoker ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${resp}= Get Request Capif @@ -54,7 +54,7 @@ Discover Published service APIs by Non Authorised API Invoker ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -63,7 +63,7 @@ Discover Published service APIs by Non Authorised API Invoker ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${resp}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} @@ -84,7 +84,7 @@ Discover Published service APIs by not registered API Invoker ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -93,7 +93,7 @@ Discover Published service APIs by not registered API Invoker ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${resp}= Get Request Capif ... ${DISCOVER_URL}${API_INVOKER_NOT_REGISTERED} @@ -114,7 +114,7 @@ Discover Published service APIs by registered API Invoker with 1 result filtered ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name_1}= Set Variable service_1 ${api_name_2}= Set Variable service_2 @@ -131,7 +131,7 @@ Discover Published service APIs by registered API Invoker with 1 result filtered ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Request all APIs for Invoker ${resp}= Get Request Capif @@ -168,7 +168,7 @@ Discover Published service APIs by registered API Invoker filtered with no match ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name_1}= Set Variable apiName1 ${api_name_2}= Set Variable apiName2 @@ -185,7 +185,7 @@ Discover Published service APIs by registered API Invoker filtered with no match ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Request all APIs for Invoker ${resp}= Get Request Capif @@ -222,7 +222,7 @@ Discover Published service APIs by registered API Invoker not filtered ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name_1}= Set Variable apiName1 ${api_name_2}= Set Variable apiName2 @@ -239,7 +239,7 @@ Discover Published service APIs by registered API Invoker not filtered ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Request all APIs for Invoker ${resp}= Get Request Capif diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index aecff95..c027769 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -22,7 +22,7 @@ Creates a new individual CAPIF Event Subscription ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Events Subscription ${resp}= Post Request Capif @@ -42,7 +42,7 @@ Creates a new individual CAPIF Event Subscription with Invalid SubscriberId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Events Subscription ${resp}= Post Request Capif @@ -65,7 +65,7 @@ Deletes an individual CAPIF Event Subscription ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Events Subscription ${resp}= Post Request Capif @@ -93,7 +93,7 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriberId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Events Subscription ${resp}= Post Request Capif @@ -128,7 +128,7 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriptionId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Events Subscription ${resp}= Post Request Capif diff --git a/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot b/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot index 48eaa87..7127cc4 100644 --- a/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot +++ b/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot @@ -37,7 +37,6 @@ Onboard NetApp Check Response Variable Type And Values ${resp} 201 APIInvokerEnrolmentDetails ${url}= Parse Url ${resp.headers['Location']} Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} Check Location Header ${resp} ${LOCATION_INVOKER_RESOURCE_REGEX} # Store dummy signed certificate @@ -49,7 +48,6 @@ Register NetApp Already Onboarded ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} ${resp}= Post Request Capif ... ${register_user_info['ccf_onboarding_url']} @@ -73,7 +71,6 @@ Update Onboarded NetApp ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} Set To Dictionary ... ${request_body} @@ -96,7 +93,6 @@ Update Not Onboarded NetApp ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} ${resp}= Put Request Capif ... /api-invoker-management/v1/onboardedInvokers/${INVOKER_NOT_REGISTERED} @@ -117,8 +113,6 @@ Offboard NetApp # Default Invoker Registration and Onboarding ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} - ${resp}= Delete Request Capif ... ${url.path} ... server=${CAPIF_HTTPS_URL} @@ -134,7 +128,6 @@ Offboard Not Previously Onboarded NetApp ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} ${resp}= Delete Request Capif ... /api-invoker-management/v1/onboardedInvokers/${INVOKER_NOT_REGISTERED} @@ -157,7 +150,6 @@ Update Onboarded NetApp Certificate ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} ${INVOKER_USERNAME_NEW}= Set Variable ${INVOKER_USERNAME}_NEW diff --git a/tests/features/CAPIF Api Logging Service/capif_logging_api.robot b/tests/features/CAPIF Api Logging Service/capif_logging_api.robot index de9c672..2604bdb 100644 --- a/tests/features/CAPIF Api Logging Service/capif_logging_api.robot +++ b/tests/features/CAPIF Api Logging Service/capif_logging_api.robot @@ -21,7 +21,7 @@ Create a log entry ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -30,7 +30,7 @@ Create a log entry ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} @@ -60,7 +60,7 @@ Create a log entry invalid aefId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -69,7 +69,7 @@ Create a log entry invalid aefId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} @@ -103,7 +103,7 @@ Create a log entry invalid serviceApi ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -112,7 +112,7 @@ Create a log entry invalid serviceApi ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} @@ -141,7 +141,7 @@ Create a log entry invalid apiInvokerId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -150,7 +150,7 @@ Create a log entry invalid apiInvokerId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} @@ -183,7 +183,7 @@ Create a log entry different aef_id in body ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish one api Publish Service Api ${register_user_info} @@ -192,7 +192,7 @@ Create a log entry different aef_id in body ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${discover_response}= Get Request Capif diff --git a/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot b/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot index fd70925..f47385f 100644 --- a/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot +++ b/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot @@ -1,12 +1,12 @@ *** Settings *** -Resource /opt/robot-tests/tests/resources/common.resource -Resource ../../resources/common.resource -Library /opt/robot-tests/tests/libraries/bodyRequests.py -Library Process -Library Collections +Resource /opt/robot-tests/tests/resources/common.resource +Resource ../../resources/common.resource +Library /opt/robot-tests/tests/libraries/bodyRequests.py +Library Process +Library Collections -Test Setup Reset Testing Environment -Suite Teardown Reset Testing Environment +Suite Teardown Reset Testing Environment +Test Setup Reset Testing Environment *** Variables *** @@ -16,7 +16,7 @@ ${API_PROVIDER_NOT_REGISTERED} notValid *** Test Cases *** Register Api Provider [Tags] capif_api_provider_management-1 - #Register Provider User An create Certificates for each function + # Register Provider User An create Certificates for each function ${register_user_info}= Register User At Jwt Auth Provider ... username=${PROVIDER_USERNAME} role=${PROVIDER_ROLE} @@ -51,12 +51,10 @@ Register Api Provider Check Response Variable Type And Values ${resp} 201 APIProviderEnrolmentDetails ${url}= Parse Url ${resp.headers['Location']} - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${register_user_info['amf_username']} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${register_user_info['amf_username']} ${resource_url}= Check Location Header ${resp} ${LOCATION_PROVIDER_RESOURCE_REGEX} - FOR ${prov} IN @{resp.json()['apiProvFuncs']} Log Dictionary ${prov} Store In File ${prov['apiProvFuncInfo']}.crt ${prov['regInfo']['apiProvCert']} @@ -66,9 +64,11 @@ Register Api Provider Already registered [Tags] capif_api_provider_management-2 ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} - + Call Method + ... ${CAPIF_USERS} + ... update_capif_users_dicts + ... ${register_user_info['resource_url'].path} + ... ${AMF_PROVIDER_USERNAME} ${resp}= Post Request Capif ... /api-provider-management/v1/registrations @@ -88,8 +88,11 @@ Update Registered Api Provider [Tags] capif_api_provider_management-3 ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + Call Method + ... ${CAPIF_USERS} + ... update_capif_users_dicts + ... ${register_user_info['resource_url'].path} + ... ${AMF_PROVIDER_USERNAME} ${request_body}= Set Variable ${register_user_info['provider_enrollment_details']} @@ -124,8 +127,11 @@ Update Not Registered Api Provider [Tags] capif_api_provider_management-4 ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + Call Method + ... ${CAPIF_USERS} + ... update_capif_users_dicts + ... ${register_user_info['resource_url'].path} + ... ${AMF_PROVIDER_USERNAME} ${request_body}= Set Variable ${register_user_info['provider_enrollment_details']} @@ -144,31 +150,34 @@ Update Not Registered Api Provider ... cause=Not found registrations to send this api provider details # Partially Update Registered Api Provider -# [Tags] capif_api_provider_management-5 -# ${register_user_info}= Provider Default Registration +# [Tags] capif_api_provider_management-5 +# ${register_user_info}= Provider Default Registration -# ${request_body}= Create Api Provider Enrolment Details Patch Body ROBOT_TESTING_MOD +# ${request_body}= Create Api Provider Enrolment Details Patch Body ROBOT_TESTING_MOD -# ${resp}= Patch Request Capif -# ... ${register_user_info['resource_url'].path} -# ... json=${request_body} -# ... server=${CAPIF_HTTPS_URL} -# ... verify=ca.crt -# ... username=${AMF_PROVIDER_USERNAME} +# ${resp}= Patch Request Capif +# ... ${register_user_info['resource_url'].path} +# ... json=${request_body} +# ... server=${CAPIF_HTTPS_URL} +# ... verify=ca.crt +# ... username=${AMF_PROVIDER_USERNAME} -# Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${register_user_info['amf_username']} -# Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} +# Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${register_user_info['amf_username']} +# -# # Check Results -# Check Response Variable Type And Values ${resp} 200 APIProviderEnrolmentDetails -# ... apiProvDomInfo=ROBOT_TESTING_MOD +# # Check Results +# Check Response Variable Type And Values ${resp} 200 APIProviderEnrolmentDetails +# ... apiProvDomInfo=ROBOT_TESTING_MOD Partially Update Not Registered Api Provider [Tags] capif_api_provider_management-6 ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + Call Method + ... ${CAPIF_USERS} + ... update_capif_users_dicts + ... ${register_user_info['resource_url'].path} + ... ${AMF_PROVIDER_USERNAME} ${request_body}= Create Api Provider Enrolment Details Patch Body @@ -190,8 +199,6 @@ Delete Registered Api Provider [Tags] capif_api_provider_management-7 ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} - ${resp}= Delete Request Capif ... ${register_user_info['resource_url'].path} ... server=${CAPIF_HTTPS_URL} @@ -205,8 +212,11 @@ Delete Not Registered Api Provider [Tags] capif_api_provider_management-8 ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + Call Method + ... ${CAPIF_USERS} + ... update_capif_users_dicts + ... ${register_user_info['resource_url'].path} + ... ${AMF_PROVIDER_USERNAME} ${resp}= Delete Request Capif ... /api-provider-management/v1/registrations/${API_PROVIDER_NOT_REGISTERED} diff --git a/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot b/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot index cd58b3e..daef4be 100644 --- a/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot +++ b/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot @@ -20,7 +20,7 @@ Publish API by Authorised API Publisher ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Test ${request_body}= Create Service Api Description service_1 @@ -42,7 +42,7 @@ Publish API by NON Authorised API Publisher ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${request_body}= Create Service Api Description ${resp}= Post Request Capif @@ -64,7 +64,7 @@ Retrieve all APIs Published by Authorised apfId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Register One Service ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api @@ -92,7 +92,7 @@ Retrieve all APIs Published by NON Authorised apfId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Retrieve Services published ${resp}= Get Request Capif @@ -113,7 +113,7 @@ Retrieve single APIs Published by Authorised apfId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} @@ -152,7 +152,7 @@ Retrieve single APIs non Published by Authorised apfId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${resp}= Get Request Capif ... /published-apis/v1/${register_user_info['apf_id']}/service-apis/${SERVICE_API_ID_NOT_VALID} @@ -172,7 +172,7 @@ Retrieve single APIs Published by NON Authorised apfId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Publish Service API ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api @@ -183,7 +183,7 @@ Retrieve single APIs Published by NON Authorised apfId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${resp}= Get Request Capif ... ${resource_url.path} @@ -203,7 +203,7 @@ Update API Published by Authorised apfId with valid serviceApiId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} @@ -236,7 +236,7 @@ Update APIs Published by Authorised apfId with invalid serviceApiId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} @@ -261,7 +261,7 @@ Update APIs Published by NON Authorised apfId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} @@ -271,7 +271,7 @@ Update APIs Published by NON Authorised apfId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Service Api Description service_1_modified ${resp}= Put Request Capif @@ -303,7 +303,7 @@ Delete API Published by Authorised apfId with valid serviceApiId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} @@ -334,7 +334,7 @@ Delete APIs Published by Authorised apfId with invalid serviceApiId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${resp}= Delete Request Capif ... /published-apis/v1/${register_user_info['apf_id']}/service-apis/${SERVICE_API_ID_NOT_VALID} @@ -353,13 +353,13 @@ Delete APIs Published by NON Authorised apfId ${register_user_info}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + #Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${resp}= Delete Request Capif ... /published-apis/v1/${register_user_info['apf_id']}/service-apis/${SERVICE_API_ID_NOT_VALID} diff --git a/tests/features/CAPIF Security Api/capif_security_api.robot b/tests/features/CAPIF Security Api/capif_security_api.robot index c920d01..4e27948 100644 --- a/tests/features/CAPIF Security Api/capif_security_api.robot +++ b/tests/features/CAPIF Security Api/capif_security_api.robot @@ -23,7 +23,7 @@ Create a security context for an API invoker ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Create Security Context ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} @@ -44,13 +44,13 @@ Create a security context for an API invoker with Provider role ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Register Provider ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Create Security Context ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} @@ -74,7 +74,7 @@ Create a security context for an API invoker with Provider entity role and inval ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Create Security Context ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} @@ -98,7 +98,7 @@ Create a security context for an API invoker with Invalid apiInvokerID ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -121,7 +121,7 @@ Retrieve the Security Context of an API Invoker ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -140,7 +140,7 @@ Retrieve the Security Context of an API Invoker ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Retrieve Security context can setup by parameters if authenticationInfo and authorizationInfo are needed at response. # ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']}?authenticationInfo=true&authorizationInfo=true @@ -166,7 +166,7 @@ Retrieve the Security Context of an API Invoker with invalid apiInvokerID ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${resp}= Get Request Capif ... /capif-security/v1/trustedInvokers/${API_INVOKER_NOT_VALID} @@ -186,7 +186,7 @@ Retrieve the Security Context of an API Invoker with invalid apfId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -218,7 +218,7 @@ Delete the Security Context of an API Invoker ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -234,7 +234,7 @@ Delete the Security Context of an API Invoker ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Remove Security Context ${resp}= Delete Request Capif @@ -264,7 +264,7 @@ Delete the Security Context of an API Invoker with Invoker entity role ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -295,7 +295,7 @@ Delete the Security Context of an API Invoker with Invoker entity role and inval ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${resp}= Delete Request Capif ... /capif-security/v1/trustedInvokers/${API_INVOKER_NOT_VALID} @@ -316,7 +316,7 @@ Delete the Security Context of an API Invoker with invalid apiInvokerID ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${resp}= Delete Request Capif ... /capif-security/v1/trustedInvokers/${API_INVOKER_NOT_VALID} @@ -337,13 +337,13 @@ Update the Security Context of an API Invoker ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Register Provider ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -390,7 +390,7 @@ Update the Security Context of an API Invoker with Provider entity role ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -406,7 +406,7 @@ Update the Security Context of an API Invoker with Provider entity role ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${resp}= Post Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']}/update @@ -428,7 +428,7 @@ Update the Security Context of an API Invoker with AEF entity role and invalid a ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Post Request Capif @@ -450,7 +450,7 @@ Update the Security Context of an API Invoker with invalid apiInvokerID ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Post Request Capif @@ -473,7 +473,7 @@ Revoke the authorization of the API invoker for APIs ${register_user_info_provider}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name}= Set Variable service_1 @@ -486,7 +486,7 @@ Revoke the authorization of the API invoker for APIs ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test @@ -546,7 +546,7 @@ Revoke the authorization of the API invoker for APIs without valid apfID. ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -564,7 +564,7 @@ Revoke the authorization of the API invoker for APIs without valid apfID. ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + # Revoke Security Context by Invoker ${request_body}= Create Security Notification Body ${register_user_info_invoker['api_invoker_id']} 1234 @@ -602,7 +602,7 @@ Revoke the authorization of the API invoker for APIs with invalid apiInvokerId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -620,7 +620,7 @@ Revoke the authorization of the API invoker for APIs with invalid apiInvokerId ${register_user_info_publisher}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${request_body}= Create Security Notification Body ${API_INVOKER_NOT_VALID} 1234 ${resp}= Post Request Capif @@ -653,7 +653,7 @@ Retrieve access token ${register_user_info_provider}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name}= Set Variable service_1 @@ -666,7 +666,7 @@ Retrieve access token ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${discover_response}= Get Request Capif @@ -712,7 +712,7 @@ Retrieve access token by Provider ${register_user_info_provider}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name}= Set Variable service_1 @@ -725,7 +725,7 @@ Retrieve access token by Provider ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${discover_response}= Get Request Capif @@ -770,7 +770,7 @@ Retrieve access token by Provider with invalid apiInvokerId ${register_user_info_provider}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name}= Set Variable service_1 @@ -783,7 +783,7 @@ Retrieve access token by Provider with invalid apiInvokerId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${discover_response}= Get Request Capif @@ -829,7 +829,7 @@ Retrieve access token with invalid apiInvokerId ${register_user_info_provider}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name}= Set Variable service_1 @@ -842,7 +842,7 @@ Retrieve access token with invalid apiInvokerId ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${discover_response}= Get Request Capif @@ -890,7 +890,7 @@ Retrieve access token with invalid client_id ${register_user_info_provider}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name}= Set Variable service_1 @@ -903,7 +903,7 @@ Retrieve access token with invalid client_id ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${discover_response}= Get Request Capif @@ -949,7 +949,7 @@ Retrieve access token with unsupported grant_type ${register_user_info_provider}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name}= Set Variable service_1 @@ -962,7 +962,7 @@ Retrieve access token with unsupported grant_type ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${discover_response}= Get Request Capif @@ -1015,7 +1015,7 @@ Retrieve access token with invalid scope ${register_user_info_provider}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name}= Set Variable service_1 @@ -1028,7 +1028,7 @@ Retrieve access token with invalid scope ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${discover_response}= Get Request Capif @@ -1076,7 +1076,7 @@ Retrieve access token with invalid aefid at scope ${register_user_info_provider}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name}= Set Variable service_1 @@ -1089,7 +1089,7 @@ Retrieve access token with invalid aefid at scope ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${discover_response}= Get Request Capif @@ -1137,7 +1137,7 @@ Retrieve access token with invalid apiName at scope ${register_user_info_provider}= Provider Default Registration Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + ${api_name}= Set Variable service_1 @@ -1150,7 +1150,7 @@ Retrieve access token with invalid apiName at scope ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} + # Test ${discover_response}= Get Request Capif diff --git a/tests/libraries/environment.py b/tests/libraries/environment.py index d8100fa..8b7a504 100644 --- a/tests/libraries/environment.py +++ b/tests/libraries/environment.py @@ -1,10 +1,10 @@ class CapifUserManager(): def __init__(self): self.capif_users = {} - self.register_users = [] + self.register_users = {} - def update_register_users(self, value): - self.register_users.append(value) + def update_register_users(self, uuid, username): + self.register_users[uuid] = username def update_capif_users_dicts(self, key, value): self.capif_users[key] = value @@ -12,14 +12,22 @@ class CapifUserManager(): def remove_capif_users_entry(self, key): self.capif_users.pop(key) - def remove_register_users_entry(self, value): - self.register_users.remove(value) + def remove_register_users_entry(self, uuid=None, username=None): + if uuid != None: + self.register_users.pop(uuid) + elif username != None: + uuid=self.get_user_uuid(username) + self.register_users.pop(uuid) def get_capif_users_dict(self): return self.capif_users - def get_register_users(self): + def get_register_users_dict(self): return self.register_users - + + def get_user_uuid(self, username): + for uuid, stored_user in self.register_users.items(): + if stored_user == username: + return uuid CAPIF_USERS = CapifUserManager() \ No newline at end of file diff --git a/tests/resources/common.resource b/tests/resources/common.resource index b111127..c46131b 100644 --- a/tests/resources/common.resource +++ b/tests/resources/common.resource @@ -1,7 +1,7 @@ *** Settings *** -Library /opt/robot-tests/tests/libraries/helpers.py -Variables /opt/robot-tests/tests/libraries/environment.py -Resource /opt/robot-tests/tests/resources/common/basicRequests.robot +Library /opt/robot-tests/tests/libraries/helpers.py +Variables /opt/robot-tests/tests/libraries/environment.py +Resource /opt/robot-tests/tests/resources/common/basicRequests.robot *** Variables *** @@ -21,12 +21,15 @@ ${CAPIF_VAULT_PORT} 8200 ${CAPIF_VAULT_TOKEN} read-ca-token ${CAPIF_REGISTER} register ${CAPIF_REGISTER_PORT} 8084 -${CAPIF_HTTP_PORT} -${CAPIF_HTTPS_PORT} +${CAPIF_HTTP_PORT} ${EMPTY} +${CAPIF_HTTPS_PORT} ${EMPTY} ${CAPIF_IP} 127.0.0.1 ${CAPIF_CALLBACK_IP} host.docker.internal ${CAPIF_CALLBACK_PORT} 8086 +${REGISTER_ADMIN_USER} admin +${REGISTER_ADMIN_PASSWORD} password123 + ${DISCOVER_URL} /service-apis/v1/allServiceAPIs?api-invoker-id= @@ -34,7 +37,7 @@ ${DISCOVER_URL} /service-apis/v1/allServiceAPIs?api-invoker-id= Reset Testing Environment Log Db capif.invokerdetails collection will be removed in order to isolate each test. - #Clean Test Information By HTTP Requests + # Clean Test Information By HTTP Requests Clean Test Information Check Location Header diff --git a/tests/resources/common/basicRequests.robot b/tests/resources/common/basicRequests.robot index 580c7d0..3a24b84 100644 --- a/tests/resources/common/basicRequests.robot +++ b/tests/resources/common/basicRequests.robot @@ -5,6 +5,7 @@ Library RequestsLibrary Library Collections Library OperatingSystem Library XML +Library Telnet *** Variables *** @@ -53,6 +54,97 @@ Create CAPIF Session RETURN ${headers} +Create Register Admin Session + [Documentation] Create needed session to reach Register as Administrator. + [Arguments] ${server}=${NONE} ${access_token}=${NONE} ${verify}=${NONE} ${vault_token}=${NONE} + IF "${server}" != "${NONE}" + IF "${access_token}" != "${NONE}" + ## Return Header with bearer + ${headers}= Create Dictionary Authorization=Bearer ${access_token} + + RETURN ${headers} + END + + # Request Admin Login to retrieve access token + Create Session register_session ${server} verify=${verify} disable_warnings=1 + + ${auth}= Set variable ${{ ('${REGISTER_ADMIN_USER}','${REGISTER_ADMIN_PASSWORD}') }} + ${resp}= POST On Session register_session /login auth=${auth} + + Log Dictionary ${resp.json()} + + ## Crear sesión con token + ${headers}= Create Dictionary Authorization=Bearer ${resp.json()['access_token']} + + RETURN ${headers} + END + + + RETURN ${NONE} + +## NEW REQUESTS TO REGISTER +Post Request Admin Register + [Timeout] 60s + [Arguments] + ... ${endpoint} + ... ${json}=${NONE} + ... ${server}=${NONE} + ... ${access_token}=${NONE} + ... ${auth}=${NONE} + ... ${verify}=${FALSE} + ... ${cert}=${NONE} + ... ${username}=${NONE} + ... ${data}=${NONE} + + ${headers}= Create Register Admin Session ${server} ${access_token} ${verify} + + IF '${username}' != '${NONE}' + ${cert}= Set variable ${{ ('${username}.crt','${username}.key') }} + END + + ${resp}= POST On Session + ... register_session + ... ${endpoint} + ... headers=${headers} + ... json=${json} + ... expected_status=any + ... verify=${verify} + ... cert=${cert} + ... data=${data} + RETURN ${resp} + +Get Request Admin Register + [Timeout] 60s + [Arguments] + ... ${endpoint} + ... ${server}=${NONE} + ... ${access_token}=${NONE} + ... ${auth}=${NONE} + ... ${verify}=${FALSE} + ... ${cert}=${NONE} + ... ${username}=${NONE} + + ${headers}= Create Register Admin Session ${server} ${access_token} + + IF '${username}' != '${NONE}' + ${cert}= Set variable ${{ ('${username}.crt','${username}.key') }} + END + + ${resp}= GET On Session + ... register_session + ... ${endpoint} + ... headers=${headers} + ... expected_status=any + ... verify=${verify} + ... cert=${cert} + RETURN ${resp} + + + + +# NEW REQUESTS END + + Post Request Capif [Timeout] 60s [Arguments] @@ -235,24 +327,15 @@ Register User At Jwt Auth END Log cn=${cn} - - &{body}= Create Dictionary - ... password=${password} - ... username=${username} - ... role=${role} - ... description=${description} - ... cn=${cn} - - Create Session jwtsession ${CAPIF_HTTPS_REGISTER_URL} verify=False disable_warnings=1 - - ${resp}= POST On Session jwtsession /register json=${body} - - Should Be Equal As Strings ${resp.status_code} 201 + + ${resp}= Create User At Register ${username} ${password} ${description} email="${username}@nobody.com" ${get_auth_response}= Get Auth For User ${username} ${password} + Log Dictionary ${get_auth_response} + ${register_user_info}= Create Dictionary - ... netappID=${resp.json()['id']} + ... netappID=${resp.json()['uuid']} ... csr_request=${csr_request} ... &{resp.json()} ... &{get_auth_response} @@ -270,6 +353,8 @@ Register User At Jwt Auth Store In File ${username}.key ${register_user_info['private_key']} END + Call Method ${CAPIF_USERS} update_register_users ${register_user_info['uuid']} ${username} + RETURN ${register_user_info} Register User At Jwt Auth Provider @@ -287,24 +372,14 @@ Register User At Jwt Auth Provider ${amf_csr_request}= Create User Csr ${amf_username} amf # Register provider - &{body}= Create Dictionary - ... password=${password} - ... username=${username} - ... role=${role} - ... description=${description} - ... cn=${username} - - Create Session jwtsession ${CAPIF_HTTPS_REGISTER_URL} verify=False disable_warnings=1 - - ${resp}= POST On Session jwtsession /register json=${body} - - Should Be Equal As Strings ${resp.status_code} 201 + ${resp}= Create User At Register ${username} ${password} ${description} email="${username}@nobody.com" - # Get Auth to obtain access_token ${get_auth_response}= Get Auth For User ${username} ${password} + Log Dictionary ${get_auth_response} + ${register_user_info}= Create Dictionary - ... netappID=${resp.json()['id']} + ... netappID=${resp.json()['uuid']} ... csr_request=${csr_request} ... apf_csr_request=${apf_csr_request} ... aef_csr_request=${aef_csr_request} @@ -317,25 +392,71 @@ Register User At Jwt Auth Provider Log Dictionary ${register_user_info} + Call Method ${CAPIF_USERS} update_register_users ${register_user_info['uuid']} ${username} + RETURN ${register_user_info} -Get Auth For User - [Arguments] ${username} ${password} - &{body}= Create Dictionary username=${username} password=${password} +Login Register Admin + ${headers}= Create Register Admin Session ${CAPIF_HTTPS_REGISTER_URL} + RETURN ${headers} + +Create User At Register + [Documentation] (Administrator) This Keyword create a user at register component. + [Arguments] ${username} ${password} ${description} ${email} + + # Obtain Admin Token to request creation of User + ${headers}= Login Register Admin + + &{body}= Create Dictionary username=${username} password=${password} description=${description} email=${email} + ${resp}= Post On Session register_session /createUser headers=${headers} json=${body} + Should Be Equal As Strings ${resp.status_code} 201 + + RETURN ${resp} - ${resp}= POST On Session jwtsession /getauth json=${body} +Delete User At Register + [Documentation] (Administrator) This Keyword delete a user from register. + [Arguments] ${username}=${NONE} ${uuid}=${NONE} + ${user_uuid}= Set Variable ${uuid} + + IF "${username}" != "${NONE}" + ${user_uuid}= Call Method ${CAPIF_USERS} get_user_uuid ${username} + END + + ${headers}= Create Register Admin Session ${CAPIF_HTTPS_REGISTER_URL} verify=False + + ${resp}= DELETE On Session register_session /deleteUser/${user_uuid} headers=${headers} + + Should Be Equal As Strings ${resp.status_code} 204 + + Call Method ${CAPIF_USERS} remove_register_users_entry ${user_uuid} + + RETURN ${resp} + +Get List of User At Register + [Documentation] (Administrator) This Keyword retrieve a list of users from register. + ${headers}= Create Register Admin Session ${CAPIF_HTTPS_REGISTER_URL} verify=False + + ${resp}= DELETE On Session register_session /getUsers headers=${headers} Should Be Equal As Strings ${resp.status_code} 200 - # Should Be Equal As Strings ${resp.json()['message']} Certificate created successfuly + RETURN ${resp.json()['users']} + +Get Auth For User + [Documentation] (User) This Keyword retrieve token to be used by user towards first interaction with CCF. + [Arguments] ${username} ${password} + ${auth}= Set variable ${{ ('${username}','${password}') }} + ${resp}= GET On Session register_session /getauth auth=${auth} + + Should Be Equal As Strings ${resp.status_code} 200 RETURN ${resp.json()} Clean Test Information ${capif_users_dict}= Call Method ${CAPIF_USERS} get_capif_users_dict - ${register_users}= Call Method ${CAPIF_USERS} get_register_users + ${register_users_dict}= Call Method ${CAPIF_USERS} get_register_users_dict ${keys}= Get Dictionary Keys ${capif_users_dict} @@ -352,18 +473,9 @@ Clean Test Information Call Method ${CAPIF_USERS} remove_capif_users_entry ${key} END - FOR ${user} IN @{register_users} - &{body}= Create Dictionary - ... password=password - ... username=${user} - - Create Session jwtsession ${CAPIF_HTTPS_REGISTER_URL} verify=False disable_warnings=1 - - ${resp}= DELETE On Session jwtsession /remove json=${body} - - Should Be Equal As Strings ${resp.status_code} 204 - - Call Method ${CAPIF_USERS} remove_register_users_entry ${user} + ${uuids}= Get Dictionary Keys ${register_users_dict} + FOR ${uuid} IN @{uuids} + Delete User At Register uuid=${uuid} END Remove entity @@ -371,10 +483,10 @@ Remove entity ${capif_users_dict}= Call Method ${CAPIF_USERS} get_capif_users_dict - ${register_users}= Call Method ${CAPIF_USERS} get_register_users + ${register_users_dict}= Call Method ${CAPIF_USERS} get_register_users_dict Log Dictionary ${capif_users_dict} - Log List ${register_users} + Log Dictionary ${register_users_dict} ${keys}= Get Dictionary Keys ${capif_users_dict} FOR ${key} IN @{keys} @@ -391,21 +503,11 @@ Remove entity Call Method ${CAPIF_USERS} remove_capif_users_entry ${key} END END - - &{body}= Create Dictionary - ... password=password - ... username=${entity_user} - - Create Session jwtsession ${CAPIF_HTTPS_REGISTER_URL} verify=False disable_warnings=1 - - ${resp}= DELETE On Session jwtsession /remove json=${body} - - Should Be Equal As Strings ${resp.status_code} 204 - - Call Method ${CAPIF_USERS} remove_register_users_entry ${entity_user} + + Delete User At Register username=${entity_user} Log Dictionary ${capif_users_dict} - Log List ${register_users} + Log Dictionary ${register_users_dict} Remove Resource [Arguments] ${resource_url} ${management_cert} ${username} @@ -560,7 +662,7 @@ Basic ACL registration ... update_capif_users_dicts ... ${register_user_info_provider['resource_url'].path} ... ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${PROVIDER_USERNAME} + Call Method ${CAPIF_USERS} update_register_users ${register_user_info_provider['uuid']} ${PROVIDER_USERNAME} ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info_provider} @@ -583,7 +685,6 @@ Basic ACL registration ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${INVOKER_USERNAME} # Test ${discover_response}= Get Request Capif -- GitLab From 123dcb2e23ba3af82f7b1bc8b0de313bd23d8cc1 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 17 Apr 2024 17:57:35 +0200 Subject: [PATCH 140/310] Code Refactor of delete entities --- .../capif_api_access_control_policy.robot | 44 +---- .../capif_auditing_api.robot | 133 ++++++--------- .../capif_api_service_discover.robot | 84 +++------- .../CAPIF Api Events/capif_events_api.robot | 37 ++--- .../capif_api_invoker_managenet.robot | 38 ++--- .../capif_logging_api.robot | 110 ++++++------- .../capif_api_provider_management.robot | 32 +--- .../capif_api_publish_service.robot | 89 +++------- .../capif_security_api.robot | 155 ++---------------- tests/resources/common/basicRequests.robot | 122 +++++++------- 10 files changed, 248 insertions(+), 596 deletions(-) diff --git a/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot b/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot index 905e4c7..04d7898 100644 --- a/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot +++ b/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot @@ -23,13 +23,6 @@ Retrieve ACL # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method - ... ${CAPIF_USERS} - ... update_capif_users_dicts - ... ${register_user_info_provider['resource_url'].path} - ... ${AMF_PROVIDER_USERNAME} - - ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info_provider} ... service_1 @@ -50,9 +43,6 @@ Retrieve ACL # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} @@ -97,12 +87,6 @@ Retrieve ACL with 2 Service APIs published # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method - ... ${CAPIF_USERS} - ... update_capif_users_dicts - ... ${register_user_info_provider['resource_url'].path} - ... ${AMF_PROVIDER_USERNAME} - ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info_provider} ... service_1 @@ -117,8 +101,6 @@ Retrieve ACL with 2 Service APIs published # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} @@ -177,12 +159,6 @@ Retrieve ACL with security context created by two different Invokers # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method - ... ${CAPIF_USERS} - ... update_capif_users_dicts - ... ${register_user_info_provider['resource_url'].path} - ... ${AMF_PROVIDER_USERNAME} - ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info_provider} ... service_1 @@ -203,16 +179,12 @@ Retrieve ACL with security context created by two different Invokers # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - ${INVOKER_USERNAME_2}= Set Variable ${INVOKER_USERNAME}_2 # Register another invoker ${register_user_info_invoker_2} ${url} ${request_body}= Invoker Default Onboarding ... ${INVOKER_USERNAME_2} - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME_2} - # Get Published APIs ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} @@ -291,13 +263,6 @@ Retrieve ACL filtered by api-invoker-id # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method - ... ${CAPIF_USERS} - ... update_capif_users_dicts - ... ${register_user_info_provider['resource_url'].path} - ... ${AMF_PROVIDER_USERNAME} - - ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info_provider} ... service_1 @@ -318,18 +283,12 @@ Retrieve ACL filtered by api-invoker-id # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${INVOKER_USERNAME_2}= Set Variable ${INVOKER_USERNAME}_2 # Register another invoker ${register_user_info_invoker_2} ${url} ${request_body}= Invoker Default Onboarding ... ${INVOKER_USERNAME_2} - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME_2} - - # Get Published APIs ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} @@ -602,7 +561,7 @@ No ACL for invoker after be removed Check Response Variable Type And Values ${resp} 200 AccessControlPolicyList - Remove entity ${INVOKER_USERNAME} + Remove entity ${INVOKER_USERNAME} ${resp}= Get Request Capif ... /access-control-policy/v1/accessControlPolicyList/${service_api_description_published['apiId']}?aef-id=${register_user_info_provider['aef_id']} @@ -619,4 +578,3 @@ No ACL for invoker after be removed ... title=Not Found ... detail=No ACLs found for the requested service: ${service_api_description_published['apiId']}, aef_id: ${register_user_info_provider['aef_id']}, invoker: None and supportedFeatures: None ... cause=Wrong id - diff --git a/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot b/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot index 4c0a393..0676168 100644 --- a/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot +++ b/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot @@ -1,12 +1,13 @@ *** Settings *** -Resource /opt/robot-tests/tests/resources/common.resource -Library /opt/robot-tests/tests/libraries/bodyRequests.py -Library Collections -Resource /opt/robot-tests/tests/resources/common/basicRequests.robot -Resource ../../resources/common.resource +Resource /opt/robot-tests/tests/resources/common.resource +Library /opt/robot-tests/tests/libraries/bodyRequests.py +Library Collections +Resource /opt/robot-tests/tests/resources/common/basicRequests.robot +Resource ../../resources/common.resource + +Suite Teardown Reset Testing Environment +Test Setup Reset Testing Environment -Test Setup Reset Testing Environment -Suite Teardown Reset Testing Environment *** Variables *** ${AEF_ID_NOT_VALID} aef-example @@ -16,34 +17,33 @@ ${NOTIFICATION_DESTINATION} http://robot.testing:1080 ${API_VERSION_VALID} v1 ${API_VERSION_NOT_VALID} v58 + *** Test Cases *** Get Log Entry [Tags] capif_api_auditing_service-1 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} - ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} # Create Log Entry - ${request_body}= Create Log Entry ${register_user_info['aef_id']} ${register_user_info_invoker['api_invoker_id']} ${api_ids} ${api_names} + ${request_body}= Create Log Entry + ... ${register_user_info['aef_id']} + ... ${register_user_info_invoker['api_invoker_id']} + ... ${api_ids} + ... ${api_names} ${resp_1}= Post Request Capif ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs ... json=${request_body} @@ -51,7 +51,6 @@ Get Log Entry ... verify=ca.crt ... username=${AEF_PROVIDER_USERNAME} - ${resp_2}= Get Request Capif ... /logs/v1/apiInvocationLogs?aef-id=${register_user_info['aef_id']}&api-invoker-id=${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} @@ -60,67 +59,56 @@ Get Log Entry # Check Results Check Response Variable Type And Values ${resp_2} 200 InvocationLog - Length Should Be ${resp_2.json()["logs"]} 2 + Length Should Be ${resp_2.json()["logs"]} 2 Get a log entry without entry created [Tags] capif_api_auditing_service-2 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - - - ${resp_1}= Get Request Capif + ${resp_1}= Get Request Capif ... /logs/v1/apiInvocationLogs?aef-id=${register_user_info['aef_id']}&api-invoker-id=${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${AMF_PROVIDER_USERNAME} # Check Results - Check Response Variable Type And Values ${resp_1} 404 ProblemDetails + Check Response Variable Type And Values ${resp_1} 404 ProblemDetails ... title=Not Found ... status=404 ... detail=aefId or/and apiInvokerId do not match any InvocationLogs ... cause=No log invocations found - Get a log entry withut aefid and apiInvokerId [Tags] capif_api_auditing_service-3 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} - ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} # Create Log Entry - ${request_body}= Create Log Entry ${register_user_info['aef_id']} ${register_user_info_invoker['api_invoker_id']} ${api_ids} ${api_names} + ${request_body}= Create Log Entry + ... ${register_user_info['aef_id']} + ... ${register_user_info_invoker['api_invoker_id']} + ... ${api_ids} + ... ${api_names} ${resp_1}= Post Request Capif ... /api-invocation-logs/v1/${AEF_ID_NOT_VALID}/logs ... json=${request_body} @@ -128,48 +116,44 @@ Get a log entry withut aefid and apiInvokerId ... verify=ca.crt ... username=${AEF_PROVIDER_USERNAME} - - ${resp_2}= Get Request Capif + ${resp_2}= Get Request Capif ... /logs/v1/apiInvocationLogs ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${AMF_PROVIDER_USERNAME} # Check Results - Check Response Variable Type And Values ${resp_2} 400 ProblemDetails + Check Response Variable Type And Values ${resp_2} 400 ProblemDetails ... title=Bad Request ... status=400 ... detail=aef_id and api_invoker_id parameters are mandatory ... cause=Mandatory parameters missing - Get Log Entry with apiVersion filter [Tags] capif_api_auditing_service-4 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} - ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} # Create Log Entry - ${request_body}= Create Log Entry ${register_user_info['aef_id']} ${register_user_info_invoker['api_invoker_id']} ${api_ids} ${api_names} + ${request_body}= Create Log Entry + ... ${register_user_info['aef_id']} + ... ${register_user_info_invoker['api_invoker_id']} + ... ${api_ids} + ... ${api_names} ${resp_1}= Post Request Capif ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs ... json=${request_body} @@ -177,45 +161,41 @@ Get Log Entry with apiVersion filter ... verify=ca.crt ... username=${AEF_PROVIDER_USERNAME} - ${resp_2}= Get Request Capif - ... /logs/v1/apiInvocationLogs?aef-id=${register_user_info['aef_id']}&api-invoker-id=${register_user_info_invoker['api_invoker_id']}&api-version=${API_VERSION_VALID} + ... /logs/v1/apiInvocationLogs?aef-id=${register_user_info['aef_id']}&api-invoker-id=${register_user_info_invoker['api_invoker_id']}&api-version=${API_VERSION_VALID} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${AMF_PROVIDER_USERNAME} # Check Results Check Response Variable Type And Values ${resp_2} 200 InvocationLog - Length Should Be ${resp_2.json()["logs"]} 1 + Length Should Be ${resp_2.json()["logs"]} 1 -Get Log Entry with no exist apiVersion filter +Get Log Entry with no exist apiVersion filter [Tags] capif_api_auditing_service-5 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - - ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} - ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} # Create Log Entry - ${request_body}= Create Log Entry ${register_user_info['aef_id']} ${register_user_info_invoker['api_invoker_id']} ${api_ids} ${api_names} + ${request_body}= Create Log Entry + ... ${register_user_info['aef_id']} + ... ${register_user_info_invoker['api_invoker_id']} + ... ${api_ids} + ... ${api_names} ${resp_1}= Post Request Capif ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs ... json=${request_body} @@ -223,21 +203,16 @@ Get Log Entry with no exist apiVersion filter ... verify=ca.crt ... username=${AEF_PROVIDER_USERNAME} - ${resp_2}= Get Request Capif - ... /logs/v1/apiInvocationLogs?aef-id=${register_user_info['aef_id']}&api-invoker-id=${register_user_info_invoker['api_invoker_id']}&api-version=${API_VERSION_NOT_VALID} + ... /logs/v1/apiInvocationLogs?aef-id=${register_user_info['aef_id']}&api-invoker-id=${register_user_info_invoker['api_invoker_id']}&api-version=${API_VERSION_NOT_VALID} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${AMF_PROVIDER_USERNAME} # Check Results - # Check Results - Check Response Variable Type And Values ${resp_2} 404 ProblemDetails + # Check Results + Check Response Variable Type And Values ${resp_2} 404 ProblemDetails ... title=Not Found ... status=404 ... detail=Parameters do not match any log entry ... cause=No logs found - - - - diff --git a/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot b/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot index 2b7ed5b..ff82ec3 100644 --- a/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot +++ b/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot @@ -1,11 +1,11 @@ *** Settings *** -Resource /opt/robot-tests/tests/resources/common.resource -Resource /opt/robot-tests/tests/resources/api_invoker_management_requests/apiInvokerManagementRequests.robot -Resource ../../resources/common.resource -Library /opt/robot-tests/tests/libraries/bodyRequests.py +Resource /opt/robot-tests/tests/resources/common.resource +Resource /opt/robot-tests/tests/resources/api_invoker_management_requests/apiInvokerManagementRequests.robot +Resource ../../resources/common.resource +Library /opt/robot-tests/tests/libraries/bodyRequests.py -Test Setup Reset Testing Environment -Suite Teardown Reset Testing Environment +Suite Teardown Reset Testing Environment +Test Setup Reset Testing Environment # Test Setup Initialize Test And Register role=invoker @@ -16,12 +16,9 @@ ${API_INVOKER_NOT_REGISTERED} not-valid *** Test Cases *** Discover Published service APIs by Authorised API Invoker [Tags] capif_api_discover_service-1 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} @@ -29,9 +26,6 @@ Discover Published service APIs by Authorised API Invoker # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${resp}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} @@ -45,26 +39,19 @@ Discover Published service APIs by Authorised API Invoker Dictionary Should Contain Key ${resp.json()} serviceAPIDescriptions Should Not Be Empty ${resp.json()['serviceAPIDescriptions']} Length Should Be ${resp.json()['serviceAPIDescriptions']} 1 - List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published} - + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published} Discover Published service APIs by Non Authorised API Invoker [Tags] capif_api_discover_service-2 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${resp}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} @@ -80,21 +67,15 @@ Discover Published service APIs by Non Authorised API Invoker Discover Published service APIs by not registered API Invoker [Tags] capif_api_discover_service-3 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${resp}= Get Request Capif ... ${DISCOVER_URL}${API_INVOKER_NOT_REGISTERED} ... server=${CAPIF_HTTPS_URL} @@ -110,12 +91,9 @@ Discover Published service APIs by not registered API Invoker Discover Published service APIs by registered API Invoker with 1 result filtered [Tags] capif_api_discover_service-4 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name_1}= Set Variable service_1 ${api_name_2}= Set Variable service_2 @@ -130,9 +108,6 @@ Discover Published service APIs by registered API Invoker with 1 result filtered # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Request all APIs for Invoker ${resp}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} @@ -145,8 +120,8 @@ Discover Published service APIs by registered API Invoker with 1 result filtered # Check returned values Should Not Be Empty ${resp.json()['serviceAPIDescriptions']} Length Should Be ${resp.json()['serviceAPIDescriptions']} 2 - List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_1} - List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_2} + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_1} + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_2} # Request api 1 ${resp}= Get Request Capif @@ -164,12 +139,9 @@ Discover Published service APIs by registered API Invoker with 1 result filtered Discover Published service APIs by registered API Invoker filtered with no match [Tags] capif_api_discover_service-5 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name_1}= Set Variable apiName1 ${api_name_2}= Set Variable apiName2 @@ -184,9 +156,6 @@ Discover Published service APIs by registered API Invoker filtered with no match # Change to invoker role and register at api invoker management ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Request all APIs for Invoker ${resp}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} @@ -199,8 +168,8 @@ Discover Published service APIs by registered API Invoker filtered with no match # Check returned values Should Not Be Empty ${resp.json()['serviceAPIDescriptions']} Length Should Be ${resp.json()['serviceAPIDescriptions']} 2 - List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_1} - List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_2} + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_1} + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_2} # Request api 1 ${resp}= Get Request Capif @@ -210,7 +179,10 @@ Discover Published service APIs by registered API Invoker filtered with no match ... username=${INVOKER_USERNAME} # Check Results - Check Response Variable Type And Values ${resp} 404 ProblemDetails + Check Response Variable Type And Values + ... ${resp} + ... 404 + ... ProblemDetails ... title=Not Found ... status=404 ... detail=API Invoker ${register_user_info_invoker['api_invoker_id']} has no API Published that accomplish filter conditions @@ -218,12 +190,9 @@ Discover Published service APIs by registered API Invoker filtered with no match Discover Published service APIs by registered API Invoker not filtered [Tags] capif_api_discover_service-6 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name_1}= Set Variable apiName1 ${api_name_2}= Set Variable apiName2 @@ -238,9 +207,6 @@ Discover Published service APIs by registered API Invoker not filtered # Change to invoker role and register at api invoker management ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Request all APIs for Invoker ${resp}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info['aef_id']} @@ -253,5 +219,5 @@ Discover Published service APIs by registered API Invoker not filtered # Check Results Should Not Be Empty ${resp.json()['serviceAPIDescriptions']} Length Should Be ${resp.json()['serviceAPIDescriptions']} 2 - List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_1} - List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_2} + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_1} + List Should Contain Value ${resp.json()['serviceAPIDescriptions']} ${service_api_description_published_2} diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index c027769..f2a5966 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -1,12 +1,12 @@ *** Settings *** -Resource /opt/robot-tests/tests/resources/common.resource -Library /opt/robot-tests/tests/libraries/bodyRequests.py -Library XML -Resource /opt/robot-tests/tests/resources/common/basicRequests.robot -Resource ../../resources/common.resource +Resource /opt/robot-tests/tests/resources/common.resource +Library /opt/robot-tests/tests/libraries/bodyRequests.py +Library XML +Resource /opt/robot-tests/tests/resources/common/basicRequests.robot +Resource ../../resources/common.resource -Test Setup Reset Testing Environment -Suite Teardown Reset Testing Environment +Suite Teardown Reset Testing Environment +Test Setup Reset Testing Environment *** Variables *** @@ -21,9 +21,6 @@ Creates a new individual CAPIF Event Subscription # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Events Subscription ${resp}= Post Request Capif ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions @@ -41,9 +38,6 @@ Creates a new individual CAPIF Event Subscription with Invalid SubscriberId # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Events Subscription ${resp}= Post Request Capif ... /capif-events/v1/${SUBSCRIBER_ID_NOT_VALID}/subscriptions @@ -53,7 +47,7 @@ Creates a new individual CAPIF Event Subscription with Invalid SubscriberId ... username=${INVOKER_USERNAME} # Check Results - Check Response Variable Type And Values ${resp} 404 ProblemDetails + Check Response Variable Type And Values ${resp} 404 ProblemDetails ... title=Not Found ... status=404 ... detail=Invoker or APF or AEF or AMF Not found @@ -64,9 +58,6 @@ Deletes an individual CAPIF Event Subscription # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Events Subscription ${resp}= Post Request Capif ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions @@ -92,9 +83,6 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriberId # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Events Subscription ${resp}= Post Request Capif ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions @@ -112,24 +100,20 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriberId ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} - + # Check Results - Check Response Variable Type And Values ${resp} 404 ProblemDetails + Check Response Variable Type And Values ${resp} 404 ProblemDetails ... title=Not Found ... status=404 ... detail=Invoker or APF or AEF or AMF Not found ... cause=Subscriber Not Found - Deletes an individual CAPIF Event Subscription with invalid SubscriptionId [Tags] capif_api_events-5 # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Events Subscription ${resp}= Post Request Capif ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions @@ -153,4 +137,3 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriptionId ... title=Unauthorized ... detail=User not authorized ... cause=You are not the owner of this resource - diff --git a/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot b/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot index 7127cc4..8c40f72 100644 --- a/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot +++ b/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot @@ -1,13 +1,13 @@ *** Settings *** -Resource /opt/robot-tests/tests/resources/common.resource -Resource /opt/robot-tests/tests/resources/api_invoker_management_requests/apiInvokerManagementRequests.robot -Resource ../../resources/common.resource -Library /opt/robot-tests/tests/libraries/bodyRequests.py -Library Process -Library Collections +Resource /opt/robot-tests/tests/resources/common.resource +Resource /opt/robot-tests/tests/resources/api_invoker_management_requests/apiInvokerManagementRequests.robot +Resource ../../resources/common.resource +Library /opt/robot-tests/tests/libraries/bodyRequests.py +Library Process +Library Collections -Test Setup Reset Testing Environment -Suite Teardown Reset Testing Environment +Suite Teardown Reset Testing Environment +Test Setup Reset Testing Environment *** Variables *** @@ -17,7 +17,7 @@ ${API_INVOKER_NOT_REGISTERED} not-valid *** Test Cases *** Onboard NetApp [Tags] capif_api_invoker_management-1 - #Register Netapp + # Register Netapp ${register_user_info}= Register User At Jwt Auth ... username=${INVOKER_USERNAME} role=${INVOKER_ROLE} @@ -34,9 +34,9 @@ Onboard NetApp ... access_token=${register_user_info['access_token']} # Check Results - Check Response Variable Type And Values ${resp} 201 APIInvokerEnrolmentDetails + Check Response Variable Type And Values ${resp} 201 APIInvokerEnrolmentDetails ${url}= Parse Url ${resp.headers['Location']} - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} + Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} Check Location Header ${resp} ${LOCATION_INVOKER_RESOURCE_REGEX} # Store dummy signed certificate @@ -47,8 +47,6 @@ Register NetApp Already Onboarded # Default Invoker Registration and Onboarding ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - ${resp}= Post Request Capif ... ${register_user_info['ccf_onboarding_url']} ... json=${request_body} @@ -70,8 +68,6 @@ Update Onboarded NetApp # Default Invoker Registration and Onboarding ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - Set To Dictionary ... ${request_body} ... notificationDestination=${new_notification_destination} @@ -92,8 +88,6 @@ Update Not Onboarded NetApp # Default Invoker Registration and Onboarding ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - ${resp}= Put Request Capif ... /api-invoker-management/v1/onboardedInvokers/${INVOKER_NOT_REGISTERED} ... ${request_body} @@ -118,6 +112,8 @@ Offboard NetApp ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} + + Call Method ${CAPIF_USERS} remove_capif_users_entry ${url.path} # Check Results Should Be Equal As Strings ${resp.status_code} 204 @@ -127,8 +123,6 @@ Offboard Not Previously Onboarded NetApp # Default Invoker Registration and Onboarding ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - ${resp}= Delete Request Capif ... /api-invoker-management/v1/onboardedInvokers/${INVOKER_NOT_REGISTERED} ... server=${CAPIF_HTTPS_URL} @@ -149,9 +143,7 @@ Update Onboarded NetApp Certificate # Default Invoker Registration and Onboarding ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${INVOKER_USERNAME_NEW}= Set Variable ${INVOKER_USERNAME}_NEW + ${INVOKER_USERNAME_NEW}= Set Variable ${INVOKER_USERNAME}_NEW ${csr_request_new}= Create User Csr ${INVOKER_USERNAME_NEW} invoker @@ -189,5 +181,3 @@ Update Onboarded NetApp Certificate # Check Results Check Response Variable Type And Values ${resp} 200 APIInvokerEnrolmentDetails ... notificationDestination=${new_notification_destination} - - diff --git a/tests/features/CAPIF Api Logging Service/capif_logging_api.robot b/tests/features/CAPIF Api Logging Service/capif_logging_api.robot index 2604bdb..a7a0253 100644 --- a/tests/features/CAPIF Api Logging Service/capif_logging_api.robot +++ b/tests/features/CAPIF Api Logging Service/capif_logging_api.robot @@ -1,12 +1,13 @@ *** Settings *** -Resource /opt/robot-tests/tests/resources/common.resource -Library /opt/robot-tests/tests/libraries/bodyRequests.py -Library Collections -Resource /opt/robot-tests/tests/resources/common/basicRequests.robot -Resource ../../resources/common.resource +Resource /opt/robot-tests/tests/resources/common.resource +Library /opt/robot-tests/tests/libraries/bodyRequests.py +Library Collections +Resource /opt/robot-tests/tests/resources/common/basicRequests.robot +Resource ../../resources/common.resource + +Suite Teardown Reset Testing Environment +Test Setup Reset Testing Environment -Test Setup Reset Testing Environment -Suite Teardown Reset Testing Environment *** Variables *** ${AEF_ID_NOT_VALID} aef-example @@ -14,34 +15,33 @@ ${SERVICE_API_ID_NOT_VALID} not-valid ${API_INVOKER_NOT_VALID} not-valid ${NOTIFICATION_DESTINATION} http://robot.testing:1080 + *** Test Cases *** Create a log entry [Tags] capif_api_logging_service-1 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} - ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} # Create Log Entry - ${request_body}= Create Log Entry ${register_user_info['aef_id']} ${register_user_info_invoker['api_invoker_id']} ${api_ids} ${api_names} + ${request_body}= Create Log Entry + ... ${register_user_info['aef_id']} + ... ${register_user_info_invoker['api_invoker_id']} + ... ${api_ids} + ... ${api_names} ${resp}= Post Request Capif ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs ... json=${request_body} @@ -49,38 +49,35 @@ Create a log entry ... verify=ca.crt ... username=${AEF_PROVIDER_USERNAME} - # Check Results Check Response Variable Type And Values ${resp} 201 InvocationLog ${resource_url}= Check Location Header ${resp} ${LOCATION_LOGGING_RESOURCE_REGEX} Create a log entry invalid aefId [Tags] capif_api_logging_service-2 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} - ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} # Create Log Entry - ${request_body}= Create Log Entry ${register_user_info['aef_id']} ${register_user_info_invoker['api_invoker_id']} ${api_ids} ${api_names} + ${request_body}= Create Log Entry + ... ${register_user_info['aef_id']} + ... ${register_user_info_invoker['api_invoker_id']} + ... ${api_ids} + ... ${api_names} ${resp}= Post Request Capif ... /api-invocation-logs/v1/${AEF_ID_NOT_VALID}/logs ... json=${request_body} @@ -89,31 +86,23 @@ Create a log entry invalid aefId ... username=${AEF_PROVIDER_USERNAME} # Check Results - Check Response Variable Type And Values ${resp} 404 ProblemDetails + Check Response Variable Type And Values ${resp} 404 ProblemDetails ... title=Not Found ... status=404 ... detail=Exposer not exist ... cause=Exposer id not found - - Create a log entry invalid serviceApi [Tags] capif_api_logging_service-3 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} @@ -121,7 +110,9 @@ Create a log entry invalid serviceApi ... username=${INVOKER_USERNAME} # Create Log Entry - ${request_body}= Create Log Entry Bad Service ${register_user_info['aef_id']} ${register_user_info_invoker['api_invoker_id']} + ${request_body}= Create Log Entry Bad Service + ... ${register_user_info['aef_id']} + ... ${register_user_info_invoker['api_invoker_id']} ${resp}= Post Request Capif ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs ... json=${request_body} @@ -137,31 +128,29 @@ Create a log entry invalid serviceApi Create a log entry invalid apiInvokerId [Tags] capif_api_logging_service-4 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} - ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} # Create Log Entry - ${request_body}= Create Log Entry ${register_user_info['aef_id']} ${API_INVOKER_NOT_VALID} ${api_ids} ${api_names} + ${request_body}= Create Log Entry + ... ${register_user_info['aef_id']} + ... ${API_INVOKER_NOT_VALID} + ... ${api_ids} + ... ${api_names} ${resp}= Post Request Capif ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs ... json=${request_body} @@ -176,35 +165,31 @@ Create a log entry invalid apiInvokerId ... detail=Invoker not exist ... cause=Invoker id not found - Create a log entry different aef_id in body [Tags] capif_api_logging_service-5 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish one api Publish Service Api ${register_user_info} - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - - ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${INVOKER_USERNAME} - ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} # Create Log Entry - ${request_body}= Create Log Entry ${AEF_ID_NOT_VALID} ${register_user_info_invoker['api_invoker_id']} ${api_ids} ${api_names} + ${request_body}= Create Log Entry + ... ${AEF_ID_NOT_VALID} + ... ${register_user_info_invoker['api_invoker_id']} + ... ${api_ids} + ... ${api_names} ${resp}= Post Request Capif ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs ... json=${request_body} @@ -218,4 +203,3 @@ Create a log entry different aef_id in body ... status=401 ... detail=AEF id not matching in request and body ... cause=Not identical AEF id - diff --git a/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot b/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot index f47385f..a6f9674 100644 --- a/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot +++ b/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot @@ -64,12 +64,6 @@ Register Api Provider Already registered [Tags] capif_api_provider_management-2 ${register_user_info}= Provider Default Registration - Call Method - ... ${CAPIF_USERS} - ... update_capif_users_dicts - ... ${register_user_info['resource_url'].path} - ... ${AMF_PROVIDER_USERNAME} - ${resp}= Post Request Capif ... /api-provider-management/v1/registrations ... json=${register_user_info['provider_enrollment_details']} @@ -88,12 +82,6 @@ Update Registered Api Provider [Tags] capif_api_provider_management-3 ${register_user_info}= Provider Default Registration - Call Method - ... ${CAPIF_USERS} - ... update_capif_users_dicts - ... ${register_user_info['resource_url'].path} - ... ${AMF_PROVIDER_USERNAME} - ${request_body}= Set Variable ${register_user_info['provider_enrollment_details']} Set To Dictionary ${request_body} apiProvDomInfo=ROBOT_TESTING_MOD @@ -127,12 +115,6 @@ Update Not Registered Api Provider [Tags] capif_api_provider_management-4 ${register_user_info}= Provider Default Registration - Call Method - ... ${CAPIF_USERS} - ... update_capif_users_dicts - ... ${register_user_info['resource_url'].path} - ... ${AMF_PROVIDER_USERNAME} - ${request_body}= Set Variable ${register_user_info['provider_enrollment_details']} ${resp}= Put Request Capif @@ -173,12 +155,6 @@ Partially Update Not Registered Api Provider [Tags] capif_api_provider_management-6 ${register_user_info}= Provider Default Registration - Call Method - ... ${CAPIF_USERS} - ... update_capif_users_dicts - ... ${register_user_info['resource_url'].path} - ... ${AMF_PROVIDER_USERNAME} - ${request_body}= Create Api Provider Enrolment Details Patch Body ${resp}= Patch Request Capif @@ -205,6 +181,8 @@ Delete Registered Api Provider ... verify=ca.crt ... username=${AMF_PROVIDER_USERNAME} + Call Method ${CAPIF_USERS} remove_capif_users_entry ${register_user_info['resource_url'].path} + # Check Results Status Should Be 204 ${resp} @@ -212,12 +190,6 @@ Delete Not Registered Api Provider [Tags] capif_api_provider_management-8 ${register_user_info}= Provider Default Registration - Call Method - ... ${CAPIF_USERS} - ... update_capif_users_dicts - ... ${register_user_info['resource_url'].path} - ... ${AMF_PROVIDER_USERNAME} - ${resp}= Delete Request Capif ... /api-provider-management/v1/registrations/${API_PROVIDER_NOT_REGISTERED} ... server=${CAPIF_HTTPS_URL} diff --git a/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot b/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot index daef4be..56484d2 100644 --- a/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot +++ b/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot @@ -1,11 +1,11 @@ *** Settings *** -Resource /opt/robot-tests/tests/resources/common.resource -Resource ../../resources/common/basicRequests.robot -Resource ../../resources/common.resource -Library /opt/robot-tests/tests/libraries/bodyRequests.py +Resource /opt/robot-tests/tests/resources/common.resource +Resource ../../resources/common/basicRequests.robot +Resource ../../resources/common.resource +Library /opt/robot-tests/tests/libraries/bodyRequests.py -Test Setup Reset Testing Environment -Suite Teardown Reset Testing Environment +Suite Teardown Reset Testing Environment +Test Setup Reset Testing Environment *** Variables *** @@ -16,12 +16,9 @@ ${SERVICE_API_ID_NOT_VALID} not-valid *** Test Cases *** Publish API by Authorised API Publisher [Tags] capif_api_publish_service-1 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Test ${request_body}= Create Service Api Description service_1 ${resp}= Post Request Capif @@ -38,12 +35,9 @@ Publish API by Authorised API Publisher Publish API by NON Authorised API Publisher [Tags] capif_api_publish_service-2 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${request_body}= Create Service Api Description ${resp}= Post Request Capif ... /published-apis/v1/${APF_ID_NOT_VALID}/service-apis @@ -60,12 +54,9 @@ Publish API by NON Authorised API Publisher Retrieve all APIs Published by Authorised apfId [Tags] capif_api_publish_service-3 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Register One Service ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} @@ -88,12 +79,9 @@ Retrieve all APIs Published by Authorised apfId Retrieve all APIs Published by NON Authorised apfId [Tags] capif_api_publish_service-4 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Retrieve Services published ${resp}= Get Request Capif ... /published-apis/v1/${APF_ID_NOT_VALID}/service-apis @@ -109,12 +97,9 @@ Retrieve all APIs Published by NON Authorised apfId Retrieve single APIs Published by Authorised apfId [Tags] capif_api_publish_service-5 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} ... service_1 @@ -148,19 +133,15 @@ Retrieve single APIs Published by Authorised apfId Retrieve single APIs non Published by Authorised apfId [Tags] capif_api_publish_service-6 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${resp}= Get Request Capif ... /published-apis/v1/${register_user_info['apf_id']}/service-apis/${SERVICE_API_ID_NOT_VALID} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${APF_PROVIDER_USERNAME} - Check Response Variable Type And Values ${resp} 401 ProblemDetails ... title=Unauthorized ... detail=User not authorized @@ -171,9 +152,6 @@ Retrieve single APIs Published by NON Authorised apfId # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Publish Service API ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} @@ -182,9 +160,6 @@ Retrieve single APIs Published by NON Authorised apfId # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${resp}= Get Request Capif ... ${resource_url.path} ... server=${CAPIF_HTTPS_URL} @@ -199,12 +174,9 @@ Retrieve single APIs Published by NON Authorised apfId Update API Published by Authorised apfId with valid serviceApiId [Tags] capif_api_publish_service-8 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} ... service_1 @@ -232,12 +204,9 @@ Update API Published by Authorised apfId with valid serviceApiId Update APIs Published by Authorised apfId with invalid serviceApiId [Tags] capif_api_publish_service-9 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} ... service_1 @@ -257,22 +226,16 @@ Update APIs Published by Authorised apfId with invalid serviceApiId Update APIs Published by NON Authorised apfId [Tags] capif_api_publish_service-10 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} ... service_1 - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Service Api Description service_1_modified ${resp}= Put Request Capif ... ${resource_url.path} @@ -299,12 +262,9 @@ Update APIs Published by NON Authorised apfId Delete API Published by Authorised apfId with valid serviceApiId [Tags] capif_api_publish_service-11 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${service_api_description_published_1} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info} ... first_service @@ -330,12 +290,9 @@ Delete API Published by Authorised apfId with valid serviceApiId Delete APIs Published by Authorised apfId with invalid serviceApiId [Tags] capif_api_publish_service-12 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${resp}= Delete Request Capif ... /published-apis/v1/${register_user_info['apf_id']}/service-apis/${SERVICE_API_ID_NOT_VALID} ... server=${CAPIF_HTTPS_URL} @@ -349,18 +306,12 @@ Delete APIs Published by Authorised apfId with invalid serviceApiId Delete APIs Published by NON Authorised apfId [Tags] capif_api_publish_service-13 - #Register APF + # Register APF ${register_user_info}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - - #Register INVOKER + # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${resp}= Delete Request Capif ... /published-apis/v1/${register_user_info['apf_id']}/service-apis/${SERVICE_API_ID_NOT_VALID} ... server=${CAPIF_HTTPS_URL} diff --git a/tests/features/CAPIF Security Api/capif_security_api.robot b/tests/features/CAPIF Security Api/capif_security_api.robot index 4e27948..6342aa9 100644 --- a/tests/features/CAPIF Security Api/capif_security_api.robot +++ b/tests/features/CAPIF Security Api/capif_security_api.robot @@ -1,12 +1,12 @@ *** Settings *** -Resource /opt/robot-tests/tests/resources/common.resource -Library /opt/robot-tests/tests/libraries/bodyRequests.py -Library Collections -Resource /opt/robot-tests/tests/resources/common/basicRequests.robot -Resource ../../resources/common.resource +Resource /opt/robot-tests/tests/resources/common.resource +Library /opt/robot-tests/tests/libraries/bodyRequests.py +Library Collections +Resource /opt/robot-tests/tests/resources/common/basicRequests.robot +Resource ../../resources/common.resource -Test Setup Reset Testing Environment -Suite Teardown Reset Testing Environment +Suite Teardown Reset Testing Environment +Test Setup Reset Testing Environment *** Variables *** @@ -22,9 +22,6 @@ Create a security context for an API invoker # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Create Security Context ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -43,15 +40,9 @@ Create a security context for an API invoker with Provider role # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Register Provider ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Create Security Context ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -73,9 +64,6 @@ Create a security context for an API invoker with Provider entity role and inval # Register APF ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Create Security Context ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif @@ -97,9 +85,6 @@ Create a security context for an API invoker with Invalid apiInvokerID # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif ... /capif-security/v1/trustedInvokers/${API_INVOKER_NOT_VALID} @@ -120,9 +105,6 @@ Retrieve the Security Context of an API Invoker # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} @@ -139,9 +121,6 @@ Retrieve the Security Context of an API Invoker # Register APF ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Retrieve Security context can setup by parameters if authenticationInfo and authorizationInfo are needed at response. # ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']}?authenticationInfo=true&authorizationInfo=true ${resp}= Get Request Capif @@ -165,9 +144,6 @@ Retrieve the Security Context of an API Invoker with invalid apiInvokerID # Register APF ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${resp}= Get Request Capif ... /capif-security/v1/trustedInvokers/${API_INVOKER_NOT_VALID} ... server=${CAPIF_HTTPS_URL} @@ -185,9 +161,6 @@ Retrieve the Security Context of an API Invoker with invalid apfId # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} @@ -217,9 +190,6 @@ Delete the Security Context of an API Invoker # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} @@ -233,9 +203,6 @@ Delete the Security Context of an API Invoker # Register APF ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Remove Security Context ${resp}= Delete Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} @@ -263,9 +230,6 @@ Delete the Security Context of an API Invoker with Invoker entity role # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} @@ -294,9 +258,6 @@ Delete the Security Context of an API Invoker with Invoker entity role and inval # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${resp}= Delete Request Capif ... /capif-security/v1/trustedInvokers/${API_INVOKER_NOT_VALID} ... server=${CAPIF_HTTPS_URL} @@ -315,9 +276,6 @@ Delete the Security Context of an API Invoker with invalid apiInvokerID # Register Provider ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${resp}= Delete Request Capif ... /capif-security/v1/trustedInvokers/${API_INVOKER_NOT_VALID} ... server=${CAPIF_HTTPS_URL} @@ -336,15 +294,9 @@ Update the Security Context of an API Invoker # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Register Provider ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} @@ -389,9 +341,6 @@ Update the Security Context of an API Invoker with Provider entity role # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} @@ -402,12 +351,9 @@ Update the Security Context of an API Invoker with Provider entity role Check Response Variable Type And Values ${resp} 201 ServiceSecurity - #Register Provider + # Register Provider ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${resp}= Post Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']}/update ... json=${request_body} @@ -424,12 +370,9 @@ Update the Security Context of an API Invoker with Provider entity role Update the Security Context of an API Invoker with AEF entity role and invalid apiInvokerId [Tags] capif_security_api-14 - #Register Provider + # Register Provider ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Post Request Capif ... /capif-security/v1/trustedInvokers/${API_INVOKER_NOT_VALID}/update @@ -449,9 +392,6 @@ Update the Security Context of an API Invoker with invalid apiInvokerID # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Post Request Capif ... /capif-security/v1/trustedInvokers/${API_INVOKER_NOT_VALID}/update @@ -472,9 +412,6 @@ Revoke the authorization of the API invoker for APIs # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name}= Set Variable service_1 # Register One Service @@ -485,10 +422,6 @@ Revoke the authorization of the API invoker for APIs # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} @@ -545,9 +478,6 @@ Revoke the authorization of the API invoker for APIs without valid apfID. # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} @@ -563,9 +493,6 @@ Revoke the authorization of the API invoker for APIs without valid apfID. # Register Provider ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - # Revoke Security Context by Invoker ${request_body}= Create Security Notification Body ${register_user_info_invoker['api_invoker_id']} 1234 ${resp}= Post Request Capif @@ -601,9 +528,6 @@ Revoke the authorization of the API invoker for APIs with invalid apiInvokerId # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - ${request_body}= Create Service Security Body ${NOTIFICATION_DESTINATION} ${resp}= Put Request Capif ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} @@ -616,12 +540,9 @@ Revoke the authorization of the API invoker for APIs with invalid apiInvokerId ${security_context}= Set Variable ${resp.json()} - #Register Provider + # Register Provider ${register_user_info_publisher}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_publisher['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${request_body}= Create Security Notification Body ${API_INVOKER_NOT_VALID} 1234 ${resp}= Post Request Capif ... /capif-security/v1/trustedInvokers/${API_INVOKER_NOT_VALID}/delete @@ -652,9 +573,6 @@ Retrieve access token # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name}= Set Variable service_1 # Register One Service @@ -665,9 +583,6 @@ Retrieve access token # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} @@ -711,9 +626,6 @@ Retrieve access token by Provider # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name}= Set Variable service_1 # Register One Service @@ -724,9 +636,6 @@ Retrieve access token by Provider # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} @@ -769,9 +678,6 @@ Retrieve access token by Provider with invalid apiInvokerId # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name}= Set Variable service_1 # Register One Service @@ -782,9 +688,6 @@ Retrieve access token by Provider with invalid apiInvokerId # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} @@ -828,9 +731,6 @@ Retrieve access token with invalid apiInvokerId # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name}= Set Variable service_1 # Register One Service @@ -841,9 +741,6 @@ Retrieve access token with invalid apiInvokerId # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} @@ -889,9 +786,6 @@ Retrieve access token with invalid client_id # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name}= Set Variable service_1 # Register One Service @@ -902,9 +796,6 @@ Retrieve access token with invalid client_id # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} @@ -948,9 +839,6 @@ Retrieve access token with unsupported grant_type # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name}= Set Variable service_1 # Register One Service @@ -961,9 +849,6 @@ Retrieve access token with unsupported grant_type # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} @@ -1014,9 +899,6 @@ Retrieve access token with invalid scope # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name}= Set Variable service_1 # Register One Service @@ -1027,9 +909,6 @@ Retrieve access token with invalid scope # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} @@ -1075,9 +954,6 @@ Retrieve access token with invalid aefid at scope # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name}= Set Variable service_1 # Register One Service @@ -1088,9 +964,6 @@ Retrieve access token with invalid aefid at scope # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} @@ -1136,9 +1009,6 @@ Retrieve access token with invalid apiName at scope # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method ${CAPIF_USERS} update_capif_users_dicts ${register_user_info_provider['resource_url'].path} ${AMF_PROVIDER_USERNAME} - - ${api_name}= Set Variable service_1 # Register One Service @@ -1149,9 +1019,6 @@ Retrieve access token with invalid apiName at scope # Default Invoker Registration and Onboarding ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${INVOKER_USERNAME} - - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&api-name=${api_name} @@ -1189,4 +1056,4 @@ Retrieve access token with invalid apiName at scope # Check Results Check Response Variable Type And Values ${resp} 400 AccessTokenErr ... error=invalid_scope - ... error_description=One of the api names does not exist or is not associated with the aef id provided \ No newline at end of file + ... error_description=One of the api names does not exist or is not associated with the aef id provided diff --git a/tests/resources/common/basicRequests.robot b/tests/resources/common/basicRequests.robot index 3a24b84..ea3a96f 100644 --- a/tests/resources/common/basicRequests.robot +++ b/tests/resources/common/basicRequests.robot @@ -5,7 +5,7 @@ Library RequestsLibrary Library Collections Library OperatingSystem Library XML -Library Telnet +Library Telnet *** Variables *** @@ -58,31 +58,31 @@ Create Register Admin Session [Documentation] Create needed session to reach Register as Administrator. [Arguments] ${server}=${NONE} ${access_token}=${NONE} ${verify}=${NONE} ${vault_token}=${NONE} IF "${server}" != "${NONE}" - IF "${access_token}" != "${NONE}" + IF "${access_token}" != "${NONE}" ## Return Header with bearer ${headers}= Create Dictionary Authorization=Bearer ${access_token} - RETURN ${headers} + RETURN ${headers} END - + # Request Admin Login to retrieve access token - Create Session register_session ${server} verify=${verify} disable_warnings=1 + Create Session register_session ${server} verify=${verify} disable_warnings=1 ${auth}= Set variable ${{ ('${REGISTER_ADMIN_USER}','${REGISTER_ADMIN_PASSWORD}') }} - ${resp}= POST On Session register_session /login auth=${auth} + ${resp}= POST On Session register_session /login auth=${auth} Log Dictionary ${resp.json()} - + ## Crear sesión con token ${headers}= Create Dictionary Authorization=Bearer ${resp.json()['access_token']} - RETURN ${headers} + RETURN ${headers} END - RETURN ${NONE} ## NEW REQUESTS TO REGISTER + Post Request Admin Register [Timeout] 60s [Arguments] @@ -139,12 +139,8 @@ Get Request Admin Register ... cert=${cert} RETURN ${resp} - - - # NEW REQUESTS END - Post Request Capif [Timeout] 60s [Arguments] @@ -327,8 +323,12 @@ Register User At Jwt Auth END Log cn=${cn} - - ${resp}= Create User At Register ${username} ${password} ${description} email="${username}@nobody.com" + + ${resp}= Create User At Register + ... ${username} + ... ${password} + ... ${description} + ... email="${username}@nobody.com" ${get_auth_response}= Get Auth For User ${username} ${password} @@ -353,7 +353,7 @@ Register User At Jwt Auth Store In File ${username}.key ${register_user_info['private_key']} END - Call Method ${CAPIF_USERS} update_register_users ${register_user_info['uuid']} ${username} + Call Method ${CAPIF_USERS} update_register_users ${register_user_info['uuid']} ${username} RETURN ${register_user_info} @@ -372,7 +372,11 @@ Register User At Jwt Auth Provider ${amf_csr_request}= Create User Csr ${amf_username} amf # Register provider - ${resp}= Create User At Register ${username} ${password} ${description} email="${username}@nobody.com" + ${resp}= Create User At Register + ... ${username} + ... ${password} + ... ${description} + ... email="${username}@nobody.com" ${get_auth_response}= Get Auth For User ${username} ${password} @@ -392,59 +396,62 @@ Register User At Jwt Auth Provider Log Dictionary ${register_user_info} - Call Method ${CAPIF_USERS} update_register_users ${register_user_info['uuid']} ${username} + Call Method ${CAPIF_USERS} update_register_users ${register_user_info['uuid']} ${username} RETURN ${register_user_info} - Login Register Admin - ${headers}= Create Register Admin Session ${CAPIF_HTTPS_REGISTER_URL} - RETURN ${headers} + ${headers}= Create Register Admin Session ${CAPIF_HTTPS_REGISTER_URL} + RETURN ${headers} Create User At Register - [Documentation] (Administrator) This Keyword create a user at register component. - [Arguments] ${username} ${password} ${description} ${email} - + [Documentation] (Administrator) This Keyword create a user at register component. + [Arguments] ${username} ${password} ${description} ${email} + # Obtain Admin Token to request creation of User ${headers}= Login Register Admin - - &{body}= Create Dictionary username=${username} password=${password} description=${description} email=${email} - ${resp}= Post On Session register_session /createUser headers=${headers} json=${body} + + &{body}= Create Dictionary + ... username=${username} + ... password=${password} + ... description=${description} + ... email=${email} + ${resp}= Post On Session register_session /createUser headers=${headers} json=${body} Should Be Equal As Strings ${resp.status_code} 201 - + RETURN ${resp} Delete User At Register - [Documentation] (Administrator) This Keyword delete a user from register. - [Arguments] ${username}=${NONE} ${uuid}=${NONE} - ${user_uuid}= Set Variable ${uuid} + [Documentation] (Administrator) This Keyword delete a user from register. + [Arguments] ${username}=${NONE} ${uuid}=${NONE} + ${user_uuid}= Set Variable ${uuid} - IF "${username}" != "${NONE}" - ${user_uuid}= Call Method ${CAPIF_USERS} get_user_uuid ${username} + IF "${username}" != "${NONE}" + ${user_uuid}= Call Method ${CAPIF_USERS} get_user_uuid ${username} END - ${headers}= Create Register Admin Session ${CAPIF_HTTPS_REGISTER_URL} verify=False + ${headers}= Create Register Admin Session ${CAPIF_HTTPS_REGISTER_URL} verify=False - ${resp}= DELETE On Session register_session /deleteUser/${user_uuid} headers=${headers} + ${resp}= DELETE On Session register_session /deleteUser/${user_uuid} headers=${headers} Should Be Equal As Strings ${resp.status_code} 204 Call Method ${CAPIF_USERS} remove_register_users_entry ${user_uuid} - RETURN ${resp} + RETURN ${resp} Get List of User At Register - [Documentation] (Administrator) This Keyword retrieve a list of users from register. - ${headers}= Create Register Admin Session ${CAPIF_HTTPS_REGISTER_URL} verify=False + [Documentation] (Administrator) This Keyword retrieve a list of users from register. + ${headers}= Create Register Admin Session ${CAPIF_HTTPS_REGISTER_URL} verify=False - ${resp}= DELETE On Session register_session /getUsers headers=${headers} + ${resp}= DELETE On Session register_session /getUsers headers=${headers} Should Be Equal As Strings ${resp.status_code} 200 - RETURN ${resp.json()['users']} + RETURN ${resp.json()['users']} Get Auth For User - [Documentation] (User) This Keyword retrieve token to be used by user towards first interaction with CCF. + [Documentation] (User) This Keyword retrieve token to be used by user towards first interaction with CCF. [Arguments] ${username} ${password} ${auth}= Set variable ${{ ('${username}','${password}') }} ${resp}= GET On Session register_session /getauth auth=${auth} @@ -475,11 +482,11 @@ Clean Test Information ${uuids}= Get Dictionary Keys ${register_users_dict} FOR ${uuid} IN @{uuids} - Delete User At Register uuid=${uuid} + Delete User At Register uuid=${uuid} END Remove entity - [Arguments] ${entity_user} ${certificate_name}=${entity_user} + [Arguments] ${entity_user} ${certificate_name}=${entity_user} ${capif_users_dict}= Call Method ${CAPIF_USERS} get_capif_users_dict @@ -491,7 +498,7 @@ Remove entity FOR ${key} IN @{keys} ${value}= Get From Dictionary ${capif_users_dict} ${key} - IF "${value}" == "${certificate_name}" + IF "${value}" == "${certificate_name}" ${resp}= Delete Request Capif ... ${key} ... server=${CAPIF_HTTPS_URL} @@ -503,14 +510,14 @@ Remove entity Call Method ${CAPIF_USERS} remove_capif_users_entry ${key} END END - - Delete User At Register username=${entity_user} + + Delete User At Register username=${entity_user} Log Dictionary ${capif_users_dict} Log Dictionary ${register_users_dict} Remove Resource - [Arguments] ${resource_url} ${management_cert} ${username} + [Arguments] ${resource_url} ${management_cert} ${username} ${resp}= Delete Request Capif ... ${resource_url} @@ -530,7 +537,6 @@ Remove Resource Should Be Equal As Strings ${resp.status_code} 204 - Invoker Default Onboarding [Arguments] ${invoker_username}=${INVOKER_USERNAME} ${register_user_info}= Register User At Jwt Auth @@ -554,11 +560,12 @@ Invoker Default Onboarding # Assertions Status Should Be 201 ${resp} Check Variable ${resp.json()} APIInvokerEnrolmentDetails - ${resource_url}= Check Location Header ${resp} ${LOCATION_INVOKER_RESOURCE_REGEX} + ${resource_url}= Check Location Header ${resp} ${LOCATION_INVOKER_RESOURCE_REGEX} # Store dummy signede certificate Store In File ${invoker_username}.crt ${resp.json()['onboardingInformation']['apiInvokerCertificate']} ${url}= Parse Url ${resp.headers['Location']} + Call Method ${CAPIF_USERS} update_capif_users_dicts ${url.path} ${invoker_username} Set To Dictionary ${register_user_info} resource_url=${resource_url} Set To Dictionary ${register_user_info} management_cert=${invoker_username} @@ -622,6 +629,12 @@ Provider Registration ... provider_register_response=${resp} ... management_cert=${register_user_info['amf_username']} + Call Method + ... ${CAPIF_USERS} + ... update_capif_users_dicts + ... ${register_user_info['resource_url'].path} + ... ${register_user_info['amf_username']} + RETURN ${register_user_info} Provider Default Registration @@ -633,6 +646,7 @@ Provider Default Registration ${register_user_info}= Provider Registration ${register_user_info} Log Dictionary ${register_user_info} + RETURN ${register_user_info} Publish Service Api @@ -657,13 +671,6 @@ Basic ACL registration # Register APF ${register_user_info_provider}= Provider Default Registration - Call Method - ... ${CAPIF_USERS} - ... update_capif_users_dicts - ... ${register_user_info_provider['resource_url'].path} - ... ${AMF_PROVIDER_USERNAME} - Call Method ${CAPIF_USERS} update_register_users ${register_user_info_provider['uuid']} ${PROVIDER_USERNAME} - ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api ... ${register_user_info_provider} ... service_1 @@ -714,7 +721,7 @@ Basic ACL registration RETURN ${register_user_info_invoker} ${register_user_info_provider} ${service_api_description_published} Create Security Context Between invoker and provider - [Arguments] ${register_user_info_invoker} ${register_user_info_provider} + [Arguments] ${register_user_info_invoker} ${register_user_info_provider} ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} @@ -737,4 +744,3 @@ Create Security Context Between invoker and provider ... username=${register_user_info_invoker['management_cert']} Check Response Variable Type And Values ${resp} 201 ServiceSecurity - -- GitLab From e98ba6dbd6519405d5e02a8e060d81c74edc62b2 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Fri, 19 Apr 2024 07:54:19 +0200 Subject: [PATCH 141/310] Change docker-compose to docker compose at scripts --- services/check_services_are_running.sh | 4 ++-- services/clean_capif_docker_services.sh | 4 ++-- services/run.sh | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/services/check_services_are_running.sh b/services/check_services_are_running.sh index 53e8e6d..3bad399 100755 --- a/services/check_services_are_running.sh +++ b/services/check_services_are_running.sh @@ -1,6 +1,6 @@ #!/bin/bash -running="$(docker-compose -f docker-compose-capif.yml ps --services --all --filter "status=running")" -services="$(docker-compose -f docker-compose-capif.yml ps --services --all)" +running="$(docker compose -f docker-compose-capif.yml ps --services --all --filter "status=running")" +services="$(docker compose -f docker-compose-capif.yml ps --services --all)" if [ "$running" != "$services" ]; then echo "Following services are not running:" # Bash specific diff --git a/services/clean_capif_docker_services.sh b/services/clean_capif_docker_services.sh index 7b3c91e..fb89497 100755 --- a/services/clean_capif_docker_services.sh +++ b/services/clean_capif_docker_services.sh @@ -62,8 +62,8 @@ echo "after check" echo "${FILES[@]}" for FILE in "${FILES[@]}"; do - echo "Executing 'docker-compose down' for file $FILE" - docker-compose -f "$FILE" down --rmi all + echo "Executing 'docker compose down' for file $FILE" + docker compose -f "$FILE" down --rmi all status=$? if [ $status -eq 0 ]; then echo "*** Removed Service from $FILE ***" diff --git a/services/run.sh b/services/run.sh index 8214516..83011c3 100755 --- a/services/run.sh +++ b/services/run.sh @@ -28,13 +28,13 @@ else fi # Read params -while getopts ":c:m:h" opt; do +while getopts ":c:mh" opt; do case $opt in c) HOSTNAME="$OPTARG" ;; m) - MONITORING_STATE="$OPTARG" + MONITORING_STATE=true ;; h) help -- GitLab From 7a46de2059a3ca78c2a235424eda05068abe9e4a Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 25 Apr 2024 10:01:26 +0200 Subject: [PATCH 142/310] kubectl cluster --- helm/DELETE.txt | 2 +- helm/capif/values.yaml | 10 +++++----- helm/vault-job/vault-job.yaml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index a2aea33..2f5e8ac 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -485,7 +485,7 @@ parametersVault: env: vaultHostname: vault-internal.mon.svc.cluster.local vaultPort: 8200 - vaultAccessToken: dev-only-token + vaultAccessToken: hvs.foen8o9OJ58z1x4WHpUPFSUN # -- With tempo.enabled: false. It won't be deployed # -- If monitoring.enable: "true". Also enable tempo.enabled: true tempo: @@ -597,7 +597,7 @@ monitoring: # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - - host: prometheus.5gnacar.int + - host: prometheus.test.int paths: - path: / pathType: Prefix @@ -608,7 +608,7 @@ monitoring: # -- If ingressRoute enable=true, use monitoring.prometheus.ingress.enabled="" ingressRoute: enable: "" - host: prometheus.5gnacar.int + host: prometheus.test.int grafana: image: # -- The docker image repository to use @@ -646,7 +646,7 @@ monitoring: # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - - host: grafana.5gnacar.int + - host: grafana.test.int paths: - path: / pathType: Prefix @@ -657,4 +657,4 @@ monitoring: # -- If ingressRoute enable=true, use monitoring.grafana.ingress.enabled="" ingressRoute: enable: "" - host: grafana.5gnacar.int + host: grafana.test.int diff --git a/helm/vault-job/vault-job.yaml b/helm/vault-job/vault-job.yaml index 6e0e9ce..9cf0206 100644 --- a/helm/vault-job/vault-job.yaml +++ b/helm/vault-job/vault-job.yaml @@ -25,8 +25,8 @@ data: # to execute the next commands in vault # otherwise, if use the vault as dev's mode. Just # type the token's dev. - export VAULT_TOKEN="dev-only-token" - export DOMAIN1=capif.mobilesandbox.cloud + export VAULT_TOKEN="hvs.foen8o9OJ58z1x4WHpUPFSUN" + export DOMAIN1=capif.test.cloud vault secrets enable pki -- GitLab From 01da7d905e1e10327ccebd3dc1eb8a6bcf9b81b3 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 25 Apr 2024 10:44:46 +0200 Subject: [PATCH 143/310] kubectl staging --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From a875acd92fbf96efebea78d1fd12c16f1a3e4084 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 25 Apr 2024 10:47:42 +0200 Subject: [PATCH 144/310] sudo ./get_helm.sh --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From c2ce5c0883b155f90487dfdcb00dc7e26d6a9a5b Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 26 Apr 2024 10:13:55 +0200 Subject: [PATCH 145/310] execution migrating runners in cluster-ocf --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 87a89145db50713e960c4818b560a398e3352229 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 26 Apr 2024 10:20:16 +0200 Subject: [PATCH 146/310] pipeline in cluster-ocf --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 271adbf8bd0755258d3cf7c4e0af346335dc41d6 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 26 Apr 2024 10:27:40 +0200 Subject: [PATCH 147/310] testing docker-in-docker in workers pipeline --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From e17f8a1a503b7852d69231332b349f964d118c82 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 26 Apr 2024 10:48:30 +0200 Subject: [PATCH 148/310] no install kubectl and helm --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From d4864f6a9f88d340855bd0fcd6e37a0e9fe7c250 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 26 Apr 2024 11:41:13 +0200 Subject: [PATCH 149/310] docker login test --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From c79e5749e56104c58eb4100707366f82a8e8879a Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 26 Apr 2024 12:16:46 +0200 Subject: [PATCH 150/310] sudo pip install --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 3c38cb8a3ddc8b3b18c2971496137a676e705069 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 26 Apr 2024 13:17:04 +0200 Subject: [PATCH 151/310] trying again --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 7adcc41d458a8c9b40b576f070996e029e458f61 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 26 Apr 2024 13:49:45 +0200 Subject: [PATCH 152/310] no sudo in pip --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 8822900cf758534ce3d3e986c07113b8b42131e1 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 26 Apr 2024 14:22:13 +0200 Subject: [PATCH 153/310] kubectl --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 6e6b85fd849301577f1b77fec208509be5fd3a2b Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 26 Apr 2024 15:09:25 +0200 Subject: [PATCH 154/310] no variable KUBECONFIG --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From f16f4e6453e73ae7c64a534e5d3d96406c161781 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 29 Apr 2024 09:03:24 +0200 Subject: [PATCH 155/310] whoami --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From acbee7fbdf99d9496bfd477f06372d28198aa2c1 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 29 Apr 2024 10:26:32 +0200 Subject: [PATCH 156/310] whois --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From ca627443c2d43566ee11b0e63af1e33fd8ab2c4c Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 29 Apr 2024 10:33:53 +0200 Subject: [PATCH 157/310] commented cancel_previous_action job --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 195524144dbbaa5a7771510a238a2d9a55115da9 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 29 Apr 2024 11:01:46 +0200 Subject: [PATCH 158/310] no file rke2.yaml --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From b9fa8ee5f64d4d780576ec98cf4269def3fffd05 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 29 Apr 2024 11:39:16 +0200 Subject: [PATCH 159/310] helm jobs --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 9ac5f5db3c13ecda0f353f83546e5613999206f0 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 29 Apr 2024 16:08:23 +0200 Subject: [PATCH 160/310] helm parameters --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From f6346402aa832f9a8f1cce9fee69f8327486142b Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 29 Apr 2024 16:27:30 +0200 Subject: [PATCH 161/310] IMAGE_TAG --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 39cbb9c82f3c83b86e78734cf8eac07c301f2646 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 29 Apr 2024 16:43:09 +0200 Subject: [PATCH 162/310] yq e -i --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 26cb9605fbf5837a7f55c35aac3464e51a038212 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 29 Apr 2024 16:58:16 +0200 Subject: [PATCH 163/310] yq e -i --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From eb6ed27a539684245880dc05f1c61bebfe8e3980 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 29 Apr 2024 17:09:21 +0200 Subject: [PATCH 164/310] appVersion --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From c5507ba95dd0a57e31d1948b2e5f08a59d02fc10 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 29 Apr 2024 18:36:03 +0200 Subject: [PATCH 165/310] Modify loggin to allow redis communication --- .../api_invocation_logs/core/invocationlogs.py | 9 +++++++++ .../api_invocation_logs/core/publisher.py | 11 +++++++++++ .../requirements.txt | 1 + tests/libraries/api_logging_service/bodyRequests.py | 6 +++--- 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/publisher.py diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py index dde0c64..aab8309 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py @@ -13,6 +13,9 @@ from ..util import dict_to_camel_case, clean_empty from .resources import Resource from .responses import bad_request_error, internal_server_error, forbidden_error, not_found_error, unauthorized_error, make_response from ..models.invocation_log import InvocationLog +from .publisher import Publisher + +publisher_ops = Publisher() class LoggingInvocationOperations(Resource): @@ -84,6 +87,12 @@ class LoggingInvocationOperations(Resource): if result is not None: return result + + if log.result: + if int(log.result) >= 200 and int(log.result) < 300: + publisher_ops.publish_message("events", "SERVICE_API_INVOCATION_SUCCESS") + else: + publisher_ops.publish_message("events", "SERVICE_API_INVOCATION_FAILURE") current_app.logger.debug("Check existing logs") my_query = {'aef_id': aef_id, 'api_invoker_id': invocationlog.api_invoker_id} diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/publisher.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/publisher.py new file mode 100644 index 0000000..3898c4b --- /dev/null +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/publisher.py @@ -0,0 +1,11 @@ +import redis +import sys +from flask import current_app + +class Publisher(): + + def __init__(self): + self. r = redis.Redis(host='redis', port=6379, db=0) + + def publish_message(self, channel, message): + self.r.publish(channel, message) diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt index 197399f..0dfa8b6 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt @@ -6,6 +6,7 @@ Flask == 2.0.3 pymongo == 4.0.1 elasticsearch == 8.4.3 flask_jwt_extended == 4.4.4 +redis == 4.5.4 opentelemetry-instrumentation == 0.38b0 opentelemetry-instrumentation-flask == 0.38b0 opentelemetry-instrumentation-redis == 0.38b0 diff --git a/tests/libraries/api_logging_service/bodyRequests.py b/tests/libraries/api_logging_service/bodyRequests.py index fe3faf1..fc2cad4 100644 --- a/tests/libraries/api_logging_service/bodyRequests.py +++ b/tests/libraries/api_logging_service/bodyRequests.py @@ -1,4 +1,4 @@ -def create_log_entry(aefId=None, apiInvokerId=None, apiId=None, apiName=None): +def create_log_entry(aefId, apiInvokerId, apiId, apiName, result='200'): data = { "aefId": aefId, "apiInvokerId": apiInvokerId, @@ -11,7 +11,7 @@ def create_log_entry(aefId=None, apiInvokerId=None, apiId=None, apiName=None): "uri": "http://resource/endpoint", "protocol": "HTTP_1_1", "operation": "GET", - "result": "string", + "result": result, "invocationTime": "2023-03-30T10:30:21.408Z", "invocationLatency": 0, "inputParameters": "string", @@ -78,7 +78,7 @@ def create_log_entry(aefId=None, apiInvokerId=None, apiId=None, apiName=None): } return data -def create_log_entry_bad_service(aefId=None, apiInvokerId=None): +def create_log_entry_bad_service(aefId, apiInvokerId): data = { "aefId": aefId, "apiInvokerId": apiInvokerId, -- GitLab From 78fd87c516ede5a55bf06bb9cb8625d3c1aebbeb Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 29 Apr 2024 18:38:10 +0200 Subject: [PATCH 166/310] modify robot docker image --- .../CAPIF Api Events/capif_events_api.robot | 44 +++++++++++++++++++ tests/requirements.txt | 8 +--- tools/robot/Dockerfile | 24 ++++++++-- tools/robot/basicRequirements.txt | 13 +++--- tools/robot/basicRobotInstall.sh | 11 ++++- 5 files changed, 81 insertions(+), 19 deletions(-) diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index f2a5966..aceeb1b 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -5,6 +5,7 @@ Library XML Resource /opt/robot-tests/tests/resources/common/basicRequests.robot Resource ../../resources/common.resource + Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment @@ -137,3 +138,46 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriptionId ... title=Unauthorized ... detail=User not authorized ... cause=You are not the owner of this resource + +Prueba JMS + [Tags] jms-1 + # Log "Prueba 1" + # Wait For Request + # Create a log entry + # [Tags] capif_api_logging_service-1 + + # Register APF + ${register_user_info}= Provider Default Registration + + # Publish one api + Publish Service Api ${register_user_info} + + # Register INVOKER + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + + ${discover_response}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + + # Create Log Entry + ${request_body}= Create Log Entry + ... ${register_user_info['aef_id']} + ... ${register_user_info_invoker['api_invoker_id']} + ... ${api_ids} + ... ${api_names} + ... '200' + ${resp}= Post Request Capif + ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${AEF_PROVIDER_USERNAME} + + # Check Results + Check Response Variable Type And Values ${resp} 201 InvocationLog + ${resource_url}= Check Location Header ${resp} ${LOCATION_LOGGING_RESOURCE_REGEX} + diff --git a/tests/requirements.txt b/tests/requirements.txt index c6d9032..b983217 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,8 +1,2 @@ # Requirements file for tests. -robotframework-mongodb-library==3.2 -requests==2.28.1 -configparser==5.3.0 -redis==4.3.4 -rfc3987==1.3.8 -robotframework-httpctrl -robotframework-archivelibrary == 0.4.2 \ No newline at end of file +robotframework-archivelibrary == 0.4.2 diff --git a/tools/robot/Dockerfile b/tools/robot/Dockerfile index 49cea17..261b1e4 100644 --- a/tools/robot/Dockerfile +++ b/tools/robot/Dockerfile @@ -25,6 +25,7 @@ VOLUME $ROBOT_RESULTS_DIRECTORY WORKDIR $ROBOT_DIRECTORY ENV DEBIAN_FRONTEND=noninteractive +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections # Install dependencies RUN apt-get update @@ -49,17 +50,32 @@ RUN apt-get install -y --no-install-recommends \ python3-venv \ python2.7-dev \ libssl-dev \ - libldap2-dev libsasl2-dev ldap-utils slapd tox lcov valgrind\ - tshark + libldap2-dev libsasl2-dev ldap-utils slapd tox lcov valgrind \ + tshark \ + nodejs \ + npm -RUN add-apt-repository ppa:deadsnakes/ppa +RUN add-apt-repository -y ppa:deadsnakes/ppa RUN apt-get update RUN apt-get install -y --fix-missing python3.10 python3.10-venv python3.10-dev - RUN mkdir /opt/venv RUN python3.10 -m venv /opt/venv +ENV PLAYWRIGHT_BROWSERS_PATH=$HOME/pw-browsers + +# Instalación de nvm y node a la última versión +# RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash +# # RUN source /root/.bashrc +# # RUN export NVM_DIR="$HOME/.nvm" \ +# # [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm \ +# # [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion + +# RUN nvm install node +# # RUN npx playwright install +# RUN yes|npx playwright install-deps + + ADD basicRequirements.txt /root/ ADD basicRobotInstall.sh /root/ diff --git a/tools/robot/basicRequirements.txt b/tools/robot/basicRequirements.txt index 8775227..99eafd7 100644 --- a/tools/robot/basicRequirements.txt +++ b/tools/robot/basicRequirements.txt @@ -13,7 +13,7 @@ certifi==2021.10.8 cffi==1.15.1 chardet==5.0.0 charset-normalizer==2.0.12 -click==8.0.1 +click==8.1.7 configparser==5.3.0 cookiecutter==2.1.1 coverage==4.5.4 @@ -26,7 +26,6 @@ exceptiongroup==1.0.0rc9 filelock==3.8.0 flake8==3.9.2 h11==0.14.0 -robotframework-httpctrl==0.3.1 idna==3.4 iniconfig==1.1.1 invoke==1.6.0 @@ -69,10 +68,12 @@ redis==4.3.4 rellu==0.7 requests==2.28.1 rfc3987==1.3.8 -robotframework==6.0 +robotframework==7.0 +robotframework-browser==18.3.0 +robotframework-httpctrl==0.3.1 robotframework-lint==1.1 robotframework-mongodb-library==3.2 -robotframework-pythonlibcore==3.0.0 +robotframework-pythonlibcore==4.4.1 robotframework-requests==0.9.3 robotframework-seleniumlibrary==6.0.0 robotframework-sshlibrary==3.8.0 @@ -91,11 +92,11 @@ tox==3.26.0 tqdm==4.64.1 trio==0.22.0 trio-websocket==0.9.2 -typing-extensions==3.10.0.2 +typing-extensions==4.11.0 urllib3==1.26.12 virtualenv==20.16.5 watchdog==0.9.0 webdrivermanager==0.10.0 -wrapt==1.14.1 +wrapt==1.15.0 wsproto==1.2.0 xlrd==2.0.1 \ No newline at end of file diff --git a/tools/robot/basicRobotInstall.sh b/tools/robot/basicRobotInstall.sh index 511821f..ba66010 100644 --- a/tools/robot/basicRobotInstall.sh +++ b/tools/robot/basicRobotInstall.sh @@ -2,6 +2,13 @@ echo "Installing basic software related with robotFramework" source /opt/venv/bin/activate; pip install --upgrade pip -pip install --upgrade robotframework; -pip install -r $1 +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash +source /root/.bashrc +nvm install node +yes|npx playwright install-deps +pip install -r $1 +rfbrowser clean-node +rfbrowser init --skip-browsers +npx playwright install +npx playwright install-deps echo "Robot framework installed" -- GitLab From 6cd10457120f099bc7bb22709b7a0cd7d6454771 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 29 Apr 2024 18:47:33 +0200 Subject: [PATCH 167/310] Updated helm files --- helm/capif/Chart.yaml | 4 +- helm/capif/templates/deployment.yaml | 72 ++++++++++++++++++- helm/capif/templates/grafana-pvc.yaml | 1 + helm/capif/templates/loki-pvc.yaml | 1 + helm/capif/templates/mongo-pvc.yaml | 1 + .../templates/mongo-register-express.yaml | 17 +++++ helm/capif/templates/mongo-register-pvc.yaml | 17 +++++ helm/capif/templates/nginx-ssl.yaml | 4 +- helm/capif/templates/prometheus-pvc.yaml | 1 + helm/capif/values.yaml | 43 ++++++++++- helm/vault-job/vault-job.yaml | 2 +- 11 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 helm/capif/templates/mongo-register-express.yaml create mode 100644 helm/capif/templates/mongo-register-pvc.yaml diff --git a/helm/capif/Chart.yaml b/helm/capif/Chart.yaml index 625f958..0c8eb5f 100644 --- a/helm/capif/Chart.yaml +++ b/helm/capif/Chart.yaml @@ -13,12 +13,12 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: v3.1.4 +version: v3.1.6 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v3.1.4" +appVersion: "v3.1.6" dependencies: - name: "tempo" condition: tempo.enabled diff --git a/helm/capif/templates/deployment.yaml b/helm/capif/templates/deployment.yaml index 4c2e026..ff5ffe2 100644 --- a/helm/capif/templates/deployment.yaml +++ b/helm/capif/templates/deployment.yaml @@ -558,8 +558,15 @@ spec: image: {{ .Values.mongoRegister.mongo.image.repository }}:{{ .Values.mongoRegister.mongo.image.tag | default .Chart.AppVersion }} imagePullPolicy: {{ .Values.mongoRegister.mongo.image.imagePullPolicy }} name: mongo-register + {{- if .Values.mongoRegister.mongo.persistence.enable }} + volumeMounts: + - name: mongo-register-pvc + mountPath: /data/db + {{- end }} ports: - containerPort: 27017 + securityContext: + runAsUser: 999 resources: {{- toYaml .Values.mongoRegister.mongo.resources | nindent 12 }} readinessProbe: @@ -567,6 +574,12 @@ spec: port: 27017 # initialDelaySeconds: 5 periodSeconds: 5 + {{- if .Values.mongoRegister.mongo.persistence.enable }} + volumes: + - name: mongo-register-pvc + persistentVolumeClaim: + claimName: mongo-register-pvc + {{- end }} restartPolicy: Always {{- end }} --- @@ -674,7 +687,7 @@ spec: ports: - containerPort: 27017 securityContext: - runAsUser: 0 + runAsUser: 999 {{- if eq .Values.mongo.persistence.enable "true" }} volumeMounts: - name: mongo-pvc @@ -765,6 +778,61 @@ spec: periodSeconds: 5 restartPolicy: Always --- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mongo-register-express + labels: + io.kompose.service: mongo-register-express + {{- include "capif.labels" . | nindent 4 }} + annotations: + kompose.cmd: kompose -f ../services/docker-compose.yml convert +spec: + replicas: {{ .Values.mongoRegisterExpress.replicas }} + selector: + matchLabels: + io.kompose.service: mongo-register-express + {{- include "capif.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + io.kompose.network/services-default: "true" + io.kompose.service: mongo-register-express + {{- include "capif.selectorLabels" . | nindent 8 }} + annotations: + date: "{{ now | unixEpoch }}" + spec: + hostAliases: + - ip: "{{ .Values.ingress.ip }}" + hostnames: + - "{{ .Values.nginx.nginx.env.capifHostname }}" + containers: + - env: + - name: ME_CONFIG_MONGODB_ADMINPASSWORD + value: {{ quote .Values.mongoRegisterExpress.mongoRegisterExpress.env.meConfigMongodbAdminpassword + }} + - name: ME_CONFIG_MONGODB_ADMINUSERNAME + value: {{ quote .Values.mongoRegisterExpress.mongoRegisterExpress.env.meConfigMongodbAdminusername + }} + - name: ME_CONFIG_MONGODB_URL + value: {{ quote .Values.mongoRegisterExpress.mongoRegisterExpress.env.meConfigMongodbUrl }} + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ quote .Values.kubernetesClusterDomain }} + image: {{ .Values.mongoRegisterExpress.mongoRegisterExpress.image.repository }}:{{ .Values.mongoRegisterExpress.mongoRegisterExpress.image.tag | default .Chart.AppVersion }} + imagePullPolicy: {{ .Values.mongoRegisterExpress.mongoRegisterExpress.image.imagePullPolicy }} + name: mongo-register-express + ports: + - containerPort: 8081 + resources: + {{- toYaml .Values.mongoRegisterExpress.mongoRegisterExpress.resources | nindent 12 }} + readinessProbe: + tcpSocket: + port: 8081 +# initialDelaySeconds: 0 + periodSeconds: 5 + restartPolicy: Always +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -998,4 +1066,4 @@ spec: items: - key: "config.yaml" path: "config.yaml" - restartPolicy: Always \ No newline at end of file + restartPolicy: Always diff --git a/helm/capif/templates/grafana-pvc.yaml b/helm/capif/templates/grafana-pvc.yaml index b2c6672..5a55282 100644 --- a/helm/capif/templates/grafana-pvc.yaml +++ b/helm/capif/templates/grafana-pvc.yaml @@ -7,6 +7,7 @@ metadata: io.kompose.service: grafana-claim0 name: grafana-claim0 spec: + storageClassName: {{ .Values.monitoring.grafana.persistence.storageClass }} accessModes: - ReadWriteOnce resources: diff --git a/helm/capif/templates/loki-pvc.yaml b/helm/capif/templates/loki-pvc.yaml index 7da7816..0b90bda 100644 --- a/helm/capif/templates/loki-pvc.yaml +++ b/helm/capif/templates/loki-pvc.yaml @@ -7,6 +7,7 @@ metadata: io.kompose.service: loki-claim0 name: loki-claim0 spec: + storageClassName: {{ .Values.monitoring.loki.persistence.storageClass }} accessModes: - ReadWriteOnce resources: diff --git a/helm/capif/templates/mongo-pvc.yaml b/helm/capif/templates/mongo-pvc.yaml index 2996d57..3c80c14 100644 --- a/helm/capif/templates/mongo-pvc.yaml +++ b/helm/capif/templates/mongo-pvc.yaml @@ -7,6 +7,7 @@ metadata: io.kompose.service: mongo-pvc name: mongo-pvc spec: + storageClassName: {{ .Values.mongo.persistence.storageClass }} accessModes: - ReadWriteOnce resources: diff --git a/helm/capif/templates/mongo-register-express.yaml b/helm/capif/templates/mongo-register-express.yaml new file mode 100644 index 0000000..5de4b22 --- /dev/null +++ b/helm/capif/templates/mongo-register-express.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongo-register-express + labels: + io.kompose.service: mongo-register-express + {{- include "capif.labels" . | nindent 4 }} + annotations: + kompose.cmd: kompose -f ../services/docker-compose.yml convert + kompose.version: 1.28.0 (c4137012e) +spec: + type: {{ .Values.mongoRegisterExpress.type }} + selector: + io.kompose.service: mongo-register-express + {{- include "capif.selectorLabels" . | nindent 4 }} + ports: + {{- .Values.mongoRegisterExpress.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/mongo-register-pvc.yaml b/helm/capif/templates/mongo-register-pvc.yaml new file mode 100644 index 0000000..4d1a259 --- /dev/null +++ b/helm/capif/templates/mongo-register-pvc.yaml @@ -0,0 +1,17 @@ +{{- if eq .Values.monitoring.enable "true" }} +{{- if .Values.mongoRegister.mongo.persistence.enable }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: mongo-register + name: mongo-register-pvc +spec: + storageClassName: {{ .Values.mongoRegister.mongo.persistence.storageClass }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.mongoRegister.mongo.persistence.storage }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/nginx-ssl.yaml b/helm/capif/templates/nginx-ssl.yaml index 275e2c7..39487d5 100644 --- a/helm/capif/templates/nginx-ssl.yaml +++ b/helm/capif/templates/nginx-ssl.yaml @@ -15,7 +15,7 @@ spec: ingressClassName: {{ .Values.nginx.ingressClassName }} {{- end }} rules: - - host: "register{{ .Values.nginx.nginx.env.capifHostname }}" + - host: "{{ .Values.nginx.nginx.env.registerHostname }}" http: paths: - backend: @@ -27,6 +27,6 @@ spec: pathType: Prefix tls: - hosts: - - "register{{ .Values.nginx.nginx.env.capifHostname }}" + - "{{ .Values.nginx.nginx.env.registerHostname }}" secretName: letsencrypt-secret {{- end }} \ No newline at end of file diff --git a/helm/capif/templates/prometheus-pvc.yaml b/helm/capif/templates/prometheus-pvc.yaml index 0ba676f..8f763c6 100644 --- a/helm/capif/templates/prometheus-pvc.yaml +++ b/helm/capif/templates/prometheus-pvc.yaml @@ -9,6 +9,7 @@ metadata: app: prometheus {{- include "capif.labels" . | nindent 4 }} spec: + storageClassName: {{ .Values.monitoring.prometheus.persistence.storageClass }} accessModes: - ReadWriteOnce resources: diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index a2aea33..4308349 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -258,6 +258,11 @@ mongoRegister: tag: "6.0.2" # -- Image pull policy: Always, IfNotPresent imagePullPolicy: Always + # -- If mongoRegister.mongo.persistence enabled. enable: true, enable: false is = not enabled + persistence: + enable: true + storage: 8Gi + storageClass: nfs-01 resources: {} # limits: # cpu: 100m @@ -333,6 +338,7 @@ mongo: persistence: enable: "true" storage: 8Gi + storageClass: nfs-01 mongoExpress: mongoExpress: env: @@ -363,6 +369,36 @@ mongoExpress: targetPort: 8081 replicas: 1 type: ClusterIP +mongoRegisterExpress: + mongoRegisterExpress: + env: + # User's password MongoDB + meConfigMongodbAdminpassword: example + # Name of User's mongodb + meConfigMongodbAdminusername: root + # URI for connecting MongoDB + meConfigMongodbUrl: mongodb://root:example@mongo-register:27017/ + image: + # -- The docker image repository to use + repository: "mongo-express" + # -- The docker image tag to use + # @default Chart version + tag: "1.0.0-alpha.4" + # -- Image pull policy: Always, IfNotPresent + imagePullPolicy: Always + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + ports: + - name: "8082" + port: 8082 + targetPort: 8081 + replicas: 1 + type: ClusterIP nginx: # -- if nginx.ingressType: "Ingress". set up monitoring.prometheus.ingress: true # and monitoring.grafana.ingress: true @@ -379,6 +415,7 @@ nginx: env: # -- Ingress's host to Capif capifHostname: "my-capif.apps.ocp-epg.hi.inet" + registerHostname: "register.app.ocp-epg.hi.inet" image: # -- The docker image repository to use repository: "public.ecr.aws/o2v4a8t6/opencapif/nginx" @@ -534,6 +571,7 @@ monitoring: persistence: enable: "true" storage: 100Mi + storageClass: nfs-01 otel: image: # -- The docker image repository to use @@ -576,7 +614,7 @@ monitoring: # -- The docker image tag to use # @default Chart version tag: "latest" - retentionTime: 5d + retentionTime: 3d resources: {} # limits: # cpu: 100m @@ -587,6 +625,7 @@ monitoring: persistence: enable: "true" storage: 8Gi + storageClass: nfs-01 service: type: ClusterIP port: 9090 @@ -635,6 +674,7 @@ monitoring: persistence: enable: "true" storage: 100Mi + storageClass: nfs-01 service: type: ClusterIP port: 3000 @@ -658,3 +698,4 @@ monitoring: ingressRoute: enable: "" host: grafana.5gnacar.int + diff --git a/helm/vault-job/vault-job.yaml b/helm/vault-job/vault-job.yaml index 6e0e9ce..8f683e2 100644 --- a/helm/vault-job/vault-job.yaml +++ b/helm/vault-job/vault-job.yaml @@ -69,7 +69,7 @@ data: vault write pki_int/intermediate/set-signed certificate=@capif_intermediate.cert.pem #Crear rol en Vault - vault write pki_int/roles/my-ca use_csr_common_name=true require_cn=false allowed_domains="*" allow_any_name=true allow_bare_domains=true allow_glob_domains=true allow_subdomains=true max_ttl=4300h ttl=4300h + vault write pki_int/roles/my-ca use_csr_common_name=false require_cn=false allowed_domains="*" allow_any_name=true allow_bare_domains=true allow_glob_domains=true allow_subdomains=true max_ttl=4300h ttl=4300h # Emitir un certificado firmado por la CA intermedia # vault write -format=json pki_int/issue/my-ca \ -- GitLab From c082fc6c9a977d55755df2aa85c9a217ac6c5612 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 30 Apr 2024 11:15:30 +0200 Subject: [PATCH 168/310] no previous_action in CICD --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From cd49bd1831c7b9de51df653deb4bd2e7e753d494 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 8 May 2024 14:14:31 +0200 Subject: [PATCH 169/310] vault job yaml file --- helm/vault-job/vault-job.yaml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/helm/vault-job/vault-job.yaml b/helm/vault-job/vault-job.yaml index 8f683e2..e30a394 100644 --- a/helm/vault-job/vault-job.yaml +++ b/helm/vault-job/vault-job.yaml @@ -3,7 +3,7 @@ apiVersion: v1 kind: ConfigMap metadata: name: vault-prepare-certs - namespace: mon + namespace: ocf-vault labels: io.kompose.service: api-invocation-logs app: capif @@ -25,8 +25,15 @@ data: # to execute the next commands in vault # otherwise, if use the vault as dev's mode. Just # type the token's dev. - export VAULT_TOKEN="dev-only-token" - export DOMAIN1=capif.mobilesandbox.cloud + export VAULT_TOKEN="hvs.mn50Q8kpMuxsPUsCNlwQekCd" + export DOMAIN1=*.pre-prod.int + export DOMAIN2=*.staging.int + export DOMAIN3=*.developer.int + + # local domains + # export DOMAIN4=*.pre-prod.svc.cluster.local + # export DOMAIN5=*.staging.svc.cluster.local + # export DOMAIN6=*.developer.svc.cluster.local vault secrets enable pki @@ -69,7 +76,7 @@ data: vault write pki_int/intermediate/set-signed certificate=@capif_intermediate.cert.pem #Crear rol en Vault - vault write pki_int/roles/my-ca use_csr_common_name=false require_cn=false allowed_domains="*" allow_any_name=true allow_bare_domains=true allow_glob_domains=true allow_subdomains=true max_ttl=4300h ttl=4300h + vault write pki_int/roles/my-ca use_csr_common_name=true require_cn=false allowed_domains="*" allow_any_name=true allow_bare_domains=true allow_glob_domains=true allow_subdomains=true max_ttl=4300h ttl=4300h # Emitir un certificado firmado por la CA intermedia # vault write -format=json pki_int/issue/my-ca \ @@ -119,8 +126,6 @@ data: DNS.3 = \$ENV::DOMAIN3 EOF - export DOMAIN2=nginx.mon.svc.cluster.local - export DOMAIN3=nginx.mon-staging.svc.cluster.local export COUNTRY=ES # 2 letter country-code export STATE=Madrid # state or province name export LOCALITY=Madrid # Locality Name (e.g. city) @@ -222,7 +227,7 @@ apiVersion: batch/v1 kind: Job metadata: name: vault-pki - namespace: mon + namespace: ocf-vault labels: io.kompose.service: vault-pki app: capif -- GitLab From afd5c39889fd49ac377767d08be3866f7a4eb60b Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Thu, 9 May 2024 14:23:02 +0200 Subject: [PATCH 170/310] Helper service --- .../core/apiinvokerenrolmentdetails.py | 2 +- .../api_invoker_management/db/db.py | 1 - .../config.yaml | 10 +- .../api_provider_management/db/db.py | 1 - .../config.yaml | 9 - services/docker-compose-capif.yml | 25 ++- services/helper/Dockerfile | 16 ++ services/helper/config.yaml | 18 ++ services/helper/helper_service/__init__.py | 0 services/helper/helper_service/__main__.py | 68 ++++++ services/helper/helper_service/certs/.gitkeep | 0 services/helper/helper_service/config.py | 20 ++ .../controllers/helper_controller.py | 115 ++++++++++ .../helper_service/core/helper_operations.py | 204 ++++++++++++++++++ services/helper/helper_service/db/db.py | 45 ++++ services/helper/requirements.txt | 8 + services/nginx/nginx.conf | 13 ++ .../core/register_operations.py | 10 +- 18 files changed, 538 insertions(+), 27 deletions(-) create mode 100644 services/helper/Dockerfile create mode 100644 services/helper/config.yaml create mode 100644 services/helper/helper_service/__init__.py create mode 100644 services/helper/helper_service/__main__.py create mode 100644 services/helper/helper_service/certs/.gitkeep create mode 100644 services/helper/helper_service/config.py create mode 100644 services/helper/helper_service/controllers/helper_controller.py create mode 100644 services/helper/helper_service/core/helper_operations.py create mode 100644 services/helper/helper_service/db/db.py create mode 100644 services/helper/requirements.txt diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py index dc186e4..2d43af2 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py @@ -84,7 +84,7 @@ class InvokerManagementOperations(Resource): invoker_dict["username"]=username invoker_dict["uuid"]=uuid - mycol.insert_one(apiinvokerenrolmentdetail.to_dict()) + mycol.insert_one(invoker_dict) current_app.logger.debug("Invoker inserted in database") current_app.logger.debug("Netapp onboarded sucessfuly") diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py index 5bfcd0b..638cc80 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py @@ -16,7 +16,6 @@ class MongoDatabse(): self.config = Config().get_config() self.db = self.__connect() self.invoker_enrolment_details = self.config['mongo']['col'] - self.capif_users = self.config['mongo']['capif_users_col'] self.service_col = self.config['mongo']["service_col"] self.certs_col = self.config['mongo']['certs_col'] diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml b/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml index e208a78..2a14561 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml @@ -3,20 +3,12 @@ mongo: { 'password': 'example', 'db': 'capif', 'col': 'invokerdetails', - 'capif_users_col': "user", 'certs_col': "certs", 'service_col': 'serviceapidescriptions', 'host': 'mongo', 'port': "27017" } -mongo_register: { - 'user': 'root', - 'password': 'example', - 'db': 'capif_users', - 'col': 'user', - 'host': 'mongo_register', - 'port': '27017' -} + ca_factory: { "url": "vault", "port": "8200", diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py index bb08025..a67e2ea 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py @@ -17,7 +17,6 @@ class MongoDatabse(): self.config = Config().get_config() self.db = self.__connect() self.provider_enrolment_details = self.config['mongo']['col'] - self.capif_users = self.config['mongo']['capif_users'] self.certs_col = self.config['mongo']['certs_col'] def get_col_by_name(self, name): diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml b/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml index 8de29a4..7d1899a 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml +++ b/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml @@ -4,19 +4,10 @@ mongo: { 'db': 'capif', 'col': 'providerenrolmentdetails', 'certs_col': "certs", - 'capif_users': 'user', 'host': 'mongo', 'port': "27017" } -mongo_register: { - 'user': 'root', - 'password': 'example', - 'db': 'capif_users', - 'col': 'user', - 'host': 'mongo_register', - 'port': '27017' -} ca_factory: { "url": "vault", diff --git a/services/docker-compose-capif.yml b/services/docker-compose-capif.yml index f667d8c..d05518f 100644 --- a/services/docker-compose-capif.yml +++ b/services/docker-compose-capif.yml @@ -11,6 +11,30 @@ services: - $PWD/redis.conf:/usr/local/etc/redis/redis.conf environment: - REDIS_REPLICATION_MODE=master + + + helper: + build: + context: ./helper + expose: + - "8080" + container_name: helper + restart: unless-stopped + volumes: + - ./helper:/usr/src/app + extra_hosts: + - host.docker.internal:host-gateway + - fluent-bit:host-gateway + - otel-collector:host-gateway + - vault:host-gateway + environment: + - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} + - CONTAINER_NAME=helper + - VAULT_HOSTNAME=vault + - VAULT_ACCESS_TOKEN=dev-only-token + - VAULT_PORT=8200 + depends_on: + - nginx access-control-policy: build: TS29222_CAPIF_Access_Control_Policy_API @@ -30,7 +54,6 @@ services: depends_on: - redis - nginx - api-invoker-management: build: diff --git a/services/helper/Dockerfile b/services/helper/Dockerfile new file mode 100644 index 0000000..2bc3b33 --- /dev/null +++ b/services/helper/Dockerfile @@ -0,0 +1,16 @@ +FROM public.ecr.aws/o2v4a8t6/opencapif/python:3-alpine + +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +COPY requirements.txt /usr/src/app/requirements.txt + +RUN apk add -U --no-cache gcc build-base linux-headers ca-certificates libffi-dev libressl-dev libxslt-dev +RUN pip3 install --no-cache-dir -r requirements.txt +RUN apk add openssl curl redis + +COPY . /usr/src/app + +EXPOSE 8080 + +CMD ["python3", "-m", "helper_service"] diff --git a/services/helper/config.yaml b/services/helper/config.yaml new file mode 100644 index 0000000..46e5801 --- /dev/null +++ b/services/helper/config.yaml @@ -0,0 +1,18 @@ +mongo: { + 'user': 'root', + 'password': 'example', + 'db': 'capif', + 'invoker_col': 'invokerdetails', + 'provider_col': 'providerenrolmentdetails', + 'col_services': "serviceapidescriptions", + 'col_security': "security", + 'col_event': "eventsdetails", + 'host': 'mongo', + 'port': "27017" +} + +ca_factory: { + "url": "vault", + "port": "8200", + "token": "dev-only-token" +} diff --git a/services/helper/helper_service/__init__.py b/services/helper/helper_service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/helper/helper_service/__main__.py b/services/helper/helper_service/__main__.py new file mode 100644 index 0000000..833284b --- /dev/null +++ b/services/helper/helper_service/__main__.py @@ -0,0 +1,68 @@ +from flask import Flask +import logging +from .controllers.helper_controller import helper_routes +from OpenSSL.crypto import PKey, TYPE_RSA, X509Req, dump_certificate_request, FILETYPE_PEM, dump_privatekey +from .config import Config +import json +import requests + + +app = Flask(__name__) +config = Config().get_config() + +# Create a superadmin CSR and keys +key = PKey() +key.generate_key(TYPE_RSA, 2048) +req = X509Req() +req.get_subject().O = 'OCF helper' +req.get_subject().OU = 'helper' +req.get_subject().L = 'Madrid' +req.get_subject().ST = 'Madrid' +req.get_subject().C = 'ES' +req.get_subject().emailAddress = 'helper@tid.es' +req.set_pubkey(key) +req.sign(key, 'sha256') + +csr_request = dump_certificate_request(FILETYPE_PEM, req) +private_key = dump_privatekey(FILETYPE_PEM, key) + +# Save superadmin private key +key_file = open("helper_service/certs/superadmin.key", 'wb+') +key_file.write(bytes(private_key)) +key_file.close() + +# Request superadmin certificate +url = 'http://{}:{}/v1/pki_int/sign/my-ca'.format(config["ca_factory"]["url"], config["ca_factory"]["port"]) +headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} +data = { + 'format':'pem_bundle', + 'ttl': '43000h', + 'csr': csr_request, + 'common_name': "superadmin" +} + +response = requests.request("POST", url, headers=headers, data=data, verify = False) +superadmin_cert = json.loads(response.text)['data']['certificate'] + +# Save the superadmin certificate +cert_file = open("helper_service/certs/superadmin.crt", 'wb') +cert_file.write(bytes(superadmin_cert, 'utf-8')) +cert_file.close() + +url = f"http://{config['ca_factory']['url']}:{config['ca_factory']['port']}/v1/secret/data/ca" +headers = { + + 'X-Vault-Token': config['ca_factory']['token'] +} +response = requests.request("GET", url, headers=headers, verify = False) + +ca_root = json.loads(response.text)['data']['data']['ca'] +cert_file = open("helper_service/certs/ca_root.crt", 'wb') +cert_file.write(bytes(ca_root, 'utf-8')) +cert_file.close() + +app.register_blueprint(helper_routes) +app.logger.setLevel(logging.DEBUG) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8080, debug=True) \ No newline at end of file diff --git a/services/helper/helper_service/certs/.gitkeep b/services/helper/helper_service/certs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/helper/helper_service/config.py b/services/helper/helper_service/config.py new file mode 100644 index 0000000..d04bd1a --- /dev/null +++ b/services/helper/helper_service/config.py @@ -0,0 +1,20 @@ +import yaml +import os + +#Config class to get config +class Config: + def __init__(self): + self.cached = 0 + self.file="./config.yaml" + self.my_config = {} + + stamp = os.stat(self.file).st_mtime + if stamp != self.cached: + self.cached = stamp + f = open(self.file) + self.my_config = yaml.safe_load(f) + f.close() + + def get_config(self): + return self.my_config + diff --git a/services/helper/helper_service/controllers/helper_controller.py b/services/helper/helper_service/controllers/helper_controller.py new file mode 100644 index 0000000..995d335 --- /dev/null +++ b/services/helper/helper_service/controllers/helper_controller.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +from flask import Blueprint, request, current_app, jsonify +from ..core.helper_operations import HelperOperations +from ..config import Config + +config = Config().get_config() + +helper_routes = Blueprint("helper_routes", __name__) +helper_operation = HelperOperations() + +@helper_routes.route("/helper/getInvokers", methods=["GET"]) +def getInvokers(): + uuid = request.args.get('uuid') + invoker_id = request.args.get('api_invoker_id') + page_size = request.args.get('page_size') + page = request.args.get('page') + sort_order = request.args.get('sort_order') + if page_size: + page_size = int(page_size) + if page_size < 0: + return jsonify(message="The value of the page_size parameter must be greater than 0"), 400 + if page: + page = int(page) + if page < 0: + return jsonify(message="The value of the page parameter must be greater than 0"), 400 + + current_app.logger.debug(f"uuid: {uuid}, invoker_id: {invoker_id}, page: {page}, page_size: {page_size}, sort_order: {sort_order}") + + return helper_operation.get_invokers(uuid, invoker_id, page, page_size, sort_order) + +@helper_routes.route("/helper/getProviders", methods=["GET"]) +def getProviders(): + uuid = request.args.get('uuid') + provider_id = request.args.get('api_prov_dom_id') + page_size = request.args.get('page_size') + page = request.args.get('page') + sort_order = request.args.get('sort_order') + + if page_size: + page_size = int(page_size) + if page_size < 0: + return jsonify(message="The value of the page_size parameter must be greater than 0"), 400 + if page: + page = int(page) + if page < 0: + return jsonify(message="The value of the page parameter must be greater than 0"), 400 + + current_app.logger.debug(f"uuid: {uuid}, provider_id: {provider_id}, page: {page}, page_size: {page_size}, sort_order: {sort_order}") + + return helper_operation.get_providers(uuid, provider_id, page, page_size, sort_order) + + +@helper_routes.route("/helper/getServices", methods=["GET"]) +def getServices(): + service_id = request.args.get('service_id') + apf_id = request.args.get('apf_id') + api_name = request.args.get('api_name') + page_size = request.args.get('page_size') + page = request.args.get('page') + sort_order = request.args.get('sort_order') + if page_size: + page_size = int(page_size) + if page_size < 0: + return jsonify(message="The value of the page_size parameter must be greater than 0"), 400 + if page: + page = int(page) + if page < 0: + return jsonify(message="The value of the page parameter must be greater than 0"), 400 + + current_app.logger.debug(f"service_id: {service_id}, apf_id: {apf_id}, api_name: {api_name}, page: {page}, page_size: {page_size}, sort_order: {sort_order}") + + return helper_operation.get_services(service_id, apf_id, api_name, page, page_size, sort_order) + +@helper_routes.route("/helper/getSecurity", methods=["GET"]) +def getSecurity(): + invoker_id = request.args.get('invoker_id') + page_size = request.args.get('page_size') + page = request.args.get('page') + if page_size: + page_size = int(page_size) + if page_size < 0: + return jsonify(message="The value of the page_size parameter must be greater than 0"), 400 + if page: + page = int(page) + if page < 0: + return jsonify(message="The value of the page parameter must be greater than 0"), 400 + + current_app.logger.debug(f"invoker_id: {invoker_id}, page: {page}, page_size: {page_size} ") + + return helper_operation.get_security(invoker_id, page, page_size) + +@helper_routes.route("/helper/getEvents", methods=["GET"]) +def getEvents(): + subscriber_id = request.args.get('subscriber_id') + subscription_id = request.args.get('subscription_id') + page_size = request.args.get('page_size') + page = request.args.get('page') + if page_size: + page_size = int(page_size) + if page_size < 0: + return jsonify(message="The value of the page_size parameter must be greater than 0"), 400 + if page: + page = int(page) + if page < 0: + return jsonify(message="The value of the page parameter must be greater than 0"), 400 + + current_app.logger.debug(f"subscriber_id: {subscriber_id}, subscription_id: {subscription_id}, page: {page}, page_size: {page_size} ") + + return helper_operation.get_events(subscriber_id, subscription_id, page, page_size) + + +@helper_routes.route("/helper/deleteEntities/", methods=["DELETE"]) +def deleteUserEntities(uuid): + return helper_operation.remove_entities(uuid) diff --git a/services/helper/helper_service/core/helper_operations.py b/services/helper/helper_service/core/helper_operations.py new file mode 100644 index 0000000..aed9da5 --- /dev/null +++ b/services/helper/helper_service/core/helper_operations.py @@ -0,0 +1,204 @@ +from flask import jsonify, current_app +import pymongo +from ..db.db import MongoDatabse +from ..config import Config +import requests +import os + +class HelperOperations: + + def __init__(self): + self.db = MongoDatabse() + self.mimetype = 'application/json' + self.config = Config().get_config() + + def get_invokers(self, uuid, invoker_id, page, page_size, sort_order): + current_app.logger.debug(f"Getting the invokers") + invoker_col = self.db.get_col_by_name(self.db.invoker_col) + + total_invokers = invoker_col.count_documents({}) + + filter = {} + if uuid: + filter["uuid"]=uuid + if invoker_id: + filter["api_invoker_id"]=invoker_id + + sort_direction = pymongo.DESCENDING if sort_order == "desc" else pymongo.ASCENDING + + if page_size and page: + index = (page - 1) * page_size + documents = invoker_col.find(filter,{"_id":0}).sort("onboarding_date", sort_direction).skip(index).limit(page_size) + pages = (total_invokers + page_size - 1) // page_size + else: + documents = invoker_col.find(filter,{"_id":0}).sort("onboarding_date", sort_direction) + pages = 1 + + list_invokers= list(documents) + long = len(list_invokers) + + return jsonify(message="Invokers returned successfully", + invokers=list_invokers, + total = total_invokers, + long = long, + totalPages = pages, + sortOrder = sort_order), 200 + + + def get_providers(self, uuid, provider_id, page, page_size, sort_order): + current_app.logger.debug(f"Getting the providers") + provider_col = self.db.get_col_by_name(self.db.provider_col) + + total_providers = provider_col.count_documents({}) + + filter = {} + if uuid: + filter["uuid"]=uuid + if provider_id: + filter["api_prov_dom_id"]=provider_id + + sort_direction = pymongo.DESCENDING if sort_order == "desc" else pymongo.ASCENDING + + if page_size and page: + index = (page - 1) * page_size + documents = provider_col.find(filter,{"_id":0}).sort("onboarding_date", sort_direction).skip(index).limit(page_size) + pages = (total_providers + page_size - 1) // page_size + else: + documents = provider_col.find(filter,{"_id":0}).sort("onboarding_date", sort_direction) + pages = 1 + + list_providers = list(documents) + long = len(list_providers) + + return jsonify(message="Providers returned successfully", + providers=list_providers, + total = total_providers, + long = long, + totalPages = pages, + sortOrder = sort_order), 200 + + def get_services(self, service_id, apf_id, api_name, page, page_size, sort_order): + current_app.logger.debug(f"Getting the services") + service_col = self.db.get_col_by_name(self.db.services_col) + + total_services = service_col.count_documents({}) + + filter = {} + if service_id: + filter["api_id"]=service_id + if apf_id: + filter["apf_id"]=apf_id + if api_name: + filter["api_name"]=api_name + + sort_direction = pymongo.DESCENDING if sort_order == "desc" else pymongo.ASCENDING + + if page_size and page: + index = (page - 1) * page_size + documents = service_col.find(filter,{"_id":0}).sort("onboarding_date", sort_direction).skip(index).limit(page_size) + pages = (total_services + page_size - 1) // page_size + else: + documents = service_col.find(filter,{"_id":0}).sort("onboarding_date", sort_direction) + pages = 1 + + list_services= list(documents) + long = len(list_services) + + return jsonify(message="Services returned successfully", + services=list_services, + total = total_services, + long = long, + totalPages = pages, + sortOrder = sort_order), 200 + + def get_security(self, invoker_id, page, page_size): + current_app.logger.debug(f"Getting the security context") + security_col = self.db.get_col_by_name(self.db.security_context_col) + + total_security = security_col.count_documents({}) + + filter = {} + + if invoker_id: + filter["api_invoker_id"]=invoker_id + + if page_size and page: + index = (page - 1) * page_size + documents = security_col.find(filter,{"_id":0}).skip(index).limit(page_size) + pages = (total_security + page_size - 1) // page_size + else: + documents = security_col.find(filter,{"_id":0}) + pages = 1 + + list_security= list(documents) + long = len(list_security) + + return jsonify(message="Security context returned successfully", + security=list_security, + total = total_security, + long = long, + totalPages = pages), 200 + + def get_events(self, subscriber_id, subscription_id, page, page_size): + current_app.logger.debug(f"Getting the events") + events_col = self.db.get_col_by_name(self.db.events) + + total_events = events_col.count_documents({}) + + filter = {} + + if subscriber_id: + filter["subscriber_id"]=subscriber_id + if subscription_id: + filter["subscription_id"]=subscription_id + + if page_size and page: + index = (page - 1) * page_size + documents = events_col.find(filter,{"_id":0}).skip(index).limit(page_size) + pages = (total_events + page_size - 1) // page_size + else: + documents = events_col.find(filter,{"_id":0}) + pages = 1 + + list_events= list(documents) + long = len(list_events) + + return jsonify(message="Events returned successfully", + events=list_events, + total = total_events, + long = long, + totalPages = pages), 200 + + def remove_entities(self, uuid): + + current_app.logger.debug(f"Removing entities for uuid: {uuid}") + invoker_col = self.db.get_col_by_name(self.db.invoker_col) + provider_col = self.db.get_col_by_name(self.db.provider_col) + + try: + if invoker_col.count_documents({'uuid':uuid}) == 0 and provider_col.count_documents({'uuid':uuid}) == 0: + current_app.logger.debug(f"No entities found for uuid: {uuid}") + return jsonify(message=f"No entities found for uuid: {uuid}"), 204 + + for invoker in invoker_col.find({'uuid':uuid}, {"_id":0}): + current_app.logger.debug(f"Removing Invoker: {invoker["api_invoker_id"]}") + url = 'https://{}/api-invoker-management/v1/onboardedInvokers/{}'.format(os.getenv('CAPIF_HOSTNAME'), invoker["api_invoker_id"]) + requests.request("DELETE", url, cert=( + '/usr/src/app/helper_service/certs/superadmin.crt', '/usr/src/app/helper_service/certs/superadmin.key'), verify='/usr/src/app/helper_service/certs/ca_root.crt') + + for provider in provider_col.find({'uuid':uuid}, {"_id":0}): + current_app.logger.debug(f"Removing Provider: {provider["api_prov_dom_id"]}") + url = 'https://{}/api-provider-management/v1/registrations/{}'.format(os.getenv('CAPIF_HOSTNAME'), provider["api_prov_dom_id"]) + + requests.request("DELETE", url, cert=( + '/usr/src/app/helper_service/certs/superadmin.crt', '/usr/src/app/helper_service/certs/superadmin.key'), verify='/usr/src/app/helper_service/certs/ca_root.crt') + except Exception as e: + current_app.logger.debug(f"Error deleting user entities: {e}") + jsonify(message=f"Error deleting user entities: {e}"), 500 + + current_app.logger.debug(f"User entities removed successfully") + return jsonify(message="User entities removed successfully"), 200 + + + + diff --git a/services/helper/helper_service/db/db.py b/services/helper/helper_service/db/db.py new file mode 100644 index 0000000..9820a92 --- /dev/null +++ b/services/helper/helper_service/db/db.py @@ -0,0 +1,45 @@ +import time +from pymongo import MongoClient +from pymongo.errors import AutoReconnect +from ..config import Config +from bson.codec_options import CodecOptions + +class MongoDatabse(): + + def __init__(self): + self.config = Config().get_config() + self.db = self.__connect() + self.invoker_col = self.config['mongo']['invoker_col'] + self.provider_col = self.config['mongo']['provider_col'] + self.services_col = self.config['mongo']['col_services'] + self.security_context_col = self.config['mongo']['col_security'] + self.events = self.config['mongo']['col_event'] + + + def get_col_by_name(self, name): + return self.db[name].with_options(codec_options=CodecOptions(tz_aware=True)) + + def __connect(self, max_retries=3, retry_delay=1): + + retries = 0 + + while retries < max_retries: + try: + uri = f"mongodb://{self.config['mongo']['user']}:{self.config['mongo']['password']}@" \ + f"{self.config['mongo']['host']}:{self.config['mongo']['port']}" + + + client = MongoClient(uri) + mydb = client[self.config['mongo']['db']] + mydb.command("ping") + return mydb + except AutoReconnect: + retries += 1 + print(f"Reconnecting... Retry {retries} of {max_retries}") + time.sleep(retry_delay) + return None + + def close_connection(self): + if self.db.client: + self.db.client.close() + diff --git a/services/helper/requirements.txt b/services/helper/requirements.txt new file mode 100644 index 0000000..c5a4f37 --- /dev/null +++ b/services/helper/requirements.txt @@ -0,0 +1,8 @@ +python_dateutil >= 2.6.0 +setuptools >= 21.0.0 +Flask >= 2.0.3 +pymongo == 4.0.1 +flask_jwt_extended +pyopenssl +pyyaml +requests diff --git a/services/nginx/nginx.conf b/services/nginx/nginx.conf index d30ded7..ec70d57 100644 --- a/services/nginx/nginx.conf +++ b/services/nginx/nginx.conf @@ -12,6 +12,10 @@ http { default ""; ~(^|,)CN=(?[^,]+) $CN; } + map "$request_method:$uri:$ssl_client_s_dn_cn" $helper_error_message { + default 'SUCCESS'; + "~*(GET|DELETE):.*:(?!(superadmin))(.*)" '{"status":401, "title":"Unauthorized" ,"detail":"Role not authorized for this API route", "cause":"User role must be superadmin"}'; + } map "$request_method:$uri:$ssl_client_s_dn_cn" $invoker_error_message { default 'SUCCESS'; "~*(PUT|DELETE):.*:(?!(INV|superadmin))(.*)" '{"status":401, "title":"Unauthorized" ,"detail":"Role not authorized for this API route", "cause":"User role must be invoker"}'; @@ -163,6 +167,15 @@ http { proxy_pass http://access-control-policy:8080; } + location /helper { + if ( $helper_error_message != SUCCESS ) { + add_header Content-Type 'application/problem+json'; + return 401 $helper_error_message; + } + proxy_set_header X-SSL-Client-Cert $ssl_client_cert; + proxy_pass http://helper:8080; + } + } } diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index a8fee91..1eb6b07 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -3,7 +3,7 @@ from flask_jwt_extended import create_access_token from ..db.db import MongoDatabse from datetime import datetime from ..config import Config -import base64 +import requests import uuid class RegisterOperations: @@ -61,11 +61,11 @@ class RegisterOperations: mycol = self.db.get_col_by_name(self.db.capif_users) try: - mycol.delete_one({"uuid": uuid}) - - # Request to the helper to delete invokers and providers - + url = f"https://capifcore/helper/deleteEntities/{uuid}" + requests.delete(url, cert=("register_service/certs/superadmin.crt", "register_service/certs/superadmin.key"), verify="register_service/certs/ca_root.crt") + + mycol.delete_one({"uuid": uuid}) return jsonify(message="User removed successfully"), 204 except Exception as e: -- GitLab From 704b6b79b35c7a4ae523adc57cccc1fcf92a344f Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 15 May 2024 13:23:08 +0200 Subject: [PATCH 171/310] Add new object at loggin to send redis message to events service --- .../capif_events/core/consumer_messager.py | 9 ++- .../capif_events/core/internal_event_ops.py | 4 +- .../capif_events/core/notifications.py | 47 ++++++++++++ .../TS29222_CAPIF_Events_API/requirements.txt | 2 + .../core/invocationlogs.py | 23 ++++-- .../api_invocation_logs/core/publisher.py | 2 +- .../api_invocation_logs/core/redis_event.py | 22 ++++++ .../CAPIF Api Events/capif_events_api.robot | 17 ++++- tests/libraries/api_events/bodyRequests.py | 75 +++++++++++-------- .../api_logging_service/bodyRequests.py | 6 +- 10 files changed, 164 insertions(+), 43 deletions(-) create mode 100644 services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py b/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py index 0f9fa6a..fa38290 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py @@ -16,13 +16,20 @@ class Subscriber(): self.notification = Notifications() self.event_ops = InternalEventOperations() self.p = self.r.pubsub() - self.p.subscribe("events", "internal-messages") + self.p.subscribe("events", "internal-messages", "events-log") def listen(self): for raw_message in self.p.listen(): + current_app.logger.info(raw_message) if raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "events": current_app.logger.info("Event received") self.notification.send_notifications(raw_message["data"].decode('utf-8')) + if raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "events-log": + current_app.logger.info("Event-log received") + event_redis=json.loads(raw_message["data"].decode('utf-8')) + current_app.logger.info(json.dumps(event_redis, indent=4)) + self.notification.send_notifications_new(event_redis) + elif raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "internal-messages": message, *invoker_id = raw_message["data"].decode('utf-8').split(":") diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/internal_event_ops.py b/services/TS29222_CAPIF_Events_API/capif_events/core/internal_event_ops.py index 3f4fc6a..437f7e3 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/internal_event_ops.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/internal_event_ops.py @@ -20,10 +20,10 @@ class InternalEventOperations(Resource): #self.auth_manager.remove_auth_all_event(subscriber_id) def get_event_subscriptions(self, event): + current_app.logger.info("get subscription from db") try: mycol = self.db.get_col_by_name(self.db.event_collection) - - query= {'events':event} + query={'events':{'$in':[event]}} subscriptions = mycol.find(query) if subscriptions is None: diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py index a02f0ab..205d145 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py @@ -8,6 +8,8 @@ from ..encoder import JSONEncoder import sys import json from flask import current_app +import asyncio +import aiohttp class Notifications(): @@ -37,7 +39,52 @@ class Notifications(): current_app.logger.error("An exception occurred ::" + str(e)) return False + def send_notifications_new(self, redis_event): + try: + if redis_event.get('event', None) == None: + raise("Event value is not present on received event from REDIS") + + current_app.logger.info("Received event " + redis_event.get('event') + ", sending notifications") + subscriptions = self.events_ops.get_event_subscriptions(redis_event.get('event')) + # message, *ids = event.split(":") + current_app.logger.info(subscriptions) + + for sub in subscriptions: + url = sub["notification_destination"] + current_app.logger.debug(url) + event_detail=None + if redis_event.get('key', None) != None and redis_event.get('information', None) != None: + # current_app.logger.debug(json.dumps(redis_event.get('information'),cls=JSONEncoder)) + # event_detail=CAPIFEventDetail().from_dict({redis_event.get('key'):redis_event.get('information')}) + # current_app.logger.debug(json.dumps(event_detail,cls=JSONEncoder)) + event_detail={redis_event.get('key'):redis_event.get('information')} + + data = EventNotification(sub["subscription_id"], events=redis_event.get('event'), event_detail=[event_detail]) + current_app.logger.debug(json.dumps(data,cls=JSONEncoder)) + + # self.request_post(url, data) + asyncio.run(self.send(url, json.dumps(data,cls=JSONEncoder))) + + except Exception as e: + current_app.logger.error("An exception occurred ::" + str(e)) + return False + def request_post(self, url, data): headers = {'content-type': 'application/json'} return requests.post(url, json={'text': str(data.to_str())}, headers=headers) + + async def send_request(self, url, data): + async with aiohttp.ClientSession() as session: + timeout = aiohttp.ClientTimeout(total=10) # Establecer timeout a 10 segundos + async with session.post(url, json=data, timeout=timeout) as response: + return await response.text() + + async def send(self, url, data): + try: + response = await self.send_request(url, data) + print(response) + except asyncio.TimeoutError: + print("Timeout: Request timeout") + + diff --git a/services/TS29222_CAPIF_Events_API/requirements.txt b/services/TS29222_CAPIF_Events_API/requirements.txt index bbef950..743f814 100644 --- a/services/TS29222_CAPIF_Events_API/requirements.txt +++ b/services/TS29222_CAPIF_Events_API/requirements.txt @@ -21,3 +21,5 @@ rfc3987 redis flask_executor Flask-APScheduler +aiohttp==3.9.5 +async-timeout==4.0.3 diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py index aab8309..5a11c59 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py @@ -13,9 +13,11 @@ from ..util import dict_to_camel_case, clean_empty from .resources import Resource from .responses import bad_request_error, internal_server_error, forbidden_error, not_found_error, unauthorized_error, make_response from ..models.invocation_log import InvocationLog -from .publisher import Publisher +# from .publisher import Publisher +from .redis_event import RedisEvent +import copy -publisher_ops = Publisher() +# publisher_ops = Publisher() class LoggingInvocationOperations(Resource): @@ -64,7 +66,7 @@ class LoggingInvocationOperations(Resource): return None def add_invocationlog(self, aef_id, invocationlog): - + mycol = self.db.get_col_by_name(self.db.invocation_logs) try: @@ -82,17 +84,28 @@ class LoggingInvocationOperations(Resource): return result current_app.logger.debug("Check service apis") + event=None + invocation_log_base=json.loads(json.dumps(invocationlog, cls=JSONEncoder)) + for log in invocationlog.logs: result = self.__check_service_apis(log.api_id, log.api_name) + current_app.logger.debug("Inside for loop.") if result is not None: return result if log.result: + current_app.logger.debug(log) if int(log.result) >= 200 and int(log.result) < 300: - publisher_ops.publish_message("events", "SERVICE_API_INVOCATION_SUCCESS") + event="SERVICE_API_INVOCATION_SUCCESS" else: - publisher_ops.publish_message("events", "SERVICE_API_INVOCATION_FAILURE") + event="SERVICE_API_INVOCATION_FAILURE" + + current_app.logger.info(event) + invocation_log_base['logs']=[log] + RedisEvent(event,invocation_log_base,"invocationLogs").send_event() + + current_app.logger.debug("After log check") current_app.logger.debug("Check existing logs") my_query = {'aef_id': aef_id, 'api_invoker_id': invocationlog.api_invoker_id} diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/publisher.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/publisher.py index 3898c4b..a15c0d9 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/publisher.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/publisher.py @@ -5,7 +5,7 @@ from flask import current_app class Publisher(): def __init__(self): - self. r = redis.Redis(host='redis', port=6379, db=0) + self.r = redis.Redis(host='redis', port=6379, db=0) def publish_message(self, channel, message): self.r.publish(channel, message) diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py new file mode 100644 index 0000000..3b80a45 --- /dev/null +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py @@ -0,0 +1,22 @@ +from ..encoder import JSONEncoder +from .publisher import Publisher +import json + +publisher_ops = Publisher() + +class RedisEvent(): + def __init__(self, event, information, event_detail_key="invocationLogs") -> None: + self.redis_event={ + "event": event, + "key": event_detail_key, + "information":information + } + + def to_string(self): + return json.dumps(self.redis_event, cls=JSONEncoder) + + def send_event(self): + publisher_ops.publish_message("events-log",self.to_string()) + + def __call__(self): + return self.redis_event \ No newline at end of file diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index aceeb1b..4bff370 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -139,6 +139,7 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriptionId ... detail=User not authorized ... cause=You are not the owner of this resource + Prueba JMS [Tags] jms-1 # Log "Prueba 1" @@ -163,13 +164,27 @@ Prueba JMS ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + # Subscribe to events + ${events_list}= Create List SERVICE_API_INVOCATION_SUCCESS SERVICE_API_INVOCATION_FAILURE + ${request_body}= Create Events Subscription events=@{events_list} notificationDestination=http://127.0.0.1:9090/ + ${resp}= Post Request Capif + ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + # Check Results + Check Response Variable Type And Values ${resp} 201 EventSubscription + ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + # Create Log Entry ${request_body}= Create Log Entry ... ${register_user_info['aef_id']} ... ${register_user_info_invoker['api_invoker_id']} ... ${api_ids} ... ${api_names} - ... '200' + ... 200 ${resp}= Post Request Capif ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs ... json=${request_body} diff --git a/tests/libraries/api_events/bodyRequests.py b/tests/libraries/api_events/bodyRequests.py index ab6e4da..3d1de26 100644 --- a/tests/libraries/api_events/bodyRequests.py +++ b/tests/libraries/api_events/bodyRequests.py @@ -1,32 +1,47 @@ -def create_events_subscription(): +def create_events_subscription(events=["SERVICE_API_AVAILABLE", "API_INVOKER_ONBOARDED"],notificationDestination="http://robot.testing", eventFilters=None, eventReq=None, requestTestNotification=None, supportedFeatures=None,websockNotifConfig=None): + event_subscription={ + "events": events, + "notificationDestination": notificationDestination, + } + if eventFilters != None: + event_subscription['eventFilters']=eventFilters + if eventReq != None: + event_subscription['eventReq']=eventReq + if requestTestNotification != None: + event_subscription['requestTestNotification']=requestTestNotification + if supportedFeatures != None: + event_subscription['supportedFeatures']=supportedFeatures + if websockNotifConfig != None: + event_subscription['websockNotifConfig']=websockNotifConfig + + return event_subscription + +def create_capif_event_filter(aefIds=None, apiIds=None, apiInvokerIds=None): + if aefIds == None and apiIds == None and apiInvokerIds: + raise("Error, no data present to create event filter") + capif_event_filter=dict() + if aefIds != None: + capif_event_filter['aefIds']=aefIds + if apiIds != None: + capif_event_filter['apiIds']=apiIds + if apiInvokerIds != None: + capif_event_filter['apiInvokerIds']=apiInvokerIds + return capif_event_filter + +def create_default_event_req(): + return { + "grpRepTime": 5, + "immRep": True, + "maxReportNbr": 0, + "monDur": "2000-01-23T04:56:07+00:00", + "partitionCriteria": ["TAC", "GEOAREA"], + "repPeriod": 6, + "sampRatio": 15 + } + +def create_websock_notif_config_default(): return { - "eventFilters": [ - { - "aefIds": ["aefIds", "aefIds"], - "apiIds": ["apiIds", "apiIds"], - "apiInvokerIds": ["apiInvokerIds", "apiInvokerIds"] - }, - { - "aefIds": ["aefIds", "aefIds"], - "apiIds": ["apiIds", "apiIds"], - "apiInvokerIds": ["apiInvokerIds", "apiInvokerIds"] - } - ], - "eventReq": { - "grpRepTime": 5, - "immRep": True, - "maxReportNbr": 0, - "monDur": "2000-01-23T04:56:07+00:00", - "partitionCriteria": ["TAC", "GEOAREA"], - "repPeriod": 6, - "sampRatio": 15 - }, - "events": ["SERVICE_API_AVAILABLE", "API_INVOKER_ONBOARDED"], - "notificationDestination": "http://robot.testing", - "requestTestNotification": True, - "supportedFeatures": "aaa", - "websockNotifConfig": { - "requestWebsocketUri": True, - "websocketUri": "websocketUri" - } + "requestWebsocketUri": True, + "websocketUri": "websocketUri" } + diff --git a/tests/libraries/api_logging_service/bodyRequests.py b/tests/libraries/api_logging_service/bodyRequests.py index fc2cad4..e0e70da 100644 --- a/tests/libraries/api_logging_service/bodyRequests.py +++ b/tests/libraries/api_logging_service/bodyRequests.py @@ -46,7 +46,7 @@ def create_log_entry(aefId, apiInvokerId, apiId, apiName, result='200'): "uri": "http://resource/endpoint", "protocol": "HTTP_1_1", "operation": "GET", - "result": "string", + "result": result, "invocationTime": "2023-03-30T10:30:21.408Z", "invocationLatency": 0, "inputParameters": "string", @@ -78,7 +78,7 @@ def create_log_entry(aefId, apiInvokerId, apiId, apiName, result='200'): } return data -def create_log_entry_bad_service(aefId, apiInvokerId): +def create_log_entry_bad_service(aefId, apiInvokerId, result=500): data = { "aefId": aefId, "apiInvokerId": apiInvokerId, @@ -91,7 +91,7 @@ def create_log_entry_bad_service(aefId, apiInvokerId): "uri": "string", "protocol": "HTTP_1_1", "operation": "GET", - "result": "string", + "result": result, "invocationTime": "2023-03-30T10:30:21.408Z", "invocationLatency": 0, "inputParameters": "string", -- GitLab From aa2a3c646f4f5600d43c5d2cb70b449d9be76912 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Wed, 15 May 2024 15:28:28 +0300 Subject: [PATCH 172/310] Move Register service to Gunicorn production server --- services/register/register_prepare.sh | 6 ++++-- .../register_service/{__main__.py => app.py} | 19 ++++++------------- services/register/register_service/config.py | 2 +- .../controllers/register_controller.py | 4 ++-- .../core/register_operations.py | 4 ++-- services/register/register_service/wsgi.py | 4 ++++ services/register/requirements.txt | 2 ++ 7 files changed, 21 insertions(+), 20 deletions(-) rename services/register/register_service/{__main__.py => app.py} (79%) create mode 100644 services/register/register_service/wsgi.py diff --git a/services/register/register_prepare.sh b/services/register/register_prepare.sh index 4a49b87..2ae8def 100644 --- a/services/register/register_prepare.sh +++ b/services/register/register_prepare.sh @@ -40,5 +40,7 @@ __EOF__ openssl x509 -req -in /usr/src/app/register_service/certs/register.csr -CA /usr/src/app/register_service/certs/registerCA.crt -CAkey /usr/src/app/register_service/certs/registerCA.key -CAcreateserial -out /usr/src/app/register_service/certs/register_cert.crt -days 365 -sha256 -cd /usr/src/app/ -python3 -m register_service \ No newline at end of file +gunicorn --certfile=/usr/src/app/register_service/certs/register_cert.crt \ + --keyfile=/usr/src/app/register_service/certs/register_key.key \ + --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/register_service wsgi:app \ No newline at end of file diff --git a/services/register/register_service/__main__.py b/services/register/register_service/app.py similarity index 79% rename from services/register/register_service/__main__.py rename to services/register/register_service/app.py index 12f6ffd..c92f175 100644 --- a/services/register/register_service/__main__.py +++ b/services/register/register_service/app.py @@ -1,13 +1,13 @@ import os from flask import Flask -from .controllers.register_controller import register_routes +from controllers.register_controller import register_routes from flask_jwt_extended import JWTManager from OpenSSL.crypto import PKey, TYPE_RSA, X509Req, dump_certificate_request, FILETYPE_PEM, dump_privatekey import requests import json import jwt -from .config import Config +from config import Config app = Flask(__name__) @@ -32,7 +32,7 @@ csr_request = dump_certificate_request(FILETYPE_PEM, req) private_key = dump_privatekey(FILETYPE_PEM, key) # Save superadmin private key -key_file = open("register_service/certs/superadmin.key", 'wb+') +key_file = open("certs/superadmin.key", 'wb+') key_file.write(bytes(private_key)) key_file.close() @@ -50,7 +50,7 @@ response = requests.request("POST", url, headers=headers, data=data, verify = Fa superadmin_cert = json.loads(response.text)['data']['certificate'] # Save the superadmin certificate -cert_file = open("register_service/certs/superadmin.crt", 'wb') +cert_file = open("certs/superadmin.crt", 'wb') cert_file.write(bytes(superadmin_cert, 'utf-8')) cert_file.close() @@ -62,7 +62,7 @@ headers = { response = requests.request("GET", url, headers=headers, verify = False) ca_root = json.loads(response.text)['data']['data']['ca'] -cert_file = open("register_service/certs/ca_root.crt", 'wb') +cert_file = open("certs/ca_root.crt", 'wb') cert_file.write(bytes(ca_root, 'utf-8')) cert_file.close() @@ -78,11 +78,4 @@ app.config['JWT_ALGORITHM'] = 'RS256' app.config['JWT_PRIVATE_KEY'] = key_data app.config['REGISTRE_SECRET_KEY'] = config["register"]["register_uuid"] -app.register_blueprint(register_routes) - -#---------------------------------------- -# launch -#---------------------------------------- - -if __name__ == "__main__": - app.run(debug=True, host = '0.0.0.0', port=8080, ssl_context= ("/usr/src/app/register_service/certs/register_cert.crt", "/usr/src/app/register_service/certs/register_key.key")) +app.register_blueprint(register_routes) \ No newline at end of file diff --git a/services/register/register_service/config.py b/services/register/register_service/config.py index d04bd1a..97ab831 100644 --- a/services/register/register_service/config.py +++ b/services/register/register_service/config.py @@ -5,7 +5,7 @@ import os class Config: def __init__(self): self.cached = 0 - self.file="./config.yaml" + self.file="../config.yaml" self.my_config = {} stamp = os.stat(self.file).st_mtime diff --git a/services/register/register_service/controllers/register_controller.py b/services/register/register_service/controllers/register_controller.py index 14877f8..fe51f4f 100644 --- a/services/register/register_service/controllers/register_controller.py +++ b/services/register/register_service/controllers/register_controller.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 from flask import current_app, Flask, jsonify, request, Blueprint -from ..core.register_operations import RegisterOperations -from ..config import Config +from register_service.core.register_operations import RegisterOperations +from register_service.config import Config from functools import wraps from datetime import datetime, timedelta diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index a8fee91..3771dfc 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -37,11 +37,11 @@ class RegisterOperations: exist_user = mycol.find_one({"username": username}) if exist_user is None: - return jsonify("Not exister user with this credentials"), 400 + return jsonify("Not existing user with this credentials"), 400 access_token = create_access_token(identity=(username + " " + exist_user["uuid"])) - cert_file = open("register_service/certs/ca_root.crt", 'rb') + cert_file = open("certs/ca_root.crt", 'rb') ca_root = cert_file.read() cert_file.close() diff --git a/services/register/register_service/wsgi.py b/services/register/register_service/wsgi.py new file mode 100644 index 0000000..6026b0f --- /dev/null +++ b/services/register/register_service/wsgi.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == "__main__": + app.run() diff --git a/services/register/requirements.txt b/services/register/requirements.txt index c5446a6..8da8ad4 100644 --- a/services/register/requirements.txt +++ b/services/register/requirements.txt @@ -2,6 +2,8 @@ python_dateutil >= 2.6.0 setuptools >= 21.0.0 Flask >= 2.0.3 pymongo == 4.0.1 +gunicorn==22.0.0 +packaging==24.0 flask_jwt_extended pyopenssl pyyaml -- GitLab From 83caa266e62a421f95fe9c66e3927de3c5479789 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 15 May 2024 17:32:09 +0200 Subject: [PATCH 173/310] mock server and changes on tests to select notification destination --- .../capif_events/core/notifications.py | 6 +- .../CAPIF Api Events/capif_events_api.robot | 2 +- tests/libraries/mock_server/mock_server.py | 55 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 tests/libraries/mock_server/mock_server.py diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py index 205d145..c6a6b87 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py @@ -69,9 +69,9 @@ class Notifications(): current_app.logger.error("An exception occurred ::" + str(e)) return False - def request_post(self, url, data): - headers = {'content-type': 'application/json'} - return requests.post(url, json={'text': str(data.to_str())}, headers=headers) + # def request_post(self, url, data): + # headers = {'content-type': 'application/json'} + # return requests.post(url, json={'text': str(data.to_str())}, headers=headers) async def send_request(self, url, data): async with aiohttp.ClientSession() as session: diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index 4bff370..9ee68ff 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -166,7 +166,7 @@ Prueba JMS # Subscribe to events ${events_list}= Create List SERVICE_API_INVOCATION_SUCCESS SERVICE_API_INVOCATION_FAILURE - ${request_body}= Create Events Subscription events=@{events_list} notificationDestination=http://127.0.0.1:9090/ + ${request_body}= Create Events Subscription events=@{events_list} notificationDestination=http://192.168.0.119:9090/testing ${resp}= Post Request Capif ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions ... json=${request_body} diff --git a/tests/libraries/mock_server/mock_server.py b/tests/libraries/mock_server/mock_server.py new file mode 100644 index 0000000..151f096 --- /dev/null +++ b/tests/libraries/mock_server/mock_server.py @@ -0,0 +1,55 @@ +from flask import Flask, request +import logging +from logging.handlers import RotatingFileHandler + + +app = Flask(__name__) + +# Lista para almacenar las solicitudes recibidas +requests_received = [] + +def verbose_formatter(): + return logging.Formatter( + '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "logger": "%(name)s", "function": "%(funcName)s", "line": %(lineno)d, "message": %(message)s}', + datefmt='%d/%m/%Y %H:%M:%S' + ) + + +def configure_logging(app): + del app.logger.handlers[:] + loggers = [app.logger, ] + handlers = [] + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + console_handler.setFormatter(verbose_formatter()) + file_handler = RotatingFileHandler(filename="mock_server.log", maxBytes=1024 * 1024 * 100, backupCount=20) + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(verbose_formatter()) + handlers.append(console_handler) + handlers.append(file_handler) + + + for l in loggers: + for handler in handlers: + l.addHandler(handler) + l.propagate = False + l.setLevel(logging.DEBUG) + +@app.route('/testing', methods=['POST', 'GET']) +def index(): + if request.method == 'POST': + app.logger.debug(request.json) + requests_received.append(request.json) + return 'Mock Server is running' + +@app.route('/requests_list', methods=['GET','DELETE']) +def requests_list(): + if request.method == 'DELETE': + requests_received.clear() + return requests_received + + +configure_logging(app) + +if __name__ == '__main__': + app.run(host='0.0.0.0',port=9090,debug=True) -- GitLab From b4c3c202443d1299827735681740764fe7f1336c Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Thu, 16 May 2024 15:12:02 +0300 Subject: [PATCH 174/310] Fix register issue and deploy Onboard_Provider service in production server --- .../{__main__.py => app.py} | 41 ++++++++++--------- .../api_provider_management/config.py | 2 +- .../controllers/default_controller.py | 12 +++--- ...i_provider_enrolment_details_controller.py | 9 ++-- .../core/provider_enrolment_details_api.py | 5 ++- .../api_provider_management/encoder.py | 3 +- .../api_provider_management/util.py | 2 +- .../api_provider_management/wsgi.py | 4 ++ .../prepare_provider.sh | 4 +- .../requirements.txt | 3 +- .../controllers/register_controller.py | 6 ++- .../core/register_operations.py | 4 +- services/register/register_service/db/db.py | 2 +- 13 files changed, 54 insertions(+), 43 deletions(-) rename services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/{__main__.py => app.py} (82%) create mode 100644 services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/wsgi.py diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/__main__.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py similarity index 82% rename from services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/__main__.py rename to services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py index f52d69c..4caa391 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/__main__.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py @@ -3,10 +3,10 @@ import connexion import logging -from api_provider_management import encoder +import encoder from flask_jwt_extended import JWTManager from logging.handlers import RotatingFileHandler -from .config import Config +from config import Config import os import sys from fluent import sender @@ -106,31 +106,32 @@ def verbose_formatter(): datefmt='%d/%m/%Y %H:%M:%S' ) -def main(): +# def main(): - with open("/usr/src/app/api_provider_management/pubkey.pem", "rb") as pub_file: - pub_data = pub_file.read() - app = connexion.App(__name__, specification_dir='./openapi/') - app.app.json_encoder = encoder.JSONEncoder - app.add_api('openapi.yaml', - arguments={'title': 'CAPIF_API_Provider_Management_API'}, - pythonic_params=True) +with open("/usr/src/app/api_provider_management/pubkey.pem", "rb") as pub_file: + pub_data = pub_file.read() - config = Config() - configure_logging(app.app) +app = connexion.App(__name__, specification_dir='./openapi/') +app.app.json_encoder = encoder.JSONEncoder +app.add_api('openapi.yaml', + arguments={'title': 'CAPIF_API_Provider_Management_API'}, + pythonic_params=True) - if eval(os.environ.get("MONITORING").lower().capitalize()): - configure_monitoring(app.app, config.get_config()) +config = Config() +configure_logging(app.app) - app.app.config['JWT_ALGORITHM'] = 'RS256' - app.app.config['JWT_PUBLIC_KEY'] = pub_data +if eval(os.environ.get("MONITORING").lower().capitalize()): + configure_monitoring(app.app, config.get_config()) - JWTManager(app.app) +app.app.config['JWT_ALGORITHM'] = 'RS256' +app.app.config['JWT_PUBLIC_KEY'] = pub_data +JWTManager(app.app) - app.run(port=8080, debug=True) +# app.run(port=8080, debug=True) -if __name__ == '__main__': - main() + +# if __name__ == '__main__': +# main() diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py index 377b14f..bed212a 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/config.py @@ -5,7 +5,7 @@ import os class Config: def __init__(self): self.cached = 0 - self.file="./config.yaml" + self.file="../config.yaml" self.my_config = {} stamp = os.stat(self.file).st_mtime if stamp != self.cached: diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py index f5bcf16..50745b8 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py @@ -3,17 +3,19 @@ import six import json from flask import Response, request, current_app + from ..core.provider_enrolment_details_api import ProviderManagementOperations -from ..encoder import JSONEncoder -from api_provider_management.models.api_provider_enrolment_details import APIProviderEnrolmentDetails # noqa: E501 -from api_provider_management.models.problem_details import ProblemDetails # noqa: E501 -from api_provider_management import util +from ..models.api_provider_enrolment_details import APIProviderEnrolmentDetails # noqa: E501 +from ..models.problem_details import ProblemDetails # noqa: E501 +from .. import util +from ..core.validate_user import ControlAccess + from cryptography import x509 from cryptography.hazmat.backends import default_backend from flask_jwt_extended import jwt_required, get_jwt_identity from cryptography import x509 from functools import wraps -from ..core.validate_user import ControlAccess + import sys diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/individual_api_provider_enrolment_details_controller.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/individual_api_provider_enrolment_details_controller.py index dde822b..50ccf0e 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/individual_api_provider_enrolment_details_controller.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/individual_api_provider_enrolment_details_controller.py @@ -6,10 +6,11 @@ import json from flask import Response, request, current_app from ..core.provider_enrolment_details_api import ProviderManagementOperations from ..encoder import JSONEncoder -from api_provider_management.models.api_provider_enrolment_details import APIProviderEnrolmentDetails # noqa: E501 -from api_provider_management.models.api_provider_enrolment_details_patch import APIProviderEnrolmentDetailsPatch # noqa: E501 -from api_provider_management.models.problem_details import ProblemDetails # noqa: E501 -from api_provider_management import util +from ..models.api_provider_enrolment_details import APIProviderEnrolmentDetails # noqa: E501 +from ..models.api_provider_enrolment_details_patch import APIProviderEnrolmentDetailsPatch # noqa: E501 +from ..models.problem_details import ProblemDetails # noqa: E501 +from .. import util + from cryptography.hazmat.backends import default_backend from cryptography import x509 diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py index 6ba9043..c58f629 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py @@ -45,7 +45,7 @@ class ProviderManagementOperations(Resource): api_provider_enrolment_details.api_prov_dom_id = secrets.token_hex(15) - current_app.logger.debug("Geretaing certs to api prov funcs") + current_app.logger.debug("Generating certs to api prov funcs") for api_provider_func in api_provider_enrolment_details.api_prov_funcs: api_provider_func.api_prov_func_id = api_provider_func.api_prov_func_role + str(secrets.token_hex(15)) @@ -64,7 +64,8 @@ class ProviderManagementOperations(Resource): current_app.logger.debug("Provider inserted in database") - res = make_response(object=api_provider_enrolment_details, status=201) + res = make_response(object=dict_to_camel_case(api_provider_enrolment_details.to_dict()), status=201) + res.headers['Location'] = "/api-provider-management/v1/registrations/" + str(api_provider_enrolment_details.api_prov_dom_id) return res diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/encoder.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/encoder.py index 01531f2..3f1c01c 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/encoder.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/encoder.py @@ -1,8 +1,7 @@ from connexion.apps.flask_app import FlaskJSONEncoder import six -from api_provider_management.models.base_model_ import Model - +from models.base_model_ import Model class JSONEncoder(FlaskJSONEncoder): include_nulls = False diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py index e769d28..5ff8054 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py @@ -2,7 +2,7 @@ import datetime import six import typing -from api_provider_management import typing_utils +import typing_utils def clean_empty(d): if isinstance(d, dict): diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/wsgi.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/wsgi.py new file mode 100644 index 0000000..6026b0f --- /dev/null +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/wsgi.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == "__main__": + app.run() diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh b/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh index 93bf420..5ec3096 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh +++ b/services/TS29222_CAPIF_API_Provider_Management_API/prepare_provider.sh @@ -13,7 +13,7 @@ curl -vv -k -retry 30 \ --request GET "$VAULT_ADDR/v1/secret/data/server_cert/pub" 2>/dev/null | jq -r '.data.data.pub_key' -j > /usr/src/app/api_provider_management/pubkey.pem +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/api_provider_management wsgi:app -cd /usr/src/app/ -python3 -m api_provider_management diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt index d45d1e5..c93b338 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt +++ b/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt @@ -18,4 +18,5 @@ fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 - +gunicorn==22.0.0 +packaging==24.0 \ No newline at end of file diff --git a/services/register/register_service/controllers/register_controller.py b/services/register/register_service/controllers/register_controller.py index fe51f4f..58620ba 100644 --- a/services/register/register_service/controllers/register_controller.py +++ b/services/register/register_service/controllers/register_controller.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 from flask import current_app, Flask, jsonify, request, Blueprint -from register_service.core.register_operations import RegisterOperations -from register_service.config import Config + +from core.register_operations import RegisterOperations +from config import Config + from functools import wraps from datetime import datetime, timedelta diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index 3771dfc..53f9806 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -1,8 +1,8 @@ from flask import Flask, jsonify, request, Response from flask_jwt_extended import create_access_token -from ..db.db import MongoDatabse +from db.db import MongoDatabse from datetime import datetime -from ..config import Config +from config import Config import base64 import uuid diff --git a/services/register/register_service/db/db.py b/services/register/register_service/db/db.py index 0b08933..3f73712 100644 --- a/services/register/register_service/db/db.py +++ b/services/register/register_service/db/db.py @@ -2,7 +2,7 @@ import atexit import time from pymongo import MongoClient from pymongo.errors import AutoReconnect -from ..config import Config +from config import Config from bson.codec_options import CodecOptions -- GitLab From a7d30c852a9c9476577de567cc19725201a6d5c0 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 20 May 2024 16:12:17 +0200 Subject: [PATCH 175/310] New test template for SERVICE_API_INVOCATION_SUCCESS and FAILURE --- .gitignore | 3 +- .../capif_events/core/notifications.py | 7 +- services/clean_capif_docker_services.sh | 2 + .../CAPIF Api Events/capif_events_api.robot | 191 +++++++++++++++++- .../api_logging_service/bodyRequests.py | 114 ++++------- tests/libraries/common/bodyRequests.py | 2 +- tests/libraries/common/types.json | 61 ++++++ tests/libraries/mock_server/mock_server.py | 1 + tests/libraries/mock_server/requirements.txt | 1 + tests/requirements.txt | 2 +- tests/resources/common.resource | 74 +++++++ tools/robot/basicRequirements.txt | 6 +- 12 files changed, 367 insertions(+), 97 deletions(-) create mode 100644 tests/libraries/mock_server/requirements.txt diff --git a/.gitignore b/.gitignore index 66e4e33..e19b88a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ __pycache__/ *.crt *.zip *.srl +*.log services/nginx/certs/sign_req_body.json services/easy_rsa/certs/pki services/easy_rsa/certs/*EasyRSA* @@ -35,4 +36,4 @@ docs/testing_with_postman/package-lock.json results helm/capif/*.lock -helm/capif/charts \ No newline at end of file +helm/capif/charts diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py index c6a6b87..e81386b 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py @@ -59,11 +59,11 @@ class Notifications(): # current_app.logger.debug(json.dumps(event_detail,cls=JSONEncoder)) event_detail={redis_event.get('key'):redis_event.get('information')} - data = EventNotification(sub["subscription_id"], events=redis_event.get('event'), event_detail=[event_detail]) + data = EventNotification(sub["subscription_id"], events=redis_event.get('event'), event_detail=event_detail) current_app.logger.debug(json.dumps(data,cls=JSONEncoder)) # self.request_post(url, data) - asyncio.run(self.send(url, json.dumps(data,cls=JSONEncoder))) + asyncio.run(self.send(url, json.loads(json.dumps(data,cls=JSONEncoder)))) except Exception as e: current_app.logger.error("An exception occurred ::" + str(e)) @@ -76,7 +76,8 @@ class Notifications(): async def send_request(self, url, data): async with aiohttp.ClientSession() as session: timeout = aiohttp.ClientTimeout(total=10) # Establecer timeout a 10 segundos - async with session.post(url, json=data, timeout=timeout) as response: + headers = {'content-type': 'application/json'} + async with session.post(url, json=data, timeout=timeout, headers=headers) as response: return await response.text() async def send(self, url, data): diff --git a/services/clean_capif_docker_services.sh b/services/clean_capif_docker_services.sh index fb89497..1bc6d6d 100755 --- a/services/clean_capif_docker_services.sh +++ b/services/clean_capif_docker_services.sh @@ -74,4 +74,6 @@ done docker network rm capif-network +docker volume prune --all --force + echo "Clean complete." diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index 9ee68ff..c4b6085 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -5,7 +5,6 @@ Library XML Resource /opt/robot-tests/tests/resources/common/basicRequests.robot Resource ../../resources/common.resource - Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment @@ -139,13 +138,12 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriptionId ... detail=User not authorized ... cause=You are not the owner of this resource - Prueba JMS [Tags] jms-1 - # Log "Prueba 1" - # Wait For Request - # Create a log entry - # [Tags] capif_api_logging_service-1 + + # Start Mock server + Check Mock Server + Clean Mock Server # Register APF ${register_user_info}= Provider Default Registration @@ -165,8 +163,10 @@ Prueba JMS ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} # Subscribe to events - ${events_list}= Create List SERVICE_API_INVOCATION_SUCCESS SERVICE_API_INVOCATION_FAILURE - ${request_body}= Create Events Subscription events=@{events_list} notificationDestination=http://192.168.0.119:9090/testing + ${events_list}= Create List SERVICE_API_INVOCATION_SUCCESS SERVICE_API_INVOCATION_FAILURE + ${request_body}= Create Events Subscription + ... events=@{events_list} + ... notificationDestination=http://192.168.0.14:9090/testing ${resp}= Post Request Capif ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions ... json=${request_body} @@ -178,13 +178,122 @@ Prueba JMS Check Response Variable Type And Values ${resp} 201 EventSubscription ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + ${results}= Create List 200 400 + + # Create Log Entry + ${request_body}= Create Log Entry + ... ${register_user_info['aef_id']} + ... ${register_user_info_invoker['api_invoker_id']} + ... ${api_ids} + ... ${api_names} + ... results=${results} + ${resp}= Post Request Capif + ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${AEF_PROVIDER_USERNAME} + + # Check Results + Check Response Variable Type And Values ${resp} 201 InvocationLog + ${resource_url}= Check Location Header ${resp} ${LOCATION_LOGGING_RESOURCE_REGEX} + + Sleep 3s + + # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. + ${resp}= Get Mock Server Messages + ${notification_events_on_mock_server}= Set Variable ${resp.json()} + # Check if message follow EventNotification definition. + Check Variable ${notification_events_on_mock_server} EventNotification + # Check if mock server receive 2 events. + Length Should Be ${notification_events_on_mock_server} 2 + + # Now we create the expected events received at notification server according to logs sent to loggin service in order to check if all are present. + ${invocation_log_base}= Copy Dictionary ${request_body} deepcopy=True + + # Store log array because each log will be notified in one Event Notification + ${invocation_log_logs}= Set Variable ${invocation_log_base['logs']} + # Remove logs array from invocationLog data + Remove From Dictionary ${invocation_log_base} logs + Length Should Be ${invocation_log_logs} 2 + + # Create 2 invocationLogs from initial invocationLog present on request + ${invocation_logs_1}= Copy Dictionary ${invocation_log_base} deepcopy=True + ${invocation_logs_2}= Copy Dictionary ${invocation_log_base} deepcopy=True + # Create a log array with only one component + ${log_1}= Create List ${invocation_log_logs[0]} + ${log_2}= Create List ${invocation_log_logs[1]} + # Setup logs array with previously created list + Set To Dictionary ${invocation_logs_1} logs=${log_1} + Set To Dictionary ${invocation_logs_2} logs=${log_2} + # Create event details for each log + ${event_details_1}= Create dictionary invocationLogs=${invocation_logs_1} + ${event_details_2}= Create dictionary invocationLogs=${invocation_logs_2} + # Create Event with Event Details from invocationLog + ${event_1}= Create Dictionary + ... subscriptionId=${subscription_id} + ... events=SERVICE_API_INVOCATION_SUCCESS + ... eventDetail=${event_details_1} + ${event_2}= Create Dictionary + ... subscriptionId=${subscription_id} + ... events=SERVICE_API_INVOCATION_FAILURE + ... eventDetail=${event_details_2} + # Check if created events follow EventNotification format defined by 3gpp + Check Variable ${event_1} EventNotification + Check Variable ${event_2} EventNotification + + List Should Contain Value ${notification_events_on_mock_server} ${event_1} + List Should Contain Value ${notification_events_on_mock_server} ${event_2} + +Prueba JMS + [Tags] jms-2 + + # Start Mock server + Check Mock Server + Clean Mock Server + + # Register APF + ${register_user_info}= Provider Default Registration + + # Publish one api + Publish Service Api ${register_user_info} + + # Register INVOKER + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + + ${discover_response}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + + # Subscribe to events + ${events_list}= Create List SERVICE_API_INVOCATION_SUCCESS SERVICE_API_INVOCATION_FAILURE + ${request_body}= Create Events Subscription + ... events=@{events_list} + ... notificationDestination=http://192.168.0.14:9090/testing + ${resp}= Post Request Capif + ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + # Check Results + Check Response Variable Type And Values ${resp} 201 EventSubscription + ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + + ${results}= Create List 200 400 + # Create Log Entry ${request_body}= Create Log Entry ... ${register_user_info['aef_id']} ... ${register_user_info_invoker['api_invoker_id']} ... ${api_ids} ... ${api_names} - ... 200 + ... results=${results} ${resp}= Post Request Capif ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs ... json=${request_body} @@ -196,3 +305,67 @@ Prueba JMS Check Response Variable Type And Values ${resp} 201 InvocationLog ${resource_url}= Check Location Header ${resp} ${LOCATION_LOGGING_RESOURCE_REGEX} + Sleep 3s + + # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. + ${resp}= Get Mock Server Messages + ${notification_events_on_mock_server}= Set Variable ${resp.json()} + # Check if message follow EventNotification definition. + Check Variable ${notification_events_on_mock_server} EventNotification + + # Create check Events to ensure all notifications were received + ${check_events} ${check_events_length}= Create Events From invocationLogs + ... ${subscription_id} + ... ${request_body} + + # Number of events received must be equal than events expected + Length Should Be ${notification_events_on_mock_server} ${check_events_length} + # Check if events received are the same than expected + FOR ${event} IN @{check_events} + Log ${event} + List Should Contain Value ${notification_events_on_mock_server} ${event} + END + + +*** Keywords *** +Create Events From invocationLogs + [Arguments] ${subscription_id} ${invocation_log} + + ${events}= Create List + + # Now we create the expected events received at notification server according to logs sent to loggin service in order to check if all are present. + ${invocation_log_base}= Copy Dictionary ${invocation_log} deepcopy=True + # Store log array because each log will be notified in one Event Notification + ${invocation_log_logs}= Copy List ${invocation_log_base['logs']} + # Remove logs array from invocationLog data + Remove From Dictionary ${invocation_log_base} logs + + FOR ${log} IN @{invocation_log_logs} + Log Dictionary ${log} + ${invocation_logs}= Copy Dictionary ${invocation_log_base} deepcopy=True + + # Get Event Enum for this result + ${event_enum}= Set Variable + IF ${log['result']} >= 200 and ${log['result']} < 300 + ${event_enum}= Set Variable SERVICE_API_INVOCATION_SUCCESS + ELSE + ${event_enum}= Set Variable SERVICE_API_INVOCATION_FAILURE + END + # Create a log array with only one component + ${log_list}= Create List ${log} + # Setup logs array with previously created list + Set To Dictionary ${invocation_logs} logs=${log_list} + # Create event details for each log + ${event_details}= Create dictionary invocationLogs=${invocation_logs} + # Create Event with Event Details from invocationLog and also is appended to events array + ${event}= Create Dictionary + ... subscriptionId=${subscription_id} + ... events=${event_enum} + ... eventDetail=${event_details} + Check Variable ${event} EventNotification + Append To List ${events} ${event} + END + + Log List ${events} + ${events_length}= Get Length ${events} + RETURN ${events} ${events_length} diff --git a/tests/libraries/api_logging_service/bodyRequests.py b/tests/libraries/api_logging_service/bodyRequests.py index e0e70da..9ab8210 100644 --- a/tests/libraries/api_logging_service/bodyRequests.py +++ b/tests/libraries/api_logging_service/bodyRequests.py @@ -1,81 +1,13 @@ -def create_log_entry(aefId, apiInvokerId, apiId, apiName, result='200'): +def create_log_entry(aefId, apiInvokerId, apiId, apiName, results=['200']): data = { "aefId": aefId, "apiInvokerId": apiInvokerId, - "logs": [ - { - "apiId": apiId[0], - "apiName": apiName[0], - "apiVersion": "v1", - "resourceName": "string", - "uri": "http://resource/endpoint", - "protocol": "HTTP_1_1", - "operation": "GET", - "result": result, - "invocationTime": "2023-03-30T10:30:21.408Z", - "invocationLatency": 0, - "inputParameters": "string", - "outputParameters": "string", - "srcInterface": { - "ipv4Addr": "192.168.1.1", - "fqdn": "string", - "port": 65535, - "apiPrefix": "string", - "securityMethods": [ - "PSK", - "string" - ] - }, - "destInterface": { - "ipv4Addr": "192.168.1.23", - "fqdn": "string", - "port": 65535, - "apiPrefix": "string", - "securityMethods": [ - "PSK", - "string" - ] - }, - "fwdInterface": "string" - }, - { - "apiId": apiId[0], - "apiName": apiName[0], - "apiVersion": "v2", - "resourceName": "string", - "uri": "http://resource/endpoint", - "protocol": "HTTP_1_1", - "operation": "GET", - "result": result, - "invocationTime": "2023-03-30T10:30:21.408Z", - "invocationLatency": 0, - "inputParameters": "string", - "outputParameters": "string", - "srcInterface": { - "ipv4Addr": "192.168.1.1", - "fqdn": "string", - "port": 65535, - "apiPrefix": "string", - "securityMethods": [ - "PSK", - "string" - ] - }, - "destInterface": { - "ipv4Addr": "192.168.1.23", - "fqdn": "string", - "port": 65535, - "apiPrefix": "string", - "securityMethods": [ - "PSK", - "string" - ] - }, - "fwdInterface": "string" - } - ], + "logs": [], "supportedFeatures": "ffff" } + if len(results) > 0: + for result in results: + data['logs'].append(create_log(apiId,apiName,result)) return data def create_log_entry_bad_service(aefId, apiInvokerId, result=500): @@ -92,28 +24,24 @@ def create_log_entry_bad_service(aefId, apiInvokerId, result=500): "protocol": "HTTP_1_1", "operation": "GET", "result": result, - "invocationTime": "2023-03-30T10:30:21.408Z", + "invocationTime": "2023-03-30T10:30:21.408000+00:00", "invocationLatency": 0, "inputParameters": "string", "outputParameters": "string", "srcInterface": { "ipv4Addr": "192.168.1.1", - "fqdn": "string", "port": 65535, - "apiPrefix": "string", "securityMethods": [ "PSK", - "string" + "PKI" ] }, "destInterface": { "ipv4Addr": "192.168.1.23", - "fqdn": "string", "port": 65535, - "apiPrefix": "string", "securityMethods": [ "PSK", - "string" + "PKI" ] }, "fwdInterface": "string" @@ -131,3 +59,29 @@ def get_api_ids_and_names_from_discover_response(discover_response): api_ids.append(service_api_description['apiId']) api_names.append(service_api_description['apiName']) return api_ids, api_names + + +def create_log(apiId, apiName, result): + log= { + "apiId": apiId[0], + "apiName": apiName[0], + "apiVersion": "v1", + "resourceName": "string", + "uri": "http://resource/endpoint", + "protocol": "HTTP_1_1", + "operation": "GET", + "result": result, + "invocationTime": "2023-03-30T10:30:21.408000+00:00", + "invocationLatency": 0, + "inputParameters": "string", + "outputParameters": "string", + "srcInterface": { + "ipv4Addr": "192.168.1.1", + "port": 65535, + "securityMethods": [ + "PSK", + "PKI" + ] + } + } + return log \ No newline at end of file diff --git a/tests/libraries/common/bodyRequests.py b/tests/libraries/common/bodyRequests.py index 3f1e482..1d4ec14 100644 --- a/tests/libraries/common/bodyRequests.py +++ b/tests/libraries/common/bodyRequests.py @@ -15,7 +15,7 @@ def check_variable(input, data_type): if isinstance(input, list): for one in input: check_variable(one, data_type) - return True + return True if data_type == "string": if isinstance(input, str): return True diff --git a/tests/libraries/common/types.json b/tests/libraries/common/types.json index 4b68359..220dd0f 100644 --- a/tests/libraries/common/types.json +++ b/tests/libraries/common/types.json @@ -259,6 +259,67 @@ "aefIds": "string" } }, + "EventNotification": { + "mandatory_attributes": { + "subscriptionId": "string", + "events": "CAPIFEvent" + }, + "optional_attributes": { + "eventDetail": "CAPIFEventDetail" + } + }, + "CAPIFEventDetail": { + "mandatory_attributes": {}, + "optional_attributes": { + "serviceAPIDescriptions": "ServiceAPIDescription", + "apiIds": "string", + "apiInvokerIds": "string", + "accCtrlPolList": "AccessControlPolicyListExt", + "invocationLogs": "InvocationLog", + "apiTopoHide": "TopologyHiding" + } + }, + "AccessControlPolicyListExt":{ + "mandatory_attributes": { + "apiId": "string" + }, + "optional_attributes": { + "apiInvokerPolicies": "ApiInvokerPolicy" + } + }, + "TopologyHiding": { + "mandatory_attributes":{ + "apiId": "string", + "routingRules": "RoutingRule" + }, + "optional_attributes": {} + }, + "RoutingRule":{ + "mandatory_attributes": { + "aefProfile": "AefProfile" + }, + "optional_attributes": { + "ipv4AddrRanges": "Ipv4AddressRange", + "ipv6AddrRanges": "Ipv6AddressRange" + } + }, + "Ipv4AddressRange":{ + "mandatory_attributes": {}, + "optional_attributes": { + "start": "Ipv4Addr", + "stop": "Ipv4Addr" + } + }, + "Ipv4Addr": { + "regex": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$" + }, + "Ipv6AddressRange":{ + "mandatory_attributes":{ + "start": "string", + "end": "string" + }, + "optional_attributes":{} + }, "ReportingInformation": { "mandatory_attributes": {}, "optional_attributes": { diff --git a/tests/libraries/mock_server/mock_server.py b/tests/libraries/mock_server/mock_server.py index 151f096..18d3f92 100644 --- a/tests/libraries/mock_server/mock_server.py +++ b/tests/libraries/mock_server/mock_server.py @@ -39,6 +39,7 @@ def configure_logging(app): def index(): if request.method == 'POST': app.logger.debug(request.json) + app.logger.debug(request.headers) requests_received.append(request.json) return 'Mock Server is running' diff --git a/tests/libraries/mock_server/requirements.txt b/tests/libraries/mock_server/requirements.txt new file mode 100644 index 0000000..abf2862 --- /dev/null +++ b/tests/libraries/mock_server/requirements.txt @@ -0,0 +1 @@ +flask==3.0.3 \ No newline at end of file diff --git a/tests/requirements.txt b/tests/requirements.txt index b983217..3119b5c 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,2 +1,2 @@ # Requirements file for tests. -robotframework-archivelibrary == 0.4.2 + diff --git a/tests/resources/common.resource b/tests/resources/common.resource index c46131b..4f8136e 100644 --- a/tests/resources/common.resource +++ b/tests/resources/common.resource @@ -1,5 +1,7 @@ *** Settings *** Library /opt/robot-tests/tests/libraries/helpers.py +Library Process +Library Collections Variables /opt/robot-tests/tests/libraries/environment.py Resource /opt/robot-tests/tests/resources/common/basicRequests.robot @@ -30,6 +32,9 @@ ${CAPIF_CALLBACK_PORT} 8086 ${REGISTER_ADMIN_USER} admin ${REGISTER_ADMIN_PASSWORD} password123 +${MOCK_SERVER_URL} http://192.168.0.14:9090 + + ${DISCOVER_URL} /service-apis/v1/allServiceAPIs?api-invoker-id= @@ -85,3 +90,72 @@ Remove Keys From Object Test ${TEST NAME} Currently Not Supported Log Test "${TEST NAME}" Currently not supported WARN Skip Test "${TEST NAME}" Currently not supported + +Start Mock server + Log Starting mock Server for Robot Framework. + + # Run Process pip3 install -r /opt/robot-tests/tests/libraries/mock_server/requirements.txt + ${server_process}= Start Process python3 /opt/robot-tests/tests/libraries/mock_server/mock_server.py + Log PID: ${server_process.pid} + + Create Session mockserver ${MOCK_SERVER_URL} + + ${endpoint}= Set Variable /testing + + ${json}= Create Dictionary events="SERVICE_API_INVOCATION_SUCCESS" subscriptionId="255545008cd3937f5554a3651bbfb9" + + ${resp}= POST On Session + ... mockserver + ... ${endpoint} + ... json=${json} + ... expected_status=any + ... verify=False + + ${result}= Terminate Process ${server_process} + +Check Mock Server + Log Checking mock Server for Robot Framework. + + Create Session mockserver ${MOCK_SERVER_URL} + + ${endpoint}= Set Variable /requests_list + + ${resp}= GET On Session + ... mockserver + ... ${endpoint} + ... expected_status=any + ... verify=False + + Status Should Be 200 ${resp} + +Clean Mock Server + Log Checking mock Server for Robot Framework. + + Create Session mockserver ${MOCK_SERVER_URL} + + ${endpoint}= Set Variable /requests_list + + ${resp}= DELETE On Session + ... mockserver + ... ${endpoint} + ... expected_status=any + ... verify=False + + Status Should Be 200 ${resp} + + +Get Mock Server Messages + Log Checking mock Server for Robot Framework. + + Create Session mockserver ${MOCK_SERVER_URL} + + ${endpoint}= Set Variable /requests_list + + ${resp}= GET On Session + ... mockserver + ... ${endpoint} + ... expected_status=any + ... verify=False + + Status Should Be 200 ${resp} + RETURN ${resp} \ No newline at end of file diff --git a/tools/robot/basicRequirements.txt b/tools/robot/basicRequirements.txt index 99eafd7..282a034 100644 --- a/tools/robot/basicRequirements.txt +++ b/tools/robot/basicRequirements.txt @@ -25,6 +25,7 @@ docutils==0.19 exceptiongroup==1.0.0rc9 filelock==3.8.0 flake8==3.9.2 +flask==3.0.3 h11==0.14.0 idna==3.4 iniconfig==1.1.1 @@ -69,6 +70,7 @@ rellu==0.7 requests==2.28.1 rfc3987==1.3.8 robotframework==7.0 +robotframework-archivelibrary == 0.4.2 robotframework-browser==18.3.0 robotframework-httpctrl==0.3.1 robotframework-lint==1.1 @@ -95,8 +97,8 @@ trio-websocket==0.9.2 typing-extensions==4.11.0 urllib3==1.26.12 virtualenv==20.16.5 -watchdog==0.9.0 +watchdog==4.0.0 webdrivermanager==0.10.0 wrapt==1.15.0 wsproto==1.2.0 -xlrd==2.0.1 \ No newline at end of file +xlrd==2.0.1 -- GitLab From fe548e6ed474b6d148ff3a65c82ddd8accb7aaf9 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 20 May 2024 16:34:26 +0200 Subject: [PATCH 176/310] Create script to run robot mock server --- .../CAPIF Api Events/capif_events_api.robot | 44 ------------------- tests/libraries/mock_server/mock_server.py | 4 +- .../libraries/mock_server/run_mock_server.sh | 40 +++++++++++++++++ tests/resources/common/basicRequests.robot | 43 ++++++++++++++++++ 4 files changed, 85 insertions(+), 46 deletions(-) create mode 100755 tests/libraries/mock_server/run_mock_server.sh diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index c4b6085..c1ea4c7 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -325,47 +325,3 @@ Prueba JMS Log ${event} List Should Contain Value ${notification_events_on_mock_server} ${event} END - - -*** Keywords *** -Create Events From invocationLogs - [Arguments] ${subscription_id} ${invocation_log} - - ${events}= Create List - - # Now we create the expected events received at notification server according to logs sent to loggin service in order to check if all are present. - ${invocation_log_base}= Copy Dictionary ${invocation_log} deepcopy=True - # Store log array because each log will be notified in one Event Notification - ${invocation_log_logs}= Copy List ${invocation_log_base['logs']} - # Remove logs array from invocationLog data - Remove From Dictionary ${invocation_log_base} logs - - FOR ${log} IN @{invocation_log_logs} - Log Dictionary ${log} - ${invocation_logs}= Copy Dictionary ${invocation_log_base} deepcopy=True - - # Get Event Enum for this result - ${event_enum}= Set Variable - IF ${log['result']} >= 200 and ${log['result']} < 300 - ${event_enum}= Set Variable SERVICE_API_INVOCATION_SUCCESS - ELSE - ${event_enum}= Set Variable SERVICE_API_INVOCATION_FAILURE - END - # Create a log array with only one component - ${log_list}= Create List ${log} - # Setup logs array with previously created list - Set To Dictionary ${invocation_logs} logs=${log_list} - # Create event details for each log - ${event_details}= Create dictionary invocationLogs=${invocation_logs} - # Create Event with Event Details from invocationLog and also is appended to events array - ${event}= Create Dictionary - ... subscriptionId=${subscription_id} - ... events=${event_enum} - ... eventDetail=${event_details} - Check Variable ${event} EventNotification - Append To List ${events} ${event} - END - - Log List ${events} - ${events_length}= Get Length ${events} - RETURN ${events} ${events_length} diff --git a/tests/libraries/mock_server/mock_server.py b/tests/libraries/mock_server/mock_server.py index 18d3f92..e45fae0 100644 --- a/tests/libraries/mock_server/mock_server.py +++ b/tests/libraries/mock_server/mock_server.py @@ -1,7 +1,7 @@ from flask import Flask, request import logging from logging.handlers import RotatingFileHandler - +import os app = Flask(__name__) @@ -53,4 +53,4 @@ def requests_list(): configure_logging(app) if __name__ == '__main__': - app.run(host='0.0.0.0',port=9090,debug=True) + app.run(host=os.environ.get("IP",'0.0.0.0'),port=os.environ.get("PORT",9090),debug=True) diff --git a/tests/libraries/mock_server/run_mock_server.sh b/tests/libraries/mock_server/run_mock_server.sh new file mode 100755 index 0000000..10c4ba2 --- /dev/null +++ b/tests/libraries/mock_server/run_mock_server.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +help() { + echo "Usage: $1 " + echo " -i : Setup different host ip for mock server (default 0.0.0.0)" + echo " -p : Setup different port for mock server (default 9090)" + echo " -h : show this help" + exit 1 +} + +IP=0.0.0.0 +PORT=9090 + +# Read params +while getopts ":i:p:h" opt; do + case $opt in + i) + IP="$OPTARG" + ;; + p) + PORT=$OPTARG + ;; + h) + help + ;; + \?) + echo "Not valid option: -$OPTARG" >&2 + help + ;; + :) + echo "The -$OPTARG option requires an argument." >&2 + help + ;; + esac +done + +echo Robot Framework Mock Server will listen on $IP:$PORT +pip install -r requirements.txt + +IP=$IP PORT=$PORT python mock_server.py diff --git a/tests/resources/common/basicRequests.robot b/tests/resources/common/basicRequests.robot index ea3a96f..5872a57 100644 --- a/tests/resources/common/basicRequests.robot +++ b/tests/resources/common/basicRequests.robot @@ -744,3 +744,46 @@ Create Security Context Between invoker and provider ... username=${register_user_info_invoker['management_cert']} Check Response Variable Type And Values ${resp} 201 ServiceSecurity + + +Create Events From invocationLogs + [Arguments] ${subscription_id} ${invocation_log} + + ${events}= Create List + + # Now we create the expected events received at notification server according to logs sent to loggin service in order to check if all are present. + ${invocation_log_base}= Copy Dictionary ${invocation_log} deepcopy=True + # Store log array because each log will be notified in one Event Notification + ${invocation_log_logs}= Copy List ${invocation_log_base['logs']} + # Remove logs array from invocationLog data + Remove From Dictionary ${invocation_log_base} logs + + FOR ${log} IN @{invocation_log_logs} + Log Dictionary ${log} + ${invocation_logs}= Copy Dictionary ${invocation_log_base} deepcopy=True + + # Get Event Enum for this result + ${event_enum}= Set Variable + IF ${log['result']} >= 200 and ${log['result']} < 300 + ${event_enum}= Set Variable SERVICE_API_INVOCATION_SUCCESS + ELSE + ${event_enum}= Set Variable SERVICE_API_INVOCATION_FAILURE + END + # Create a log array with only one component + ${log_list}= Create List ${log} + # Setup logs array with previously created list + Set To Dictionary ${invocation_logs} logs=${log_list} + # Create event details for each log + ${event_details}= Create dictionary invocationLogs=${invocation_logs} + # Create Event with Event Details from invocationLog and also is appended to events array + ${event}= Create Dictionary + ... subscriptionId=${subscription_id} + ... events=${event_enum} + ... eventDetail=${event_details} + Check Variable ${event} EventNotification + Append To List ${events} ${event} + END + + Log List ${events} + ${events_length}= Get Length ${events} + RETURN ${events} ${events_length} -- GitLab From c75dc334fa086dd4f3d2a6aa697cd62d56761c93 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 20 May 2024 17:21:50 +0200 Subject: [PATCH 177/310] Fix previous tests --- .../capif_events/core/notifications.py | 22 ++++--------------- .../api_logging_service/bodyRequests.py | 15 ++++++++----- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py index e81386b..e7b7dd5 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py @@ -19,21 +19,12 @@ class Notifications(): def send_notifications(self, event): current_app.logger.info("Received event, sending notifications") subscriptions = self.events_ops.get_event_subscriptions(event) - # message, *ids = event.split(":") try: for sub in subscriptions: url = sub["notification_destination"] data = EventNotification(sub["subscription_id"], events=event) - # details = CAPIFEventDetail() - # if message == "ACCESS_CONTROL_POLICY_UPDATE": - # current_app.logger.info("event: ACCESS_CONTROL_POLICY_UPDATE") - # acls = self.events_ops.get_acls(ids[0]) - # details.acc_ctrl_pol_list = AccessControlPolicyListExt(api_id=acls['service_id'], api_invoker_policies=acls['apiInvokerPolicies']) - - # data.event_detail=details self.request_post(url, data) - #current_app.logger.info("notification sended") except Exception as e: current_app.logger.error("An exception occurred ::" + str(e)) @@ -46,7 +37,6 @@ class Notifications(): current_app.logger.info("Received event " + redis_event.get('event') + ", sending notifications") subscriptions = self.events_ops.get_event_subscriptions(redis_event.get('event')) - # message, *ids = event.split(":") current_app.logger.info(subscriptions) for sub in subscriptions: @@ -54,24 +44,20 @@ class Notifications(): current_app.logger.debug(url) event_detail=None if redis_event.get('key', None) != None and redis_event.get('information', None) != None: - # current_app.logger.debug(json.dumps(redis_event.get('information'),cls=JSONEncoder)) - # event_detail=CAPIFEventDetail().from_dict({redis_event.get('key'):redis_event.get('information')}) - # current_app.logger.debug(json.dumps(event_detail,cls=JSONEncoder)) event_detail={redis_event.get('key'):redis_event.get('information')} data = EventNotification(sub["subscription_id"], events=redis_event.get('event'), event_detail=event_detail) current_app.logger.debug(json.dumps(data,cls=JSONEncoder)) - - # self.request_post(url, data) + asyncio.run(self.send(url, json.loads(json.dumps(data,cls=JSONEncoder)))) except Exception as e: current_app.logger.error("An exception occurred ::" + str(e)) return False - # def request_post(self, url, data): - # headers = {'content-type': 'application/json'} - # return requests.post(url, json={'text': str(data.to_str())}, headers=headers) + def request_post(self, url, data): + headers = {'content-type': 'application/json'} + return requests.post(url, json={'text': str(data.to_str())}, headers=headers) async def send_request(self, url, data): async with aiohttp.ClientSession() as session: diff --git a/tests/libraries/api_logging_service/bodyRequests.py b/tests/libraries/api_logging_service/bodyRequests.py index 9ab8210..a4d2a18 100644 --- a/tests/libraries/api_logging_service/bodyRequests.py +++ b/tests/libraries/api_logging_service/bodyRequests.py @@ -1,4 +1,4 @@ -def create_log_entry(aefId, apiInvokerId, apiId, apiName, results=['200']): +def create_log_entry(aefId, apiInvokerId, apiId, apiName, results=['200','500'],api_versions=['v1','v2']): data = { "aefId": aefId, "apiInvokerId": apiInvokerId, @@ -6,11 +6,16 @@ def create_log_entry(aefId, apiInvokerId, apiId, apiName, results=['200']): "supportedFeatures": "ffff" } if len(results) > 0: + count=0 for result in results: - data['logs'].append(create_log(apiId,apiName,result)) + data['logs'].append(create_log(apiId,apiName,result,api_versions[count])) + count=count+1 + if count == len(api_versions): + count=0 + return data -def create_log_entry_bad_service(aefId, apiInvokerId, result=500): +def create_log_entry_bad_service(aefId, apiInvokerId, result='500'): data = { "aefId": aefId, "apiInvokerId": apiInvokerId, @@ -61,11 +66,11 @@ def get_api_ids_and_names_from_discover_response(discover_response): return api_ids, api_names -def create_log(apiId, apiName, result): +def create_log(apiId, apiName, result, api_version='v1'): log= { "apiId": apiId[0], "apiName": apiName[0], - "apiVersion": "v1", + "apiVersion": api_version, "resourceName": "string", "uri": "http://resource/endpoint", "protocol": "HTTP_1_1", -- GitLab From 084ceee2ccb958c3616bf137df537f9e5ccd77a5 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 20 May 2024 17:37:15 +0200 Subject: [PATCH 178/310] Setup robot script mockserver --- .../api_invocation_logs/core/redis_event.py | 2 +- services/run_capif_tests.sh | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py index 3b80a45..d42a1ef 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py @@ -5,7 +5,7 @@ import json publisher_ops = Publisher() class RedisEvent(): - def __init__(self, event, information, event_detail_key="invocationLogs") -> None: + def __init__(self, event, information, event_detail_key) -> None: self.redis_event={ "event": event, "key": event_detail_key, diff --git a/services/run_capif_tests.sh b/services/run_capif_tests.sh index b73893a..f294bac 100755 --- a/services/run_capif_tests.sh +++ b/services/run_capif_tests.sh @@ -12,6 +12,7 @@ ROBOT_DOCKER_FILE_FOLDER=$REPOSITORY_BASE_FOLDER/tools/robot CAPIF_HOSTNAME=capifcore CAPIF_HTTP_PORT=8080 CAPIF_HTTPS_PORT=443 +MOCK_SERVER_URL=http://mockserver:9090 echo "HOSTNAME = $CAPIF_HOSTNAME" echo "CAPIF_HTTP_PORT = $CAPIF_HTTP_PORT" @@ -45,8 +46,10 @@ docker run -ti --rm --network="host" \ --add-host host.docker.internal:host-gateway \ --add-host vault:host-gateway \ --add-host register:host-gateway \ + --add-host mockserver:host-gateway \ -v $TEST_FOLDER:/opt/robot-tests/tests \ -v $RESULT_FOLDER:/opt/robot-tests/results ${DOCKER_ROBOT_IMAGE}:${DOCKER_ROBOT_IMAGE_VERSION} \ --variable CAPIF_HOSTNAME:$CAPIF_HOSTNAME \ --variable CAPIF_HTTP_PORT:$CAPIF_HTTP_PORT \ - --variable CAPIF_HTTPS_PORT:$CAPIF_HTTPS_PORT $@ + --variable CAPIF_HTTPS_PORT:$CAPIF_HTTPS_PORT \ + --variable MOCK_SERVER_URL:$MOCK_SERVER_URL $@ -- GitLab From 35846b0133d1ffe44a1f7bda166fbd36fbe3b54c Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Wed, 22 May 2024 11:34:57 +0300 Subject: [PATCH 179/310] Change ProblemDetails initialization to properly be serialized --- .../core/provider_enrolment_details_api.py | 12 +++++---- .../api_provider_management/core/responses.py | 26 +++++++++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py index c58f629..67229fa 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py @@ -124,13 +124,12 @@ class ProviderManagementOperations(Resource): for api_func in api_prov_funcs: if func.api_prov_func_id == api_func["api_prov_func_id"]: if func.api_prov_func_role != api_func["api_prov_func_role"]: - return bad_request_error(detail="Bad Role in provider", cause="Different role in update reqeuest", invalid_params=[{"param":"api_prov_func_role","reason":"differente role with same id"}]) + return bad_request_error(detail="Bad Role in provider", cause="Different role in update reqeuest", invalid_params=[{"param":"api_prov_func_role","reason":"different role with same id"}]) if func.reg_info.api_prov_pub_key != api_func["reg_info"]["api_prov_pub_key"]: certificate = sign_certificate(func.reg_info.api_prov_pub_key, api_func["api_prov_func_id"]) func.reg_info.api_prov_cert = certificate self.auth_manager.update_auth_provider(certificate, func.api_prov_func_id, api_prov_dom_id, func.api_prov_func_role) - api_provider_enrolment_details = api_provider_enrolment_details.to_dict() api_provider_enrolment_details = clean_empty(api_provider_enrolment_details) @@ -139,7 +138,9 @@ class ProviderManagementOperations(Resource): result = clean_empty(result) current_app.logger.debug("Provider domain updated in database") - return make_response(object=APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) + provider_updated = APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)) + # return make_response(object=APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) + return make_response(object=dict_to_camel_case(provider_updated.to_dict()), status=200) except Exception as e: exception = "An exception occurred in update provider" @@ -164,8 +165,9 @@ class ProviderManagementOperations(Resource): result = clean_empty(result) current_app.logger.debug("Provider domain updated in database") - - return make_response(object=APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) + provider_updated = APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)) + # return make_response(object=APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) + return make_response(object=dict_to_camel_case(provider_updated.to_dict()), status=200) except Exception as e: exception = "An exception occurred in patch provider" diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py index 962c4b6..6bee6ec 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py @@ -1,31 +1,41 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder +from ..util import dict_to_camel_case from flask import Response import json mimetype = "application/json" + def make_response(object, status): res = Response(json.dumps(object, cls=JSONEncoder), status=status, mimetype=mimetype) return res + def internal_server_error(detail, cause): - prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Internal Server Error", status=500, detail=detail, cause=cause, invalid_params=[{"param":"","reason":""}], supported_features="fffff") + # BEFORE: prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + + return Response(json.dumps(dict_to_camel_case(prob.to_dict()), cls=JSONEncoder), status=500, mimetype=mimetype) - return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): - prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Forbidden", status=403, detail=detail, cause=cause, invalid_params=[{"param":"","reason":""}], supported_features="fffff") + # BEFORE: prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + + return Response(json.dumps(dict_to_camel_case(prob.to_dict()), cls=JSONEncoder), status=403, mimetype=mimetype) - return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): - prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params, supported_features="fffff") + # BEFORE: prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + + return Response(json.dumps(dict_to_camel_case(prob.to_dict()), cls=JSONEncoder), status=400, mimetype=cause) - return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): - prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Not Found", status=404, detail=detail, cause=cause, invalid_params=[{"param":"","reason":""}], supported_features="fffff") + # BEFORE: prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) - return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file + return Response(json.dumps(dict_to_camel_case(prob.to_dict()), cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file -- GitLab From 11227f06d7f5625c132fd4cdd4f82f039048194d Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 22 May 2024 10:51:19 +0200 Subject: [PATCH 180/310] improvement of scripts --- services/run_capif_tests.sh | 26 ++++++++++++++++++- .../CAPIF Api Events/capif_events_api.robot | 4 +++ tests/resources/common.resource | 8 +++--- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/services/run_capif_tests.sh b/services/run_capif_tests.sh index f294bac..944e3b2 100755 --- a/services/run_capif_tests.sh +++ b/services/run_capif_tests.sh @@ -9,14 +9,33 @@ RESULT_FOLDER=$REPOSITORY_BASE_FOLDER/results ROBOT_DOCKER_FILE_FOLDER=$REPOSITORY_BASE_FOLDER/tools/robot # nginx Hostname and http port (80 by default) to reach for tests +# CAPIF_REGISTER=registercapif.mobilesandbox.cloud +CAPIF_REGISTER=capifcore +# CAPIF_REGISTER_PORT=37211 +CAPIF_REGISTER_PORT=8084 +# CAPIF_HOSTNAME=capif.mobilesandbox.cloud CAPIF_HOSTNAME=capifcore CAPIF_HTTP_PORT=8080 CAPIF_HTTPS_PORT=443 + +# VAULT access configuration +# CAPIF_VAULT=vault.5gnacar.int +CAPIF_VAULT=vault +CAPIF_VAULT_PORT=8200 +# CAPIF_VAULT_TOKEN=dev-only-token +CAPIF_VAULT_TOKEN=read-ca-token + MOCK_SERVER_URL=http://mockserver:9090 -echo "HOSTNAME = $CAPIF_HOSTNAME" + +echo "CAPIF_HOSTNAME = $CAPIF_HOSTNAME" +echo "CAPIF_REGISTER = $CAPIF_REGISTER" echo "CAPIF_HTTP_PORT = $CAPIF_HTTP_PORT" echo "CAPIF_HTTPS_PORT = $CAPIF_HTTPS_PORT" +echo "CAPIF_VAULT = $CAPIF_VAULT" +echo "CAPIF_VAULT_PORT = $CAPIF_VAULT_PORT" +echo "CAPIF_VAULT_TOKEN = $CAPIF_VAULT_TOKEN" +echo "MOCK_SERVER_URL = $MOCK_SERVER_URL" docker >/dev/null 2>/dev/null if [[ $? -ne 0 ]] @@ -52,4 +71,9 @@ docker run -ti --rm --network="host" \ --variable CAPIF_HOSTNAME:$CAPIF_HOSTNAME \ --variable CAPIF_HTTP_PORT:$CAPIF_HTTP_PORT \ --variable CAPIF_HTTPS_PORT:$CAPIF_HTTPS_PORT \ + --variable CAPIF_REGISTER:$CAPIF_REGISTER \ + --variable CAPIF_REGISTER_PORT:$CAPIF_REGISTER_PORT \ + --variable CAPIF_VAULT:$CAPIF_VAULT \ + --variable CAPIF_VAULT_PORT:$CAPIF_VAULT_PORT \ + --variable CAPIF_VAULT_TOKEN:$CAPIF_VAULT_TOKEN \ --variable MOCK_SERVER_URL:$MOCK_SERVER_URL $@ diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index c1ea4c7..d60f5c7 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -138,6 +138,10 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriptionId ... detail=User not authorized ... cause=You are not the owner of this resource +# Prueba Mock Server +# [Tags] jms-3 +# Start Mock server + Prueba JMS [Tags] jms-1 diff --git a/tests/resources/common.resource b/tests/resources/common.resource index 4f8136e..9521310 100644 --- a/tests/resources/common.resource +++ b/tests/resources/common.resource @@ -95,9 +95,11 @@ Start Mock server Log Starting mock Server for Robot Framework. # Run Process pip3 install -r /opt/robot-tests/tests/libraries/mock_server/requirements.txt - ${server_process}= Start Process python3 /opt/robot-tests/tests/libraries/mock_server/mock_server.py - Log PID: ${server_process.pid} - + ${server_process}= Start Process python3 /opt/robot-tests/tests/libraries/mock_server/mock_server.py shell=True + # Log PID: ${server_process.pid} + Log output: ${server_process.stdout} + + Sleep 5m Create Session mockserver ${MOCK_SERVER_URL} ${endpoint}= Set Variable /testing -- GitLab From 29660fcf62e0496fa60e33fb02a8d06a1083b305 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Wed, 22 May 2024 11:06:30 +0200 Subject: [PATCH 181/310] Setup fixed versions --- services/helper/requirements.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/services/helper/requirements.txt b/services/helper/requirements.txt index c5a4f37..68d82b2 100644 --- a/services/helper/requirements.txt +++ b/services/helper/requirements.txt @@ -1,8 +1,8 @@ -python_dateutil >= 2.6.0 -setuptools >= 21.0.0 -Flask >= 2.0.3 +python_dateutil == 2.9.0.post0 +setuptools == 68.2.2 +Flask == 3.0.3 pymongo == 4.0.1 -flask_jwt_extended -pyopenssl -pyyaml -requests +flask_jwt_extended == 4.6.0 +pyopenssl == 24.1.0 +pyyaml == 6.0.1 +requests == 2.32.2 -- GitLab From b9ca1f231229f8dabf7490639c9d3e92033be8ad Mon Sep 17 00:00:00 2001 From: torrespel Date: Wed, 22 May 2024 09:09:32 +0000 Subject: [PATCH 182/310] Update config.py --- services/helper/helper_service/config.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/services/helper/helper_service/config.py b/services/helper/helper_service/config.py index d04bd1a..444a232 100644 --- a/services/helper/helper_service/config.py +++ b/services/helper/helper_service/config.py @@ -3,18 +3,18 @@ import os #Config class to get config class Config: - def __init__(self): - self.cached = 0 - self.file="./config.yaml" - self.my_config = {} + def __init__(self): + self.cached = 0 + self.file="./config.yaml" + self.my_config = {} - stamp = os.stat(self.file).st_mtime - if stamp != self.cached: - self.cached = stamp - f = open(self.file) - self.my_config = yaml.safe_load(f) - f.close() + stamp = os.stat(self.file).st_mtime + if stamp != self.cached: + self.cached = stamp + f = open(self.file) + self.my_config = yaml.safe_load(f) + f.close() - def get_config(self): - return self.my_config + def get_config(self): + return self.my_config -- GitLab From 936bf13be61ce7f8ac3415bf76b019fc771f43b7 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Wed, 22 May 2024 12:45:01 +0200 Subject: [PATCH 183/310] commit --- services/helper/helper_service/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/services/helper/helper_service/__main__.py b/services/helper/helper_service/__main__.py index 833284b..69411aa 100644 --- a/services/helper/helper_service/__main__.py +++ b/services/helper/helper_service/__main__.py @@ -6,7 +6,6 @@ from .config import Config import json import requests - app = Flask(__name__) config = Config().get_config() -- GitLab From da2838818d6bd310799e6ca6fe9086edd5e23b93 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Wed, 22 May 2024 14:39:21 +0300 Subject: [PATCH 184/310] improve error function responses --- .../core/provider_enrolment_details_api.py | 1 - .../api_provider_management/core/responses.py | 40 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py index 67229fa..976cb32 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py @@ -134,7 +134,6 @@ class ProviderManagementOperations(Resource): api_provider_enrolment_details = clean_empty(api_provider_enrolment_details) result = mycol.find_one_and_update(result, {"$set":api_provider_enrolment_details}, projection={'_id': 0},return_document=ReturnDocument.AFTER ,upsert=False) - result = clean_empty(result) current_app.logger.debug("Provider domain updated in database") diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py index 6bee6ec..f047297 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py @@ -1,7 +1,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder -from ..util import dict_to_camel_case -from flask import Response +from ..util import dict_to_camel_case, clean_empty +from flask import Response, current_app import json mimetype = "application/json" @@ -14,28 +14,40 @@ def make_response(object, status): def internal_server_error(detail, cause): - prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Internal Server Error", status=500, detail=detail, cause=cause, invalid_params=[{"param":"","reason":""}], supported_features="fffff") - # BEFORE: prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + # prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Internal Server Error", status=500, detail=detail, cause=cause, invalid_params=[{"param":"","reason":""}], supported_features="fffff") + prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) - return Response(json.dumps(dict_to_camel_case(prob.to_dict()), cls=JSONEncoder), status=500, mimetype=mimetype) + prob = prob.to_dict() + prob = clean_empty(prob) + + return Response(json.dumps(dict_to_camel_case(prob), cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): - prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Forbidden", status=403, detail=detail, cause=cause, invalid_params=[{"param":"","reason":""}], supported_features="fffff") - # BEFORE: prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + + prob = prob.to_dict() + prob = clean_empty(prob) - return Response(json.dumps(dict_to_camel_case(prob.to_dict()), cls=JSONEncoder), status=403, mimetype=mimetype) + # return Response(json.dumps(dict_to_camel_case(prob.to_dict()), cls=JSONEncoder), status=403, mimetype=mimetype) + return Response(json.dumps(dict_to_camel_case(prob), cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): - prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params, supported_features="fffff") - # BEFORE: prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + # prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params, supported_features="fffff") + prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) - return Response(json.dumps(dict_to_camel_case(prob.to_dict()), cls=JSONEncoder), status=400, mimetype=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + + return Response(json.dumps(dict_to_camel_case(prob), cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): - prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Not Found", status=404, detail=detail, cause=cause, invalid_params=[{"param":"","reason":""}], supported_features="fffff") - # BEFORE: prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + # prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Not Found", status=404, detail=detail, cause=cause, invalid_params=[{"param":"","reason":""}], supported_features="fffff") + prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + + prob = prob.to_dict() + prob = clean_empty(prob) - return Response(json.dumps(dict_to_camel_case(prob.to_dict()), cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file + return Response(json.dumps(dict_to_camel_case(prob), cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file -- GitLab From 57a814b290d2a580556926c25c0269f36e7aa0f5 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 22 May 2024 15:22:18 +0200 Subject: [PATCH 185/310] Minor improbe con common library --- .../CAPIF Api Events/capif_events_api.robot | 212 +++++++++--------- tests/resources/common/basicRequests.robot | 1 + 2 files changed, 107 insertions(+), 106 deletions(-) diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index d60f5c7..2c977c0 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -142,112 +142,112 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriptionId # [Tags] jms-3 # Start Mock server -Prueba JMS - [Tags] jms-1 - - # Start Mock server - Check Mock Server - Clean Mock Server - - # Register APF - ${register_user_info}= Provider Default Registration - - # Publish one api - Publish Service Api ${register_user_info} - - # Register INVOKER - ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - - ${discover_response}= Get Request Capif - ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} - ... server=${CAPIF_HTTPS_URL} - ... verify=ca.crt - ... username=${INVOKER_USERNAME} - - ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} - - # Subscribe to events - ${events_list}= Create List SERVICE_API_INVOCATION_SUCCESS SERVICE_API_INVOCATION_FAILURE - ${request_body}= Create Events Subscription - ... events=@{events_list} - ... notificationDestination=http://192.168.0.14:9090/testing - ${resp}= Post Request Capif - ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions - ... json=${request_body} - ... server=${CAPIF_HTTPS_URL} - ... verify=ca.crt - ... username=${INVOKER_USERNAME} - - # Check Results - Check Response Variable Type And Values ${resp} 201 EventSubscription - ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} - - ${results}= Create List 200 400 - - # Create Log Entry - ${request_body}= Create Log Entry - ... ${register_user_info['aef_id']} - ... ${register_user_info_invoker['api_invoker_id']} - ... ${api_ids} - ... ${api_names} - ... results=${results} - ${resp}= Post Request Capif - ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs - ... json=${request_body} - ... server=${CAPIF_HTTPS_URL} - ... verify=ca.crt - ... username=${AEF_PROVIDER_USERNAME} - - # Check Results - Check Response Variable Type And Values ${resp} 201 InvocationLog - ${resource_url}= Check Location Header ${resp} ${LOCATION_LOGGING_RESOURCE_REGEX} - - Sleep 3s - - # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. - ${resp}= Get Mock Server Messages - ${notification_events_on_mock_server}= Set Variable ${resp.json()} - # Check if message follow EventNotification definition. - Check Variable ${notification_events_on_mock_server} EventNotification - # Check if mock server receive 2 events. - Length Should Be ${notification_events_on_mock_server} 2 - - # Now we create the expected events received at notification server according to logs sent to loggin service in order to check if all are present. - ${invocation_log_base}= Copy Dictionary ${request_body} deepcopy=True - - # Store log array because each log will be notified in one Event Notification - ${invocation_log_logs}= Set Variable ${invocation_log_base['logs']} - # Remove logs array from invocationLog data - Remove From Dictionary ${invocation_log_base} logs - Length Should Be ${invocation_log_logs} 2 - - # Create 2 invocationLogs from initial invocationLog present on request - ${invocation_logs_1}= Copy Dictionary ${invocation_log_base} deepcopy=True - ${invocation_logs_2}= Copy Dictionary ${invocation_log_base} deepcopy=True - # Create a log array with only one component - ${log_1}= Create List ${invocation_log_logs[0]} - ${log_2}= Create List ${invocation_log_logs[1]} - # Setup logs array with previously created list - Set To Dictionary ${invocation_logs_1} logs=${log_1} - Set To Dictionary ${invocation_logs_2} logs=${log_2} - # Create event details for each log - ${event_details_1}= Create dictionary invocationLogs=${invocation_logs_1} - ${event_details_2}= Create dictionary invocationLogs=${invocation_logs_2} - # Create Event with Event Details from invocationLog - ${event_1}= Create Dictionary - ... subscriptionId=${subscription_id} - ... events=SERVICE_API_INVOCATION_SUCCESS - ... eventDetail=${event_details_1} - ${event_2}= Create Dictionary - ... subscriptionId=${subscription_id} - ... events=SERVICE_API_INVOCATION_FAILURE - ... eventDetail=${event_details_2} - # Check if created events follow EventNotification format defined by 3gpp - Check Variable ${event_1} EventNotification - Check Variable ${event_2} EventNotification - - List Should Contain Value ${notification_events_on_mock_server} ${event_1} - List Should Contain Value ${notification_events_on_mock_server} ${event_2} +# Prueba JMS +# [Tags] jms-1 + +# # Start Mock server +# Check Mock Server +# Clean Mock Server + +# # Register APF +# ${register_user_info}= Provider Default Registration + +# # Publish one api +# Publish Service Api ${register_user_info} + +# # Register INVOKER +# ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + +# ${discover_response}= Get Request Capif +# ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} +# ... server=${CAPIF_HTTPS_URL} +# ... verify=ca.crt +# ... username=${INVOKER_USERNAME} + +# ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + +# # Subscribe to events +# ${events_list}= Create List SERVICE_API_INVOCATION_SUCCESS SERVICE_API_INVOCATION_FAILURE +# ${request_body}= Create Events Subscription +# ... events=@{events_list} +# ... notificationDestination=http://192.168.0.14:9090/testing +# ${resp}= Post Request Capif +# ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions +# ... json=${request_body} +# ... server=${CAPIF_HTTPS_URL} +# ... verify=ca.crt +# ... username=${INVOKER_USERNAME} + +# # Check Results +# Check Response Variable Type And Values ${resp} 201 EventSubscription +# ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + +# ${results}= Create List 200 400 + +# # Create Log Entry +# ${request_body}= Create Log Entry +# ... ${register_user_info['aef_id']} +# ... ${register_user_info_invoker['api_invoker_id']} +# ... ${api_ids} +# ... ${api_names} +# ... results=${results} +# ${resp}= Post Request Capif +# ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs +# ... json=${request_body} +# ... server=${CAPIF_HTTPS_URL} +# ... verify=ca.crt +# ... username=${AEF_PROVIDER_USERNAME} + +# # Check Results +# Check Response Variable Type And Values ${resp} 201 InvocationLog +# ${resource_url}= Check Location Header ${resp} ${LOCATION_LOGGING_RESOURCE_REGEX} + +# Sleep 3s + +# # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. +# ${resp}= Get Mock Server Messages +# ${notification_events_on_mock_server}= Set Variable ${resp.json()} +# # Check if message follow EventNotification definition. +# Check Variable ${notification_events_on_mock_server} EventNotification +# # Check if mock server receive 2 events. +# Length Should Be ${notification_events_on_mock_server} 2 + +# # Now we create the expected events received at notification server according to logs sent to loggin service in order to check if all are present. +# ${invocation_log_base}= Copy Dictionary ${request_body} deepcopy=True + +# # Store log array because each log will be notified in one Event Notification +# ${invocation_log_logs}= Set Variable ${invocation_log_base['logs']} +# # Remove logs array from invocationLog data +# Remove From Dictionary ${invocation_log_base} logs +# Length Should Be ${invocation_log_logs} 2 + +# # Create 2 invocationLogs from initial invocationLog present on request +# ${invocation_logs_1}= Copy Dictionary ${invocation_log_base} deepcopy=True +# ${invocation_logs_2}= Copy Dictionary ${invocation_log_base} deepcopy=True +# # Create a log array with only one component +# ${log_1}= Create List ${invocation_log_logs[0]} +# ${log_2}= Create List ${invocation_log_logs[1]} +# # Setup logs array with previously created list +# Set To Dictionary ${invocation_logs_1} logs=${log_1} +# Set To Dictionary ${invocation_logs_2} logs=${log_2} +# # Create event details for each log +# ${event_details_1}= Create dictionary invocationLogs=${invocation_logs_1} +# ${event_details_2}= Create dictionary invocationLogs=${invocation_logs_2} +# # Create Event with Event Details from invocationLog +# ${event_1}= Create Dictionary +# ... subscriptionId=${subscription_id} +# ... events=SERVICE_API_INVOCATION_SUCCESS +# ... eventDetail=${event_details_1} +# ${event_2}= Create Dictionary +# ... subscriptionId=${subscription_id} +# ... events=SERVICE_API_INVOCATION_FAILURE +# ... eventDetail=${event_details_2} +# # Check if created events follow EventNotification format defined by 3gpp +# Check Variable ${event_1} EventNotification +# Check Variable ${event_2} EventNotification + +# List Should Contain Value ${notification_events_on_mock_server} ${event_1} +# List Should Contain Value ${notification_events_on_mock_server} ${event_2} Prueba JMS [Tags] jms-2 diff --git a/tests/resources/common/basicRequests.robot b/tests/resources/common/basicRequests.robot index 5872a57..9565fb0 100644 --- a/tests/resources/common/basicRequests.robot +++ b/tests/resources/common/basicRequests.robot @@ -457,6 +457,7 @@ Get Auth For User ${resp}= GET On Session register_session /getauth auth=${auth} Should Be Equal As Strings ${resp.status_code} 200 + Log Dictionary ${resp.json()} RETURN ${resp.json()} -- GitLab From b7657fd02b7fa3317107ebfacc417430da8c7ccd Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 22 May 2024 16:17:46 +0200 Subject: [PATCH 186/310] test ocf deployment in the cluster --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 7178861de43bd801c532fec4bd5d38bb3dd3fb6a Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 22 May 2024 16:45:18 +0200 Subject: [PATCH 187/310] CI_PROJECT_PATH_SLUG --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From a90c0d503de1069508327fbf8f0368b8e9746b42 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 22 May 2024 16:53:19 +0200 Subject: [PATCH 188/310] Rename new test under events suite --- services/run_capif_tests.sh | 2 +- .../CAPIF Api Events/capif_events_api.robot | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/services/run_capif_tests.sh b/services/run_capif_tests.sh index 944e3b2..4da8622 100755 --- a/services/run_capif_tests.sh +++ b/services/run_capif_tests.sh @@ -25,7 +25,7 @@ CAPIF_VAULT_PORT=8200 # CAPIF_VAULT_TOKEN=dev-only-token CAPIF_VAULT_TOKEN=read-ca-token -MOCK_SERVER_URL=http://mockserver:9090 +MOCK_SERVER_URL=http://192.168.0.14:9090 echo "CAPIF_HOSTNAME = $CAPIF_HOSTNAME" diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index 2c977c0..afb44da 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -249,8 +249,8 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriptionId # List Should Contain Value ${notification_events_on_mock_server} ${event_1} # List Should Contain Value ${notification_events_on_mock_server} ${event_2} -Prueba JMS - [Tags] jms-2 +Invoker receives Service API Invocation events + [Tags] capif_api_events-6 mockserver # Start Mock server Check Mock Server @@ -275,9 +275,14 @@ Prueba JMS # Subscribe to events ${events_list}= Create List SERVICE_API_INVOCATION_SUCCESS SERVICE_API_INVOCATION_FAILURE + ${aef_ids}= Create List ${register_user_info['aef_id']} + ${event_filter}= Create Capif Event Filter aefIds=${aef_ids} + ${event_filters}= Create List ${event_filter} + ${request_body}= Create Events Subscription ... events=@{events_list} - ... notificationDestination=http://192.168.0.14:9090/testing + ... notificationDestination=${MOCK_SERVER_URL}/testing + ... eventFilters=${event_filters} ${resp}= Post Request Capif ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions ... json=${request_body} @@ -289,9 +294,8 @@ Prueba JMS Check Response Variable Type And Values ${resp} 201 EventSubscription ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + # Create Log Entry, emulate success and failure api invocation ${results}= Create List 200 400 - - # Create Log Entry ${request_body}= Create Log Entry ... ${register_user_info['aef_id']} ... ${register_user_info_invoker['api_invoker_id']} -- GitLab From efdc0a94cd4d45a26ab3da603273b3feac1ea626 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 09:30:57 +0200 Subject: [PATCH 189/310] no atomic in helm prometheus.enabled null --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index b2d5a18..a173834 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -607,7 +607,7 @@ monitoring: prometheus: # -- With prometheus.enabled: "". It won't be deployed. prometheus.enable: "true" # -- It will deploy prometheus - enable: "true" + enable: "" image: # -- The docker image repository to use repository: "prom/prometheus" -- GitLab From 3e075823f11eaed525605201058a15de49481890 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 09:40:48 +0200 Subject: [PATCH 190/310] helm command --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 36f8f133a460ad0996778d5d58e1a604334bf04f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 09:54:45 +0200 Subject: [PATCH 191/310] helm script --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 467b79e46e99d8c05415e4d1266ef7710e265ec6 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 10:34:30 +0200 Subject: [PATCH 192/310] adding image.repository in helm command --- helm/DELETE.txt | 2 +- helm/capif/values.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index a173834..647c09b 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -36,7 +36,7 @@ accessControlPolicy: CapifClient: # -- If enable capif client. - enable: "true" + enable: "" image: # -- The docker image repository to use repository: "public.ecr.aws/o2v4a8t6/opencapif/client" -- GitLab From 533d9114aef35a49a7e105dfc3a1af53c2c2878f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 11:05:18 +0200 Subject: [PATCH 193/310] not helm uninstall command --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 9a10a322ac6c8ad0f863a85866d1a3e0c93b8366 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 12:59:05 +0200 Subject: [PATCH 194/310] DOMAIN_DEV --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From bb30ddfdceba8ffe8640954d14acb77d9969e0e5 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 13:20:02 +0200 Subject: [PATCH 195/310] helm update --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 25540b00ca94a0142a45f81015124e10afdbc740 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Thu, 23 May 2024 13:43:35 +0200 Subject: [PATCH 196/310] SERVICE_API_AVAILABLE and UNAVAILABLE implemented, also the test related --- .../core/invocationlogs.py | 3 +- .../api_invocation_logs/core/redis_event.py | 34 ++- .../controllers/default_controller.py | 6 +- .../published_apis/core/redis_event.py | 40 ++++ services/run_capif_tests.sh | 2 +- .../CAPIF Api Events/capif_events_api.robot | 210 ++++++++---------- tests/libraries/api_events/bodyRequests.py | 48 ++++ tests/resources/common/basicRequests.robot | 10 +- 8 files changed, 217 insertions(+), 136 deletions(-) create mode 100644 services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py index 5a11c59..00cf9b7 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py @@ -103,7 +103,8 @@ class LoggingInvocationOperations(Resource): current_app.logger.info(event) invocation_log_base['logs']=[log] - RedisEvent(event,invocation_log_base,"invocationLogs").send_event() + invocationLogs=[invocation_log_base] + RedisEvent(event,"invocationLogs",invocationLogs).send_event() current_app.logger.debug("After log check") diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py index d42a1ef..037eade 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py @@ -4,19 +4,37 @@ import json publisher_ops = Publisher() + class RedisEvent(): - def __init__(self, event, information, event_detail_key) -> None: - self.redis_event={ + def __init__(self, event, event_detail_key, information) -> None: + self.EVENTS_ENUM = [ + 'SERVICE_API_AVAILABLE', + 'SERVICE_API_UNAVAILABLE', + 'SERVICE_API_UPDATE', + 'API_INVOKER_ONBOARDED', + 'API_INVOKER_OFFBOARDED', + 'SERVICE_API_INVOCATION_SUCCESS', + 'SERVICE_API_INVOCATION_FAILURE', + 'ACCESS_CONTROL_POLICY_UPDATE', + 'ACCESS_CONTROL_POLICY_UNAVAILABLE', + 'API_INVOKER_AUTHORIZATION_REVOKED', + 'API_INVOKER_UPDATED', + 'API_TOPOLOGY_HIDING_CREATED', + 'API_TOPOLOGY_HIDING_REVOKED'] + if event not in self.EVENTS_ENUM: + raise Exception( + "Event (" + event + ") is not on event enum (" + ','.join(self.EVENTS_ENUM) + ")") + self.redis_event = { "event": event, "key": event_detail_key, - "information":information + "information": information } - + def to_string(self): return json.dumps(self.redis_event, cls=JSONEncoder) - + def send_event(self): - publisher_ops.publish_message("events-log",self.to_string()) - + publisher_ops.publish_message("events-log", self.to_string()) + def __call__(self): - return self.redis_event \ No newline at end of file + return self.redis_event diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py index 8fc2b62..6636803 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py @@ -15,6 +15,7 @@ from cryptography.hazmat.backends import default_backend from ..core.validate_user import ControlAccess from functools import wraps import pymongo +from ..core.redis_event import RedisEvent service_operations = PublishServiceOperations() @@ -84,7 +85,8 @@ def apf_id_service_apis_post(apf_id, body): # noqa: E501 if res.status_code == 201: current_app.logger.info("Service published") - publisher_ops.publish_message("events", "SERVICE_API_AVAILABLE") + api_id=res.headers['Location'].split('/')[-1] + RedisEvent("SERVICE_API_AVAILABLE", "apiIds", [api_id] ).send_event() return res @@ -107,7 +109,7 @@ def apf_id_service_apis_service_api_id_delete(service_api_id, apf_id): # noqa: if res.status_code == 204: current_app.logger.info("Removed service published") - publisher_ops.publish_message("events", "SERVICE_API_UNAVAILABLE") + RedisEvent("SERVICE_API_UNAVAILABLE", "apiIds", [service_api_id] ).send_event() publisher_ops.publish_message("internal-messages", f"service-removed:{service_api_id}") return res diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py new file mode 100644 index 0000000..037eade --- /dev/null +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py @@ -0,0 +1,40 @@ +from ..encoder import JSONEncoder +from .publisher import Publisher +import json + +publisher_ops = Publisher() + + +class RedisEvent(): + def __init__(self, event, event_detail_key, information) -> None: + self.EVENTS_ENUM = [ + 'SERVICE_API_AVAILABLE', + 'SERVICE_API_UNAVAILABLE', + 'SERVICE_API_UPDATE', + 'API_INVOKER_ONBOARDED', + 'API_INVOKER_OFFBOARDED', + 'SERVICE_API_INVOCATION_SUCCESS', + 'SERVICE_API_INVOCATION_FAILURE', + 'ACCESS_CONTROL_POLICY_UPDATE', + 'ACCESS_CONTROL_POLICY_UNAVAILABLE', + 'API_INVOKER_AUTHORIZATION_REVOKED', + 'API_INVOKER_UPDATED', + 'API_TOPOLOGY_HIDING_CREATED', + 'API_TOPOLOGY_HIDING_REVOKED'] + if event not in self.EVENTS_ENUM: + raise Exception( + "Event (" + event + ") is not on event enum (" + ','.join(self.EVENTS_ENUM) + ")") + self.redis_event = { + "event": event, + "key": event_detail_key, + "information": information + } + + def to_string(self): + return json.dumps(self.redis_event, cls=JSONEncoder) + + def send_event(self): + publisher_ops.publish_message("events-log", self.to_string()) + + def __call__(self): + return self.redis_event diff --git a/services/run_capif_tests.sh b/services/run_capif_tests.sh index 4da8622..90be28d 100755 --- a/services/run_capif_tests.sh +++ b/services/run_capif_tests.sh @@ -25,7 +25,7 @@ CAPIF_VAULT_PORT=8200 # CAPIF_VAULT_TOKEN=dev-only-token CAPIF_VAULT_TOKEN=read-ca-token -MOCK_SERVER_URL=http://192.168.0.14:9090 +MOCK_SERVER_URL=http://10.95.115.22:9090 echo "CAPIF_HOSTNAME = $CAPIF_HOSTNAME" diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index afb44da..0e9ebf8 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -2,6 +2,7 @@ Resource /opt/robot-tests/tests/resources/common.resource Library /opt/robot-tests/tests/libraries/bodyRequests.py Library XML +Library String Resource /opt/robot-tests/tests/resources/common/basicRequests.robot Resource ../../resources/common.resource @@ -138,119 +139,8 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriptionId ... detail=User not authorized ... cause=You are not the owner of this resource -# Prueba Mock Server -# [Tags] jms-3 -# Start Mock server - -# Prueba JMS -# [Tags] jms-1 - -# # Start Mock server -# Check Mock Server -# Clean Mock Server - -# # Register APF -# ${register_user_info}= Provider Default Registration - -# # Publish one api -# Publish Service Api ${register_user_info} - -# # Register INVOKER -# ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding - -# ${discover_response}= Get Request Capif -# ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} -# ... server=${CAPIF_HTTPS_URL} -# ... verify=ca.crt -# ... username=${INVOKER_USERNAME} - -# ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} - -# # Subscribe to events -# ${events_list}= Create List SERVICE_API_INVOCATION_SUCCESS SERVICE_API_INVOCATION_FAILURE -# ${request_body}= Create Events Subscription -# ... events=@{events_list} -# ... notificationDestination=http://192.168.0.14:9090/testing -# ${resp}= Post Request Capif -# ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions -# ... json=${request_body} -# ... server=${CAPIF_HTTPS_URL} -# ... verify=ca.crt -# ... username=${INVOKER_USERNAME} - -# # Check Results -# Check Response Variable Type And Values ${resp} 201 EventSubscription -# ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} - -# ${results}= Create List 200 400 - -# # Create Log Entry -# ${request_body}= Create Log Entry -# ... ${register_user_info['aef_id']} -# ... ${register_user_info_invoker['api_invoker_id']} -# ... ${api_ids} -# ... ${api_names} -# ... results=${results} -# ${resp}= Post Request Capif -# ... /api-invocation-logs/v1/${register_user_info['aef_id']}/logs -# ... json=${request_body} -# ... server=${CAPIF_HTTPS_URL} -# ... verify=ca.crt -# ... username=${AEF_PROVIDER_USERNAME} - -# # Check Results -# Check Response Variable Type And Values ${resp} 201 InvocationLog -# ${resource_url}= Check Location Header ${resp} ${LOCATION_LOGGING_RESOURCE_REGEX} - -# Sleep 3s - -# # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. -# ${resp}= Get Mock Server Messages -# ${notification_events_on_mock_server}= Set Variable ${resp.json()} -# # Check if message follow EventNotification definition. -# Check Variable ${notification_events_on_mock_server} EventNotification -# # Check if mock server receive 2 events. -# Length Should Be ${notification_events_on_mock_server} 2 - -# # Now we create the expected events received at notification server according to logs sent to loggin service in order to check if all are present. -# ${invocation_log_base}= Copy Dictionary ${request_body} deepcopy=True - -# # Store log array because each log will be notified in one Event Notification -# ${invocation_log_logs}= Set Variable ${invocation_log_base['logs']} -# # Remove logs array from invocationLog data -# Remove From Dictionary ${invocation_log_base} logs -# Length Should Be ${invocation_log_logs} 2 - -# # Create 2 invocationLogs from initial invocationLog present on request -# ${invocation_logs_1}= Copy Dictionary ${invocation_log_base} deepcopy=True -# ${invocation_logs_2}= Copy Dictionary ${invocation_log_base} deepcopy=True -# # Create a log array with only one component -# ${log_1}= Create List ${invocation_log_logs[0]} -# ${log_2}= Create List ${invocation_log_logs[1]} -# # Setup logs array with previously created list -# Set To Dictionary ${invocation_logs_1} logs=${log_1} -# Set To Dictionary ${invocation_logs_2} logs=${log_2} -# # Create event details for each log -# ${event_details_1}= Create dictionary invocationLogs=${invocation_logs_1} -# ${event_details_2}= Create dictionary invocationLogs=${invocation_logs_2} -# # Create Event with Event Details from invocationLog -# ${event_1}= Create Dictionary -# ... subscriptionId=${subscription_id} -# ... events=SERVICE_API_INVOCATION_SUCCESS -# ... eventDetail=${event_details_1} -# ${event_2}= Create Dictionary -# ... subscriptionId=${subscription_id} -# ... events=SERVICE_API_INVOCATION_FAILURE -# ... eventDetail=${event_details_2} -# # Check if created events follow EventNotification format defined by 3gpp -# Check Variable ${event_1} EventNotification -# Check Variable ${event_2} EventNotification - -# List Should Contain Value ${notification_events_on_mock_server} ${event_1} -# List Should Contain Value ${notification_events_on_mock_server} ${event_2} - Invoker receives Service API Invocation events - [Tags] capif_api_events-6 mockserver + [Tags] capif_api_events-6 mockserver # Start Mock server Check Mock Server @@ -275,9 +165,9 @@ Invoker receives Service API Invocation events # Subscribe to events ${events_list}= Create List SERVICE_API_INVOCATION_SUCCESS SERVICE_API_INVOCATION_FAILURE - ${aef_ids}= Create List ${register_user_info['aef_id']} - ${event_filter}= Create Capif Event Filter aefIds=${aef_ids} - ${event_filters}= Create List ${event_filter} + ${aef_ids}= Create List ${register_user_info['aef_id']} + ${event_filter}= Create Capif Event Filter aefIds=${aef_ids} + ${event_filters}= Create List ${event_filter} ${request_body}= Create Events Subscription ... events=@{events_list} @@ -322,7 +212,7 @@ Invoker receives Service API Invocation events Check Variable ${notification_events_on_mock_server} EventNotification # Create check Events to ensure all notifications were received - ${check_events} ${check_events_length}= Create Events From invocationLogs + ${check_events} ${check_events_length}= Create Events From InvocationLogs ... ${subscription_id} ... ${request_body} @@ -333,3 +223,91 @@ Invoker receives Service API Invocation events Log ${event} List Should Contain Value ${notification_events_on_mock_server} ${event} END + +Invoker subscribe to Service API Available and Unavailable events + [Tags] capif_api_events-7 mockserver + + # Start Mock server + Check Mock Server + Clean Mock Server + + # Register APF + ${register_user_info_provider}= Provider Default Registration + + # Publish one api + ${service_api_description_published_1} ${resource_url_1} ${request_body}= Publish Service Api + ... ${register_user_info_provider} + + # Register INVOKER + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + + ${discover_response}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + + # Subscribe to events + ${events_list}= Create List SERVICE_API_AVAILABLE SERVICE_API_UNAVAILABLE + ${aef_ids}= Create List ${register_user_info_provider['aef_id']} + ${event_filter}= Create Capif Event Filter aefIds=${aef_ids} + ${event_filters}= Create List ${event_filter} + + ${request_body}= Create Events Subscription + ... events=@{events_list} + ... notificationDestination=${MOCK_SERVER_URL}/testing + ... eventFilters=${event_filters} + ${resp}= Post Request Capif + ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + # Check Results + Check Response Variable Type And Values ${resp} 201 EventSubscription + ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + + # Provider publish new API + ${service_api_description_published_2} ${resource_url_2} ${request_body}= Publish Service Api + ... ${register_user_info_provider} + ... service_2 + + # Provider Remove service_1 published API + ${resp}= Delete Request Capif + ... ${resource_url_1.path} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${APF_PROVIDER_USERNAME} + + Status Should Be 204 ${resp} + + # Check Results + + Sleep 3s + # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. + ${resp}= Get Mock Server Messages + ${notification_events_on_mock_server}= Set Variable ${resp.json()} + # Check if message follow EventNotification definition. + Check Variable ${notification_events_on_mock_server} EventNotification + + # Create Notification Events expected to be received + ${api_id_1}= Fetch From Right ${resource_url_1.path} / + ${notification_event_expected_removed}= Create Notification Event + ... ${subscription_id} + ... SERVICE_API_UNAVAILABLE + ... apiIds=${api_id_1} + Check Variable ${notification_event_expected_removed} EventNotification + ${api_id_2}= Fetch From Right ${resource_url_2.path} / + ${notification_event_expected_created}= Create Notification Event + ... ${subscription_id} + ... SERVICE_API_AVAILABLE + ... apiIds=${api_id_2} + Check Variable ${notification_event_expected_created} EventNotification + + # Check results + Length Should Be ${notification_events_on_mock_server} 2 + List Should Contain Value ${notification_events_on_mock_server} ${notification_event_expected_removed} + List Should Contain Value ${notification_events_on_mock_server} ${notification_event_expected_created} diff --git a/tests/libraries/api_events/bodyRequests.py b/tests/libraries/api_events/bodyRequests.py index 3d1de26..24f22f3 100644 --- a/tests/libraries/api_events/bodyRequests.py +++ b/tests/libraries/api_events/bodyRequests.py @@ -45,3 +45,51 @@ def create_websock_notif_config_default(): "websocketUri": "websocketUri" } +def create_notification_event(subscriptionId, event, serviceAPIDescriptions=None, apiIds=None, apiInvokerIds=None, accCtrlPolList=None, invocationLogs=None, apiTopoHide=None): + result={ + "subscriptionId":subscriptionId, + "events": event, + "eventDetail": dict() + } + count=0 + if serviceAPIDescriptions != None: + if isinstance(serviceAPIDescriptions,list()): + result['eventDetail']['serviceAPIDescriptions']=serviceAPIDescriptions + else: + result['eventDetail']['serviceAPIDescriptions']=[serviceAPIDescriptions] + count=count+1 + if apiIds != None: + if isinstance(apiIds,list): + result['eventDetail']['apiIds']=apiIds + else: + result['eventDetail']['apiIds']=[apiIds] + count=count+1 + if apiInvokerIds != None: + if isinstance(apiInvokerIds,list): + result['eventDetail']['apiInvokerIds']=apiInvokerIds + else: + result['eventDetail']['apiInvokerIds']=[apiInvokerIds] + count=count+1 + if accCtrlPolList != None: + if isinstance(accCtrlPolList,list): + result['eventDetail']['accCtrlPolList']=accCtrlPolList + else: + result['eventDetail']['accCtrlPolList']=[accCtrlPolList] + count=count+1 + if invocationLogs != None: + if isinstance(invocationLogs,list): + result['eventDetail']['invocationLogs']=invocationLogs + else: + result['eventDetail']['invocationLogs']=[invocationLogs] + count=count+1 + if apiTopoHide != None: + if isinstance(apiTopoHide): + result['eventDetail']['apiTopoHide']=apiTopoHide + else: + result['eventDetail']['apiTopoHide']=[apiTopoHide] + count=count+1 + + if count == 0: + del result['eventDetail'] + + return result diff --git a/tests/resources/common/basicRequests.robot b/tests/resources/common/basicRequests.robot index 9565fb0..9ba5f21 100644 --- a/tests/resources/common/basicRequests.robot +++ b/tests/resources/common/basicRequests.robot @@ -747,7 +747,7 @@ Create Security Context Between invoker and provider Check Response Variable Type And Values ${resp} 201 ServiceSecurity -Create Events From invocationLogs +Create Events From InvocationLogs [Arguments] ${subscription_id} ${invocation_log} ${events}= Create List @@ -774,13 +774,7 @@ Create Events From invocationLogs ${log_list}= Create List ${log} # Setup logs array with previously created list Set To Dictionary ${invocation_logs} logs=${log_list} - # Create event details for each log - ${event_details}= Create dictionary invocationLogs=${invocation_logs} - # Create Event with Event Details from invocationLog and also is appended to events array - ${event}= Create Dictionary - ... subscriptionId=${subscription_id} - ... events=${event_enum} - ... eventDetail=${event_details} + ${event}= Create Notification Event ${subscription_id} ${event_enum} invocationLogs=${invocation_logs} Check Variable ${event} EventNotification Append To List ${events} ${event} END -- GitLab From 8973eaad6ed0206f5053cfac51445d0472b3df44 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 14:08:37 +0200 Subject: [PATCH 197/310] ### --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From b3f68d9a4f4a5246fc62989e4b52f73ebd4f0dfb Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 14:16:22 +0200 Subject: [PATCH 198/310] helm command --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 3b2928a6242314ebe30111372f07ae862e3e7539 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 14:37:38 +0200 Subject: [PATCH 199/310] image.tag --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 93b0ce0e36a0870a0feb55f91791334f88e13d5b Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 23 May 2024 14:47:52 +0200 Subject: [PATCH 200/310] nginx.nginx.image --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 03f83118009e805a05f632f98bba884f7d94204b Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Thu, 23 May 2024 17:16:40 +0200 Subject: [PATCH 201/310] SERVICE_API_UPDATE upgraded --- .../controllers/default_controller.py | 41 ++++------ .../core/serviceapidescriptions.py | 70 +++++++++++----- services/run_capif_tests.sh | 2 +- .../CAPIF Api Events/capif_events_api.robot | 81 +++++++++++++++++++ tests/libraries/api_events/bodyRequests.py | 4 +- .../api_publish_service/bodyRequests.py | 17 +--- 6 files changed, 150 insertions(+), 65 deletions(-) diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py index 6636803..dd43886 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py @@ -4,7 +4,6 @@ from ..core import serviceapidescriptions from ..core.serviceapidescriptions import PublishServiceOperations from ..core.publisher import Publisher -import json from flask import Response, request, current_app from flask_jwt_extended import jwt_required, get_jwt_identity from flask import current_app @@ -14,15 +13,12 @@ from cryptography import x509 from cryptography.hazmat.backends import default_backend from ..core.validate_user import ControlAccess from functools import wraps -import pymongo -from ..core.redis_event import RedisEvent - service_operations = PublishServiceOperations() -publisher_ops = Publisher() valid_user = ControlAccess() + def cert_validation(): def _cert_validation(f): @wraps(f) @@ -32,13 +28,16 @@ def cert_validation(): cert_tmp = request.headers['X-Ssl-Client-Cert'] cert_raw = cert_tmp.replace('\t', '') - cert = x509.load_pem_x509_certificate(str.encode(cert_raw), default_backend()) + cert = x509.load_pem_x509_certificate( + str.encode(cert_raw), default_backend()) - cn = cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value.strip() + cn = cert.subject.get_attributes_for_oid( + x509.OID_COMMON_NAME)[0].value.strip() if cn != "superadmin": cert_signature = cert.signature.hex() - result = valid_user.validate_user_cert(args["apfId"], args["serviceApiId"], cert_signature) + result = valid_user.validate_user_cert( + args["apfId"], args["serviceApiId"], cert_signature) if result is not None: return result @@ -48,6 +47,7 @@ def cert_validation(): return __cert_validation return _cert_validation + def apf_id_service_apis_get(apf_id): # noqa: E501 """apf_id_service_apis_get @@ -83,13 +83,9 @@ def apf_id_service_apis_post(apf_id, body): # noqa: E501 res = service_operations.add_serviceapidescription(apf_id, body) - if res.status_code == 201: - current_app.logger.info("Service published") - api_id=res.headers['Location'].split('/')[-1] - RedisEvent("SERVICE_API_AVAILABLE", "apiIds", [api_id] ).send_event() - return res + @cert_validation() def apf_id_service_apis_service_api_id_delete(service_api_id, apf_id): # noqa: E501 """apf_id_service_apis_service_api_id_delete @@ -105,15 +101,12 @@ def apf_id_service_apis_service_api_id_delete(service_api_id, apf_id): # noqa: """ current_app.logger.info("Removing service published") - res = service_operations.delete_serviceapidescription(service_api_id, apf_id) - - if res.status_code == 204: - current_app.logger.info("Removed service published") - RedisEvent("SERVICE_API_UNAVAILABLE", "apiIds", [service_api_id] ).send_event() - publisher_ops.publish_message("internal-messages", f"service-removed:{service_api_id}") + res = service_operations.delete_serviceapidescription( + service_api_id, apf_id) return res + @cert_validation() def apf_id_service_apis_service_api_id_get(service_api_id, apf_id): # noqa: E501 """apf_id_service_apis_service_api_id_get @@ -133,6 +126,7 @@ def apf_id_service_apis_service_api_id_get(service_api_id, apf_id): # noqa: E50 return res + @cert_validation() def apf_id_service_apis_service_api_id_put(service_api_id, apf_id, body): # noqa: E501 """apf_id_service_apis_service_api_id_put @@ -149,14 +143,13 @@ def apf_id_service_apis_service_api_id_put(service_api_id, apf_id, body): # noq :rtype: ServiceAPIDescription """ - current_app.logger.info("Updating service api id with id: " + service_api_id) + current_app.logger.info( + "Updating service api id with id: " + service_api_id) if connexion.request.is_json: body = ServiceAPIDescription.from_dict(connexion.request.get_json()) # noqa: E501 - response = service_operations.update_serviceapidescription(service_api_id, apf_id, body) - - if response.status_code == 200: - publisher_ops.publish_message("events", "SERVICE_API_UPDATE") + response = service_operations.update_serviceapidescription( + service_api_id, apf_id, body) return response diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py index 5c903d1..85b9b78 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py @@ -17,26 +17,33 @@ from ..util import dict_to_camel_case, clean_empty from .responses import bad_request_error, internal_server_error, forbidden_error, not_found_error, unauthorized_error, make_response from bson import json_util from .auth_manager import AuthManager +from .redis_event import RedisEvent +from .publisher import Publisher + +publisher_ops = Publisher() service_api_not_found_message = "Service API not found" + class PublishServiceOperations(Resource): def __check_apf(self, apf_id): providers_col = self.db.get_col_by_name(self.db.capif_provider_col) current_app.logger.debug("Checking apf id") - provider = providers_col.find_one({"api_prov_funcs.api_prov_func_id": apf_id}) + provider = providers_col.find_one( + {"api_prov_funcs.api_prov_func_id": apf_id}) if provider is None: current_app.logger.error("Publisher not exist") - return unauthorized_error(detail = "Publisher not existing", cause = "Publisher id not found") + return unauthorized_error(detail="Publisher not existing", cause="Publisher id not found") - list_apf_ids = [func["api_prov_func_id"] for func in provider["api_prov_funcs"] if func["api_prov_func_role"] == "APF"] + list_apf_ids = [func["api_prov_func_id"] + for func in provider["api_prov_funcs"] if func["api_prov_func_role"] == "APF"] if apf_id not in list_apf_ids: current_app.logger.debug("This id not belongs to APF") - return unauthorized_error(detail ="You are not a publisher", cause ="This API is only available for publishers") + return unauthorized_error(detail="You are not a publisher", cause="This API is only available for publishers") return None @@ -57,7 +64,8 @@ class PublishServiceOperations(Resource): if result != None: return result - service = mycol.find({"apf_id": apf_id}, {"_id":0, "api_name":1, "api_id":1, "aef_profiles":1, "description":1, "supported_features":1, "shareable_info":1, "service_api_category":1, "api_supp_feats":1, "pub_api_path":1, "ccf_id":1}) + service = mycol.find({"apf_id": apf_id}, {"_id": 0, "api_name": 1, "api_id": 1, "aef_profiles": 1, "description": 1, + "supported_features": 1, "shareable_info": 1, "service_api_category": 1, "api_supp_feats": 1, "pub_api_path": 1, "ccf_id": 1}) current_app.logger.debug(service) if service is None: current_app.logger.error("Not found services for this apf id") @@ -92,9 +100,11 @@ class PublishServiceOperations(Resource): if result != None: return result - service = mycol.find_one({"api_name": serviceapidescription.api_name}) + service = mycol.find_one( + {"api_name": serviceapidescription.api_name}) if service is not None: - current_app.logger.error("Service already registered with same api name") + current_app.logger.error( + "Service already registered with same api name") return forbidden_error(detail="Already registered service with same api name", cause="Found service with same api name") api_id = secrets.token_hex(15) @@ -110,8 +120,13 @@ class PublishServiceOperations(Resource): current_app.logger.debug("Service inserted in database") res = make_response(object=serviceapidescription, status=201) - res.headers['Location'] = "http://localhost:8080/published-apis/v1/" + str(apf_id) + "/service-apis/" + str(api_id) + res.headers['Location'] = "http://localhost:8080/published-apis/v1/" + \ + str(apf_id) + "/service-apis/" + str(api_id) + if res.status_code == 201: + current_app.logger.info("Service published") + RedisEvent("SERVICE_API_AVAILABLE", "apiIds", + [str(api_id)]).send_event() return res except Exception as e: @@ -119,26 +134,25 @@ class PublishServiceOperations(Resource): current_app.logger.error(exception + "::" + str(e)) return internal_server_error(detail=exception, cause=str(e)) - - def get_one_serviceapi(self, service_api_id, apf_id): mycol = self.db.get_col_by_name(self.db.service_api_descriptions) try: - current_app.logger.debug("Geting service api with id: " + service_api_id) + current_app.logger.debug( + "Geting service api with id: " + service_api_id) result = self.__check_apf(apf_id) if result != None: return result my_query = {'apf_id': apf_id, 'api_id': service_api_id} - service_api = mycol.find_one(my_query, {"_id":0, "api_name":1, "api_id":1, "aef_profiles":1, "description":1, "supported_features":1, "shareable_info":1, "service_api_category":1, "api_supp_feats":1, "pub_api_path":1, "ccf_id":1}) + service_api = mycol.find_one(my_query, {"_id": 0, "api_name": 1, "api_id": 1, "aef_profiles": 1, "description": 1, + "supported_features": 1, "shareable_info": 1, "service_api_category": 1, "api_supp_feats": 1, "pub_api_path": 1, "ccf_id": 1}) if service_api is None: current_app.logger.error(service_api_not_found_message) return not_found_error(detail=service_api_not_found_message, cause="No Service with specific credentials exists") - my_service_api = dict_to_camel_case(service_api) my_service_api = clean_empty(my_service_api) @@ -157,7 +171,8 @@ class PublishServiceOperations(Resource): try: - current_app.logger.debug("Removing api service with id: " + service_api_id) + current_app.logger.debug( + "Removing api service with id: " + service_api_id) result = self.__check_apf(apf_id) if result != None: @@ -175,22 +190,29 @@ class PublishServiceOperations(Resource): self.auth_manager.remove_auth_service(service_api_id, apf_id) current_app.logger.debug("Removed service from database") - out = "The service matching api_id " + service_api_id + " was deleted." - return make_response(out, status=204) + out = "The service matching api_id " + service_api_id + " was deleted." + res = make_response(out, status=204) + if res.status_code == 204: + current_app.logger.info("Removed service published") + RedisEvent("SERVICE_API_UNAVAILABLE", "apiIds", + [service_api_id]).send_event() + publisher_ops.publish_message( + "internal-messages", f"service-removed:{service_api_id}") + return res except Exception as e: exception = "An exception occurred in delete service" current_app.logger.error(exception + "::" + str(e)) return internal_server_error(detail=exception, cause=str(e)) - def update_serviceapidescription(self, service_api_id, apf_id, service_api_description): mycol = self.db.get_col_by_name(self.db.service_api_descriptions) try: - current_app.logger.debug("Updating service api with id: " + service_api_id) + current_app.logger.debug( + "Updating service api with id: " + service_api_id) result = self.__check_apf(apf_id) @@ -207,18 +229,22 @@ class PublishServiceOperations(Resource): service_api_description = service_api_description.to_dict() service_api_description = clean_empty(service_api_description) - result = mycol.find_one_and_update(serviceapidescription, {"$set":service_api_description}, projection={"_id":0, "api_name":1, "api_id":1, "aef_profiles":1, "description":1, "supported_features":1, "shareable_info":1, "service_api_category":1, "api_supp_feats":1, "pub_api_path":1, "ccf_id":1},return_document=ReturnDocument.AFTER ,upsert=False) + result = mycol.find_one_and_update(serviceapidescription, {"$set": service_api_description}, projection={"_id": 0, "api_name": 1, "api_id": 1, "aef_profiles": 1, "description": 1, + "supported_features": 1, "shareable_info": 1, "service_api_category": 1, "api_supp_feats": 1, "pub_api_path": 1, "ccf_id": 1}, return_document=ReturnDocument.AFTER, upsert=False) result = clean_empty(result) current_app.logger.debug("Updated service api") - - response = make_response(object=dict_to_camel_case(result), status=200) + service_api_description_updated = dict_to_camel_case(result) + response = make_response( + object=service_api_description_updated, status=200) + if response.status_code == 200: + RedisEvent("SERVICE_API_UPDATE", "serviceAPIDescriptions", [ + service_api_description_updated]).send_event() return response except Exception as e: exception = "An exception occurred in update service" current_app.logger.error(exception + "::" + str(e)) return internal_server_error(detail=exception, cause=str(e)) - diff --git a/services/run_capif_tests.sh b/services/run_capif_tests.sh index 90be28d..06de230 100755 --- a/services/run_capif_tests.sh +++ b/services/run_capif_tests.sh @@ -25,7 +25,7 @@ CAPIF_VAULT_PORT=8200 # CAPIF_VAULT_TOKEN=dev-only-token CAPIF_VAULT_TOKEN=read-ca-token -MOCK_SERVER_URL=http://10.95.115.22:9090 +MOCK_SERVER_URL=http://192.168.0.119:9090 echo "CAPIF_HOSTNAME = $CAPIF_HOSTNAME" diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index 0e9ebf8..eabace4 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -311,3 +311,84 @@ Invoker subscribe to Service API Available and Unavailable events Length Should Be ${notification_events_on_mock_server} 2 List Should Contain Value ${notification_events_on_mock_server} ${notification_event_expected_removed} List Should Contain Value ${notification_events_on_mock_server} ${notification_event_expected_created} + + +Invoker subscribe to Service API Update + [Tags] capif_api_events-8 mockserver + + # Start Mock server + Check Mock Server + Clean Mock Server + + # Register APF + ${register_user_info_provider}= Provider Default Registration + + # Publish one api + ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api + ... ${register_user_info_provider} + + # Register INVOKER + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + + ${discover_response}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + ${api_ids} ${api_names}= Get Api Ids And Names From Discover Response ${discover_response} + + # Subscribe to events + ${events_list}= Create List SERVICE_API_UPDATE + ${aef_ids}= Create List ${register_user_info_provider['aef_id']} + ${event_filter}= Create Capif Event Filter aefIds=${aef_ids} + ${event_filters}= Create List ${event_filter} + + ${request_body}= Create Events Subscription + ... events=@{events_list} + ... notificationDestination=${MOCK_SERVER_URL}/testing + ... eventFilters=${event_filters} + ${resp}= Post Request Capif + ... /capif-events/v1/${register_user_info_invoker['api_invoker_id']}/subscriptions + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + # Check Results + Check Response Variable Type And Values ${resp} 201 EventSubscription + ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + + # Update Service API + ${request_body_modified}= Create Service Api Description service_1_modified + ${resp}= Put Request Capif + ... ${resource_url.path} + ... json=${request_body_modified} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${APF_PROVIDER_USERNAME} + + Check Response Variable Type And Values ${resp} 200 ServiceAPIDescription + ... apiName=service_1_modified + + # Check Results + Sleep 3s + # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. + ${resp}= Get Mock Server Messages + ${notification_events_on_mock_server}= Set Variable ${resp.json()} + # Check if message follow EventNotification definition. + Check Variable ${notification_events_on_mock_server} EventNotification + + # Create Notification Events expected to be received + ${api_id}= Fetch From Right ${resource_url.path} / + Set To Dictionary ${request_body_modified} apiId=${api_id} + ${notification_event_expected}= Create Notification Event + ... ${subscription_id} + ... SERVICE_API_UPDATE + ... serviceAPIDescriptions=${request_body_modified} + Check Variable ${notification_event_expected} EventNotification + + # Check results + Length Should Be ${notification_events_on_mock_server} 1 + List Should Contain Value ${notification_events_on_mock_server} ${notification_event_expected} + diff --git a/tests/libraries/api_events/bodyRequests.py b/tests/libraries/api_events/bodyRequests.py index 24f22f3..14c8bb4 100644 --- a/tests/libraries/api_events/bodyRequests.py +++ b/tests/libraries/api_events/bodyRequests.py @@ -53,7 +53,7 @@ def create_notification_event(subscriptionId, event, serviceAPIDescriptions=None } count=0 if serviceAPIDescriptions != None: - if isinstance(serviceAPIDescriptions,list()): + if isinstance(serviceAPIDescriptions,list): result['eventDetail']['serviceAPIDescriptions']=serviceAPIDescriptions else: result['eventDetail']['serviceAPIDescriptions']=[serviceAPIDescriptions] @@ -83,7 +83,7 @@ def create_notification_event(subscriptionId, event, serviceAPIDescriptions=None result['eventDetail']['invocationLogs']=[invocationLogs] count=count+1 if apiTopoHide != None: - if isinstance(apiTopoHide): + if isinstance(apiTopoHide,list): result['eventDetail']['apiTopoHide']=apiTopoHide else: result['eventDetail']['apiTopoHide']=[apiTopoHide] diff --git a/tests/libraries/api_publish_service/bodyRequests.py b/tests/libraries/api_publish_service/bodyRequests.py index 17ac4a7..69e7bb1 100644 --- a/tests/libraries/api_publish_service/bodyRequests.py +++ b/tests/libraries/api_publish_service/bodyRequests.py @@ -7,7 +7,7 @@ def create_service_api_description(api_name="service_1",aef_id="aef_id"): "versions": [ { "apiVersion": "v1", - "expiry": "2021-11-30T10:32:02.004000Z", + "expiry": "2021-11-30T10:32:02.004000+00:00", "resources": [ { "resourceName": "string", @@ -20,27 +20,12 @@ def create_service_api_description(api_name="service_1",aef_id="aef_id"): "description": "string" } ], - "custOperations": [ - { - "commType": "REQUEST_RESPONSE", - "custOpName": "string", - "operations": [ - "GET" - ], - "description": "string" - } - ] } ], "protocol": "HTTP_1_1", "dataFormat": "JSON", "securityMethods": ["PSK"], "interfaceDescriptions": [ - { - "ipv4Addr": "string", - "port": 65535, - "securityMethods": ["PSK"] - }, { "ipv4Addr": "string", "port": 65535, -- GitLab From 1624c79be037394dad4b7efb956c82df7adc4c7b Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 27 May 2024 11:18:24 +0200 Subject: [PATCH 202/310] Adding API Invoker events --- .../controllers/default_controller.py | 13 --- .../core/apiinvokerenrolmentdetails.py | 19 +++- .../core/redis_event.py | 40 +++++++ .../core/provider_enrolment_details_api.py | 2 +- .../capif_events/core/consumer_messager.py | 12 +-- .../capif_events/core/internal_event_ops.py | 29 ++--- .../published_apis/core/consumer_messager.py | 2 +- .../capif_security/core/consumer_messager.py | 8 +- .../CAPIF Api Events/capif_events_api.robot | 100 ++++++++++++++++++ 9 files changed, 172 insertions(+), 53 deletions(-) create mode 100644 services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py index 27eb1c8..12c217c 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py @@ -15,7 +15,6 @@ from ..core.publisher import Publisher from functools import wraps invoker_operations = InvokerManagementOperations() -publisher_ops = Publisher() valid_user = ControlAccess() @@ -59,11 +58,6 @@ def onboarded_invokers_onboarding_id_delete(onboarding_id): # noqa: E501 current_app.logger.info("Removing invoker") res = invoker_operations.remove_apiinvokerenrolmentdetail(onboarding_id) - if res.status_code == 204: - current_app.logger.info("Invoker Removed") - publisher_ops.publish_message("events", "API_INVOKER_OFFBOARDED") - publisher_ops.publish_message("internal-messages", f"invoker-removed:{onboarding_id}") - return res @cert_validation() @@ -84,10 +78,6 @@ def onboarded_invokers_onboarding_id_put(onboarding_id, body): # noqa: E501 body = APIInvokerEnrolmentDetails.from_dict(connexion.request.get_json()) # noqa: E501 res = invoker_operations.update_apiinvokerenrolmentdetail(onboarding_id,body) - if res.status_code == 200: - current_app.logger.info("Invoker Updated") - publisher_ops.publish_message("events", "API_INVOKER_UPDATED") - return res @@ -111,8 +101,5 @@ def onboarded_invokers_post(body): # noqa: E501 current_app.logger.info("Creating Invoker") res = invoker_operations.add_apiinvokerenrolmentdetail(body, username, uuid) - if res.status_code == 201: - current_app.logger.info("Invoker Created") - publisher_ops.publish_message("events", "API_INVOKER_ONBOARDED") return res diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py index 2d43af2..3a8ec3f 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py @@ -11,9 +11,10 @@ from .auth_manager import AuthManager from .resources import Resource from ..config import Config from api_invoker_management.models.api_invoker_enrolment_details import APIInvokerEnrolmentDetails +from .redis_event import RedisEvent +from .publisher import Publisher - - +publisher_ops = Publisher() class InvokerManagementOperations(Resource): def __check_api_invoker_id(self, api_invoker_id): @@ -93,6 +94,10 @@ class InvokerManagementOperations(Resource): res = make_response(object=apiinvokerenrolmentdetail, status=201) res.headers['Location'] = "/api-invoker-management/v1/onboardedInvokers/" + str(api_invoker_id) + + if res.status_code == 201: + current_app.logger.info("Invoker Created") + RedisEvent("API_INVOKER_ONBOARDED", "apiInvokerIds", [str(api_invoker_id)]).send_event() return res # except Exception as e: @@ -130,6 +135,9 @@ class InvokerManagementOperations(Resource): current_app.logger.debug("Invoker Resource inserted in database") res = make_response(object=APIInvokerEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) + if res.status_code == 200: + current_app.logger.info("Invoker Updated") + RedisEvent("API_INVOKER_UPDATED", "apiInvokerIds", [onboard_id]).send_event() return res except Exception as e: @@ -153,7 +161,12 @@ class InvokerManagementOperations(Resource): current_app.logger.debug("Invoker resource removed from database") current_app.logger.debug("Netapp offboarded sucessfuly") out = "The Netapp matching onboardingId " + onboard_id + " was offboarded." - return make_response(out, status=204) + res = make_response(out, status=204) + if res.status_code == 204: + current_app.logger.info("Invoker Removed") + RedisEvent("API_INVOKER_OFFBOARDED", "apiInvokerIds", [onboard_id]).send_event() + publisher_ops.publish_message("internal-messages", f"invoker-removed:{onboard_id}") + return res except Exception as e: exception = "An exception occurred in remove invoker" diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py new file mode 100644 index 0000000..037eade --- /dev/null +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py @@ -0,0 +1,40 @@ +from ..encoder import JSONEncoder +from .publisher import Publisher +import json + +publisher_ops = Publisher() + + +class RedisEvent(): + def __init__(self, event, event_detail_key, information) -> None: + self.EVENTS_ENUM = [ + 'SERVICE_API_AVAILABLE', + 'SERVICE_API_UNAVAILABLE', + 'SERVICE_API_UPDATE', + 'API_INVOKER_ONBOARDED', + 'API_INVOKER_OFFBOARDED', + 'SERVICE_API_INVOCATION_SUCCESS', + 'SERVICE_API_INVOCATION_FAILURE', + 'ACCESS_CONTROL_POLICY_UPDATE', + 'ACCESS_CONTROL_POLICY_UNAVAILABLE', + 'API_INVOKER_AUTHORIZATION_REVOKED', + 'API_INVOKER_UPDATED', + 'API_TOPOLOGY_HIDING_CREATED', + 'API_TOPOLOGY_HIDING_REVOKED'] + if event not in self.EVENTS_ENUM: + raise Exception( + "Event (" + event + ") is not on event enum (" + ','.join(self.EVENTS_ENUM) + ")") + self.redis_event = { + "event": event, + "key": event_detail_key, + "information": information + } + + def to_string(self): + return json.dumps(self.redis_event, cls=JSONEncoder) + + def send_event(self): + publisher_ops.publish_message("events-log", self.to_string()) + + def __call__(self): + return self.redis_event diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py index 6ba9043..399ac74 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py @@ -93,7 +93,7 @@ class ProviderManagementOperations(Resource): self.auth_manager.remove_auth_provider([apf_id[0], aef_id[0], amf_id[0]]) - self.publish_ops.publish_message("internal-messages", f"provider-removed:{aef_id[0]}:{apf_id[0]}") + self.publish_ops.publish_message("internal-messages", f"provider-removed:{aef_id[0]}:{apf_id[0]}:{amf_id[0]}") return make_response(object=out, status=204) except Exception as e: diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py b/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py index fa38290..5012448 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py @@ -24,17 +24,17 @@ class Subscriber(): if raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "events": current_app.logger.info("Event received") self.notification.send_notifications(raw_message["data"].decode('utf-8')) - if raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "events-log": + elif raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "events-log": current_app.logger.info("Event-log received") event_redis=json.loads(raw_message["data"].decode('utf-8')) current_app.logger.info(json.dumps(event_redis, indent=4)) self.notification.send_notifications_new(event_redis) - - elif raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "internal-messages": - message, *invoker_id = raw_message["data"].decode('utf-8').split(":") - if message == "invoker-removed" and len(invoker_id)>0: - self.event_ops.delete_all_events(invoker_id[0]) + message, *subscriber_ids = raw_message["data"].decode('utf-8').split(":") + if message == "invoker-removed" and len(subscriber_ids)>0: + self.event_ops.delete_all_events(subscriber_ids) + if message == "provider-removed" and len(subscriber_ids)>0: + self.event_ops.delete_all_events(subscriber_ids) diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/internal_event_ops.py b/services/TS29222_CAPIF_Events_API/capif_events/core/internal_event_ops.py index 437f7e3..e49089a 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/internal_event_ops.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/internal_event_ops.py @@ -8,13 +8,14 @@ class InternalEventOperations(Resource): Resource.__init__(self) self.auth_manager = AuthManager() - def delete_all_events(self, subscriber_id): + def delete_all_events(self, subscriber_ids): - mycol = self.db.get_col_by_name(self.db.event_collection) - my_query = {'subscriber_id': subscriber_id} - mycol.delete_many(my_query) + for subscriber_id in subscriber_ids: + mycol = self.db.get_col_by_name(self.db.event_collection) + my_query = {'subscriber_id': subscriber_id} + mycol.delete_many(my_query) - current_app.logger.info(f"Removed events for this subscriber: {subscriber_id}") + current_app.logger.info(f"Removed events for this subscriber: {subscriber_id}") #We dont need remove all auth events, becase when invoker is removed, remove auth entry #self.auth_manager.remove_auth_all_event(subscriber_id) @@ -39,21 +40,3 @@ class InternalEventOperations(Resource): except Exception as e: current_app.logger.error("An exception occurred ::" + str(e)) return False - - # def get_acls(self, service_id): - # try: - # mycol = self.db.get_col_by_name(self.db.acls_col) - - # query= {'api_id': service_id} - # acls = mycol.find(query) - - # if acls is None: - # current_app.logger.error("Not found event subscriptions") - - # else: - - # return acls - - # except Exception as e: - # current_app.logger.error("An exception occurred ::" + str(e)) - # return False \ No newline at end of file diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/consumer_messager.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/consumer_messager.py index 93d35e4..6f40a04 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/consumer_messager.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/consumer_messager.py @@ -21,7 +21,7 @@ class Subscriber(): for raw_message in self.p.listen(): if raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "internal-messages": message, *ids = raw_message["data"].decode('utf-8').split(":") - if message == "provider-removed" and len(ids)==2: + if message == "provider-removed" and len(ids) > 0: self.security_ops.delete_intern_service(ids[1]) diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/consumer_messager.py b/services/TS29222_CAPIF_Security_API/capif_security/core/consumer_messager.py index fd9c328..8bb8574 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/consumer_messager.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/consumer_messager.py @@ -8,6 +8,7 @@ from threading import Thread from .internal_security_ops import InternalSecurityOps from flask import current_app + class Subscriber(): def __init__(self): @@ -21,12 +22,7 @@ class Subscriber(): for raw_message in self.p.listen(): if raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "internal-messages": message, *ids = raw_message["data"].decode('utf-8').split(":") - if message == "invoker-removed" and len(ids)>0: + if message == "invoker-removed" and len(ids) > 0: self.security_ops.delete_intern_servicesecurity(ids[0]) if message == "provider-removed" or message == "service-removed" and len(ids) > 0: self.security_ops.update_intern_servicesecurity(ids[0]) - - - - - diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index eabace4..8d6150e 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -392,3 +392,103 @@ Invoker subscribe to Service API Update Length Should Be ${notification_events_on_mock_server} 1 List Should Contain Value ${notification_events_on_mock_server} ${notification_event_expected} +Provider subscribe to API Invoker events + [Tags] capif_api_events-9 mockserver + + # Start Mock server + Check Mock Server + Clean Mock Server + + # Register APF + ${register_user_info_provider}= Provider Default Registration + + # Subscribe to events + ${events_list}= Create List API_INVOKER_ONBOARDED API_INVOKER_UPDATED API_INVOKER_OFFBOARDED + ${request_body}= Create Events Subscription + ... events=@{events_list} + ... notificationDestination=${MOCK_SERVER_URL}/testing + ${resp}= Post Request Capif + ... /capif-events/v1/${register_user_info_provider['amf_id']}/subscriptions + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${AMF_PROVIDER_USERNAME} + + # Check Results + Check Response Variable Type And Values ${resp} 201 EventSubscription + ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + + # Register INVOKER + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + + # Update Invoker onboarded information + ${new_notification_destination}= Set Variable + ... http://${CAPIF_CALLBACK_IP}:${CAPIF_CALLBACK_PORT}/netapp_new_callback + Set To Dictionary + ... ${request_body} + ... notificationDestination=${new_notification_destination} + ${resp}= Put Request Capif + ... ${url.path} + ... ${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + # Check Update + Check Response Variable Type And Values ${resp} 200 APIInvokerEnrolmentDetails + ... notificationDestination=${new_notification_destination} + + # Remove Invoker from CCF + ${resp}= Delete Request Capif + ... ${url.path} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Call Method ${CAPIF_USERS} remove_capif_users_entry ${url.path} + + # Check Remove + Should Be Equal As Strings ${resp.status_code} 204 + + # Check Results + Sleep 3s + # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. + ${resp}= Get Mock Server Messages + ${notification_events_on_mock_server}= Set Variable ${resp.json()} + # Check if message follow EventNotification definition. + Check Variable ${notification_events_on_mock_server} EventNotification + + ## Create events expected + ${events_expected}= Create List + ${api_invoker_id}= Set Variable ${register_user_info_invoker['api_invoker_id']} + # Create Notification Events expected to be received for Onboard event + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... API_INVOKER_ONBOARDED + ... apiInvokerIds=${api_invoker_id} + Append To List ${events_expected} ${event_expected} + + # Create Notification Events expected to be received for Updated event + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... API_INVOKER_UPDATED + ... apiInvokerIds=${api_invoker_id} + Append To List ${events_expected} ${event_expected} + + # Create Notification Events expected to be received for Offboard event + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... API_INVOKER_OFFBOARDED + ... apiInvokerIds=${api_invoker_id} + Append To List ${events_expected} ${event_expected} + + Check Variable ${events_expected} EventNotification + + # Check results + ${events_expected_length}= Get Length ${event_expected} + Length Should Be ${notification_events_on_mock_server} ${events_expected_length} + FOR ${event_expected} IN @{events_expected} + Log ${event_expected} + List Should Contain Value ${notification_events_on_mock_server} ${event_expected} + END + -- GitLab From 8dad4a19e6701f54aa308e285e547137699ccc4f Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 27 May 2024 11:33:52 +0200 Subject: [PATCH 203/310] ocf deploy staging --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From d30943cc7fba94f396deead6cf2acca6bd33c9de Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 27 May 2024 11:48:42 +0200 Subject: [PATCH 204/310] ocf-deploy script staging --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From b26a36e698a391f97feae6d5c0e4615bcb088c47 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 27 May 2024 11:57:17 +0200 Subject: [PATCH 205/310] CI_COMMIT_REF_SLUG --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 2e687c4d72bb2c0ca744ec872194e86f26fe2e22 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 27 May 2024 12:54:13 +0200 Subject: [PATCH 206/310] deploy oficial staging --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 11c144504f194e3cb11194571b6fde51fbdd61ca Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 27 May 2024 12:59:02 +0200 Subject: [PATCH 207/310] staging_post_mr --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 89800d5421597ce7a07c5a59527ab213d35c8863 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 27 May 2024 13:02:18 +0200 Subject: [PATCH 208/310] rules --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 3d5d62066e8d84848f8c415c033614b34cb0bb9c Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 27 May 2024 13:03:41 +0200 Subject: [PATCH 209/310] deploy_ocf_oficial_staging --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 4180ea074edb23e303b585af6f8f9a6dc74cc5a6 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 27 May 2024 15:12:28 +0200 Subject: [PATCH 210/310] testing deploy_ocf_oficial_staging --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From ee1562b3594a765bde96fceca50576d221831f35 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 27 May 2024 15:46:50 +0200 Subject: [PATCH 211/310] $CI_COMMIT_REF_SLUG --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index 2d030d7..f6c4fd0 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me +delete me \ No newline at end of file -- GitLab From 90818df71049f2be7247d987660752b5624f8f93 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 27 May 2024 20:13:21 +0200 Subject: [PATCH 212/310] Adding accCtrlPolList --- .../core/accesscontrolpolicyapi.py | 4 +- .../core/internal_service_ops.py | 113 +++++++++++------- .../openapi_server/core/redis_event.py | 41 +++++++ .../capif_events/core/notifications.py | 2 +- .../CAPIF Api Events/capif_events_api.robot | 107 ++++++++++++++++- tests/libraries/api_events/bodyRequests.py | 84 ++++++------- 6 files changed, 261 insertions(+), 90 deletions(-) create mode 100644 services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/redis_event.py diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py index fd14e80..b5698be 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py @@ -37,8 +37,8 @@ class accessControlPolicyApi(Resource): current_app.logger.debug(policies) - api_invoker_policies = policies[0]['apiInvokerPolicies'] - current_app.logger.debug(f"apiinvokerPolicies: {api_invoker_policies}") + api_invoker_policies = policies[0]['api_invoker_policies'] + current_app.logger.debug(f"api_invoker_policies: {api_invoker_policies}") if not api_invoker_policies: current_app.logger.info(f"ACLs list is present but empty, then no ACLs found for the requested service: {service_api_id}, aef_id: {aef_id}, invoker: {api_invoker_id} and supportedFeatures: {supported_features}") #Not found error diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py index e10986a..8d6a66c 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py @@ -4,82 +4,105 @@ from .resources import Resource from ..models.api_invoker_policy import ApiInvokerPolicy from ..models.time_range_list import TimeRangeList from datetime import datetime, timedelta -from ..core.publisher import Publisher +from .redis_event import RedisEvent +from ..util import dict_to_camel_case, clean_empty + -publisher_ops = Publisher() class InternalServiceOps(Resource): - + def create_acl(self, invoker_id, service_id, aef_id): current_app.logger.info(f"Creating ACL for invoker: {invoker_id}") if "acls" not in self.db.db.list_collection_names(): self.db.db.create_collection("acls") - + mycol = self.db.get_col_by_name(self.db.acls) - res = mycol.find_one({"service_id": service_id, "aef_id":aef_id}, {"_id":0}) - + res = mycol.find_one( + {"service_id": service_id, "aef_id": aef_id}, {"_id": 0}) + if res: - current_app.logger.info(f"Adding invoker ACL for invoker {invoker_id}") - range_list = [TimeRangeList(datetime.utcnow(), datetime.utcnow()+timedelta(days=365))] - invoker_acl = ApiInvokerPolicy(invoker_id, current_app.config["invocations"]["total"], current_app.config["invocations"]["perSecond"], range_list) - r = mycol.find_one({"service_id": service_id, "aef_id":aef_id, "apiInvokerPolicies.api_invoker_id": invoker_id}, {"_id":0}) + current_app.logger.info( + f"Adding invoker ACL for invoker {invoker_id}") + range_list = [TimeRangeList( + datetime.utcnow(), datetime.utcnow()+timedelta(days=365))] + invoker_acl = ApiInvokerPolicy( + invoker_id, current_app.config["invocations"]["total"], current_app.config["invocations"]["perSecond"], range_list) + r = mycol.find_one({"service_id": service_id, "aef_id": aef_id, + "apiInvokerPolicies.api_invoker_id": invoker_id}, {"_id": 0}) if r is None: - mycol.update_one({"service_id": service_id, "aef_id":aef_id }, {"$push":{"apiInvokerPolicies":invoker_acl.to_dict()}}) + mycol.update_one({"service_id": service_id, "aef_id": aef_id}, { + "$push": {"apiInvokerPolicies": invoker_acl.to_dict()}}) else: - current_app.logger.info(f"Creating service ACLs for service: {service_id}") - range_list = [TimeRangeList(datetime.utcnow(), datetime.utcnow()+timedelta(days=365))] - invoker_acl = ApiInvokerPolicy(invoker_id, current_app.config["invocations"]["total"], current_app.config["invocations"]["perSecond"], range_list) - - + current_app.logger.info( + f"Creating service ACLs for service: {service_id}") + range_list = [TimeRangeList( + datetime.utcnow(), datetime.utcnow()+timedelta(days=365))] + invoker_acl = ApiInvokerPolicy( + invoker_id, current_app.config["invocations"]["total"], current_app.config["invocations"]["perSecond"], range_list) service_acls = { "service_id": service_id, "aef_id": aef_id, - "apiInvokerPolicies": [invoker_acl.to_dict()] + "api_invoker_policies": [invoker_acl.to_dict()] } - mycol.insert_one(service_acls) - publisher_ops.publish_message("events", "ACCESS_CONTROL_POLICY_UPDATE") - - current_app.logger.info(f"Invoker ACL added for invoker: {invoker_id} for service: {service_id}") - + result = mycol.insert_one(service_acls) + + inserted_service_acls=mycol.find_one({"_id": result.inserted_id}, {"_id": 0}) + current_app.logger.info(inserted_service_acls) + inserted_service_acls_camel=dict_to_camel_case(inserted_service_acls) + current_app.logger.info(inserted_service_acls_camel) + accCtrlPolListExt = { + "apiId": service_id, + "apiInvokerPolicies": inserted_service_acls_camel['apiInvokerPolicies'] + } + RedisEvent("ACCESS_CONTROL_POLICY_UPDATE", + "accCtrlPolList", accCtrlPolListExt).send_event() + + current_app.logger.info( + f"Invoker ACL added for invoker: {invoker_id} for service: {service_id}") + def remove_acl(self, invoker_id, service_id, aef_id): current_app.logger.info(f"Removing ACL for invoker: {invoker_id}") - + mycol = self.db.get_col_by_name(self.db.acls) - res = mycol.find_one({"service_id": service_id, "aef_id":aef_id}, {"_id":0}) - + res = mycol.find_one( + {"service_id": service_id, "aef_id": aef_id}, {"_id": 0}) + if res: - mycol.update_many({"service_id": service_id, "aef_id":aef_id}, - {"$pull":{ "apiInvokerPolicies": { "api_invoker_id": invoker_id }}} - ) + mycol.update_many({"service_id": service_id, "aef_id": aef_id}, + {"$pull": {"api_invoker_policies": { + "api_invoker_id": invoker_id}}} + ) else: - current_app.logger.info(f"Not found: {service_id} for api : {service_id}") - - publisher_ops.publish_message("events", "ACCESS_CONTROL_POLICY_UNAVAILABLE") - - current_app.logger.info(f"Invoker ACL removed for invoker: {invoker_id} for service: {service_id}") - + current_app.logger.info( + f"Not found: {service_id} for api : {service_id}") + + RedisEvent("ACCESS_CONTROL_POLICY_UNAVAILABLE").send_event() + + current_app.logger.info( + f"Invoker ACL removed for invoker: {invoker_id} for service: {service_id}") + def remove_invoker_acl(self, invoker_id): current_app.logger.info(f"Removing ACLs for invoker: {invoker_id}") mycol = self.db.get_col_by_name(self.db.acls) - - mycol.update_many({"apiInvokerPolicies.api_invoker_id": invoker_id}, - {"$pull":{ "apiInvokerPolicies": { "api_invoker_id": invoker_id }}} - ) - publisher_ops.publish_message("events", "ACCESS_CONTROL_POLICY_UNAVAILABLE") + + mycol.update_many({"api_invoker_policies.api_invoker_id": invoker_id}, + {"$pull": {"api_invoker_policies": { + "api_invoker_id": invoker_id}}} + ) + RedisEvent("ACCESS_CONTROL_POLICY_UNAVAILABLE").send_event() current_app.logger.info(f"ACLs for invoker: {invoker_id} removed") - + def remove_provider_acls(self, id): current_app.logger.info(f"Removing ACLs for provider/service: {id}") mycol = self.db.get_col_by_name(self.db.acls) - - mycol.delete_many({"$or":[{"service_id":id}, {"aef_id":id}]} - ) - publisher_ops.publish_message("events", "ACCESS_CONTROL_POLICY_UNAVAILABLE") - current_app.logger.info(f"ACLs for provider/service: {id} removed") \ No newline at end of file + + mycol.delete_many({"$or": [{"service_id": id}, {"aef_id": id}]}) + RedisEvent("ACCESS_CONTROL_POLICY_UNAVAILABLE").send_event() + current_app.logger.info(f"ACLs for provider/service: {id} removed") diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/redis_event.py b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/redis_event.py new file mode 100644 index 0000000..da494eb --- /dev/null +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/redis_event.py @@ -0,0 +1,41 @@ +from ..encoder import JSONEncoder +from .publisher import Publisher +import json + +publisher_ops = Publisher() + + +class RedisEvent(): + def __init__(self, event, event_detail_key=None, information=None) -> None: + self.EVENTS_ENUM = [ + 'SERVICE_API_AVAILABLE', + 'SERVICE_API_UNAVAILABLE', + 'SERVICE_API_UPDATE', + 'API_INVOKER_ONBOARDED', + 'API_INVOKER_OFFBOARDED', + 'SERVICE_API_INVOCATION_SUCCESS', + 'SERVICE_API_INVOCATION_FAILURE', + 'ACCESS_CONTROL_POLICY_UPDATE', + 'ACCESS_CONTROL_POLICY_UNAVAILABLE', + 'API_INVOKER_AUTHORIZATION_REVOKED', + 'API_INVOKER_UPDATED', + 'API_TOPOLOGY_HIDING_CREATED', + 'API_TOPOLOGY_HIDING_REVOKED'] + if event not in self.EVENTS_ENUM: + raise Exception( + "Event (" + event + ") is not on event enum (" + ','.join(self.EVENTS_ENUM) + ")") + self.redis_event = { + "event": event + } + if event_detail_key != None and information != None: + self.redis_event['key'] = event_detail_key + self.redis_event['information'] = information + + def to_string(self): + return json.dumps(self.redis_event, cls=JSONEncoder) + + def send_event(self): + publisher_ops.publish_message("events-log", self.to_string()) + + def __call__(self): + return self.redis_event diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py index e7b7dd5..0ece38e 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py @@ -45,7 +45,7 @@ class Notifications(): event_detail=None if redis_event.get('key', None) != None and redis_event.get('information', None) != None: event_detail={redis_event.get('key'):redis_event.get('information')} - + current_app.logger.debug(event_detail) data = EventNotification(sub["subscription_id"], events=redis_event.get('event'), event_detail=event_detail) current_app.logger.debug(json.dumps(data,cls=JSONEncoder)) diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index 8d6150e..1e86788 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -485,7 +485,112 @@ Provider subscribe to API Invoker events Check Variable ${events_expected} EventNotification # Check results - ${events_expected_length}= Get Length ${event_expected} + ${events_expected_length}= Get Length ${events_expected} + Length Should Be ${notification_events_on_mock_server} ${events_expected_length} + FOR ${event_expected} IN @{events_expected} + Log ${event_expected} + List Should Contain Value ${notification_events_on_mock_server} ${event_expected} + END + +Invoker subscribed to ACL update event + [Tags] capif_api_events-10 mockserver + + # Start Mock server + Check Mock Server + Clean Mock Server + + # Register APF + ${register_user_info_provider}= Provider Default Registration + + # Publish one api + ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api + ... ${register_user_info_provider} + + # Store apiId1 + ${serviceApiId}= Set Variable ${service_api_description_published['apiId']} + + # Register INVOKER + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + + # Subscribe to events + ${events_list}= Create List ACCESS_CONTROL_POLICY_UPDATE + ${request_body}= Create Events Subscription + ... events=@{events_list} + ... notificationDestination=${MOCK_SERVER_URL}/testing + ${resp}= Post Request Capif + ... /capif-events/v1/${register_user_info_provider['amf_id']}/subscriptions + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${AMF_PROVIDER_USERNAME} + + # Check Results + Check Response Variable Type And Values ${resp} 201 EventSubscription + ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + + + # Test + ${discover_response}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${discover_response} 200 DiscoveredAPIs + + # create Security Context + ${request_service_security_body}= Create Service Security From Discover Response + ... http://${CAPIF_HOSTNAME}:${CAPIF_HTTP_PORT}/test + ... ${discover_response} + ${resp}= Put Request Capif + ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} + ... json=${request_service_security_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + # Check Service Security + Check Response Variable Type And Values ${resp} 201 ServiceSecurity + ${resource_url}= Check Location Header ${resp} ${LOCATION_SECURITY_RESOURCE_REGEX} + + ${resp}= Get Request Capif + ... /access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${register_user_info_provider['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${AEF_PROVIDER_USERNAME} + + Check Response Variable Type And Values ${resp} 200 AccessControlPolicyList + # Check returned values + Should Not Be Empty ${resp.json()['apiInvokerPolicies']} + Length Should Be ${resp.json()['apiInvokerPolicies']} 1 + Should Be Equal As Strings + ... ${resp.json()['apiInvokerPolicies'][0]['apiInvokerId']} + ... ${register_user_info_invoker['api_invoker_id']} + + ${api_invoker_policies}= Set Variable ${resp.json()['apiInvokerPolicies']} + + # Check Results + Sleep 3s + # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. + ${resp}= Get Mock Server Messages + ${notification_events_on_mock_server}= Set Variable ${resp.json()} + + ## Create events expected + ${acc_ctrl_pol_list}= Create Dictionary apiId=${serviceApiId} apiInvokerPolicies=${api_invoker_policies} + Check Variable ${acc_ctrl_pol_list} AccessControlPolicyListExt + + ${events_expected}= Create List + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... ACCESS_CONTROL_POLICY_UPDATE + ... accCtrlPolList=${acc_ctrl_pol_list} + + Append To List ${events_expected} ${event_expected} + + Check Variable ${events_expected} EventNotification + + # Check results + ${events_expected_length}= Get Length ${events_expected} Length Should Be ${notification_events_on_mock_server} ${events_expected_length} FOR ${event_expected} IN @{events_expected} Log ${event_expected} diff --git a/tests/libraries/api_events/bodyRequests.py b/tests/libraries/api_events/bodyRequests.py index 14c8bb4..17fd1b5 100644 --- a/tests/libraries/api_events/bodyRequests.py +++ b/tests/libraries/api_events/bodyRequests.py @@ -1,33 +1,35 @@ -def create_events_subscription(events=["SERVICE_API_AVAILABLE", "API_INVOKER_ONBOARDED"],notificationDestination="http://robot.testing", eventFilters=None, eventReq=None, requestTestNotification=None, supportedFeatures=None,websockNotifConfig=None): - event_subscription={ +def create_events_subscription(events=["SERVICE_API_AVAILABLE", "API_INVOKER_ONBOARDED"], notificationDestination="http://robot.testing", eventFilters=None, eventReq=None, requestTestNotification=None, supportedFeatures=None, websockNotifConfig=None): + event_subscription = { "events": events, "notificationDestination": notificationDestination, } if eventFilters != None: - event_subscription['eventFilters']=eventFilters + event_subscription['eventFilters'] = eventFilters if eventReq != None: - event_subscription['eventReq']=eventReq + event_subscription['eventReq'] = eventReq if requestTestNotification != None: - event_subscription['requestTestNotification']=requestTestNotification + event_subscription['requestTestNotification'] = requestTestNotification if supportedFeatures != None: - event_subscription['supportedFeatures']=supportedFeatures + event_subscription['supportedFeatures'] = supportedFeatures if websockNotifConfig != None: - event_subscription['websockNotifConfig']=websockNotifConfig - + event_subscription['websockNotifConfig'] = websockNotifConfig + return event_subscription + def create_capif_event_filter(aefIds=None, apiIds=None, apiInvokerIds=None): if aefIds == None and apiIds == None and apiInvokerIds: - raise("Error, no data present to create event filter") - capif_event_filter=dict() + raise ("Error, no data present to create event filter") + capif_event_filter = dict() if aefIds != None: - capif_event_filter['aefIds']=aefIds + capif_event_filter['aefIds'] = aefIds if apiIds != None: - capif_event_filter['apiIds']=apiIds + capif_event_filter['apiIds'] = apiIds if apiInvokerIds != None: - capif_event_filter['apiInvokerIds']=apiInvokerIds + capif_event_filter['apiInvokerIds'] = apiInvokerIds return capif_event_filter + def create_default_event_req(): return { "grpRepTime": 5, @@ -39,55 +41,55 @@ def create_default_event_req(): "sampRatio": 15 } + def create_websock_notif_config_default(): return { "requestWebsocketUri": True, "websocketUri": "websocketUri" } + def create_notification_event(subscriptionId, event, serviceAPIDescriptions=None, apiIds=None, apiInvokerIds=None, accCtrlPolList=None, invocationLogs=None, apiTopoHide=None): - result={ - "subscriptionId":subscriptionId, + result = { + "subscriptionId": subscriptionId, "events": event, "eventDetail": dict() } - count=0 + count = 0 if serviceAPIDescriptions != None: - if isinstance(serviceAPIDescriptions,list): - result['eventDetail']['serviceAPIDescriptions']=serviceAPIDescriptions + if isinstance(serviceAPIDescriptions, list): + result['eventDetail']['serviceAPIDescriptions'] = serviceAPIDescriptions else: - result['eventDetail']['serviceAPIDescriptions']=[serviceAPIDescriptions] - count=count+1 + result['eventDetail']['serviceAPIDescriptions'] = [ + serviceAPIDescriptions] + count = count+1 if apiIds != None: - if isinstance(apiIds,list): - result['eventDetail']['apiIds']=apiIds + if isinstance(apiIds, list): + result['eventDetail']['apiIds'] = apiIds else: - result['eventDetail']['apiIds']=[apiIds] - count=count+1 + result['eventDetail']['apiIds'] = [apiIds] + count = count+1 if apiInvokerIds != None: - if isinstance(apiInvokerIds,list): - result['eventDetail']['apiInvokerIds']=apiInvokerIds + if isinstance(apiInvokerIds, list): + result['eventDetail']['apiInvokerIds'] = apiInvokerIds else: - result['eventDetail']['apiInvokerIds']=[apiInvokerIds] - count=count+1 + result['eventDetail']['apiInvokerIds'] = [apiInvokerIds] + count = count+1 if accCtrlPolList != None: - if isinstance(accCtrlPolList,list): - result['eventDetail']['accCtrlPolList']=accCtrlPolList - else: - result['eventDetail']['accCtrlPolList']=[accCtrlPolList] - count=count+1 + result['eventDetail']['accCtrlPolList'] = accCtrlPolList + count = count+1 if invocationLogs != None: - if isinstance(invocationLogs,list): - result['eventDetail']['invocationLogs']=invocationLogs + if isinstance(invocationLogs, list): + result['eventDetail']['invocationLogs'] = invocationLogs else: - result['eventDetail']['invocationLogs']=[invocationLogs] - count=count+1 + result['eventDetail']['invocationLogs'] = [invocationLogs] + count = count+1 if apiTopoHide != None: - if isinstance(apiTopoHide,list): - result['eventDetail']['apiTopoHide']=apiTopoHide + if isinstance(apiTopoHide, list): + result['eventDetail']['apiTopoHide'] = apiTopoHide else: - result['eventDetail']['apiTopoHide']=[apiTopoHide] - count=count+1 + result['eventDetail']['apiTopoHide'] = [apiTopoHide] + count = count+1 if count == 0: del result['eventDetail'] -- GitLab From 62a965e035c01490377a612caf9eb5dcb789c82f Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 28 May 2024 12:08:53 +0200 Subject: [PATCH 213/310] Fixed issue storing data with camelcase mixed with snake case --- .../openapi_server/core/accesscontrolpolicyapi.py | 4 ++-- .../openapi_server/core/internal_service_ops.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py index b5698be..f644cae 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py @@ -20,8 +20,8 @@ class accessControlPolicyApi(Resource): projection = {"_id":0} if api_invoker_id is not None: - query['apiInvokerPolicies.api_invoker_id'] = api_invoker_id - projection['apiInvokerPolicies.$'] = 1 + query['api_invoker_policies.api_invoker_id'] = api_invoker_id + projection['api_invoker_policies.$'] = 1 if supported_features is not None: current_app.logger.debug(f"SupportedFeatures present on query with value {supported_features}, but currently not used") diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py index 8d6a66c..26a7d53 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py @@ -30,10 +30,10 @@ class InternalServiceOps(Resource): invoker_acl = ApiInvokerPolicy( invoker_id, current_app.config["invocations"]["total"], current_app.config["invocations"]["perSecond"], range_list) r = mycol.find_one({"service_id": service_id, "aef_id": aef_id, - "apiInvokerPolicies.api_invoker_id": invoker_id}, {"_id": 0}) + "api_invoker_policies.api_invoker_id": invoker_id}, {"_id": 0}) if r is None: mycol.update_one({"service_id": service_id, "aef_id": aef_id}, { - "$push": {"apiInvokerPolicies": invoker_acl.to_dict()}}) + "$push": {"api_invoker_policies": invoker_acl.to_dict()}}) else: current_app.logger.info( f"Creating service ACLs for service: {service_id}") -- GitLab From 6b59d7fa18c150680c9e872eae8a1e7479a16a42 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 28 May 2024 12:11:07 +0200 Subject: [PATCH 214/310] Code Format and Code refactor at Security Service --- .../controllers/default_controller.py | 5 +- .../capif_security/core/servicesecurity.py | 193 +++++++++++------- 2 files changed, 122 insertions(+), 76 deletions(-) diff --git a/services/TS29222_CAPIF_Security_API/capif_security/controllers/default_controller.py b/services/TS29222_CAPIF_Security_API/capif_security/controllers/default_controller.py index aee7098..e37ecde 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/controllers/default_controller.py @@ -117,10 +117,7 @@ def trusted_invokers_api_invoker_id_delete_post(api_invoker_id, body): # noqa: current_app.logger.info("Revoking permissions") res = service_security_ops.revoke_api_authorization(api_invoker_id, body) - if res.status_code == 204: - current_app.logger.info("Permissions revoked") - publish_ops.publish_message("events", "API_INVOKER_AUTHORIZATION_REVOKED") - + return res @cert_validation() diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py index 69475ac..8f48465 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py @@ -25,19 +25,22 @@ from .responses import not_found_error, make_response, bad_request_error, intern from .notification import Notifications from .resources import Resource import os +from .redis_event import RedisEvent publish_ops = Publisher() security_context_not_found_detail = "Security context not found" api_invoker_no_context_cause = "API Invoker has no security context" + class SecurityOperations(Resource): def __check_invoker(self, api_invoker_id): invokers_col = self.db.get_col_by_name(self.db.capif_invokers) - current_app.logger.debug("Checking api invoker with id: " + api_invoker_id) - invoker = invokers_col.find_one({"api_invoker_id": api_invoker_id}) + current_app.logger.debug( + "Checking api invoker with id: " + api_invoker_id) + invoker = invokers_col.find_one({"api_invoker_id": api_invoker_id}) if invoker is None: current_app.logger.error("Invoker not found") return not_found_error(detail="Invoker not found", cause="API Invoker not exists or invalid ID") @@ -52,12 +55,14 @@ class SecurityOperations(Resource): header = scope[0:4] if header != "3gpp": current_app.logger.error("Bad format scope") - token_error = AccessTokenErr(error="invalid_scope", error_description="The first characters must be '3gpp'") + token_error = AccessTokenErr( + error="invalid_scope", error_description="The first characters must be '3gpp'") return make_response(object=token_error, status=400) _, body = scope.split("#") - capif_service_col = self.db.get_col_by_name(self.db.capif_service_col) + capif_service_col = self.db.get_col_by_name( + self.db.capif_service_col) security_info = security_context["security_info"] aef_security_context = [info["aef_id"] for info in security_info] @@ -65,22 +70,28 @@ class SecurityOperations(Resource): for group in groups: aef_id, api_names = group.split(":") if aef_id not in aef_security_context: - current_app.logger.error("Bad format Scope, not valid aef id ") - token_error = AccessTokenErr(error="invalid_scope", error_description="One of aef_id not belongs of your security context") + current_app.logger.error( + "Bad format Scope, not valid aef id ") + token_error = AccessTokenErr( + error="invalid_scope", error_description="One of aef_id not belongs of your security context") return make_response(object=token_error, status=400) api_names = api_names.split(",") for api_name in api_names: - service = capif_service_col.find_one({"$and": [{"api_name":api_name},{self.filter_aef_id:aef_id}]}) + service = capif_service_col.find_one( + {"$and": [{"api_name": api_name}, {self.filter_aef_id: aef_id}]}) if service is None: - current_app.logger.error("Bad format Scope, not valid api name") - token_error = AccessTokenErr(error="invalid_scope", error_description="One of the api names does not exist or is not associated with the aef id provided") + current_app.logger.error( + "Bad format Scope, not valid api name") + token_error = AccessTokenErr( + error="invalid_scope", error_description="One of the api names does not exist or is not associated with the aef id provided") return make_response(object=token_error, status=400) return None except Exception as e: current_app.logger.error("Bad format Scope: " + e) - token_error = AccessTokenErr(error="invalid_scope", error_description="malformed scope") + token_error = AccessTokenErr( + error="invalid_scope", error_description="malformed scope") return make_response(object=token_error, status=400) def __init__(self): @@ -93,16 +104,18 @@ class SecurityOperations(Resource): try: - current_app.logger.debug("Obtainig security context with id: " + api_invoker_id) + current_app.logger.debug( + "Obtainig security context with id: " + api_invoker_id) result = self.__check_invoker(api_invoker_id) if result != None: return result else: - services_security_object = mycol.find_one({"api_invoker_id": api_invoker_id}, {"_id":0, "api_invoker_id":0}) + services_security_object = mycol.find_one({"api_invoker_id": api_invoker_id}, { + "_id": 0, "api_invoker_id": 0}) if services_security_object is None: current_app.logger.error("Not found security context") - return not_found_error(detail= security_context_not_found_detail, cause=api_invoker_no_context_cause) + return not_found_error(detail=security_context_not_found_detail, cause=api_invoker_no_context_cause) if not authentication_info: for security_info_obj in services_security_object['security_info']: @@ -111,12 +124,15 @@ class SecurityOperations(Resource): for security_info_obj in services_security_object['security_info']: del security_info_obj['authorization_info'] - properyly_json= json.dumps(services_security_object, default=json_util.default) - my_service_security = dict_to_camel_case(json.loads(properyly_json)) + properyly_json = json.dumps( + services_security_object, default=json_util.default) + my_service_security = dict_to_camel_case( + json.loads(properyly_json)) my_service_security = clean_empty(my_service_security) - current_app.logger.debug("Obtained security context from database") - + current_app.logger.debug( + "Obtained security context from database") + res = make_response(object=my_service_security, status=200) return res @@ -125,7 +141,6 @@ class SecurityOperations(Resource): current_app.logger.error(exception + "::" + str(e)) return internal_server_error(detail=exception, cause=str(e)) - def create_servicesecurity(self, api_invoker_id, service_security): mycol = self.db.get_col_by_name(self.db.security_info) @@ -139,43 +154,54 @@ class SecurityOperations(Resource): if rfc3987.match(service_security.notification_destination, rule="URI") is None: current_app.logger.error("Bad url format") - return bad_request_error(detail="Bad Param", cause = "Detected Bad format of param", invalid_params=[{"param": "notificationDestination", "reason": "Not valid URL format"}]) + return bad_request_error(detail="Bad Param", cause="Detected Bad format of param", invalid_params=[{"param": "notificationDestination", "reason": "Not valid URL format"}]) - services_security_object = mycol.find_one({"api_invoker_id": api_invoker_id}) + services_security_object = mycol.find_one( + {"api_invoker_id": api_invoker_id}) if services_security_object is not None: - current_app.logger.error("Already security context defined with same api invoker id") + current_app.logger.error( + "Already security context defined with same api invoker id") return forbidden_error(detail="Security method already defined", cause="Identical AEF Profile IDs") - for service_instance in service_security.security_info: if service_instance.interface_details is not None: security_methods = service_instance.interface_details.security_methods pref_security_methods = service_instance.pref_security_methods - valid_security_method = set(security_methods) & set(pref_security_methods) + valid_security_method = set( + security_methods) & set(pref_security_methods) else: - capif_service_col = self.db.get_col_by_name(self.db.capif_service_col) - services_security_object = capif_service_col.find_one({"api_id":service_instance.api_id, self.filter_aef_id: service_instance.aef_id}, {"aef_profiles.security_methods.$":1}) + capif_service_col = self.db.get_col_by_name( + self.db.capif_service_col) + services_security_object = capif_service_col.find_one( + {"api_id": service_instance.api_id, self.filter_aef_id: service_instance.aef_id}, {"aef_profiles.security_methods.$": 1}) if services_security_object is None: - current_app.logger.error("Not found service with this aef id: " + service_instance.aef_id) + current_app.logger.error( + "Not found service with this aef id: " + service_instance.aef_id) return not_found_error(detail="Service with this aefId not found", cause="Not found Service") pref_security_methods = service_instance.pref_security_methods - valid_security_methods = [security_method for array_methods in services_security_object["aef_profiles"] for security_method in array_methods["security_methods"]] - valid_security_method = set(valid_security_methods) & set(pref_security_methods) + valid_security_methods = [security_method for array_methods in services_security_object["aef_profiles"] + for security_method in array_methods["security_methods"]] + valid_security_method = set( + valid_security_methods) & set(pref_security_methods) if len(list(valid_security_method)) == 0: - current_app.logger.error("Not found comptaible security method with pref security method") + current_app.logger.error( + "Not found comptaible security method with pref security method") return bad_request_error(detail="Not found compatible security method with pref security method", cause="Error pref security method", invalid_params=[{"param": "prefSecurityMethods", "reason": "pref security method not compatible with security method available"}]) - service_instance.sel_security_method = list(valid_security_method)[0] + service_instance.sel_security_method = list( + valid_security_method)[0] # Send service instance to ACL current_app.logger.debug("Sending message to create ACL") - publish_ops.publish_message("acls-messages", "create-acl:"+str(api_invoker_id)+":"+str(service_instance.api_id)+":"+str(service_instance.aef_id)) - current_app.logger.debug("Inserted security context in database") + publish_ops.publish_message("acls-messages", "create-acl:"+str( + api_invoker_id)+":"+str(service_instance.api_id)+":"+str(service_instance.aef_id)) + current_app.logger.debug( + "Inserted security context in database") rec = dict() rec['api_invoker_id'] = api_invoker_id @@ -183,7 +209,8 @@ class SecurityOperations(Resource): mycol.insert_one(rec) res = make_response(object=service_security, status=201) - res.headers['Location'] = "https://{}/capif-security/v1/trustedInvokers/{}".format(os.getenv('CAPIF_HOSTNAME'),str(api_invoker_id)) + res.headers['Location'] = "https://{}/capif-security/v1/trustedInvokers/{}".format( + os.getenv('CAPIF_HOSTNAME'), str(api_invoker_id)) return res except Exception as e: @@ -191,7 +218,6 @@ class SecurityOperations(Resource): current_app.logger.error(exception + "::" + str(e)) return internal_server_error(detail=exception, cause=str(e)) - def delete_servicesecurity(self, api_invoker_id): mycol = self.db.get_col_by_name(self.db.security_info) @@ -210,19 +236,22 @@ class SecurityOperations(Resource): if services_security_count == 0: current_app.logger.error(security_context_not_found_detail) return not_found_error(detail=security_context_not_found_detail, cause=api_invoker_no_context_cause) - + mycol.delete_many(my_query) - publish_ops.publish_message("acls-messages", "remove-acl:"+api_invoker_id) + publish_ops.publish_message( + "acls-messages", "remove-acl:"+api_invoker_id) - current_app.logger.debug("Removed security context from database") - out= "The security info of Netapp with Netapp ID " + api_invoker_id + " were deleted.", 204 + current_app.logger.debug( + "Removed security context from database") + out = "The security info of Netapp with Netapp ID " + \ + api_invoker_id + " were deleted.", 204 return make_response(out, status=204) except Exception as e: exception = "An exception occurred in create security info" current_app.logger.error(exception + "::" + str(e)) - return internal_server_error(detail=exception, cause = str(e)) + return internal_server_error(detail=exception, cause=str(e)) def delete_intern_servicesecurity(self, api_invoker_id): @@ -240,34 +269,41 @@ class SecurityOperations(Resource): invokers_col = self.db.get_col_by_name(self.db.capif_invokers) - current_app.logger.debug("Checking api invoker with id: " + access_token_req["client_id"]) - invoker = invokers_col.find_one({"api_invoker_id": access_token_req["client_id"]}) + current_app.logger.debug( + "Checking api invoker with id: " + access_token_req["client_id"]) + invoker = invokers_col.find_one( + {"api_invoker_id": access_token_req["client_id"]}) if invoker is None: - client_id_error = AccessTokenErr(error="invalid_client", error_description="Client Id not found") + client_id_error = AccessTokenErr( + error="invalid_client", error_description="Client Id not found") return make_response(object=client_id_error, status=400) - if access_token_req["grant_type"] != "client_credentials": - client_id_error = AccessTokenErr(error="unsupported_grant_type", error_description="Invalid value for `grant_type` ({0}), must be one of ['client_credentials'] - 'grant_type'" - .format(access_token_req["grant_type"])) + client_id_error = AccessTokenErr(error="unsupported_grant_type", error_description="Invalid value for `grant_type` ({0}), must be one of ['client_credentials'] - 'grant_type'" + .format(access_token_req["grant_type"])) return make_response(object=client_id_error, status=400) service_security = mycol.find_one({"api_invoker_id": security_id}) if service_security is None: - current_app.logger.error("Not found securoty context with id: " + security_id) - return not_found_error(detail= security_context_not_found_detail, cause=api_invoker_no_context_cause) + current_app.logger.error( + "Not found security context with id: " + security_id) + return not_found_error(detail=security_context_not_found_detail, cause=api_invoker_no_context_cause) - result = self.__check_scope(access_token_req["scope"], service_security) + result = self.__check_scope( + access_token_req["scope"], service_security) if result != None: return result expire_time = timedelta(minutes=10) - now=datetime.now() + now = datetime.now() - claims = AccessTokenClaims(iss = access_token_req["client_id"], scope=access_token_req["scope"], exp=int((now+expire_time).timestamp())) - access_token = create_access_token(identity = access_token_req["client_id"] , additional_claims=claims.to_dict()) - access_token_resp = AccessTokenRsp(access_token=access_token, token_type="Bearer", expires_in=int(expire_time.total_seconds()), scope=access_token_req["scope"]) + claims = AccessTokenClaims(iss=access_token_req["client_id"], scope=access_token_req["scope"], exp=int( + (now+expire_time).timestamp())) + access_token = create_access_token( + identity=access_token_req["client_id"], additional_claims=claims.to_dict()) + access_token_resp = AccessTokenRsp(access_token=access_token, token_type="Bearer", expires_in=int( + expire_time.total_seconds()), scope=access_token_req["scope"]) current_app.logger.debug("Created access token") @@ -278,7 +314,6 @@ class SecurityOperations(Resource): current_app.logger.error(exception + "::" + str(e)) return internal_server_error(detail=exception, cause=str(e)) - def update_servicesecurity(self, api_invoker_id, service_security): mycol = self.db.get_col_by_name(self.db.security_info) try: @@ -291,38 +326,48 @@ class SecurityOperations(Resource): old_object = mycol.find_one({"api_invoker_id": api_invoker_id}) if old_object is None: - current_app.logger.error("Service api not found with id: " + api_invoker_id) + current_app.logger.error( + "Service api not found with id: " + api_invoker_id) return not_found_error(detail="Service API not existing", cause="Not exist securiy information for this invoker") for service_instance in service_security.security_info: if service_instance.interface_details is not None: security_methods = service_instance.interface_details.security_methods pref_security_methods = service_instance.pref_security_methods - valid_security_method = set(security_methods) & set(pref_security_methods) - service_instance.sel_security_method = list(valid_security_method)[0] + valid_security_method = set( + security_methods) & set(pref_security_methods) + service_instance.sel_security_method = list( + valid_security_method)[0] else: - capif_service_col = self.db.get_col_by_name(self.db.capif_service_col) - services_security_object = capif_service_col.find_one({self.filter_aef_id: service_instance.aef_id}, {"aef_profiles.security_methods.$":1}) + capif_service_col = self.db.get_col_by_name( + self.db.capif_service_col) + services_security_object = capif_service_col.find_one( + {self.filter_aef_id: service_instance.aef_id}, {"aef_profiles.security_methods.$": 1}) if services_security_object is None: - current_app.logger.error("Service api with this aefId not found: " + service_instance.aef_id) + current_app.logger.error( + "Service api with this aefId not found: " + service_instance.aef_id) return not_found_error(detail="Service with this aefId not found", cause="Not found Service") pref_security_methods = service_instance.pref_security_methods - valid_security_methods = [security_method for array_methods in services_security_object["aef_profiles"] for security_method in array_methods["security_methods"]] - valid_security_method = set(valid_security_methods) & set(pref_security_methods) - service_instance.sel_security_method = list(valid_security_method)[0] + valid_security_methods = [security_method for array_methods in services_security_object["aef_profiles"] + for security_method in array_methods["security_methods"]] + valid_security_method = set( + valid_security_methods) & set(pref_security_methods) + service_instance.sel_security_method = list( + valid_security_method)[0] service_security = service_security.to_dict() service_security = clean_empty(service_security) - result = mycol.find_one_and_update(old_object, {"$set":service_security}, projection={'_id': 0, "api_invoker_id":0},return_document=ReturnDocument.AFTER ,upsert=False) + result = mycol.find_one_and_update(old_object, {"$set": service_security}, projection={ + '_id': 0, "api_invoker_id": 0}, return_document=ReturnDocument.AFTER, upsert=False) result = clean_empty(result) current_app.logger.debug("Updated security context") - res= make_response(object=dict_to_camel_case(result), status=200) + res = make_response(object=dict_to_camel_case(result), status=200) res.headers['Location'] = "https://${CAPIF_HOSTNAME}/capif-security/v1/trustedInvokers/" + str( api_invoker_id) return res @@ -331,7 +376,6 @@ class SecurityOperations(Resource): current_app.logger.error(exception + "::" + str(e)) return internal_server_error(detail=exception, cause=str(e)) - def revoke_api_authorization(self, api_invoker_id, security_notification): mycol = self.db.get_col_by_name(self.db.security_info) @@ -352,10 +396,12 @@ class SecurityOperations(Resource): updated_security_context = services_security_context.copy() for context in services_security_context["security_info"]: - index = services_security_context["security_info"].index(context) + index = services_security_context["security_info"].index( + context) if security_notification.aef_id == context["aef_id"] or context["api_id"] in security_notification.api_ids: current_app.logger.debug("Sending message.") - publish_ops.publish_message("acls-messages", "remove-acl:"+str(api_invoker_id)+":"+str(context["api_id"])+":"+str(security_notification.aef_id)) + publish_ops.publish_message("acls-messages", "remove-acl:"+str( + api_invoker_id)+":"+str(context["api_id"])+":"+str(security_notification.aef_id)) current_app.logger.debug("message sended.") updated_security_context["security_info"].pop(index) @@ -364,13 +410,16 @@ class SecurityOperations(Resource): if len(updated_security_context["security_info"]) == 0: mycol.delete_many(my_query) - #self.notification.send_notification(services_security_context["notification_destination"], security_notification) - current_app.logger.debug("Revoked security context") - out= "Netapp with ID " + api_invoker_id + " was revoked by some APIs.", 204 - return make_response(out, status=204) + out = "Netapp with ID " + api_invoker_id + " was revoked by some APIs.", 204 + res = make_response(out, status=204) + if res.status_code == 204: + current_app.logger.info("Permissions revoked") + RedisEvent("API_INVOKER_AUTHORIZATION_REVOKED").send_event() + + return res except Exception as e: exception = "An exception occurred in revoke security auth" current_app.logger.error(exception + "::" + str(e)) - return internal_server_error(detail=exception, cause=str(e)) \ No newline at end of file + return internal_server_error(detail=exception, cause=str(e)) -- GitLab From 62a582012f034097a45ca3ff166b00a3b9548134 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 28 May 2024 12:13:28 +0200 Subject: [PATCH 215/310] Added test 11 and 12 at Events suite --- .../capif_api_access_control_policy.robot | 1 - .../CAPIF Api Events/capif_events_api.robot | 195 ++++++++++++++++++ tests/resources/common.resource | 1 + 3 files changed, 196 insertions(+), 1 deletion(-) diff --git a/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot b/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot index 04d7898..ec0f082 100644 --- a/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot +++ b/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot @@ -238,7 +238,6 @@ Retrieve ACL with security context created by two different Invokers ... username=${AEF_PROVIDER_USERNAME} Check Response Variable Type And Values ${resp} 200 AccessControlPolicyList - # Check returned values Should Not Be Empty ${resp.json()['apiInvokerPolicies']} Length Should Be ${resp.json()['apiInvokerPolicies']} 2 diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index 1e86788..950cdad 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -597,3 +597,198 @@ Invoker subscribed to ACL update event List Should Contain Value ${notification_events_on_mock_server} ${event_expected} END +Provider receives an ACL unavailable event when invoker remove Security Context. + [Tags] capif_api_events-11 mockserver + + # Start Mock server + Check Mock Server + Clean Mock Server + + # Register APF + ${register_user_info_provider}= Provider Default Registration + + # Publish one api + ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api + ... ${register_user_info_provider} + + # Store apiId1 + ${serviceApiId}= Set Variable ${service_api_description_published['apiId']} + + # Register INVOKER + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + + # Subscribe to events + ${events_list}= Create List ACCESS_CONTROL_POLICY_UNAVAILABLE + ${request_body}= Create Events Subscription + ... events=@{events_list} + ... notificationDestination=${MOCK_SERVER_URL}/testing + ${resp}= Post Request Capif + ... /capif-events/v1/${register_user_info_provider['amf_id']}/subscriptions + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + # Check Results + Check Response Variable Type And Values ${resp} 201 EventSubscription + ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + + + # Test + ${discover_response}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${discover_response} 200 DiscoveredAPIs + + # create Security Context + ${request_service_security_body}= Create Service Security From Discover Response + ... http://${CAPIF_HOSTNAME}:${CAPIF_HTTP_PORT}/test + ... ${discover_response} + ${resp}= Put Request Capif + ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} + ... json=${request_service_security_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + # Check Service Security + Check Response Variable Type And Values ${resp} 201 ServiceSecurity + ${resource_url}= Check Location Header ${resp} ${LOCATION_SECURITY_RESOURCE_REGEX} + + # Remove Security Context by Provider + ${resp}= Delete Request Capif + ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${AEF_PROVIDER_USERNAME} + + Status Should Be 204 ${resp} + + # Check Results + Sleep 3s + # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. + ${resp}= Get Mock Server Messages + ${notification_events_on_mock_server}= Set Variable ${resp.json()} + + ## Create events expected + ${events_expected}= Create List + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... ACCESS_CONTROL_POLICY_UNAVAILABLE + Append To List ${events_expected} ${event_expected} + Check Variable ${events_expected} EventNotification + + # Check results + ${events_expected_length}= Get Length ${events_expected} + Length Should Be ${notification_events_on_mock_server} ${events_expected_length} + FOR ${event_expected} IN @{events_expected} + Log ${event_expected} + List Should Contain Value ${notification_events_on_mock_server} ${event_expected} + END + +Invoker receives an Invoker Authorization Revoked and ACL unavailable event when Provider revoke Invoker Authorization. + [Tags] capif_api_events-12 mockserver + + # Start Mock server + Check Mock Server + Clean Mock Server + + # Register APF + ${register_user_info_provider}= Provider Default Registration + + # Publish one api + ${service_api_description_published} ${resource_url} ${request_body}= Publish Service Api + ... ${register_user_info_provider} + + # Store apiId1 + ${serviceApiId}= Set Variable ${service_api_description_published['apiId']} + + # Register INVOKER + ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding + + # Subscribe to events + ${events_list}= Create List ACCESS_CONTROL_POLICY_UNAVAILABLE API_INVOKER_AUTHORIZATION_REVOKED + ${request_body}= Create Events Subscription + ... events=@{events_list} + ... notificationDestination=${MOCK_SERVER_URL}/testing + ${resp}= Post Request Capif + ... /capif-events/v1/${register_user_info_provider['amf_id']}/subscriptions + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + # Check Results + Check Response Variable Type And Values ${resp} 201 EventSubscription + ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} + + # Test + ${discover_response}= Get Request Capif + ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + Check Response Variable Type And Values ${discover_response} 200 DiscoveredAPIs + + ${api_ids}= Get Api Ids From Discover Response ${discover_response} + + # create Security Context + ${request_service_security_body}= Create Service Security From Discover Response + ... http://${CAPIF_HOSTNAME}:${CAPIF_HTTP_PORT}/test + ... ${discover_response} + ${resp}= Put Request Capif + ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']} + ... json=${request_service_security_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${INVOKER_USERNAME} + + # Check Service Security + Check Response Variable Type And Values ${resp} 201 ServiceSecurity + ${resource_url}= Check Location Header ${resp} ${LOCATION_SECURITY_RESOURCE_REGEX} + + # Revoke Security Context by Provider + ${request_body}= Create Security Notification Body + ... ${register_user_info_invoker['api_invoker_id']} + ... ${api_ids} + ${resp}= Post Request Capif + ... /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']}/delete + ... json=${request_body} + ... server=${CAPIF_HTTPS_URL} + ... verify=ca.crt + ... username=${AEF_PROVIDER_USERNAME} + + # Check Results + Status Should Be 204 ${resp} + + # Check Results + Sleep 3s + # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. + ${resp}= Get Mock Server Messages + ${notification_events_on_mock_server}= Set Variable ${resp.json()} + + ## Create events expected + ${events_expected}= Create List + ## ACCESS_CONTROL_POLICY_UNAVAILABLE event + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... ACCESS_CONTROL_POLICY_UNAVAILABLE + Append To List ${events_expected} ${event_expected} + ## API_INVOKER_AUTHORIZATION_REVOKED event + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... API_INVOKER_AUTHORIZATION_REVOKED + Append To List ${events_expected} ${event_expected} + Check Variable ${events_expected} EventNotification + + # Check results + ${events_expected_length}= Get Length ${events_expected} + Length Should Be ${notification_events_on_mock_server} ${events_expected_length} + FOR ${event_expected} IN @{events_expected} + Log ${event_expected} + List Should Contain Value ${notification_events_on_mock_server} ${event_expected} + END \ No newline at end of file diff --git a/tests/resources/common.resource b/tests/resources/common.resource index 9521310..d02891d 100644 --- a/tests/resources/common.resource +++ b/tests/resources/common.resource @@ -160,4 +160,5 @@ Get Mock Server Messages ... verify=False Status Should Be 200 ${resp} + Log List ${resp.json()} RETURN ${resp} \ No newline at end of file -- GitLab From dece008c92c4685212f9dbfaaf305d019ca2b87f Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 28 May 2024 12:14:07 +0200 Subject: [PATCH 216/310] Added RedisEvent class to Security Service --- .../capif_security/core/redis_event.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 services/TS29222_CAPIF_Security_API/capif_security/core/redis_event.py diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/redis_event.py b/services/TS29222_CAPIF_Security_API/capif_security/core/redis_event.py new file mode 100644 index 0000000..da494eb --- /dev/null +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/redis_event.py @@ -0,0 +1,41 @@ +from ..encoder import JSONEncoder +from .publisher import Publisher +import json + +publisher_ops = Publisher() + + +class RedisEvent(): + def __init__(self, event, event_detail_key=None, information=None) -> None: + self.EVENTS_ENUM = [ + 'SERVICE_API_AVAILABLE', + 'SERVICE_API_UNAVAILABLE', + 'SERVICE_API_UPDATE', + 'API_INVOKER_ONBOARDED', + 'API_INVOKER_OFFBOARDED', + 'SERVICE_API_INVOCATION_SUCCESS', + 'SERVICE_API_INVOCATION_FAILURE', + 'ACCESS_CONTROL_POLICY_UPDATE', + 'ACCESS_CONTROL_POLICY_UNAVAILABLE', + 'API_INVOKER_AUTHORIZATION_REVOKED', + 'API_INVOKER_UPDATED', + 'API_TOPOLOGY_HIDING_CREATED', + 'API_TOPOLOGY_HIDING_REVOKED'] + if event not in self.EVENTS_ENUM: + raise Exception( + "Event (" + event + ") is not on event enum (" + ','.join(self.EVENTS_ENUM) + ")") + self.redis_event = { + "event": event + } + if event_detail_key != None and information != None: + self.redis_event['key'] = event_detail_key + self.redis_event['information'] = information + + def to_string(self): + return json.dumps(self.redis_event, cls=JSONEncoder) + + def send_event(self): + publisher_ops.publish_message("events-log", self.to_string()) + + def __call__(self): + return self.redis_event -- GitLab From 45bd21d68284f1839340c16048bc707a2d2b836a Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 28 May 2024 12:27:13 +0200 Subject: [PATCH 217/310] Setup common RedisEvent object to all services that sends events to Event Service --- .../api_invoker_management/core/redis_event.py | 9 +++++---- .../api_invocation_logs/core/redis_event.py | 9 +++++---- .../published_apis/core/redis_event.py | 9 +++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py index 037eade..da494eb 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py @@ -6,7 +6,7 @@ publisher_ops = Publisher() class RedisEvent(): - def __init__(self, event, event_detail_key, information) -> None: + def __init__(self, event, event_detail_key=None, information=None) -> None: self.EVENTS_ENUM = [ 'SERVICE_API_AVAILABLE', 'SERVICE_API_UNAVAILABLE', @@ -25,10 +25,11 @@ class RedisEvent(): raise Exception( "Event (" + event + ") is not on event enum (" + ','.join(self.EVENTS_ENUM) + ")") self.redis_event = { - "event": event, - "key": event_detail_key, - "information": information + "event": event } + if event_detail_key != None and information != None: + self.redis_event['key'] = event_detail_key + self.redis_event['information'] = information def to_string(self): return json.dumps(self.redis_event, cls=JSONEncoder) diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py index 037eade..da494eb 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py @@ -6,7 +6,7 @@ publisher_ops = Publisher() class RedisEvent(): - def __init__(self, event, event_detail_key, information) -> None: + def __init__(self, event, event_detail_key=None, information=None) -> None: self.EVENTS_ENUM = [ 'SERVICE_API_AVAILABLE', 'SERVICE_API_UNAVAILABLE', @@ -25,10 +25,11 @@ class RedisEvent(): raise Exception( "Event (" + event + ") is not on event enum (" + ','.join(self.EVENTS_ENUM) + ")") self.redis_event = { - "event": event, - "key": event_detail_key, - "information": information + "event": event } + if event_detail_key != None and information != None: + self.redis_event['key'] = event_detail_key + self.redis_event['information'] = information def to_string(self): return json.dumps(self.redis_event, cls=JSONEncoder) diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py index 037eade..da494eb 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py @@ -6,7 +6,7 @@ publisher_ops = Publisher() class RedisEvent(): - def __init__(self, event, event_detail_key, information) -> None: + def __init__(self, event, event_detail_key=None, information=None) -> None: self.EVENTS_ENUM = [ 'SERVICE_API_AVAILABLE', 'SERVICE_API_UNAVAILABLE', @@ -25,10 +25,11 @@ class RedisEvent(): raise Exception( "Event (" + event + ") is not on event enum (" + ','.join(self.EVENTS_ENUM) + ")") self.redis_event = { - "event": event, - "key": event_detail_key, - "information": information + "event": event } + if event_detail_key != None and information != None: + self.redis_event['key'] = event_detail_key + self.redis_event['information'] = information def to_string(self): return json.dumps(self.redis_event, cls=JSONEncoder) -- GitLab From efc75241f990c61d00b593714903a6e06133786c Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 28 May 2024 16:25:32 +0200 Subject: [PATCH 218/310] Change redis bus name to events, also code refactor of events --- .../core/redis_event.py | 2 +- .../openapi_server/core/redis_event.py | 2 +- .../capif_events/core/consumer_messager.py | 11 +++----- .../capif_events/core/notifications.py | 26 +++++-------------- .../api_invocation_logs/core/redis_event.py | 2 +- .../published_apis/core/redis_event.py | 2 +- .../capif_security/core/redis_event.py | 2 +- 7 files changed, 15 insertions(+), 32 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py index da494eb..aadbdbb 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/redis_event.py @@ -35,7 +35,7 @@ class RedisEvent(): return json.dumps(self.redis_event, cls=JSONEncoder) def send_event(self): - publisher_ops.publish_message("events-log", self.to_string()) + publisher_ops.publish_message("events", self.to_string()) def __call__(self): return self.redis_event diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/redis_event.py b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/redis_event.py index da494eb..aadbdbb 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/redis_event.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/redis_event.py @@ -35,7 +35,7 @@ class RedisEvent(): return json.dumps(self.redis_event, cls=JSONEncoder) def send_event(self): - publisher_ops.publish_message("events-log", self.to_string()) + publisher_ops.publish_message("events", self.to_string()) def __call__(self): return self.redis_event diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py b/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py index 5012448..ce6479a 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/consumer_messager.py @@ -16,19 +16,16 @@ class Subscriber(): self.notification = Notifications() self.event_ops = InternalEventOperations() self.p = self.r.pubsub() - self.p.subscribe("events", "internal-messages", "events-log") + self.p.subscribe("events", "internal-messages") def listen(self): for raw_message in self.p.listen(): current_app.logger.info(raw_message) if raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "events": current_app.logger.info("Event received") - self.notification.send_notifications(raw_message["data"].decode('utf-8')) - elif raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "events-log": - current_app.logger.info("Event-log received") - event_redis=json.loads(raw_message["data"].decode('utf-8')) - current_app.logger.info(json.dumps(event_redis, indent=4)) - self.notification.send_notifications_new(event_redis) + redis_event=json.loads(raw_message["data"].decode('utf-8')) + current_app.logger.info(json.dumps(redis_event, indent=4)) + self.notification.send_notifications(redis_event) elif raw_message["type"] == "message" and raw_message["channel"].decode('utf-8') == "internal-messages": message, *subscriber_ids = raw_message["data"].decode('utf-8').split(":") if message == "invoker-removed" and len(subscriber_ids)>0: diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py index 0ece38e..2229a71 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py @@ -16,21 +16,7 @@ class Notifications(): def __init__(self): self.events_ops = InternalEventOperations() - def send_notifications(self, event): - current_app.logger.info("Received event, sending notifications") - subscriptions = self.events_ops.get_event_subscriptions(event) - - try: - for sub in subscriptions: - url = sub["notification_destination"] - data = EventNotification(sub["subscription_id"], events=event) - self.request_post(url, data) - - except Exception as e: - current_app.logger.error("An exception occurred ::" + str(e)) - return False - - def send_notifications_new(self, redis_event): + def send_notifications(self, redis_event): try: if redis_event.get('event', None) == None: raise("Event value is not present on received event from REDIS") @@ -69,9 +55,9 @@ class Notifications(): async def send(self, url, data): try: response = await self.send_request(url, data) - print(response) + current_app.logger.debug(response) except asyncio.TimeoutError: - print("Timeout: Request timeout") - - - + current_app.logger.error("Timeout: Request timeout") + except Exception as e: + current_app.logger.error("An exception occurred sending notification::" + str(e)) + return False diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py index da494eb..aadbdbb 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/redis_event.py @@ -35,7 +35,7 @@ class RedisEvent(): return json.dumps(self.redis_event, cls=JSONEncoder) def send_event(self): - publisher_ops.publish_message("events-log", self.to_string()) + publisher_ops.publish_message("events", self.to_string()) def __call__(self): return self.redis_event diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py index da494eb..aadbdbb 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/redis_event.py @@ -35,7 +35,7 @@ class RedisEvent(): return json.dumps(self.redis_event, cls=JSONEncoder) def send_event(self): - publisher_ops.publish_message("events-log", self.to_string()) + publisher_ops.publish_message("events", self.to_string()) def __call__(self): return self.redis_event diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/redis_event.py b/services/TS29222_CAPIF_Security_API/capif_security/core/redis_event.py index da494eb..aadbdbb 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/redis_event.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/redis_event.py @@ -35,7 +35,7 @@ class RedisEvent(): return json.dumps(self.redis_event, cls=JSONEncoder) def send_event(self): - publisher_ops.publish_message("events-log", self.to_string()) + publisher_ops.publish_message("events", self.to_string()) def __call__(self): return self.redis_event -- GitLab From 6d760a6053be10e2884affff14114461fa5d5218 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 28 May 2024 17:09:58 +0200 Subject: [PATCH 219/310] Some improvements over robot framework docker image --- services/run_capif_tests.sh | 3 ++- tests/resources/common.resource | 32 ++++++------------------------- tools/robot/Dockerfile | 12 ------------ tools/robot/basicRequirements.txt | 1 - tools/robot/basicRobotInstall.sh | 10 +--------- 5 files changed, 9 insertions(+), 49 deletions(-) diff --git a/services/run_capif_tests.sh b/services/run_capif_tests.sh index 06de230..8f9f414 100755 --- a/services/run_capif_tests.sh +++ b/services/run_capif_tests.sh @@ -25,7 +25,8 @@ CAPIF_VAULT_PORT=8200 # CAPIF_VAULT_TOKEN=dev-only-token CAPIF_VAULT_TOKEN=read-ca-token -MOCK_SERVER_URL=http://192.168.0.119:9090 + +MOCK_SERVER_URL=http://192.168.0.11:9090 echo "CAPIF_HOSTNAME = $CAPIF_HOSTNAME" diff --git a/tests/resources/common.resource b/tests/resources/common.resource index d02891d..1be4429 100644 --- a/tests/resources/common.resource +++ b/tests/resources/common.resource @@ -32,7 +32,7 @@ ${CAPIF_CALLBACK_PORT} 8086 ${REGISTER_ADMIN_USER} admin ${REGISTER_ADMIN_PASSWORD} password123 -${MOCK_SERVER_URL} http://192.168.0.14:9090 +${MOCK_SERVER_URL} ${DISCOVER_URL} /service-apis/v1/allServiceAPIs?api-invoker-id= @@ -91,33 +91,13 @@ Test ${TEST NAME} Currently Not Supported Log Test "${TEST NAME}" Currently not supported WARN Skip Test "${TEST NAME}" Currently not supported -Start Mock server - Log Starting mock Server for Robot Framework. - - # Run Process pip3 install -r /opt/robot-tests/tests/libraries/mock_server/requirements.txt - ${server_process}= Start Process python3 /opt/robot-tests/tests/libraries/mock_server/mock_server.py shell=True - # Log PID: ${server_process.pid} - Log output: ${server_process.stdout} - - Sleep 5m - Create Session mockserver ${MOCK_SERVER_URL} - - ${endpoint}= Set Variable /testing - - ${json}= Create Dictionary events="SERVICE_API_INVOCATION_SUCCESS" subscriptionId="255545008cd3937f5554a3651bbfb9" - - ${resp}= POST On Session - ... mockserver - ... ${endpoint} - ... json=${json} - ... expected_status=any - ... verify=False - - ${result}= Terminate Process ${server_process} - Check Mock Server Log Checking mock Server for Robot Framework. + IF "${MOCK_SERVER_URL}" == "" + Fail Mock Server Url is not setup on Tests execution, check MOCK_SERVER_URL variable mockserver-not-ready + END + Create Session mockserver ${MOCK_SERVER_URL} ${endpoint}= Set Variable /requests_list @@ -128,7 +108,7 @@ Check Mock Server ... expected_status=any ... verify=False - Status Should Be 200 ${resp} + Status Should Be 200 ${resp} Mock Server response is not 200 OK at ${MOCK_SERVER_URL}, check server status. Clean Mock Server Log Checking mock Server for Robot Framework. diff --git a/tools/robot/Dockerfile b/tools/robot/Dockerfile index 261b1e4..5cdc3cf 100644 --- a/tools/robot/Dockerfile +++ b/tools/robot/Dockerfile @@ -64,18 +64,6 @@ RUN python3.10 -m venv /opt/venv ENV PLAYWRIGHT_BROWSERS_PATH=$HOME/pw-browsers -# Instalación de nvm y node a la última versión -# RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash -# # RUN source /root/.bashrc -# # RUN export NVM_DIR="$HOME/.nvm" \ -# # [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm \ -# # [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion - -# RUN nvm install node -# # RUN npx playwright install -# RUN yes|npx playwright install-deps - - ADD basicRequirements.txt /root/ ADD basicRobotInstall.sh /root/ diff --git a/tools/robot/basicRequirements.txt b/tools/robot/basicRequirements.txt index 282a034..aa3e60e 100644 --- a/tools/robot/basicRequirements.txt +++ b/tools/robot/basicRequirements.txt @@ -71,7 +71,6 @@ requests==2.28.1 rfc3987==1.3.8 robotframework==7.0 robotframework-archivelibrary == 0.4.2 -robotframework-browser==18.3.0 robotframework-httpctrl==0.3.1 robotframework-lint==1.1 robotframework-mongodb-library==3.2 diff --git a/tools/robot/basicRobotInstall.sh b/tools/robot/basicRobotInstall.sh index ba66010..ae0bd6b 100644 --- a/tools/robot/basicRobotInstall.sh +++ b/tools/robot/basicRobotInstall.sh @@ -1,14 +1,6 @@ #!/bin/bash echo "Installing basic software related with robotFramework" -source /opt/venv/bin/activate; +source /opt/venv/bin/activate pip install --upgrade pip -curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash -source /root/.bashrc -nvm install node -yes|npx playwright install-deps pip install -r $1 -rfbrowser clean-node -rfbrowser init --skip-browsers -npx playwright install -npx playwright install-deps echo "Robot framework installed" -- GitLab From 32ece8bc71265e0a47bcc18cac9c8abb08aab941 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 29 May 2024 09:33:04 +0200 Subject: [PATCH 220/310] remove not used env variable at robot docker image --- tools/robot/Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/robot/Dockerfile b/tools/robot/Dockerfile index 5cdc3cf..347f226 100644 --- a/tools/robot/Dockerfile +++ b/tools/robot/Dockerfile @@ -62,8 +62,6 @@ RUN apt-get install -y --fix-missing python3.10 python3.10-venv python3.10-dev RUN mkdir /opt/venv RUN python3.10 -m venv /opt/venv -ENV PLAYWRIGHT_BROWSERS_PATH=$HOME/pw-browsers - ADD basicRequirements.txt /root/ ADD basicRobotInstall.sh /root/ -- GitLab From 458c1c712c6d83779f1652983209ba7621180e1a Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 29 May 2024 11:35:41 +0200 Subject: [PATCH 221/310] Code Refactor on Event tests --- .../CAPIF Api Events/capif_events_api.robot | 412 +++++++++--------- tests/resources/common.resource | 25 +- tests/resources/common/basicRequests.robot | 36 -- 3 files changed, 234 insertions(+), 239 deletions(-) diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index 950cdad..d5e02d2 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -142,9 +142,8 @@ Deletes an individual CAPIF Event Subscription with invalid SubscriptionId Invoker receives Service API Invocation events [Tags] capif_api_events-6 mockserver - # Start Mock server - Check Mock Server - Clean Mock Server + # Initialize Mock server + Init Mock Server # Register APF ${register_user_info}= Provider Default Registration @@ -203,33 +202,19 @@ Invoker receives Service API Invocation events Check Response Variable Type And Values ${resp} 201 InvocationLog ${resource_url}= Check Location Header ${resp} ${LOCATION_LOGGING_RESOURCE_REGEX} - Sleep 3s - - # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. - ${resp}= Get Mock Server Messages - ${notification_events_on_mock_server}= Set Variable ${resp.json()} - # Check if message follow EventNotification definition. - Check Variable ${notification_events_on_mock_server} EventNotification - - # Create check Events to ensure all notifications were received - ${check_events} ${check_events_length}= Create Events From InvocationLogs + # Check Event Notifications + ## Create check Events to ensure all notifications were received + ${events_expected}= Create Events From InvocationLogs ... ${subscription_id} ... ${request_body} - - # Number of events received must be equal than events expected - Length Should Be ${notification_events_on_mock_server} ${check_events_length} - # Check if events received are the same than expected - FOR ${event} IN @{check_events} - Log ${event} - List Should Contain Value ${notification_events_on_mock_server} ${event} - END + ## Check Events Expected towards received notifications at mock server + Check Mock Server Notification Events ${events_expected} Invoker subscribe to Service API Available and Unavailable events [Tags] capif_api_events-7 mockserver - # Start Mock server - Check Mock Server - Clean Mock Server + # Initialize Mock server + Init Mock Server # Register APF ${register_user_info_provider}= Provider Default Registration @@ -284,41 +269,22 @@ Invoker subscribe to Service API Available and Unavailable events Status Should Be 204 ${resp} - # Check Results - - Sleep 3s - # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. - ${resp}= Get Mock Server Messages - ${notification_events_on_mock_server}= Set Variable ${resp.json()} - # Check if message follow EventNotification definition. - Check Variable ${notification_events_on_mock_server} EventNotification - - # Create Notification Events expected to be received - ${api_id_1}= Fetch From Right ${resource_url_1.path} / - ${notification_event_expected_removed}= Create Notification Event - ... ${subscription_id} - ... SERVICE_API_UNAVAILABLE - ... apiIds=${api_id_1} - Check Variable ${notification_event_expected_removed} EventNotification - ${api_id_2}= Fetch From Right ${resource_url_2.path} / - ${notification_event_expected_created}= Create Notification Event - ... ${subscription_id} - ... SERVICE_API_AVAILABLE - ... apiIds=${api_id_2} - Check Variable ${notification_event_expected_created} EventNotification - - # Check results - Length Should Be ${notification_events_on_mock_server} 2 - List Should Contain Value ${notification_events_on_mock_server} ${notification_event_expected_removed} - List Should Contain Value ${notification_events_on_mock_server} ${notification_event_expected_created} - + # Check Event Notifications + ## Create check Events to ensure all notifications were received + ${service_api_available_resources}= Create List ${resource_url_2} + ${service_api_unavailable_resources}= Create List ${resource_url_1} + ${events_expected}= Create Expected Events For Service API Notifications + ... subscription_id=${subscription_id} + ... service_api_available_resources=${service_api_available_resources} + ... service_api_unavailable_resources=${service_api_unavailable_resources} + ## Check Events Expected towards received notifications at mock server + Check Mock Server Notification Events ${events_expected} Invoker subscribe to Service API Update [Tags] capif_api_events-8 mockserver - # Start Mock server - Check Mock Server - Clean Mock Server + # Initialize Mock server + Init Mock Server # Register APF ${register_user_info_provider}= Provider Default Registration @@ -360,10 +326,10 @@ Invoker subscribe to Service API Update ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} # Update Service API - ${request_body_modified}= Create Service Api Description service_1_modified + ${service_api_description_modified}= Create Service Api Description service_1_modified ${resp}= Put Request Capif ... ${resource_url.path} - ... json=${request_body_modified} + ... json=${service_api_description_modified} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${APF_PROVIDER_USERNAME} @@ -371,39 +337,23 @@ Invoker subscribe to Service API Update Check Response Variable Type And Values ${resp} 200 ServiceAPIDescription ... apiName=service_1_modified - # Check Results - Sleep 3s - # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. - ${resp}= Get Mock Server Messages - ${notification_events_on_mock_server}= Set Variable ${resp.json()} - # Check if message follow EventNotification definition. - Check Variable ${notification_events_on_mock_server} EventNotification - - # Create Notification Events expected to be received - ${api_id}= Fetch From Right ${resource_url.path} / - Set To Dictionary ${request_body_modified} apiId=${api_id} - ${notification_event_expected}= Create Notification Event - ... ${subscription_id} - ... SERVICE_API_UPDATE - ... serviceAPIDescriptions=${request_body_modified} - Check Variable ${notification_event_expected} EventNotification - - # Check results - Length Should Be ${notification_events_on_mock_server} 1 - List Should Contain Value ${notification_events_on_mock_server} ${notification_event_expected} + # Check Event Notifications + ## Create check Events to ensure all notifications were received + ${events_expected}= Create Expected Service Update Event ${subscription_id} ${resource_url} ${service_api_description_modified} + ## Check Events Expected towards received notifications at mock server + Check Mock Server Notification Events ${events_expected} Provider subscribe to API Invoker events [Tags] capif_api_events-9 mockserver - # Start Mock server - Check Mock Server - Clean Mock Server + # Initialize Mock server + Init Mock Server # Register APF ${register_user_info_provider}= Provider Default Registration # Subscribe to events - ${events_list}= Create List API_INVOKER_ONBOARDED API_INVOKER_UPDATED API_INVOKER_OFFBOARDED + ${events_list}= Create List API_INVOKER_ONBOARDED API_INVOKER_UPDATED API_INVOKER_OFFBOARDED ${request_body}= Create Events Subscription ... events=@{events_list} ... notificationDestination=${MOCK_SERVER_URL}/testing @@ -450,54 +400,19 @@ Provider subscribe to API Invoker events # Check Remove Should Be Equal As Strings ${resp.status_code} 204 - # Check Results - Sleep 3s - # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. - ${resp}= Get Mock Server Messages - ${notification_events_on_mock_server}= Set Variable ${resp.json()} - # Check if message follow EventNotification definition. - Check Variable ${notification_events_on_mock_server} EventNotification - - ## Create events expected - ${events_expected}= Create List - ${api_invoker_id}= Set Variable ${register_user_info_invoker['api_invoker_id']} - # Create Notification Events expected to be received for Onboard event - ${event_expected}= Create Notification Event - ... ${subscription_id} - ... API_INVOKER_ONBOARDED - ... apiInvokerIds=${api_invoker_id} - Append To List ${events_expected} ${event_expected} - - # Create Notification Events expected to be received for Updated event - ${event_expected}= Create Notification Event - ... ${subscription_id} - ... API_INVOKER_UPDATED - ... apiInvokerIds=${api_invoker_id} - Append To List ${events_expected} ${event_expected} - - # Create Notification Events expected to be received for Offboard event - ${event_expected}= Create Notification Event + # Check Event Notifications + ## Create check Events to ensure all notifications were received + ${events_expected}= Create Expected Api Invoker Events ... ${subscription_id} - ... API_INVOKER_OFFBOARDED - ... apiInvokerIds=${api_invoker_id} - Append To List ${events_expected} ${event_expected} - - Check Variable ${events_expected} EventNotification - - # Check results - ${events_expected_length}= Get Length ${events_expected} - Length Should Be ${notification_events_on_mock_server} ${events_expected_length} - FOR ${event_expected} IN @{events_expected} - Log ${event_expected} - List Should Contain Value ${notification_events_on_mock_server} ${event_expected} - END + ... ${register_user_info_invoker['api_invoker_id']} + ## Check Events Expected towards received notifications at mock server + Check Mock Server Notification Events ${events_expected} Invoker subscribed to ACL update event [Tags] capif_api_events-10 mockserver - # Start Mock server - Check Mock Server - Clean Mock Server + # Initialize Mock server + Init Mock Server # Register APF ${register_user_info_provider}= Provider Default Registration @@ -507,7 +422,7 @@ Invoker subscribed to ACL update event ... ${register_user_info_provider} # Store apiId1 - ${serviceApiId}= Set Variable ${service_api_description_published['apiId']} + ${service_api_id}= Set Variable ${service_api_description_published['apiId']} # Register INVOKER ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding @@ -528,7 +443,6 @@ Invoker subscribed to ACL update event Check Response Variable Type And Values ${resp} 201 EventSubscription ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} @@ -554,7 +468,7 @@ Invoker subscribed to ACL update event ${resource_url}= Check Location Header ${resp} ${LOCATION_SECURITY_RESOURCE_REGEX} ${resp}= Get Request Capif - ... /access-control-policy/v1/accessControlPolicyList/${serviceApiId}?aef-id=${register_user_info_provider['aef_id']} + ... /access-control-policy/v1/accessControlPolicyList/${service_api_id}?aef-id=${register_user_info_provider['aef_id']} ... server=${CAPIF_HTTPS_URL} ... verify=ca.crt ... username=${AEF_PROVIDER_USERNAME} @@ -567,42 +481,22 @@ Invoker subscribed to ACL update event ... ${resp.json()['apiInvokerPolicies'][0]['apiInvokerId']} ... ${register_user_info_invoker['api_invoker_id']} - ${api_invoker_policies}= Set Variable ${resp.json()['apiInvokerPolicies']} - - # Check Results - Sleep 3s - # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. - ${resp}= Get Mock Server Messages - ${notification_events_on_mock_server}= Set Variable ${resp.json()} - - ## Create events expected - ${acc_ctrl_pol_list}= Create Dictionary apiId=${serviceApiId} apiInvokerPolicies=${api_invoker_policies} - Check Variable ${acc_ctrl_pol_list} AccessControlPolicyListExt + ${api_invoker_policies}= Set Variable ${resp.json()['apiInvokerPolicies']} - ${events_expected}= Create List - ${event_expected}= Create Notification Event + # Check Event Notifications + ## Create check Events to ensure all notifications were received + ${events_expected}= Create Expected Access Control Policy Update Event ... ${subscription_id} - ... ACCESS_CONTROL_POLICY_UPDATE - ... accCtrlPolList=${acc_ctrl_pol_list} - - Append To List ${events_expected} ${event_expected} - - Check Variable ${events_expected} EventNotification - - # Check results - ${events_expected_length}= Get Length ${events_expected} - Length Should Be ${notification_events_on_mock_server} ${events_expected_length} - FOR ${event_expected} IN @{events_expected} - Log ${event_expected} - List Should Contain Value ${notification_events_on_mock_server} ${event_expected} - END + ... ${service_api_id} + ... ${api_invoker_policies} + ## Check Events Expected towards received notifications at mock server + Check Mock Server Notification Events ${events_expected} Provider receives an ACL unavailable event when invoker remove Security Context. [Tags] capif_api_events-11 mockserver - # Start Mock server - Check Mock Server - Clean Mock Server + # Initialize Mock server + Init Mock Server # Register APF ${register_user_info_provider}= Provider Default Registration @@ -633,7 +527,6 @@ Provider receives an ACL unavailable event when invoker remove Security Context. Check Response Variable Type And Values ${resp} 201 EventSubscription ${subscriber_id} ${subscription_id}= Check Event Location Header ${resp} - # Test ${discover_response}= Get Request Capif ... ${DISCOVER_URL}${register_user_info_invoker['api_invoker_id']}&aef-id=${register_user_info_provider['aef_id']} @@ -667,34 +560,17 @@ Provider receives an ACL unavailable event when invoker remove Security Context. Status Should Be 204 ${resp} - # Check Results - Sleep 3s - # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. - ${resp}= Get Mock Server Messages - ${notification_events_on_mock_server}= Set Variable ${resp.json()} - - ## Create events expected - ${events_expected}= Create List - ${event_expected}= Create Notification Event - ... ${subscription_id} - ... ACCESS_CONTROL_POLICY_UNAVAILABLE - Append To List ${events_expected} ${event_expected} - Check Variable ${events_expected} EventNotification - - # Check results - ${events_expected_length}= Get Length ${events_expected} - Length Should Be ${notification_events_on_mock_server} ${events_expected_length} - FOR ${event_expected} IN @{events_expected} - Log ${event_expected} - List Should Contain Value ${notification_events_on_mock_server} ${event_expected} - END + # Check Event Notifications + ## Create check Events to ensure all notifications were received + ${events_expected}= Create Expected Access Control Policy Unavailable ${subscription_id} + ## Check Events Expected towards received notifications at mock server + Check Mock Server Notification Events ${events_expected} Invoker receives an Invoker Authorization Revoked and ACL unavailable event when Provider revoke Invoker Authorization. [Tags] capif_api_events-12 mockserver - # Start Mock server - Check Mock Server - Clean Mock Server + # Initialize Mock server + Init Mock Server # Register APF ${register_user_info_provider}= Provider Default Registration @@ -710,7 +586,7 @@ Invoker receives an Invoker Authorization Revoked and ACL unavailable event when ${register_user_info_invoker} ${url} ${request_body}= Invoker Default Onboarding # Subscribe to events - ${events_list}= Create List ACCESS_CONTROL_POLICY_UNAVAILABLE API_INVOKER_AUTHORIZATION_REVOKED + ${events_list}= Create List ACCESS_CONTROL_POLICY_UNAVAILABLE API_INVOKER_AUTHORIZATION_REVOKED ${request_body}= Create Events Subscription ... events=@{events_list} ... notificationDestination=${MOCK_SERVER_URL}/testing @@ -765,30 +641,162 @@ Invoker receives an Invoker Authorization Revoked and ACL unavailable event when # Check Results Status Should Be 204 ${resp} - # Check Results - Sleep 3s - # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. - ${resp}= Get Mock Server Messages - ${notification_events_on_mock_server}= Set Variable ${resp.json()} + # Check Event Notifications + ## Create check Events to ensure all notifications were received + ${events_expected}= Create Expected Access Control Policy Unavailable ${subscription_id} + ${events_expected}= Create Expected Api Invoker Authorization Revoked + ... ${subscription_id} + ... events_expected=${events_expected} + ## Check Events Expected towards received notifications at mock server + Check Mock Server Notification Events ${events_expected} + + +*** Keywords *** +Create Events From InvocationLogs + [Arguments] ${subscription_id} ${invocation_log} ${events_expected}=${NONE} + IF ${events_expected} == ${NONE} + ${events_expected}= Create List + END + + # Now we create the expected events received at notification server according to logs sent to loggin service in order to check if all are present. + ${invocation_log_base}= Copy Dictionary ${invocation_log} deepcopy=True + # Store log array because each log will be notified in one Event Notification + ${invocation_log_logs}= Copy List ${invocation_log_base['logs']} + # Remove logs array from invocationLog data + Remove From Dictionary ${invocation_log_base} logs + + FOR ${log} IN @{invocation_log_logs} + Log Dictionary ${log} + ${invocation_logs}= Copy Dictionary ${invocation_log_base} deepcopy=True + + # Get Event Enum for this result + ${event_enum}= Set Variable + IF ${log['result']} >= 200 and ${log['result']} < 300 + ${event_enum}= Set Variable SERVICE_API_INVOCATION_SUCCESS + ELSE + ${event_enum}= Set Variable SERVICE_API_INVOCATION_FAILURE + END + # Create a log array with only one component + ${log_list}= Create List ${log} + # Setup logs array with previously created list + Set To Dictionary ${invocation_logs} logs=${log_list} + ${event_expected}= Create Notification Event ${subscription_id} ${event_enum} invocationLogs=${invocation_logs} + Append To List ${events_expected} ${event_expected} + END + + RETURN ${events_expected} +Create Expected Events For Service API Notifications + [Arguments] + ... ${subscription_id} + ... ${service_api_available_resources}=${NONE} + ... ${service_api_unavailable_resources}=${NONE} + ... ${events_expected}=${NONE} + + IF ${events_expected} == ${NONE} + ${events_expected}= Create List + END + + FOR ${service_api_available_resource} IN @{service_api_available_resources} + Log ${service_api_available_resource} + ${api_id}= Fetch From Right ${service_api_available_resource.path} / + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... SERVICE_API_AVAILABLE + ... apiIds=${api_id} + Append To List ${events_expected} ${event_expected} + END + + FOR ${service_api_unavailable_resource} IN @{service_api_unavailable_resources} + Log ${service_api_unavailable_resource} + ${api_id}= Fetch From Right ${service_api_unavailable_resource.path} / + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... SERVICE_API_UNAVAILABLE + ... apiIds=${api_id} + Append To List ${events_expected} ${event_expected} + END + + RETURN ${events_expected} +Create Expected Api Invoker Events + [Arguments] ${subscription_id} ${api_invoker_id} ${events_expected}=${NONE} + IF ${events_expected} == ${NONE} + ${events_expected}= Create List + END ## Create events expected - ${events_expected}= Create List - ## ACCESS_CONTROL_POLICY_UNAVAILABLE event + # Create Notification Events expected to be received for Onboard event + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... API_INVOKER_ONBOARDED + ... apiInvokerIds=${api_invoker_id} + Append To List ${events_expected} ${event_expected} + + # Create Notification Events expected to be received for Updated event + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... API_INVOKER_UPDATED + ... apiInvokerIds=${api_invoker_id} + Append To List ${events_expected} ${event_expected} + + # Create Notification Events expected to be received for Offboard event + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... API_INVOKER_OFFBOARDED + ... apiInvokerIds=${api_invoker_id} + Append To List ${events_expected} ${event_expected} + + RETURN ${events_expected} + +Create Expected Access Control Policy Update Event + [Arguments] ${subscription_id} ${service_api_id} ${api_invoker_policies} ${events_expected}=${NONE} + IF ${events_expected} == ${NONE} + ${events_expected}= Create List + END + ${acc_ctrl_pol_list}= Create Dictionary apiId=${service_api_id} apiInvokerPolicies=${api_invoker_policies} + Check Variable ${acc_ctrl_pol_list} AccessControlPolicyListExt + + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... ACCESS_CONTROL_POLICY_UPDATE + ... accCtrlPolList=${acc_ctrl_pol_list} + Append To List ${events_expected} ${event_expected} + + RETURN ${events_expected} + +Create Expected Access Control Policy Unavailable + [Arguments] ${subscription_id} ${events_expected}=${NONE} + IF ${events_expected} == ${NONE} + ${events_expected}= Create List + END ${event_expected}= Create Notification Event ... ${subscription_id} ... ACCESS_CONTROL_POLICY_UNAVAILABLE - Append To List ${events_expected} ${event_expected} - ## API_INVOKER_AUTHORIZATION_REVOKED event + Append To List ${events_expected} ${event_expected} + + RETURN ${events_expected} + +Create Expected Api Invoker Authorization Revoked + [Arguments] ${subscription_id} ${events_expected}=${NONE} + IF ${events_expected} == ${NONE} + ${events_expected}= Create List + END ${event_expected}= Create Notification Event ... ${subscription_id} ... API_INVOKER_AUTHORIZATION_REVOKED - Append To List ${events_expected} ${event_expected} - Check Variable ${events_expected} EventNotification - - # Check results - ${events_expected_length}= Get Length ${events_expected} - Length Should Be ${notification_events_on_mock_server} ${events_expected_length} - FOR ${event_expected} IN @{events_expected} - Log ${event_expected} - List Should Contain Value ${notification_events_on_mock_server} ${event_expected} - END \ No newline at end of file + Append To List ${events_expected} ${event_expected} + RETURN ${events_expected} + +Create Expected Service Update Event + [Arguments] ${subscription_id} ${service_api_resource} ${service_api_descriptions} ${events_expected}=${NONE} + IF ${events_expected} == ${NONE} + ${events_expected}= Create List + END + ${api_id}= Fetch From Right ${service_api_resource.path} / + Set To Dictionary ${service_api_descriptions} apiId=${api_id} + ${events_expected}= Create List + ${event_expected}= Create Notification Event + ... ${subscription_id} + ... SERVICE_API_UPDATE + ... serviceAPIDescriptions=${service_api_descriptions} + Append To List ${events_expected} ${event_expected} + RETURN ${events_expected} \ No newline at end of file diff --git a/tests/resources/common.resource b/tests/resources/common.resource index 1be4429..d4733b8 100644 --- a/tests/resources/common.resource +++ b/tests/resources/common.resource @@ -91,6 +91,10 @@ Test ${TEST NAME} Currently Not Supported Log Test "${TEST NAME}" Currently not supported WARN Skip Test "${TEST NAME}" Currently not supported +Init Mock Server + Check Mock Server + Clean Mock Server + Check Mock Server Log Checking mock Server for Robot Framework. @@ -141,4 +145,23 @@ Get Mock Server Messages Status Should Be 200 ${resp} Log List ${resp.json()} - RETURN ${resp} \ No newline at end of file + RETURN ${resp} + +Check Mock Server Notification Events + [Arguments] ${events_expected} + + Check Variable ${events_expected} EventNotification + # Check results + ${events_expected_length}= Get Length ${events_expected} + + Sleep 3s + # Get from Mock server the EventNotification Messages sent to callback setup on event subscription. + ${resp}= Get Mock Server Messages + ${notification_events_on_mock_server}= Set Variable ${resp.json()} + Check Variable ${notification_events_on_mock_server} EventNotification + + Length Should Be ${notification_events_on_mock_server} ${events_expected_length} + FOR ${event_expected} IN @{events_expected} + Log ${event_expected} + List Should Contain Value ${notification_events_on_mock_server} ${event_expected} + END \ No newline at end of file diff --git a/tests/resources/common/basicRequests.robot b/tests/resources/common/basicRequests.robot index 9ba5f21..335743f 100644 --- a/tests/resources/common/basicRequests.robot +++ b/tests/resources/common/basicRequests.robot @@ -746,39 +746,3 @@ Create Security Context Between invoker and provider Check Response Variable Type And Values ${resp} 201 ServiceSecurity - -Create Events From InvocationLogs - [Arguments] ${subscription_id} ${invocation_log} - - ${events}= Create List - - # Now we create the expected events received at notification server according to logs sent to loggin service in order to check if all are present. - ${invocation_log_base}= Copy Dictionary ${invocation_log} deepcopy=True - # Store log array because each log will be notified in one Event Notification - ${invocation_log_logs}= Copy List ${invocation_log_base['logs']} - # Remove logs array from invocationLog data - Remove From Dictionary ${invocation_log_base} logs - - FOR ${log} IN @{invocation_log_logs} - Log Dictionary ${log} - ${invocation_logs}= Copy Dictionary ${invocation_log_base} deepcopy=True - - # Get Event Enum for this result - ${event_enum}= Set Variable - IF ${log['result']} >= 200 and ${log['result']} < 300 - ${event_enum}= Set Variable SERVICE_API_INVOCATION_SUCCESS - ELSE - ${event_enum}= Set Variable SERVICE_API_INVOCATION_FAILURE - END - # Create a log array with only one component - ${log_list}= Create List ${log} - # Setup logs array with previously created list - Set To Dictionary ${invocation_logs} logs=${log_list} - ${event}= Create Notification Event ${subscription_id} ${event_enum} invocationLogs=${invocation_logs} - Check Variable ${event} EventNotification - Append To List ${events} ${event} - END - - Log List ${events} - ${events_length}= Get Length ${events} - RETURN ${events} ${events_length} -- GitLab From 8235a91295f70618f36f7be65faedde4ee8b9ed7 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Thu, 30 May 2024 12:29:02 +0200 Subject: [PATCH 222/310] minor fixes --- services/check_services_are_running.sh | 33 +++++++++++++++++-- services/docker-compose-capif.yml | 2 -- services/docker-compose-register.yml | 2 -- services/docker-compose-vault.yml | 1 - .../run_mock_server.sh | 8 +++-- 5 files changed, 36 insertions(+), 10 deletions(-) rename {tests/libraries/mock_server => services}/run_mock_server.sh (76%) diff --git a/services/check_services_are_running.sh b/services/check_services_are_running.sh index 3bad399..16de704 100755 --- a/services/check_services_are_running.sh +++ b/services/check_services_are_running.sh @@ -1,12 +1,39 @@ #!/bin/bash +export CAPIF_PRIV_KEY= +export CAPIF_PRIV_KEY_BASE_64= +export MONITORING= + +running="$(docker compose -f docker-compose-vault.yml ps --services --all --filter "status=running")" +services="$(docker compose -f docker-compose-vault.yml ps --services --all)" +if [ "$running" != "$services" ]; then + echo "Following Vault services are not running:" + # Bash specific + comm -13 <(sort <<<"$running") <(sort <<<"$services") + exit 1 +else + echo "All Vault services are running" +fi + running="$(docker compose -f docker-compose-capif.yml ps --services --all --filter "status=running")" services="$(docker compose -f docker-compose-capif.yml ps --services --all)" if [ "$running" != "$services" ]; then - echo "Following services are not running:" + echo "Following CCF services are not running:" + # Bash specific + comm -13 <(sort <<<"$running") <(sort <<<"$services") + exit 1 +else + echo "All CCF services are running" +fi + +running="$(docker compose -f docker-compose-register.yml ps --services --all --filter "status=running")" +services="$(docker compose -f docker-compose-register.yml ps --services --all)" +if [ "$running" != "$services" ]; then + echo "Following Register services are not running:" # Bash specific comm -13 <(sort <<<"$running") <(sort <<<"$services") exit 1 else - echo "All services are running" - exit 0 + echo "All Register services are running" fi + +exit 0 diff --git a/services/docker-compose-capif.yml b/services/docker-compose-capif.yml index d05518f..779df21 100644 --- a/services/docker-compose-capif.yml +++ b/services/docker-compose-capif.yml @@ -1,5 +1,3 @@ -version: '3.7' - services: redis: image: "redis:alpine" diff --git a/services/docker-compose-register.yml b/services/docker-compose-register.yml index 5363e01..b0e5571 100644 --- a/services/docker-compose-register.yml +++ b/services/docker-compose-register.yml @@ -1,5 +1,3 @@ -version: '3.7' - services: register: build: diff --git a/services/docker-compose-vault.yml b/services/docker-compose-vault.yml index e0bb7bc..82a38d0 100644 --- a/services/docker-compose-vault.yml +++ b/services/docker-compose-vault.yml @@ -1,4 +1,3 @@ -version: '3.7' services: vault: build: diff --git a/tests/libraries/mock_server/run_mock_server.sh b/services/run_mock_server.sh similarity index 76% rename from tests/libraries/mock_server/run_mock_server.sh rename to services/run_mock_server.sh index 10c4ba2..b6f1035 100755 --- a/tests/libraries/mock_server/run_mock_server.sh +++ b/services/run_mock_server.sh @@ -8,6 +8,10 @@ help() { exit 1 } +cd .. +REPOSITORY_BASE_FOLDER=${PWD} +MOCK_SERVER_FOLDER=${PWD}/tests/libraries/mock_server + IP=0.0.0.0 PORT=9090 @@ -35,6 +39,6 @@ while getopts ":i:p:h" opt; do done echo Robot Framework Mock Server will listen on $IP:$PORT -pip install -r requirements.txt +pip install -r ${MOCK_SERVER_FOLDER}/requirements.txt -IP=$IP PORT=$PORT python mock_server.py +IP=$IP PORT=$PORT python ${MOCK_SERVER_FOLDER}/mock_server.py -- GitLab From 199d58157769fe3bdb113054ed37289b068bb97a Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Thu, 30 May 2024 17:40:44 +0200 Subject: [PATCH 223/310] Changes in Register --- services/register/config.yaml | 4 +- .../register/register_service/__main__.py | 8 ++-- .../controllers/register_controller.py | 43 ++++++++++++++++--- .../core/register_operations.py | 9 ++-- services/register/register_service/db/db.py | 1 + 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/services/register/config.yaml b/services/register/config.yaml index bc1370f..dd33a97 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -3,6 +3,7 @@ mongo: { 'password': 'example', 'db': 'capif_users', 'col': 'user', + 'admins': 'admins', 'host': 'mongo_register', 'port': '27017' } @@ -15,6 +16,5 @@ ca_factory: { register: { register_uuid: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', refresh_expiration: 30, #days - token_expiration: 10, #mins - admin_users: {admin: "password123"} + token_expiration: 10 #mins } \ No newline at end of file diff --git a/services/register/register_service/__main__.py b/services/register/register_service/__main__.py index 12f6ffd..563ab71 100644 --- a/services/register/register_service/__main__.py +++ b/services/register/register_service/__main__.py @@ -1,13 +1,11 @@ - -import os from flask import Flask from .controllers.register_controller import register_routes from flask_jwt_extended import JWTManager from OpenSSL.crypto import PKey, TYPE_RSA, X509Req, dump_certificate_request, FILETYPE_PEM, dump_privatekey import requests import json -import jwt from .config import Config +from .db.db import MongoDatabse app = Flask(__name__) @@ -73,6 +71,10 @@ response = requests.request("GET", url, headers=headers, verify = False) key_data = json.loads(response.text)["data"]["data"]["key"] +# Create an Admin in the Admin Collection +client = MongoDatabse() +client.get_col_by_name(client.capif_admins).insert_one({"admin_name": config["mongo"]["user"], "admin_pass": config["mongo"]["password"]}) + app.config['JWT_ALGORITHM'] = 'RS256' app.config['JWT_PRIVATE_KEY'] = key_data diff --git a/services/register/register_service/controllers/register_controller.py b/services/register/register_service/controllers/register_controller.py index 14877f8..ded8e8b 100644 --- a/services/register/register_service/controllers/register_controller.py +++ b/services/register/register_service/controllers/register_controller.py @@ -5,6 +5,7 @@ from ..core.register_operations import RegisterOperations from ..config import Config from functools import wraps from datetime import datetime, timedelta +from ..db.db import MongoDatabse from flask_httpauth import HTTPBasicAuth import jwt @@ -34,7 +35,9 @@ def generate_tokens(username): @auth.verify_password def verify_password(username, password): users = register_operation.get_users()[0].json["users"] - if username in config["register"]["admin_users"] and password == config["register"]["admin_users"][username]: + client = MongoDatabse() + admin = client.get_col_by_name(client.capif_admins).find_one({"admin_name": username, "admin_pass": password}) + if admin: return username, "admin" for user in users: if user["username"] == username and user["password"]==password: @@ -84,12 +87,40 @@ def refresh_token(username): @register_routes.route("/createUser", methods=["POST"]) @admin_required() def register(username): - username = request.json["username"] - password = request.json["password"] - description = request.json["description"] - email = request.json["email"] + required_fields = { + "username": str, + "password": str, + "enterprise": str, + "country": str, + "email": str, + "purpose": str + } + + optional_fields = { + "phone_number": str, + "company_web": str, + "description": str + } + + user_info = request.get_json() + + missing_fields = [] + for field, field_type in required_fields.items(): + if field not in user_info: + missing_fields.append(field) + elif not isinstance(user_info[field], field_type): + return jsonify({"error": f"Field '{field}' must be of type {field_type.__name__}"}), 400 + + for field, field_type in optional_fields.items(): + if field in user_info and not isinstance(user_info[field], field_type): + return jsonify({"error": f"Optional field '{field}' must be of type {field_type.__name__}"}), 400 + if field not in user_info: + user_info[field] = None + + if missing_fields: + return jsonify({"error": "Missing required fields", "fields": missing_fields}), 400 - return register_operation.register_user(username, password, description, email) + return register_operation.register_user(user_info) @register_routes.route("/getauth", methods=["GET"]) @auth.login_required diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index 1eb6b07..e897074 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -13,17 +13,18 @@ class RegisterOperations: self.mimetype = 'application/json' self.config = Config().get_config() - def register_user(self, username, password, description, email): + def register_user(self, user_info): mycol = self.db.get_col_by_name(self.db.capif_users) - exist_user = mycol.find_one({"username": username}) + exist_user = mycol.find_one({"username": user_info["username"]}) if exist_user: return jsonify("user already exists"), 409 name_space = uuid.UUID(self.config["register"]["register_uuid"]) - user_uuid = str(uuid.uuid5(name_space, username)) + user_uuid = str(uuid.uuid5(name_space,user_info["username"])) - user_info = dict(uuid=user_uuid, username=username, password=password, description=description, email=email, onboarding_date=datetime.now()) + user_info["user_uuid"] = user_uuid + user_info["onboarding_date"]=datetime.now() obj = mycol.insert_one(user_info) return jsonify(message="User registered successfully", uuid=user_uuid), 201 diff --git a/services/register/register_service/db/db.py b/services/register/register_service/db/db.py index 0b08933..65a8a83 100644 --- a/services/register/register_service/db/db.py +++ b/services/register/register_service/db/db.py @@ -12,6 +12,7 @@ class MongoDatabse(): self.config = Config().get_config() self.db = self.__connect() self.capif_users = self.config['mongo']['col'] + self.capif_admins = self.config['mongo']['admins'] def get_col_by_name(self, name): -- GitLab From 4d9685d32452184b40008344343a61f472fc88af Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Thu, 30 May 2024 18:07:08 +0200 Subject: [PATCH 224/310] Remove invoker gui --- .../CAPIFInvokerGUI/invoker_gui/Dockerfile | 13 -- .../invoker_gui/capif_ops/__init__.py | 0 .../config_files/capif_registration.json | 17 -- .../config_files/credentials.properties | 14 -- .../capif_ops/config_files/demo_values.json | 1 - .../capif_ops/config_files/events.json | 6 - .../config_files/invoker_details.json | 13 -- .../capif_ops/config_files/security_info.json | 18 -- .../config_files/service_request_body.json | 32 ---- .../capif_ops/config_files/token_request.json | 6 - .../invoker_gui/capif_ops/invoker_delete.py | 71 -------- .../capif_ops/invoker_discover_service.py | 96 ---------- .../invoker_gui/capif_ops/invoker_get_auth.py | 71 -------- .../capif_ops/invoker_get_security_auth.py | 90 ---------- .../capif_ops/invoker_previous_register.py | 134 -------------- .../capif_ops/invoker_register_to_capif.py | 134 -------------- .../invoker_remove_security_context.py | 82 --------- .../capif_ops/invoker_secutiry_context.py | 96 ---------- .../capif_ops/invoker_to_service.py | 168 ------------------ .../capif_ops/nef_calback_server/__init__.py | 0 .../capif_ops/nef_calback_server/callback.py | 26 --- .../CAPIFInvokerGUI/invoker_gui/main.py | 98 ---------- .../CAPIFInvokerGUI/invoker_gui/prepare.sh | 11 -- .../invoker_gui/requirements.txt | 13 -- services/capif-client/Dockerfile | 5 - 25 files changed, 1215 deletions(-) delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/Dockerfile delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/__init__.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/capif_registration.json delete mode 100755 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/credentials.properties delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/demo_values.json delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/events.json delete mode 100755 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/invoker_details.json delete mode 100755 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/security_info.json delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/service_request_body.json delete mode 100755 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/token_request.json delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_delete.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_discover_service.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_get_auth.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_get_security_auth.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_previous_register.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_register_to_capif.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_remove_security_context.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_secutiry_context.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_to_service.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/nef_calback_server/__init__.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/nef_calback_server/callback.py delete mode 100644 services/capif-client/CAPIFInvokerGUI/invoker_gui/main.py delete mode 100755 services/capif-client/CAPIFInvokerGUI/invoker_gui/prepare.sh delete mode 100755 services/capif-client/CAPIFInvokerGUI/invoker_gui/requirements.txt delete mode 100644 services/capif-client/Dockerfile diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/Dockerfile b/services/capif-client/CAPIFInvokerGUI/invoker_gui/Dockerfile deleted file mode 100644 index 8357bf9..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3.8 -ENV PYTHONUNBUFFERED 1 - -RUN apt-get update && apt-get install -y jq && apt-get clean -RUN apt-get install -y iputils-ping - -RUN mkdir -p /usr/src/app -WORKDIR /usr/src/app -ADD requirements.txt /usr/src/app/ -RUN pip install -r requirements.txt -ADD . /usr/src/app/ - -CMD ["sh", "prepare.sh"] \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/__init__.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/capif_registration.json b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/capif_registration.json deleted file mode 100644 index 498f284..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/capif_registration.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "folder_to_store_certificates": "/usr/src/app/capif_onboarding", - "capif_host": "capifcore", - "capif_http_port": "8080", - "capif_https_port": "443", - "capif_netapp_username": "test_netapp_23", - "capif_netapp_password": "test_netapp_password", - "capif_callback_url": "http://192.168.1.11:5000", - "description": ",test_app_description", - "csr_common_name": "test_app_common_name", - "csr_organizational_unit": "test_app_ou", - "csr_organization": "test_app_o", - "crs_locality": "Madrid", - "csr_state_or_province_name": "Madrid", - "csr_country_name": "ES", - "csr_email_address": "test@example.com" -} \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/credentials.properties b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/credentials.properties deleted file mode 100755 index 0c39e2c..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/credentials.properties +++ /dev/null @@ -1,14 +0,0 @@ -[credentials] -invoker_username = customnetapp -invoker_password = pass123 -invoker_role = invoker -invoker_description = Dummy NetApp -invoker_cn = invoker -#capif_ip = capicore -#capif_port = 8080 -capif_callback_ip = host.docker.internal -capif_callback_port = 8086 -nef_ip = host.docker.internal -nef_port = 8888 -nef_callback_ip = host.docker.internal -nef_callback_port = 8085 \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/demo_values.json b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/demo_values.json deleted file mode 100644 index 3a8a2ab..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/demo_values.json +++ /dev/null @@ -1 +0,0 @@ -{"netappID": "93caff2e486955", "ccf_onboarding_url": "api-invoker-management/v1/onboardedInvokers", "ccf_discover_url": "service-apis/v1/allServiceAPIs?api-invoker-id=", "capif_access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY4OTU4NzA1OCwianRpIjoiMDViOGMwYWMtYmU4OC00OWViLWFhNWItZTA0ZDM4NWJlYWFkIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImN1c3RvbW5ldGFwcCBpbnZva2VyIiwibmJmIjoxNjg5NTg3MDU4LCJleHAiOjE2ODk1ODc5NTh9.k8ZXlgS0CJS-aDJCHgUv0oA4B6CLBjYpp5z3qIrzsvgr20wflpKXiO03c6U3G87T33ocEPR6BWG-ZhpQ1bfml2CKU16gef4nIDIgOKh17yBF0M1eW-gULBZL9exJQIpDWJXQK9oZOrkyHjgN89ieXlVYW9hKaGQfRl_B_HZL0hllWq6E9uE7kHG-VJTEmLJTEyP6uqmfIPLz2znHeTk8eP7IB_vxeIh-7Fr6LcyziDoxMskPDqxzg_6oLyd7biH9qZyWQYvtrEPsh_kJdK5Yc7vK1Kuh01uY9JRk9E97sXU8x0yBjejHgDn9K7_kH1gugPo7eP6nEPvm3BROWHFUmw", "api_id_0": "1d3c0c7803e650264ba30963b96549", "api_name_0": "/nef/api/v1/3gpp-as-session-with-qos/", "aef_id_0": "8cadd2bf35c6adf24bda897b4e8e99", "demo_ipv4_addr_0": "3gppnef", "demo_port_0": 8090, "demo_url_0": "/nef/api/v1/3gpp-as-session-with-qos/v1/{scsAsId}/subscriptions", "netapp_service_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY4OTU4ODEwOCwianRpIjoiY2NiNDNkNzktNTRjYS00NDI4LTg1OGMtN2MyNDI1MzhiZjU5IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6IjIwZjAxMTY0OTRkNTA0MjhmZTI2ZDdhNjNkYjNlNSIsIm5iZiI6MTY4OTU4ODEwOCwiZXhwIjoxNjg5NTg4NzA4LCJpc3MiOiIyMGYwMTE2NDk0ZDUwNDI4ZmUyNmQ3YTYzZGIzZTUiLCJzY29wZSI6IjNncHAjOGNhZGQyYmYzNWM2YWRmMjRiZGE4OTdiNGU4ZTk5Oi9uZWYvYXBpL3YxLzNncHAtYXMtc2Vzc2lvbi13aXRoLXFvcy8ifQ.PTdAWSGCqEdxroxd1qF_cNA_JRohvNlgw_A49CyaUVqEKrky2_LnVtRll_KOHPGgGcIa8g_vqdM6We71CGx7w_KQPuSccb_wUkPQ5U2kfDT2-dkBZX8le_M1aQ9346tl3iQHqPsoMv3KdiD6mNSbO8f7vlRbQ1o7HQLtLaULB_0xbFr1iJAWdwO6Dm0KOKP_rM6kC5gKyVaLzUPUQBHGQwncQWlKp1Cey3G2cW5Aw_O6kF8mt1R1wgNCedU77JUmW3-ptc1kWmWlSo3UypYNm-XRAMWh44yYnGok5gE1tf451cRc5s9Hfl6Ya2fYYBI1by9x1S_zGlxi6f_OV9z4zg", "api_id_1": "cbc42102826d69a35de147c790a983", "api_name_1": "/nef/api/v1/3gpp-as-session-with-qos/", "aef_id_1": "1f1883a784c43f47aa4fae1dc4821f", "demo_ipv4_addr_1": "127.0.0.1", "demo_port_1": 8090, "demo_url_1": "/{scsAsId}/subscriptions", "demo_resource_id": ""} \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/events.json b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/events.json deleted file mode 100644 index 3d10920..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/events.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "events": [ - "SERVICE_API_AVAILABLE" - ], - "notificationDestination": "http://192.168.1.11:8080/capifcallbacks" - } \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/invoker_details.json b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/invoker_details.json deleted file mode 100755 index d94aa7f..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/invoker_details.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "notificationDestination" : "http://X:Y/netapp_callback", - "supportedFeatures" : "fffffff", - "apiInvokerInformation" : "dummy", - "websockNotifConfig" : { - "requestWebsocketUri" : true, - "websocketUri" : "websocketUri" - }, - "onboardingInformation" : { - "apiInvokerPublicKey" : "" - }, - "requestTestNotification" : true -} \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/security_info.json b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/security_info.json deleted file mode 100755 index c1b0867..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/security_info.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "securityInfo": [ - { - "prefSecurityMethods": [ - "OAUTH" - ], - "authenticationInfo": "string", - "authorizationInfo": "string" - } - ], - "notificationDestination": "https://mynotificationdest.com", - "requestTestNotification": true, - "websockNotifConfig": { - "websocketUri": "string", - "requestWebsocketUri": true - }, - "supportedFeatures": "fff" - } \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/service_request_body.json b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/service_request_body.json deleted file mode 100644 index 189c49e..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/service_request_body.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "ipv4Addr": "10.0.0.3", - "notificationDestination": "http://invoker_gui:9091/nefcallbacks", - "snssai": { - "sst": 1, - "sd": "000001" - }, -"dnn": "province1.mnc01.mcc202.gprs", -"qosReference": 82, - "altQoSReferences": [ -0 - ], -"usageThreshold": { -"duration": 0, - "totalVolume": 0, -"downlinkVolume": 0, - "uplinkVolume": 0 - }, -"qosMonInfo": { -"reqQosMonParams": [ -"DOWNLINK" -], -"repFreqs": [ -"EVENT_TRIGGERED" -], - "latThreshDl": 0, -"latThreshUl": 0, - "latThreshRp": 0, -"waitTime": 0, - "repPeriod": 0 -} -} \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/token_request.json b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/token_request.json deleted file mode 100755 index 5408b74..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/config_files/token_request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "grant_type": "client_credentials", - "client_id": "", - "client_secret": "string", - "scope": "" -} \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_delete.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_delete.py deleted file mode 100644 index 297f88e..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_delete.py +++ /dev/null @@ -1,71 +0,0 @@ -from dis import dis -import requests -import json -import configparser -import os -from termcolor import colored - - -class RemoveInvoker(): - - - def __offboard_netapp_to_capif(self, capif_ip, invoker_id, log_level): - - print(colored("Removing netapp from CAPIF","yellow")) - url = 'https://{}/api-invoker-management/v1/onboardedInvokers/{}'.format(capif_ip, invoker_id) - - headers = { - 'Content-Type': 'application/json' - } - - try: - - if log_level == "debug": - print(colored("''''''''''REQUEST'''''''''''''''''","blue")) - print(colored(f"Request: to {url}","blue")) - print(colored(f"Request Headers: {headers}", "blue")) - print(colored(f"''''''''''REQUEST'''''''''''''''''", "blue")) - - response = requests.request("DELETE", url, headers=headers, cert=( - 'capif_ops/certs/dummy.crt', 'capif_ops/certs/invoker_private_key.key'), verify='capif_ops/certs/ca.crt') - response.raise_for_status() - - if log_level == "debug": - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - print(colored(f"Response to: {response.url}","green")) - print(colored(f"Response Headers: {response.headers}","green")) - print(colored(f"Response: {response.json()}","green")) - print(colored(f"Response Status code: {response.status_code}","green")) - print(colored("Success onboard invoker","green")) - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - - except requests.exceptions.HTTPError as err: - raise Exception(err.response.text, err.response.status_code) - - - - def execute_remove_invoker(self, log_level): - - - capif_ip = os.getenv('CAPIF_HOSTNAME') - - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - - try: - - self.__offboard_netapp_to_capif(capif_ip, demo_values["invokerID"], log_level) - - print("ApiInvokerID: {}\n".format(demo_values["invokerID"])) - demo_values.pop("invokerID") - demo_values.pop("pub_key") - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - - except Exception as e: - status_code = e.args[0] - if status_code == 403: - print("Invoker already registered.") - print("Chanage invoker public key in invoker_details.json\n") - else: - print(e) diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_discover_service.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_discover_service.py deleted file mode 100644 index 7b608ac..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_discover_service.py +++ /dev/null @@ -1,96 +0,0 @@ -from dis import dis -import requests -import json -import configparser -import redis -import os -from termcolor import colored - - -class DiscoverService(): - - def __discover_service_apis(self, capif_ip, api_invoker_id, jwt_token, ccf_url, log_level): - - print(colored("Discover Service", "yellow")) - url = "https://{}/{}{}".format(capif_ip, ccf_url, api_invoker_id) - - payload = {} - files = {} - headers = { - 'Content-Type': 'application/json' - } - - try: - - if log_level == "debug": - print(colored("''''''''''REQUEST'''''''''''''''''", "blue")) - print(colored(f"Request: to {url}", "blue")) - print(colored(f"Request Headers: {headers}", "blue")) - print(colored(f"''''''''''REQUEST'''''''''''''''''", "blue")) - - response = requests.request("GET", url, headers=headers, data=payload, files=files, cert=( - 'capif_ops/certs/dummy.crt', 'capif_ops/certs/invoker_private_key.key'), verify='capif_ops/certs/ca.crt') - response.raise_for_status() - response_payload = json.loads(response.text) - - if log_level == "debug": - print(colored("''''''''''RESPONSE'''''''''''''''''", "green")) - print(colored(f"Response to: {response.url}", "green")) - print(colored(f"Response Headers: {response.headers}", "green")) - print(colored(f"Response: {response.json()}", "green")) - print( - colored(f"Response Status code: {response.status_code}", "green")) - print(colored("''''''''''RESPONSE'''''''''''''''''", "green")) - - return response_payload - except requests.exceptions.HTTPError as err: - print(err.response.text) - message = json.loads(err.response.text) - status = err.response.status_code - raise Exception(message, status) - - def execute_discover_service(self, log_level): - - - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - - capif_ip = os.getenv('CAPIF_HOSTNAME') - - try: - if 'invokerID' in demo_values: - invokerID = demo_values['invokerID'] - capif_access_token = demo_values['capif_access_token'] - ccf_discover_url = demo_values['ccf_discover_url'] - discovered_apis = self.__discover_service_apis( - capif_ip, invokerID, capif_access_token, ccf_discover_url, log_level) - print(colored(json.dumps(discovered_apis, indent=2), "yellow")) - - count = 0 - api_list = discovered_apis["serviceAPIDescriptions"] - for api in api_list: - getAEF_profiles = api["aefProfiles"][0] - getAEF_interfaces = getAEF_profiles["interfaceDescriptions"][0] - getAEF_versions = getAEF_profiles["versions"][0] - getAEF_resources = getAEF_versions["resources"][0] - demo_values[f'api_id_{count}'] = api["apiId"] - demo_values[f'api_name_{count}'] = api["apiName"] - demo_values[f'aef_id_{count}'] = getAEF_profiles["aefId"] - demo_values[f'demo_ipv4_addr_{count}'] = getAEF_interfaces["ipv4Addr"] - demo_values[f'demo_port_{count}'] = getAEF_interfaces["port"] - demo_values[f'demo_url_{count}'] = api["apiName"] + getAEF_versions["apiVersion"]+ getAEF_resources['uri'] - count += 1 - - print(colored("Discovered APIs", "yellow")) - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - - except Exception as e: - status_code = e.args[0] - if status_code == 401: - print("API Invoker is not authorized") - elif status_code == 403: - print("API Invoker does not exist. API Invoker id not found") - else: - print(e) - diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_get_auth.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_get_auth.py deleted file mode 100644 index dde3694..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_get_auth.py +++ /dev/null @@ -1,71 +0,0 @@ -import requests -import json -import configparser -import os -from termcolor import colored - -class PreviousAuth(): - - def __get_capif_auth(self, capif_ip, capif_port, username, password): - - #print("Geting Auth to exposer") - #url = "http://{}:{}/getauth".format(capif_ip, capif_port) - url = "https://register:8084/getauth".format(capif_port) - #url = "http://{}:{}/getauth".format(capif_ip, capif_port) - - payload = dict() - payload['username'] = username - payload['password'] = password - - headers = { - 'Content-Type': 'application/json' - } - - try: - response = requests.request("POST", url, headers=headers, data=json.dumps(payload), verify=False) - - response.raise_for_status() - response_payload = json.loads(response.text) - - return response_payload['access_token'] - - except requests.exceptions.HTTPError as err: - raise Exception(err.response.text, err.response.status_code) - - - def execute_get_auth(self, log_level): - - config = configparser.ConfigParser() - config.read('capif_ops/config_files/credentials.properties') - - username = config.get("credentials", "invoker_username") - password = config.get("credentials", "invoker_password") - - capif_ip = os.getenv('CAPIF_HOSTNAME') - capif_port = os.getenv('CAPIF_PORT') - - if os.path.exists("capif_ops/config_files/demo_values.json"): - #os.remove("capif_ops/config_files/demo_values.json") - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - else: - demo_values = {} - - #First we need register exposer in CAPIF - try: - if 'netappID' in demo_values: - access_token = self.__get_capif_auth(capif_ip, capif_port, username, password) - demo_values['capif_access_token'] = access_token - - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - - print("Invoker auth Success!") - except Exception as e: - status_code = e.args[0] - if status_code == 409: - print("User already registed. Continue with token request\n") - else: - print(e) - - return True diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_get_security_auth.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_get_security_auth.py deleted file mode 100644 index 74b026f..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_get_security_auth.py +++ /dev/null @@ -1,90 +0,0 @@ -from dis import dis -from email import charset -import requests -import json -import configparser -import redis -import os -from termcolor import colored - - -class InvokerGetSecurityAuth(): - - def __get_security_token(self, capif_ip, api_invoker_id, jwt_token, ccf_url, aef_id, api_name, log_level): - - - url = "https://{}/capif-security/v1/securities/{}/token".format(capif_ip, api_invoker_id) - - with open('capif_ops/config_files/token_request.json', "rb") as f: - payload = json.load(f) - - payload["client_id"] = api_invoker_id - payload["scope"] = "3gpp#"+aef_id+":"+api_name - - headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - } - - - payload_dict = json.dumps(payload, indent=2) - - print(colored(f"Request Body: {payload_dict}", "yellow")) - - try: - - if log_level == "debug": - print(colored("''''''''''REQUEST'''''''''''''''''","blue")) - print(colored(f"Request: to {url}","blue")) - print(colored(f"Request Headers: {headers}", "blue")) - print(colored(f"''''''''''REQUEST'''''''''''''''''", "blue")) - - response = requests.post(url, headers=headers, data=payload, cert=('capif_ops/certs/dummy.crt', 'capif_ops/certs/invoker_private_key.key'), verify='capif_ops/certs/ca.crt') - print(response.request.body) - response.raise_for_status() - response_payload = json.loads(response.text) - - if log_level == "debug": - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - print(colored(f"Response to: {response.url}","green")) - print(colored(f"Response Headers: {response.headers}","green")) - print(colored(f"Response: {response.json()}","green")) - print(colored(f"Response Status code: {response.status_code}","green")) - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - - return response_payload - except requests.exceptions.HTTPError as err: - print(err.response.text) - message = json.loads(err.response.text) - status = err.response.status_code - raise Exception(message, status) - - def execute_get_security_auth(self, log_level): - - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - - config = configparser.ConfigParser() - config.read('credentials.properties') - - capif_ip = os.getenv('CAPIF_HOSTNAME') - invokerID = demo_values['invokerID'] - capif_access_token = demo_values['capif_access_token'] - ccf_discover_url = demo_values['ccf_discover_url'] - - try: - if 'aef_id_0' in demo_values and 'api_name_0' in demo_values: - token = self.__get_security_token(capif_ip, invokerID, capif_access_token, ccf_discover_url, demo_values['aef_id_0'], demo_values['api_name_0'],log_level) - print(colored(json.dumps(token, indent=2),"yellow")) - demo_values["netapp_service_token"] = token["access_token"] - print(colored("Obtained Security Token","yellow")) - - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - except Exception as e: - status_code = e.args[0] - if status_code == 401: - print("API Invoker is not authorized") - elif status_code == 403: - print("API Invoker does not exist. API Invoker id not found") - else: - print(e) diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_previous_register.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_previous_register.py deleted file mode 100644 index 6a77264..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_previous_register.py +++ /dev/null @@ -1,134 +0,0 @@ -import requests -import json -import configparser -import os -from termcolor import colored - -class PreviousRegister(): - - def __register_invoker_to_capif(self, capif_ip, capif_port, username, password, role, description, cn): - - #print(colored("Registering exposer to CAPIF","yellow")) - url = "https://register:8084/register".format(capif_port) - #url = "http://{}:{}/register".format(capif_ip, capif_port) - - payload = dict() - payload['username'] = username - payload['password'] = password - payload['role'] = role - payload['description'] = description - payload['cn'] = cn - - headers = { - 'Content-Type': 'application/json' - } - - try: - # print(colored("''''''''''REQUEST'''''''''''''''''","blue")) - # print(colored(f"Request: to {url}","blue")) - # print(colored(f"Request Headers: {headers}", "blue")) - # print(colored(f"Request Body: {json.dumps(payload)}", "blue")) - # print(colored(f"''''''''''REQUEST'''''''''''''''''", "blue")) - - response = requests.request("POST", url, headers=headers, data=json.dumps(payload), verify=False) - response.raise_for_status() - response_payload = json.loads(response.text) - - # print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - # print(colored(f"Response to: {response.url}","green")) - # print(colored(f"Response Headers: {response.headers}","green")) - # print(colored(f"Response: {response.json()}","green")) - # print(colored(f"Response Status code: {response.status_code}","green")) - # print(colored("Success to register new exposer","green")) - # print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - return response_payload['id'], response_payload['ccf_onboarding_url'], response_payload['ccf_discover_url'], - except requests.exceptions.HTTPError as err: - raise Exception(err.response.status_code) - - - def __get_capif_auth(self, capif_ip, capif_port, username, password): - - #print("Geting Auth to exposer") - url = "https://register:8084/getauth".format(capif_port) - #url = "http://{}:{}/getauth".format(capif_ip, capif_port) - - payload = dict() - payload['username'] = username - payload['password'] = password - - headers = { - 'Content-Type': 'application/json' - } - - try: - # print("''''''''''REQUEST'''''''''''''''''") - # print("Request: to ",url) - # print("Request Headers: ", headers) - # print("Request Body: ", json.dumps(payload)) - # print("''''''''''REQUEST'''''''''''''''''") - - response = requests.request("POST", url, headers=headers, data=json.dumps(payload), verify = False) - - response.raise_for_status() - response_payload = json.loads(response.text) - - # print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - # print(colored(f"Response to: {response.url}","green")) - # print(colored(f"Response Headers: {response.headers}","green")) - # print(colored(f"Response: {response.json()}","green")) - # print(colored(f"Response Status code: {response.status_code}","green")) - # print(colored("Get AUTH Success. Received access token", "green")) - # print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - return response_payload['access_token'] - - except requests.exceptions.HTTPError as err: - raise Exception(err.response.text, err.response.status_code) - - - def execute_previous_register_invoker(self): - - config = configparser.ConfigParser() - config.read('capif_ops/config_files/credentials.properties') - - username = config.get("credentials", "invoker_username") - password = config.get("credentials", "invoker_password") - role = config.get("credentials", "invoker_role") - description = config.get("credentials", "invoker_description") - cn = config.get("credentials", "invoker_cn") - - capif_ip = os.getenv('CAPIF_HOSTNAME') - capif_port = os.getenv('CAPIF_PORT') - - if os.path.exists("capif_ops/config_files/demo_values.json"): - #os.remove("capif_ops/config_files/demo_values.json") - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - else: - demo_values = {} - - #First we need register exposer in CAPIF - try: - netappID, ccf_onboarding_url, ccf_discover_url = self.__register_invoker_to_capif(capif_ip, capif_port, username, password, role, description, cn) - demo_values['netappID'] = netappID - demo_values['ccf_onboarding_url'] = ccf_onboarding_url - demo_values['ccf_discover_url'] = ccf_discover_url - #print(colored(f"NetAppID: {netappID}\n","yellow")) - #print("provider ID: {}".format(providerID)) - - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - - if 'netappID' in demo_values: - access_token = self.__get_capif_auth(capif_ip, capif_port, username, password) - demo_values['capif_access_token'] = access_token - - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - except Exception as e: - status_code = e.args[0] - if status_code == 409: - print() - else: - print(e) - - return True diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_register_to_capif.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_register_to_capif.py deleted file mode 100644 index 49be731..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_register_to_capif.py +++ /dev/null @@ -1,134 +0,0 @@ -from dis import dis -import requests -import json -import configparser -import redis -import os -from termcolor import colored - - -from OpenSSL.SSL import FILETYPE_PEM -from OpenSSL.crypto import (dump_certificate_request, dump_privatekey, load_publickey, PKey, TYPE_RSA, X509Req, dump_publickey) - - -class RegisterInvoker(): - def __create_csr(self, name): - - # create public/private key - key = PKey() - key.generate_key(TYPE_RSA, 2048) - - # Generate CSR - req = X509Req() - req.get_subject().CN = name - req.get_subject().O = 'Telefonica I+D' - req.get_subject().OU = 'Innovation' - req.get_subject().L = 'Madrid' - req.get_subject().ST = 'Madrid' - req.get_subject().C = 'ES' - req.get_subject().emailAddress = 'inno@tid.es' - req.set_pubkey(key) - req.sign(key, 'sha256') - - csr_request = dump_certificate_request(FILETYPE_PEM, req) - - private_key = dump_privatekey(FILETYPE_PEM, key) - - return csr_request, private_key - - - - - def __onboard_netapp_to_capif(self, capif_ip, capif_callback_ip, capif_callback_port, jwt_token, ccf_url, log_level): - - print(colored("Onboarding netapp to CAPIF","yellow")) - url = 'https://{}/{}'.format(capif_ip, ccf_url) - - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - - csr_request, private_key = self.__create_csr("invoker") - - if 'pub_key' not in demo_values: - private_key_file = open("capif_ops/certs/invoker_private_key.key", 'wb+') - private_key_file.write(bytes(private_key)) - - json_file = open('capif_ops/config_files/invoker_details.json', 'rb') - payload_dict = json.load(json_file) - if 'pub_key' not in demo_values: - payload_dict['onboardingInformation']['apiInvokerPublicKey'] = csr_request.decode("utf-8") - else: - payload_dict['onboardingInformation']['apiInvokerPublicKey'] = demo_values['pub_key'] - payload_dict['notificationDestination'] = payload_dict['notificationDestination'].replace("X", capif_callback_ip) - payload_dict['notificationDestination'] = payload_dict['notificationDestination'].replace("Y", capif_callback_port) - payload = json.dumps(payload_dict, indent=2) - - print(colored(f"Request Body: {payload}", "yellow")) - - headers = { - 'Authorization': 'Bearer {}'.format(jwt_token), - 'Content-Type': 'application/json' - } - - try: - - if log_level == "debug": - print(colored("''''''''''REQUEST'''''''''''''''''","blue")) - print(colored(f"Request: to {url}","blue")) - print(colored(f"Request Headers: {headers}", "blue")) - print(colored(f"Request Body: {json.dumps(payload)}", "blue")) - print(colored(f"''''''''''REQUEST'''''''''''''''''", "blue")) - - response = requests.request("POST", url, headers=headers, data=payload, verify='capif_ops/certs/ca.crt') - response.raise_for_status() - response_payload = json.loads(response.text) - certification_file = open('capif_ops/certs/dummy.crt', 'wb') - certification_file.write(bytes(response_payload['onboardingInformation']['apiInvokerCertificate'], 'utf-8')) - certification_file.close() - - if log_level == "debug": - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - print(colored(f"Response to: {response.url}","green")) - print(colored(f"Response Headers: {response.headers}","green")) - print(colored(f"Response: {response.json()}","green")) - print(colored(f"Response Status code: {response.status_code}","green")) - print(colored("Success onboard invoker","green")) - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - return response_payload['apiInvokerId'], payload_dict['onboardingInformation']['apiInvokerPublicKey'] - except requests.exceptions.HTTPError as err: - raise Exception(err.response.text, err.response.status_code) - - - - - - def execute_register_invoker(self, log_level): - - config = configparser.ConfigParser() - config.read('capif_ops/config_files/credentials.properties') - - capif_ip = os.getenv('CAPIF_HOSTNAME') - - capif_callback_ip = config.get("credentials", "capif_callback_ip") - capif_callback_port = config.get("credentials", "capif_callback_port") - - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - - try: - capif_access_token = demo_values['capif_access_token'] - ccf_onboarding_url = demo_values['ccf_onboarding_url'] - invokerID, pub_key = self.__onboard_netapp_to_capif(capif_ip, capif_callback_ip, capif_callback_port, capif_access_token, ccf_onboarding_url, log_level) - demo_values['invokerID'] = invokerID - demo_values['pub_key'] = pub_key - print("ApiInvokerID: {}\n".format(invokerID)) - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - - except Exception as e: - status_code = e.args[0] - if status_code == 403: - print("Invoker already registered.") - print("Chanage invoker public key in invoker_details.json\n") - else: - print(e) diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_remove_security_context.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_remove_security_context.py deleted file mode 100644 index 18bb3f3..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_remove_security_context.py +++ /dev/null @@ -1,82 +0,0 @@ -from dis import dis -from email import charset -import requests -import json -import configparser -import redis -import os -from termcolor import colored - - -class InvokerRemoveSecurityContext(): - - def __remove_security_service(self, capif_ip, api_invoker_id, jwt_token, ccf_url, demo_values, log_level): - - - url = "https://{}/capif-security/v1/trustedInvokers/{}".format(capif_ip, api_invoker_id) - - headers = { - 'Content-Type': 'application/json' - } - - try: - - if log_level == "debug": - print(colored("''''''''''REQUEST'''''''''''''''''","blue")) - print(colored(f"Request: to {url}","blue")) - print(colored(f"Request Headers: {headers}", "blue")) - print(colored(f"''''''''''REQUEST'''''''''''''''''", "blue")) - - response = requests.delete(url, cert=('capif_ops/certs/dummy.crt', 'capif_ops/certs/invoker_private_key.key'), verify='capif_ops/certs/ca.crt') - response.raise_for_status() - - if log_level == "debug": - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - print(colored(f"Response to: {response.url}","green")) - print(colored(f"Response Headers: {response.headers}","green")) - print(colored(f"Response: {response.json()}","green")) - print(colored(f"Response Status code: {response.status_code}","green")) - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - - return - except requests.exceptions.HTTPError as err: - print(err.response.text) - message = json.loads(err.response.text) - status = err.response.status_code - raise Exception(message, status) - - def execute_remove_security_context(self, log_level): - - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - - config = configparser.ConfigParser() - config.read('credentials.properties') - - capif_ip = os.getenv('CAPIF_HOSTNAME') - invokerID = "" - capif_access_token = "" - ccf_discover_url = "" - - try: - - invokerID = demo_values['invokerID'] - capif_access_token = demo_values['capif_access_token'] - ccf_discover_url = demo_values['ccf_discover_url'] - security_information = self.__remove_security_service(capif_ip, invokerID, capif_access_token, ccf_discover_url, demo_values,log_level) - print(colored(json.dumps(security_information, indent=2),"yellow")) - print(colored("Register Security context","yellow")) - - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - - - except Exception as e: - status_code = e.args[0] - if status_code == 401: - print("API Invoker is not authorized") - elif status_code == 403: - print("API Invoker does not exist. API Invoker id not found") - else: - print(e) - diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_secutiry_context.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_secutiry_context.py deleted file mode 100644 index a26988f..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_secutiry_context.py +++ /dev/null @@ -1,96 +0,0 @@ -from dis import dis -from email import charset -import requests -import json -import configparser -import redis -import os -from termcolor import colored - - -class InvokerSecurityContext(): - - def __register_security_service(self, capif_ip, api_invoker_id, jwt_token, ccf_url, demo_values, log_level): - - - url = "https://{}/capif-security/v1/trustedInvokers/{}".format(capif_ip, api_invoker_id) - - with open('capif_ops/config_files/security_info.json', "rb") as f: - payload = json.load(f) - - count = 0 - for profile in payload["securityInfo"]: - profile["aefId"] = demo_values[f"aef_id_{count}"] - profile["apiId"] = demo_values[f"api_id_{count}"] - count += 1 - - headers = { - 'Content-Type': 'application/json' - } - - # payload_dict = json.dumps(payload, indent=2) - - # print(colored(f"Request Body: {payload_dict}", "yellow")) - - try: - - if log_level == "debug": - print(colored("''''''''''REQUEST'''''''''''''''''","blue")) - print(colored(f"Request: to {url}","blue")) - print(colored(f"Request Headers: {headers}", "blue")) - print(colored(f"''''''''''REQUEST'''''''''''''''''", "blue")) - - response = requests.put(url, json=payload, cert=('capif_ops/certs/dummy.crt', 'capif_ops/certs/invoker_private_key.key'), verify='capif_ops/certs/ca.crt') - response.raise_for_status() - response_payload = response.json() - - if log_level == "debug": - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - print(colored(f"Response to: {response.url}","green")) - print(colored(f"Response Headers: {response.headers}","green")) - print(colored(f"Response: {response.json()}","green")) - print(colored(f"Response Status code: {response.status_code}","green")) - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - - return response_payload - except requests.exceptions.HTTPError as err: - print(err.response.text) - message = json.loads(err.response.text) - status = err.response.status_code - raise Exception(message, status) - - def execute_register_security_context(self, log_level): - - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - - config = configparser.ConfigParser() - config.read('credentials.properties') - - capif_ip = os.getenv('CAPIF_HOSTNAME') - invokerID = "" - capif_access_token = "" - ccf_discover_url = "" - - try: - - invokerID = demo_values['invokerID'] - capif_access_token = demo_values['capif_access_token'] - ccf_discover_url = demo_values['ccf_discover_url'] - security_information = self.__register_security_service(capif_ip, invokerID, capif_access_token, ccf_discover_url, demo_values,log_level) - print(colored(json.dumps(security_information, indent=2),"yellow")) - print(colored("Register Security context","yellow")) - - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - - - except Exception as e: - status_code = e.args[0] - if status_code == 401: - print("API Invoker is not authorized") - elif status_code == 403: - print("API Invoker does not exist. API Invoker id not found") - else: - print(e) - diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_to_service.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_to_service.py deleted file mode 100644 index bc54ad7..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/invoker_to_service.py +++ /dev/null @@ -1,168 +0,0 @@ -from dis import dis -import requests -import json -import configparser -import redis -import os -import argparse -import re -from termcolor import colored - -# Get environment variables - - -class InvokerToService(): - def __demo_to_aef(self, operation, demo_ip, demo_port, demo_url, jwt_token, name, log_level): - - #def register_netapp_to_nef(nef_ip, nef_port): - access_token_url = "https://{}:{}/api/v1/login/access-token".format(demo_ip, demo_port) - - access_payload = { - "username": "admin@my-email.com", - "password": "pass" - } - - response = requests.request('POST', access_token_url, data=access_payload, verify=False) - parsed = json.loads(response.text) - - access_token = parsed['access_token'] - - print(colored("Using AEF Service API","yellow")) - url = "https://{}:{}{}".format(demo_ip, demo_port, demo_url) - #url = "http://python_aef:8086/hello" - - - - json_file = open('capif_ops/config_files/service_request_body.json', 'rb') - payload_dict = json.load(json_file) - payload = json.dumps(payload_dict, indent=2) - - files = {} - headers = { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer '+ access_token + "," +jwt_token - } - - if operation == "create": - print(colored(f"Request Body: {payload}", "yellow")) - try: - if log_level == "debug": - print(colored("''''''''''REQUEST'''''''''''''''''","blue")) - print(colored(f"Request: to {url}","blue")) - print(colored(f"Request Headers: {headers}", "blue")) - print(colored(f"Request Body: {json.dumps(payload, indent=2)}", "blue")) - print(colored(f"''''''''''REQUEST'''''''''''''''''", "blue")) - response = requests.request("POST", url, headers=headers, data=payload, files=files, verify=False) - response.raise_for_status() - response_payload = json.loads(response.text) - - if log_level == "debug": - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - print(colored(f"Response to: {response.url}","green")) - print(colored(f"Response Headers: {response.headers}","green")) - print(colored(f"Response: {response.json()}","green")) - print(colored(f"Response Status code: {response.status_code}","green")) - print(colored("Success to invoke service","green")) - print(colored(response_payload,"green")) - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - - link_created_resource = response_payload["link"] - resource_id = link_created_resource.rsplit('/', 1)[-1] - if resource_id: - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - demo_values["demo_resource_id"] = resource_id - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - - return response_payload - except requests.exceptions.HTTPError as err: - print(err.response.text) - message = json.loads(err.response.text) - status = err.response.status_code - raise Exception(message, status) - - elif operation == "delete": - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - resource_id = demo_values["demo_resource_id"] - if resource_id == "": - print("Not found resource to delete") - return - - url = url + "/" + resource_id - try: - if log_level == "debug": - print(colored("''''''''''REQUEST'''''''''''''''''","blue")) - print(colored(f"Request: to {url}","blue")) - print(colored(f"Request Headers: {headers}", "blue")) - print(colored(f"''''''''''REQUEST'''''''''''''''''", "blue")) - response = requests.request("DELETE", url, headers=headers, verify=False) - response.raise_for_status() - response_payload = json.loads(response.text) - - if log_level == "debug": - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - print(colored(f"Response to: {response.url}","green")) - print(colored(f"Response Headers: {response.headers}","green")) - print(colored(f"Response: {response.json()}","green")) - print(colored(f"Response Status code: {response.status_code}","green")) - print(colored("Success to invoke service","green")) - print(colored(response_payload,"green")) - print(colored("''''''''''RESPONSE'''''''''''''''''","green")) - - demo_values["demo_resource_id"] = "" - with open('capif_ops/config_files/demo_values.json', 'w') as outfile: - json.dump(demo_values, outfile) - - return response_payload - except requests.exceptions.HTTPError as err: - print(err.response.text) - message = json.loads(err.response.text) - status = err.response.status_code - raise Exception(message, status) - - else: - print("You must spicify if you want create or delete resource") - - - def execute_invoker_to_service(self, input): - - # parser = argparse.ArgumentParser() - # parser.add_argument('--name', metavar= "name", type=str, default="Evolve5G", help="Name to send to the aef service") - # args = parser.parse_args() - input_name = "prueba" - operation = "" - log_level = "" - params = input.split() - if len(params) > 0: - operation = params[0] - if len(params) > 1: - log_level = params[1] - - with open('capif_ops/config_files/demo_values.json', 'r') as demo_file: - demo_values = json.load(demo_file) - - try: - if 'netapp_service_token' in demo_values: - - print(colored("Doing test","yellow")) - jwt_token = demo_values['netapp_service_token'] - invokerID = demo_values['invokerID'] - demo_ip = demo_values['demo_ipv4_addr_0'] - #demo_port = demo_values['demo_port_0'] - demo_port = 4443 - demo_url = demo_values['demo_url_0'] - demo_url = re.sub(r'\{scsAsId\}', 'myNetapp', demo_url) - - result = self.__demo_to_aef(operation, demo_ip, demo_port, demo_url, jwt_token, input_name, log_level) - print(colored(f"Response: {json.dumps(result, indent=2)}", "yellow")) - print(colored("Success","yellow")) - except Exception as e: - status_code = e.args[0] - if status_code == 401: - print("API Invoker is not authorized") - elif status_code == 403: - print("API Invoker does not exist. API Invoker id not found") - else: - print(e) \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/nef_calback_server/__init__.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/nef_calback_server/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/nef_calback_server/callback.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/nef_calback_server/callback.py deleted file mode 100644 index 682d358..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/capif_ops/nef_calback_server/callback.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 - -from flask import Flask, jsonify, request -# import redis -# from redis.commands.json.path import Path -import secrets -from werkzeug import serving -import os - - - -app = Flask(__name__) - -COUNTER = 0 - -@app.route("/nefcallbacks", methods=["POST"]) -def nefcallback(): - global COUNTER - COUNTER += 1 - print(f"Notification received from NEF: {COUNTER}") - - return jsonify(message="Receive message"), 200 - - -if __name__ == '__main__': - serving.run_simple('0.0.0.0', 8080, app) \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/main.py b/services/capif-client/CAPIFInvokerGUI/invoker_gui/main.py deleted file mode 100644 index bed7c8b..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/main.py +++ /dev/null @@ -1,98 +0,0 @@ - -from cmd import Cmd -from capif_ops.invoker_previous_register import PreviousRegister -from capif_ops.invoker_register_to_capif import RegisterInvoker -from capif_ops.invoker_discover_service import DiscoverService -from capif_ops.invoker_secutiry_context import InvokerSecurityContext -from capif_ops.invoker_get_security_auth import InvokerGetSecurityAuth -from capif_ops.invoker_delete import RemoveInvoker -from capif_ops.invoker_get_auth import PreviousAuth -from capif_ops.invoker_remove_security_context import InvokerRemoveSecurityContext -from capif_ops.invoker_to_service import InvokerToService -import shlex -import subprocess -from art import * - -prev_register = PreviousRegister() -regiter_capif = RegisterInvoker() -discover_service = DiscoverService() -register_security_context = InvokerSecurityContext() -security_context_auth = InvokerGetSecurityAuth() -remove_invoker = RemoveInvoker() -invoker_auth = PreviousAuth() -remove_security_service = InvokerRemoveSecurityContext() -invoker_service = InvokerToService() - - - -class CAPIFProvider(Cmd): - - def __init__(self): - Cmd.__init__(self) - self.prompt = "> " - self.intro = tprint("Welcome to Invoker Console") - - def emptyline(self): - """Do nothing on empty input line""" - pass - - def preloop(self): - state = prev_register.execute_previous_register_invoker() - self.previous_register_state = state - - def precmd(self, line): - - line = line.lower() - args = shlex.split(line) - - if len(args) >= 1 and args[0] in ["goodbye"]: - print("The first argument is username") - return "" - - elif len(args) >= 1 and args[0] not in ["->", "wall", "follows", "exit", "help"]: - pass - - return line - - def do_register_invoker(self, input): - 'Register invoker to CAPIF' - regiter_capif.execute_register_invoker(input) - - def do_discover_service(self, input): - 'Discover all services published in CAPIF' - discover_service.execute_discover_service(input) - - def do_register_security_context(self, input): - 'Create security context to use services' - register_security_context.execute_register_security_context(input) - - def do_get_security_auth(self, input): - "If you select Oauth as security method use this command to obtain jwt token to access service" - security_context_auth.execute_get_security_auth(input) - - def do_get_auth(self, input): - 'Get jwt token to register invoker in CAPIF (Optional, only if token expires)' - invoker_auth.execute_get_auth(input) - - def do_remove_security_context(self, input): - print("Not implemented yet") - #remove_security_service.execute_remove_security_context(input) - - def do_remove_invoker(self, input): - "Remove invoker from CAPIF" - remove_invoker.execute_remove_invoker(input) - - def do_call_service(self, input): - "Test invocation os service API" - invoker_service.execute_invoker_to_service(input) - - def do_exit(self, input): - print('\nExiting...') - return True - - -if __name__ == '__main__': - try: - CAPIFProvider().cmdloop() - except KeyboardInterrupt: - print('\nExiting...') \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/prepare.sh b/services/capif-client/CAPIFInvokerGUI/invoker_gui/prepare.sh deleted file mode 100755 index ca0305a..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/prepare.sh +++ /dev/null @@ -1,11 +0,0 @@ -VAULT_ADDR="http://$VAULT_HOSTNAME:$VAULT_PORT" -VAULT_TOKEN=$VAULT_ACCESS_TOKEN - -curl -k -retry 30 \ - --retry-all-errors \ - --connect-timeout 5 \ - --max-time 10 \ - --retry-delay 10 \ - --retry-max-time 300 \ - --header "X-Vault-Token: $VAULT_TOKEN" \ - --request GET "$VAULT_ADDR/v1/secret/data/ca" 2>/dev/null | jq -r '.data.data.ca' -j > ./capif_ops/certs/ca.crt \ No newline at end of file diff --git a/services/capif-client/CAPIFInvokerGUI/invoker_gui/requirements.txt b/services/capif-client/CAPIFInvokerGUI/invoker_gui/requirements.txt deleted file mode 100755 index a9e6ede..0000000 --- a/services/capif-client/CAPIFInvokerGUI/invoker_gui/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -requests == 2.26.0 -evolved5g >= 0.8.9 -python_dateutil >= 2.6.0 -setuptools >= 21.0.0 -Flask -watchdog -redis -configparser -redis -pyopenssl -pyjwt -art -termcolor \ No newline at end of file diff --git a/services/capif-client/Dockerfile b/services/capif-client/Dockerfile deleted file mode 100644 index ad7e503..0000000 --- a/services/capif-client/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM public.ecr.aws/o2v4a8t6/opencapif/client:3.1.3 - -COPY ./CAPIFInvokerGUI . -RUN pip3 install -r ./invoker_gui/requirements.txt -CMD ["sleep", "infinity"] \ No newline at end of file -- GitLab From f8ec76b8f4dcf6a05325f755ebaa2371702f3e47 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 3 Jun 2024 14:37:18 +0200 Subject: [PATCH 225/310] ocf-pre-staging --- helm/DELETE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/DELETE.txt b/helm/DELETE.txt index f6c4fd0..2d030d7 100644 --- a/helm/DELETE.txt +++ b/helm/DELETE.txt @@ -1 +1 @@ -delete me \ No newline at end of file +delete me -- GitLab From 161c83d2acd66285cbc9be097b7bc86a2b6cabc6 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 3 Jun 2024 17:08:36 +0200 Subject: [PATCH 226/310] no token in values.yaml --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index 647c09b..8b9a920 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -522,7 +522,7 @@ parametersVault: env: vaultHostname: vault-internal.mon.svc.cluster.local vaultPort: 8200 - vaultAccessToken: hvs.foen8o9OJ58z1x4WHpUPFSUN + vaultAccessToken: dev-only-token # -- With tempo.enabled: false. It won't be deployed # -- If monitoring.enable: "true". Also enable tempo.enabled: true tempo: -- GitLab From 62da2036a79ba67a05eae3fe08f5f903fed76d2a Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Tue, 4 Jun 2024 12:07:18 +0200 Subject: [PATCH 227/310] admin collection --- services/register/config.yaml | 4 +++- services/register/register_service/__main__.py | 3 ++- .../register/register_service/core/register_operations.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/register/config.yaml b/services/register/config.yaml index dd33a97..f63df9f 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -16,5 +16,7 @@ ca_factory: { register: { register_uuid: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', refresh_expiration: 30, #days - token_expiration: 10 #mins + token_expiration: 10, #mins + admin_users: {admin_user: "admin", + admin_pass: "password123"} } \ No newline at end of file diff --git a/services/register/register_service/__main__.py b/services/register/register_service/__main__.py index 563ab71..7554fa2 100644 --- a/services/register/register_service/__main__.py +++ b/services/register/register_service/__main__.py @@ -73,7 +73,8 @@ key_data = json.loads(response.text)["data"]["data"]["key"] # Create an Admin in the Admin Collection client = MongoDatabse() -client.get_col_by_name(client.capif_admins).insert_one({"admin_name": config["mongo"]["user"], "admin_pass": config["mongo"]["password"]}) +if not client.get_col_by_name(client.capif_admins).find_one({"admin_name": config["register"]["admin_users"]["admin_user"], "admin_pass": config["register"]["admin_users"]["admin_pass"]}): + client.get_col_by_name(client.capif_admins).insert_one({"admin_name": config["register"]["admin_users"]["admin_user"], "admin_pass": config["register"]["admin_users"]["admin_pass"]}) app.config['JWT_ALGORITHM'] = 'RS256' diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index e897074..16f3132 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -40,7 +40,7 @@ class RegisterOperations: if exist_user is None: return jsonify("Not exister user with this credentials"), 400 - access_token = create_access_token(identity=(username + " " + exist_user["uuid"])) + access_token = create_access_token(identity=(username + " " + exist_user["user_uuid"])) cert_file = open("register_service/certs/ca_root.crt", 'rb') ca_root = cert_file.read() -- GitLab From 699c8856144ef2d5391888a73852c1054cd9d526 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 12:32:47 +0200 Subject: [PATCH 228/310] fist commit helper --- .gitignore | 2 +- helm/capif/Chart.yaml | 1 + helm/capif/charts/helper/.helmignore | 23 ++++ helm/capif/charts/helper/Chart.yaml | 24 ++++ helm/capif/charts/helper/templates/NOTES.txt | 22 ++++ .../charts/helper/templates/_helpers.tpl | 62 +++++++++ .../charts/helper/templates/deployment.yaml | 76 +++++++++++ helm/capif/charts/helper/templates/hpa.yaml | 32 +++++ .../charts/helper/templates/ingress.yaml | 61 +++++++++ .../templates/ocf-helper-configmap.yaml | 24 ++++ .../charts/helper/templates/service.yaml | 15 +++ .../helper/templates/serviceaccount.yaml | 13 ++ .../templates/tests/test-connection.yaml | 15 +++ helm/capif/charts/helper/values.yaml | 119 ++++++++++++++++++ helm/capif/values.yaml | 12 ++ 15 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 helm/capif/charts/helper/.helmignore create mode 100644 helm/capif/charts/helper/Chart.yaml create mode 100644 helm/capif/charts/helper/templates/NOTES.txt create mode 100644 helm/capif/charts/helper/templates/_helpers.tpl create mode 100644 helm/capif/charts/helper/templates/deployment.yaml create mode 100644 helm/capif/charts/helper/templates/hpa.yaml create mode 100644 helm/capif/charts/helper/templates/ingress.yaml create mode 100644 helm/capif/charts/helper/templates/ocf-helper-configmap.yaml create mode 100644 helm/capif/charts/helper/templates/service.yaml create mode 100644 helm/capif/charts/helper/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/helper/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/helper/values.yaml diff --git a/.gitignore b/.gitignore index 66e4e33..c65c28d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,4 @@ docs/testing_with_postman/package-lock.json results helm/capif/*.lock -helm/capif/charts \ No newline at end of file +helm/capif/charts/tempo* \ No newline at end of file diff --git a/helm/capif/Chart.yaml b/helm/capif/Chart.yaml index 0c8eb5f..83f8ebf 100644 --- a/helm/capif/Chart.yaml +++ b/helm/capif/Chart.yaml @@ -24,3 +24,4 @@ dependencies: condition: tempo.enabled repository: "https://grafana.github.io/helm-charts" version: "^1.3.1" + - name: helper diff --git a/helm/capif/charts/helper/.helmignore b/helm/capif/charts/helper/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/helper/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/helper/Chart.yaml b/helm/capif/charts/helper/Chart.yaml new file mode 100644 index 0000000..4ddfbf3 --- /dev/null +++ b/helm/capif/charts/helper/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: helper +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/helper/templates/NOTES.txt b/helm/capif/charts/helper/templates/NOTES.txt new file mode 100644 index 0000000..f8f6f77 --- /dev/null +++ b/helm/capif/charts/helper/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "helper.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "helper.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "helper.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "helper.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/helper/templates/_helpers.tpl b/helm/capif/charts/helper/templates/_helpers.tpl new file mode 100644 index 0000000..f4a197b --- /dev/null +++ b/helm/capif/charts/helper/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "helper.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "helper.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "helper.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "helper.labels" -}} +helm.sh/chart: {{ include "helper.chart" . }} +{{ include "helper.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "helper.selectorLabels" -}} +app.kubernetes.io/name: {{ include "helper.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "helper.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "helper.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/helper/templates/deployment.yaml b/helm/capif/charts/helper/templates/deployment.yaml new file mode 100644 index 0000000..a3f43d3 --- /dev/null +++ b/helm/capif/charts/helper/templates/deployment.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "helper.fullname" . }} + labels: + {{- include "helper.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "helper.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + checksum/config: {{ include (print $.Template.BasePath "/ocf-helper-configmap.yaml") . | sha256sum }} + labels: + {{- include "helper.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "helper.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + env: + - name: CAPIF_HOSTNAME + value: {{ quote .Values.env.capifHostname }} + - name: VAULT_HOSTNAME + value: {{ quote .Values.env.vaultHostname }} + - name: VAULT_PORT + value: {{ quote .Values.env.vaultPort }} + - name: VAULT_ACCESS_TOKEN + value: {{ quote .Values.env.vaultAccessToken }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/helper/templates/hpa.yaml b/helm/capif/charts/helper/templates/hpa.yaml new file mode 100644 index 0000000..046148d --- /dev/null +++ b/helm/capif/charts/helper/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "helper.fullname" . }} + labels: + {{- include "helper.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "helper.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/helper/templates/ingress.yaml b/helm/capif/charts/helper/templates/ingress.yaml new file mode 100644 index 0000000..b3817bf --- /dev/null +++ b/helm/capif/charts/helper/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "helper.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "helper.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/helper/templates/ocf-helper-configmap.yaml b/helm/capif/charts/helper/templates/ocf-helper-configmap.yaml new file mode 100644 index 0000000..796a55c --- /dev/null +++ b/helm/capif/charts/helper/templates/ocf-helper-configmap.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: ocf-helper-configmap +data: + config.yaml: | + mongo: { + 'user': '{{ .Values.env.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoInitdbRootPassword }}', + 'db': 'capif', + 'invoker_col': 'invokerdetails', + 'provider_col': 'providerenrolmentdetails', + 'col_services': "serviceapidescriptions", + 'col_security': "security", + 'col_event': "eventsdetails", + 'host': '{{ .Values.env.mongoHost }}', + 'port': "{{ .Values.env.mongoPort }}" + } + + ca_factory: { + "url": {{ quote .Values.env.vaultHostname }}, + "port": {{ quote .Values.env.vaultPort }}, + "token": {{ quote .Values.env.vaultAccessToken }} + } \ No newline at end of file diff --git a/helm/capif/charts/helper/templates/service.yaml b/helm/capif/charts/helper/templates/service.yaml new file mode 100644 index 0000000..4a74370 --- /dev/null +++ b/helm/capif/charts/helper/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: helper + labels: + {{- include "helper.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "helper.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/helper/templates/serviceaccount.yaml b/helm/capif/charts/helper/templates/serviceaccount.yaml new file mode 100644 index 0000000..e0e6d79 --- /dev/null +++ b/helm/capif/charts/helper/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "helper.serviceAccountName" . }} + labels: + {{- include "helper.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/helper/templates/tests/test-connection.yaml b/helm/capif/charts/helper/templates/tests/test-connection.yaml new file mode 100644 index 0000000..f3959cc --- /dev/null +++ b/helm/capif/charts/helper/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "helper.fullname" . }}-test-connection" + labels: + {{- include "helper.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "helper.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/helper/values.yaml b/helm/capif/charts/helper/values.yaml new file mode 100644 index 0000000..55d7821 --- /dev/null +++ b/helm/capif/charts/helper/values.yaml @@ -0,0 +1,119 @@ +# Default values for helper. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: "helper" + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + vaultHostname: vault-internal.mon.svc.cluster.local + vaultPort: 8200 + vaultAccessToken: dev-only-token + mongoHost: mongo + mongoPort: 27017 + capifHostname: capif + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: + app: ocf-helper + +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: ocf-helper-configmap + items: + - key: config.yaml + path: config.yaml + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: ocf-helper-configmap + mountPath: "/usr/src/app/config.yaml" + subPath: config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index 8b9a920..f017ac0 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -523,6 +523,18 @@ parametersVault: vaultHostname: vault-internal.mon.svc.cluster.local vaultPort: 8200 vaultAccessToken: dev-only-token + +helper: + env: + vaultHostname: vault-internal.mon.svc.cluster.local + vaultPort: 8200 + vaultAccessToken: dev-only-token + mongoHost: mongo + mongoPort: 27017 + capifHostname: my-capif.apps.ocp-epg.hi.inet + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + # -- With tempo.enabled: false. It won't be deployed # -- If monitoring.enable: "true". Also enable tempo.enabled: true tempo: -- GitLab From afc2e11af5894b010da99b2884f280ac7afd327d Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 12:38:24 +0200 Subject: [PATCH 229/310] helm/capif/charts/helper/Chart.yaml --- helm/capif/charts/helper/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/charts/helper/values.yaml b/helm/capif/charts/helper/values.yaml index 55d7821..4fad2c7 100644 --- a/helm/capif/charts/helper/values.yaml +++ b/helm/capif/charts/helper/values.yaml @@ -116,4 +116,4 @@ nodeSelector: {} tolerations: [] -affinity: {} +affinity: {} \ No newline at end of file -- GitLab From 00870c22b403f5afb733c9ad8a1d511262d925ca Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 12:42:09 +0200 Subject: [PATCH 230/310] - name: helper --- helm/capif/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/Chart.yaml b/helm/capif/Chart.yaml index 83f8ebf..a9699ed 100644 --- a/helm/capif/Chart.yaml +++ b/helm/capif/Chart.yaml @@ -20,8 +20,8 @@ version: v3.1.6 # It is recommended to use it with quotes. appVersion: "v3.1.6" dependencies: + - name: helper - name: "tempo" condition: tempo.enabled repository: "https://grafana.github.io/helm-charts" version: "^1.3.1" - - name: helper -- GitLab From c8e10f2e7f81d7808172dc4182602a385edacb69 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 12:46:42 +0200 Subject: [PATCH 231/310] version helper charts --- helm/capif/Chart.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/capif/Chart.yaml b/helm/capif/Chart.yaml index a9699ed..fc0c967 100644 --- a/helm/capif/Chart.yaml +++ b/helm/capif/Chart.yaml @@ -21,6 +21,7 @@ version: v3.1.6 appVersion: "v3.1.6" dependencies: - name: helper + version: "*" - name: "tempo" condition: tempo.enabled repository: "https://grafana.github.io/helm-charts" -- GitLab From 0163299aa5a4dd1660b4a6716ffca90f1eee8c1a Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 13:01:34 +0200 Subject: [PATCH 232/310] volume --- helm/capif/charts/helper/values.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helm/capif/charts/helper/values.yaml b/helm/capif/charts/helper/values.yaml index 4fad2c7..8e53f32 100644 --- a/helm/capif/charts/helper/values.yaml +++ b/helm/capif/charts/helper/values.yaml @@ -103,13 +103,13 @@ autoscaling: volumes: - name: ocf-helper-configmap items: - - key: config.yaml - path: config.yaml + - key: "config.yaml" + path: "config.yaml" # Additional volumeMounts on the output Deployment definition. volumeMounts: - name: ocf-helper-configmap - mountPath: "/usr/src/app/config.yaml" + mountPath: /usr/src/app/config.yaml subPath: config.yaml nodeSelector: {} -- GitLab From 87215cc00c7c92fe8b18f1d8d123c56d8826a16c Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 13:41:25 +0200 Subject: [PATCH 233/310] no volumes --- helm/capif/charts/helper/values.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/helm/capif/charts/helper/values.yaml b/helm/capif/charts/helper/values.yaml index 8e53f32..d759b26 100644 --- a/helm/capif/charts/helper/values.yaml +++ b/helm/capif/charts/helper/values.yaml @@ -100,17 +100,17 @@ autoscaling: # targetMemoryUtilizationPercentage: 80 # Additional volumes on the output Deployment definition. -volumes: - - name: ocf-helper-configmap - items: - - key: "config.yaml" - path: "config.yaml" +volumes: [] +# - name: ocf-helper-configmap +# items: +# - key: "config.yaml" +# path: "config.yaml" # Additional volumeMounts on the output Deployment definition. -volumeMounts: - - name: ocf-helper-configmap - mountPath: /usr/src/app/config.yaml - subPath: config.yaml +volumeMounts: [] +# - name: ocf-helper-configmap +# mountPath: /usr/src/app/config.yaml +# subPath: config.yaml nodeSelector: {} -- GitLab From ed79878201a2ffc67960a5d27f9fefb2cadb356d Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 13:48:03 +0200 Subject: [PATCH 234/310] volumes --- helm/capif/charts/helper/values.yaml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/helm/capif/charts/helper/values.yaml b/helm/capif/charts/helper/values.yaml index d759b26..d631818 100644 --- a/helm/capif/charts/helper/values.yaml +++ b/helm/capif/charts/helper/values.yaml @@ -100,17 +100,19 @@ autoscaling: # targetMemoryUtilizationPercentage: 80 # Additional volumes on the output Deployment definition. -volumes: [] -# - name: ocf-helper-configmap -# items: -# - key: "config.yaml" -# path: "config.yaml" +volumes: + - name: ocf-helper-configmap + configMap: + name: ocf-helper-configmap + items: + - key: "config.yaml" + path: "config.yaml" # Additional volumeMounts on the output Deployment definition. -volumeMounts: [] -# - name: ocf-helper-configmap -# mountPath: /usr/src/app/config.yaml -# subPath: config.yaml +volumeMounts: + - name: ocf-helper-configmap + mountPath: /usr/src/app/config.yaml + subPath: config.yaml nodeSelector: {} -- GitLab From 3cfa7f37ee12eaef3b34dc0a3583c365b8af3e37 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 13:51:48 +0200 Subject: [PATCH 235/310] trigger --- helm/capif/charts/helper/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/charts/helper/values.yaml b/helm/capif/charts/helper/values.yaml index d631818..fce1936 100644 --- a/helm/capif/charts/helper/values.yaml +++ b/helm/capif/charts/helper/values.yaml @@ -118,4 +118,4 @@ nodeSelector: {} tolerations: [] -affinity: {} \ No newline at end of file +affinity: {} -- GitLab From da44322d2fcefa09e5ee54f4dcc242baa5cf567e Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 13:58:04 +0200 Subject: [PATCH 236/310] live and read probe --- helm/capif/charts/helper/values.yaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/helm/capif/charts/helper/values.yaml b/helm/capif/charts/helper/values.yaml index fce1936..147c003 100644 --- a/helm/capif/charts/helper/values.yaml +++ b/helm/capif/charts/helper/values.yaml @@ -84,13 +84,11 @@ resources: {} # memory: 128Mi livenessProbe: - httpGet: - path: / - port: http + tcpSocket: + port: 8080 readinessProbe: - httpGet: - path: / - port: http + tcpSocket: + port: 8080 autoscaling: enabled: false -- GitLab From af0d4883a1dcac23505fae5307b6f47b481e2ffe Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 14:05:43 +0200 Subject: [PATCH 237/310] dict register --- helm/capif/templates/register-configmap.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helm/capif/templates/register-configmap.yaml b/helm/capif/templates/register-configmap.yaml index 51293a5..7dcc300 100644 --- a/helm/capif/templates/register-configmap.yaml +++ b/helm/capif/templates/register-configmap.yaml @@ -18,4 +18,10 @@ data: "url": "{{ .Values.parametersVault.env.vaultHostname }}", "port": "{{ .Values.parametersVault.env.vaultPort }}", "token": "{{ .Values.parametersVault.env.vaultAccessToken }}" + } + register: { + register_uuid: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + refresh_expiration: 30, #days + token_expiration: 10, #mins + admin_users: {admin: "password123"} } \ No newline at end of file -- GitLab From 397727fc76384a57ae85f751b3ac97e63ea785ae Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 15:20:38 +0200 Subject: [PATCH 238/310] trigger --- helm/capif/charts/helper/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/charts/helper/values.yaml b/helm/capif/charts/helper/values.yaml index 147c003..389f212 100644 --- a/helm/capif/charts/helper/values.yaml +++ b/helm/capif/charts/helper/values.yaml @@ -116,4 +116,4 @@ nodeSelector: {} tolerations: [] -affinity: {} +affinity: {} \ No newline at end of file -- GitLab From c00b2479557b402da1ce83ecf2e292bf2ce96e65 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 15:40:53 +0200 Subject: [PATCH 239/310] trigger --- helm/capif/charts/helper/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/charts/helper/values.yaml b/helm/capif/charts/helper/values.yaml index 389f212..147c003 100644 --- a/helm/capif/charts/helper/values.yaml +++ b/helm/capif/charts/helper/values.yaml @@ -116,4 +116,4 @@ nodeSelector: {} tolerations: [] -affinity: {} \ No newline at end of file +affinity: {} -- GitLab From 67429e5ebb4c9031f127b0784ce7970594e5216e Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 4 Jun 2024 15:55:52 +0200 Subject: [PATCH 240/310] ReadWriteMany --- helm/capif/templates/mongo-register-pvc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/templates/mongo-register-pvc.yaml b/helm/capif/templates/mongo-register-pvc.yaml index 4d1a259..b5a11d6 100644 --- a/helm/capif/templates/mongo-register-pvc.yaml +++ b/helm/capif/templates/mongo-register-pvc.yaml @@ -9,7 +9,7 @@ metadata: spec: storageClassName: {{ .Values.mongoRegister.mongo.persistence.storageClass }} accessModes: - - ReadWriteOnce + - ReadWriteMany resources: requests: storage: {{ .Values.mongoRegister.mongo.persistence.storage }} -- GitLab From cb619ca67217d252e5b3f805ead89ea309151990 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 4 Jun 2024 17:18:01 +0200 Subject: [PATCH 241/310] Update mock server, to be included in a simply way to test locally --- services/clean_capif_docker_services.sh | 9 +++++-- services/clean_mock_server.sh | 16 +++++++++++ services/docker-compose-mock-server.yml | 20 ++++++++++++++ services/mock_server/Dockerfile | 21 +++++++++++++++ .../mock_server/mock_server.py | 0 .../mock_server/requirements.txt | 0 services/run.sh | 27 +++++++++++++++++-- services/run_capif_tests.sh | 4 +-- services/run_mock_server.sh | 15 ++++++----- services/show_logs.sh | 9 +++++-- 10 files changed, 107 insertions(+), 14 deletions(-) create mode 100755 services/clean_mock_server.sh create mode 100644 services/docker-compose-mock-server.yml create mode 100644 services/mock_server/Dockerfile rename {tests/libraries => services}/mock_server/mock_server.py (100%) rename {tests/libraries => services}/mock_server/requirements.txt (100%) diff --git a/services/clean_capif_docker_services.sh b/services/clean_capif_docker_services.sh index 1bc6d6d..cfc5b71 100755 --- a/services/clean_capif_docker_services.sh +++ b/services/clean_capif_docker_services.sh @@ -6,6 +6,7 @@ help() { echo " -v : Clean vault service" echo " -r : Clean register service" echo " -m : Clean monitoring service" + echo " -s : Clean Robot Mock service" echo " -a : Clean all services" echo " -h : show this help" exit 1 @@ -21,7 +22,7 @@ FILES=() echo "${FILES[@]}" # Read params -while getopts "cvrahm" opt; do +while getopts "cvrahms" opt; do case $opt in c) echo "Remove Capif services" @@ -39,9 +40,13 @@ while getopts "cvrahm" opt; do echo "Remove monitoring service" FILES+=("../monitoring/docker-compose.yml") ;; + s) + echo "Robot Mock Server" + FILES+=("docker-compose-mock-server.yml") + ;; a) echo "Remove all services" - FILES=("docker-compose-capif.yml" "docker-compose-vault.yml" "docker-compose-register.yml" "../monitoring/docker-compose.yml") + FILES=("docker-compose-capif.yml" "docker-compose-vault.yml" "docker-compose-register.yml" "docker-compose-mock-server.yml" "../monitoring/docker-compose.yml") ;; h) help diff --git a/services/clean_mock_server.sh b/services/clean_mock_server.sh new file mode 100755 index 0000000..5ea886c --- /dev/null +++ b/services/clean_mock_server.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +FILE="docker-compose-mock-server.yml" + +echo "Executing 'docker compose down' for file $FILE" +docker compose -f "$FILE" down --rmi all +status=$? + if [ $status -eq 0 ]; then + echo "*** Removed Service from $FILE ***" + else + echo "*** Some services of $FILE failed on clean ***" + fi + +docker volume prune --all --force + +echo "Clean complete." diff --git a/services/docker-compose-mock-server.yml b/services/docker-compose-mock-server.yml new file mode 100644 index 0000000..1415b43 --- /dev/null +++ b/services/docker-compose-mock-server.yml @@ -0,0 +1,20 @@ +services: + mock-server: + build: + context: ./mock_server + ports: + - 9090:9090 + volumes: + - ./mock_server:/usr/src/app + extra_hosts: + - host.docker.internal:host-gateway + restart: unless-stopped + image: public.ecr.aws/o2v4a8t6/opencapif/mock_server:latest + +networks: + default: + name: capif-network + external: true + + + diff --git a/services/mock_server/Dockerfile b/services/mock_server/Dockerfile new file mode 100644 index 0000000..6d72bf5 --- /dev/null +++ b/services/mock_server/Dockerfile @@ -0,0 +1,21 @@ +# start by pulling the python image +FROM python:3.10.0-alpine + +# copy the requirements file into the image +COPY ./requirements.txt /app/requirements.txt + +# switch working directory +WORKDIR /app + +# install the dependencies and packages in the requirements file +RUN pip install -r requirements.txt + +# copy every content from the local file to the image +COPY . /app + +EXPOSE 9090 + +# configure the container to run in an executed manner +ENTRYPOINT [ "python" ] + +CMD ["mock_server.py" ] \ No newline at end of file diff --git a/tests/libraries/mock_server/mock_server.py b/services/mock_server/mock_server.py similarity index 100% rename from tests/libraries/mock_server/mock_server.py rename to services/mock_server/mock_server.py diff --git a/tests/libraries/mock_server/requirements.txt b/services/mock_server/requirements.txt similarity index 100% rename from tests/libraries/mock_server/requirements.txt rename to services/mock_server/requirements.txt diff --git a/services/run.sh b/services/run.sh index 83011c3..4465aaa 100755 --- a/services/run.sh +++ b/services/run.sh @@ -3,6 +3,7 @@ help() { echo "Usage: $1 " echo " -c : Setup different hostname for capif" + echo " -s : Run Mock server" echo " -m : Clean monitoring service" echo " -h : show this help" exit 1 @@ -12,10 +13,14 @@ HOSTNAME=capifcore MONITORING_STATE=false DEPLOY=all -#Needed to avoid write permissions on bind volumes with prometheus and grafana +# Needed to avoid write permissions on bind volumes with prometheus and grafana DUID=$(id -u) DGID=$(id -g) +# Mock Server configuration +IP=0.0.0.0 +PORT=9090 + # Get docker compose version docker_version=$(docker compose version --short | cut -d',' -f1) IFS='.' read -ra version_components <<< "$docker_version" @@ -28,7 +33,7 @@ else fi # Read params -while getopts ":c:mh" opt; do +while getopts ":c:msh" opt; do case $opt in c) HOSTNAME="$OPTARG" @@ -36,6 +41,9 @@ while getopts ":c:mh" opt; do m) MONITORING_STATE=true ;; + s) + ROBOT_MOCK_SERVER=true + ;; h) help ;; @@ -97,6 +105,21 @@ if [ $status -eq 0 ]; then echo "*** Register Service are running ***" else echo "*** Register Service failed to start ***" + exit $status +fi + +if [ "$ROBOT_MOCK_SERVER" == "true" ] ; then + echo '***Robot Mock Server set as true***' + echo '***Creating Robot Mock Server stack***' + + IP=$IP PORT=$PORT docker compose -f "docker-compose-mock-server.yml" up --detach + status=$? + if [ $status -eq 0 ]; then + echo "*** Monitoring Stack Runing ***" + else + echo "*** Monitoring Stack failed to start ***" + exit $status + fi fi exit $status diff --git a/services/run_capif_tests.sh b/services/run_capif_tests.sh index 8f9f414..7762fcd 100755 --- a/services/run_capif_tests.sh +++ b/services/run_capif_tests.sh @@ -26,7 +26,7 @@ CAPIF_VAULT_PORT=8200 CAPIF_VAULT_TOKEN=read-ca-token -MOCK_SERVER_URL=http://192.168.0.11:9090 +MOCK_SERVER_URL=http://mock-server:9090 echo "CAPIF_HOSTNAME = $CAPIF_HOSTNAME" @@ -66,7 +66,7 @@ docker run -ti --rm --network="host" \ --add-host host.docker.internal:host-gateway \ --add-host vault:host-gateway \ --add-host register:host-gateway \ - --add-host mockserver:host-gateway \ + --add-host mock-server:host-gateway \ -v $TEST_FOLDER:/opt/robot-tests/tests \ -v $RESULT_FOLDER:/opt/robot-tests/results ${DOCKER_ROBOT_IMAGE}:${DOCKER_ROBOT_IMAGE_VERSION} \ --variable CAPIF_HOSTNAME:$CAPIF_HOSTNAME \ diff --git a/services/run_mock_server.sh b/services/run_mock_server.sh index b6f1035..5a194c4 100755 --- a/services/run_mock_server.sh +++ b/services/run_mock_server.sh @@ -8,10 +8,6 @@ help() { exit 1 } -cd .. -REPOSITORY_BASE_FOLDER=${PWD} -MOCK_SERVER_FOLDER=${PWD}/tests/libraries/mock_server - IP=0.0.0.0 PORT=9090 @@ -39,6 +35,13 @@ while getopts ":i:p:h" opt; do done echo Robot Framework Mock Server will listen on $IP:$PORT -pip install -r ${MOCK_SERVER_FOLDER}/requirements.txt -IP=$IP PORT=$PORT python ${MOCK_SERVER_FOLDER}/mock_server.py +IP=$IP PORT=$PORT docker compose -f "docker-compose-mock-server.yml" up --detach --build + +status=$? +if [ $status -eq 0 ]; then + echo "*** All Capif services are running ***" +else + echo "*** Some Capif services failed to start ***" + exit $status +fi diff --git a/services/show_logs.sh b/services/show_logs.sh index b53641d..c2bcf52 100755 --- a/services/show_logs.sh +++ b/services/show_logs.sh @@ -5,6 +5,7 @@ help() { echo " -c : Show capif services" echo " -v : Show vault service" echo " -r : Show register service" + echo " -s : Show Robot Mock Server service" echo " -m : Show monitoring service" echo " -a : Show all services" echo " -f : Follow log output" @@ -23,7 +24,7 @@ echo "${FILES[@]}" FOLLOW="" # Read params -while getopts "cvrahmf" opt; do +while getopts "cvrahmfs" opt; do case $opt in c) echo "Show Capif services" @@ -37,13 +38,17 @@ while getopts "cvrahmf" opt; do echo "Show register service" FILES+=("-f docker-compose-register.yml") ;; + s) + echo "Show Mock Server service" + FILES+=("-f docker-compose-mock-server.yml") + ;; m) echo "Show monitoring service" FILES+=("-f ../monitoring/docker-compose.yml") ;; a) echo "Show all services" - FILES=("-f docker-compose-capif.yml" -f "docker-compose-vault.yml" -f "docker-compose-register.yml" -f "../monitoring/docker-compose.yml") + FILES=("-f docker-compose-capif.yml" -f "docker-compose-vault.yml" -f "docker-compose-register.yml" -f "docker-compose-mock-server.yml" -f "../monitoring/docker-compose.yml") ;; f) echo "Setup follow logs" -- GitLab From 61677dbb1193ffacf767c216373e1d004bd6b23a Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Wed, 5 Jun 2024 10:20:53 +0200 Subject: [PATCH 242/310] fix uuid --- .../register/register_service/core/register_operations.py | 4 ++-- tests/resources/common/basicRequests.robot | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index 16f3132..b76d092 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -23,7 +23,7 @@ class RegisterOperations: name_space = uuid.UUID(self.config["register"]["register_uuid"]) user_uuid = str(uuid.uuid5(name_space,user_info["username"])) - user_info["user_uuid"] = user_uuid + user_info["uuid"] = user_uuid user_info["onboarding_date"]=datetime.now() obj = mycol.insert_one(user_info) @@ -40,7 +40,7 @@ class RegisterOperations: if exist_user is None: return jsonify("Not exister user with this credentials"), 400 - access_token = create_access_token(identity=(username + " " + exist_user["user_uuid"])) + access_token = create_access_token(identity=(username + " " + exist_user["uuid"])) cert_file = open("register_service/certs/ca_root.crt", 'rb') ca_root = cert_file.read() diff --git a/tests/resources/common/basicRequests.robot b/tests/resources/common/basicRequests.robot index ea3a96f..5c5f650 100644 --- a/tests/resources/common/basicRequests.robot +++ b/tests/resources/common/basicRequests.robot @@ -416,6 +416,11 @@ Create User At Register ... password=${password} ... description=${description} ... email=${email} + ... enterprise=enterprise + ... country=Spain + ... purpose=testing + ... phone_number=123456789 + ... company_web=www.enterprise.com ${resp}= Post On Session register_session /createUser headers=${headers} json=${body} Should Be Equal As Strings ${resp.status_code} 201 -- GitLab From e5a8cd2bc4c727cdf3d35c9730a24c96e6a73452 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 5 Jun 2024 16:57:42 +0200 Subject: [PATCH 243/310] access-control-policy --- helm/capif/Chart.yaml | 2 + .../charts/access-control-policy/.helmignore | 23 ++++ .../charts/access-control-policy/Chart.yaml | 24 ++++ .../access-control-policy/templates/NOTES.txt | 22 ++++ .../templates/_helpers.tpl | 62 ++++++++++ .../templates/deployment.yaml | 71 +++++++++++ .../access-control-policy/templates/hpa.yaml | 32 +++++ .../templates/ingress.yaml | 61 ++++++++++ .../templates/service.yaml | 15 +++ .../templates/serviceaccount.yaml | 13 ++ .../templates/tests/test-connection.yaml | 15 +++ .../charts/access-control-policy/values.yaml | 112 ++++++++++++++++++ .../templates/access-control-policy.yaml | 17 --- helm/capif/templates/deployment.yaml | 95 --------------- helm/capif/values.yaml | 25 ---- 15 files changed, 452 insertions(+), 137 deletions(-) create mode 100644 helm/capif/charts/access-control-policy/.helmignore create mode 100644 helm/capif/charts/access-control-policy/Chart.yaml create mode 100644 helm/capif/charts/access-control-policy/templates/NOTES.txt create mode 100644 helm/capif/charts/access-control-policy/templates/_helpers.tpl create mode 100644 helm/capif/charts/access-control-policy/templates/deployment.yaml create mode 100644 helm/capif/charts/access-control-policy/templates/hpa.yaml create mode 100644 helm/capif/charts/access-control-policy/templates/ingress.yaml create mode 100644 helm/capif/charts/access-control-policy/templates/service.yaml create mode 100644 helm/capif/charts/access-control-policy/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/access-control-policy/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/access-control-policy/values.yaml delete mode 100644 helm/capif/templates/access-control-policy.yaml diff --git a/helm/capif/Chart.yaml b/helm/capif/Chart.yaml index fc0c967..b76f468 100644 --- a/helm/capif/Chart.yaml +++ b/helm/capif/Chart.yaml @@ -20,6 +20,8 @@ version: v3.1.6 # It is recommended to use it with quotes. appVersion: "v3.1.6" dependencies: + - name: access-control-policy + version: "*" - name: helper version: "*" - name: "tempo" diff --git a/helm/capif/charts/access-control-policy/.helmignore b/helm/capif/charts/access-control-policy/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/access-control-policy/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/access-control-policy/Chart.yaml b/helm/capif/charts/access-control-policy/Chart.yaml new file mode 100644 index 0000000..b13bbf0 --- /dev/null +++ b/helm/capif/charts/access-control-policy/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: access-control-policy +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/access-control-policy/templates/NOTES.txt b/helm/capif/charts/access-control-policy/templates/NOTES.txt new file mode 100644 index 0000000..2c54f9d --- /dev/null +++ b/helm/capif/charts/access-control-policy/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "access-control-policy.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "access-control-policy.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "access-control-policy.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "access-control-policy.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/access-control-policy/templates/_helpers.tpl b/helm/capif/charts/access-control-policy/templates/_helpers.tpl new file mode 100644 index 0000000..4b87b90 --- /dev/null +++ b/helm/capif/charts/access-control-policy/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "access-control-policy.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "access-control-policy.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "access-control-policy.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "access-control-policy.labels" -}} +helm.sh/chart: {{ include "access-control-policy.chart" . }} +{{ include "access-control-policy.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "access-control-policy.selectorLabels" -}} +app.kubernetes.io/name: {{ include "access-control-policy.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "access-control-policy.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "access-control-policy.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/access-control-policy/templates/deployment.yaml b/helm/capif/charts/access-control-policy/templates/deployment.yaml new file mode 100644 index 0000000..2e4d15f --- /dev/null +++ b/helm/capif/charts/access-control-policy/templates/deployment.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "access-control-policy.fullname" . }} + labels: + {{- include "access-control-policy.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "access-control-policy.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + labels: + {{- include "access-control-policy.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "access-control-policy.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: CAPIF_HOSTNAME + value: {{ quote .Values.env.capifHostname }} + - name: MONITORING + value: {{ quote .Values.env.monitoring }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/access-control-policy/templates/hpa.yaml b/helm/capif/charts/access-control-policy/templates/hpa.yaml new file mode 100644 index 0000000..67eb195 --- /dev/null +++ b/helm/capif/charts/access-control-policy/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "access-control-policy.fullname" . }} + labels: + {{- include "access-control-policy.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "access-control-policy.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/access-control-policy/templates/ingress.yaml b/helm/capif/charts/access-control-policy/templates/ingress.yaml new file mode 100644 index 0000000..dcafedb --- /dev/null +++ b/helm/capif/charts/access-control-policy/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "access-control-policy.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "access-control-policy.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/access-control-policy/templates/service.yaml b/helm/capif/charts/access-control-policy/templates/service.yaml new file mode 100644 index 0000000..c10293a --- /dev/null +++ b/helm/capif/charts/access-control-policy/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: access-control-policy + labels: + {{- include "access-control-policy.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "access-control-policy.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/access-control-policy/templates/serviceaccount.yaml b/helm/capif/charts/access-control-policy/templates/serviceaccount.yaml new file mode 100644 index 0000000..fc12b54 --- /dev/null +++ b/helm/capif/charts/access-control-policy/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "access-control-policy.serviceAccountName" . }} + labels: + {{- include "access-control-policy.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/access-control-policy/templates/tests/test-connection.yaml b/helm/capif/charts/access-control-policy/templates/tests/test-connection.yaml new file mode 100644 index 0000000..0e67abf --- /dev/null +++ b/helm/capif/charts/access-control-policy/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "access-control-policy.fullname" . }}-test-connection" + labels: + {{- include "access-control-policy.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "access-control-policy.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/access-control-policy/values.yaml b/helm/capif/charts/access-control-policy/values.yaml new file mode 100644 index 0000000..61aba34 --- /dev/null +++ b/helm/capif/charts/access-control-policy/values.yaml @@ -0,0 +1,112 @@ +# Default values for access-control-policy. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: access-control-policy + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + capifHostname: my-capif.apps.ocp-epg.hi.inet + monitoring: "true" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/templates/access-control-policy.yaml b/helm/capif/templates/access-control-policy.yaml deleted file mode 100644 index 8b2b198..0000000 --- a/helm/capif/templates/access-control-policy.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: access-control-policy - labels: - io.kompose.service: access-control-policy - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.accessControlPolicy.type }} - selector: - io.kompose.service: access-control-policy - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.accessControlPolicy.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/deployment.yaml b/helm/capif/templates/deployment.yaml index ff5ffe2..4f58100 100644 --- a/helm/capif/templates/deployment.yaml +++ b/helm/capif/templates/deployment.yaml @@ -1,98 +1,3 @@ -{{- if eq .Values.CapifClient.enable "true" }} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: capif-client - labels: - io.kompose.service: capif-client - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.CapifClient.replicas }} - selector: - matchLabels: - io.kompose.service: capif-client - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: capif-client - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: CAPIF_HOSTNAME - value: nginx.mon.svc.cluster.local - - name: VAULT_HOSTNAME - value: {{ quote .Values.parametersVault.env.vaultHostname }} - - name: VAULT_PORT - value: {{ quote .Values.parametersVault.env.vaultPort }} - - name: VAULT_ACCESS_TOKEN - value: {{ quote .Values.parametersVault.env.vaultAccessToken }} - image: {{ .Values.CapifClient.image.repository }}:{{ .Values.CapifClient.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.CapifClient.image.imagePullPolicy }} - name: capif-client - resources: - {{- toYaml .Values.CapifClient.resources | nindent 12 }} - restartPolicy: Always -{{- end }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: access-control-policy - labels: - io.kompose.service: access-control-policy - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.accessControlPolicy.replicas }} - selector: - matchLabels: - io.kompose.service: access-control-policy - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: access-control-policy - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: CAPIF_HOSTNAME - value: {{ quote .Values.nginx.nginx.env.capifHostname }} - - name: MONITORING - value: {{ quote .Values.accessControlPolicy.env.monitoring }} - image: {{ .Values.accessControlPolicy.image.repository }}:{{ .Values.accessControlPolicy.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.accessControlPolicy.image.imagePullPolicy }} - name: access-control-policy - ports: - - containerPort: 8080 - resources: - {{- toYaml .Values.accessControlPolicy.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 8080 - initialDelaySeconds: 10 - periodSeconds: 5 - restartPolicy: Always - --- apiVersion: apps/v1 kind: Deployment diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index f017ac0..0a50782 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -34,31 +34,6 @@ accessControlPolicy: replicas: 1 type: ClusterIP -CapifClient: - # -- If enable capif client. - enable: "" - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/client" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - replicas: 1 - type: ClusterIP - apiInvocationLogs: apiInvocationLogs: image: -- GitLab From 7abca1446ef23e22739052da7f1feb6bfaa62402 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Mon, 10 Jun 2024 10:58:42 +0300 Subject: [PATCH 244/310] Move Publish Service from testing to production server --- .../Dockerfile | 6 ++-- .../published_apis/{__main__.py => app.py} | 22 +++++++-------- .../published_apis/config.py | 2 +- .../controllers/default_controller.py | 10 ++----- .../published_apis/core/resources.py | 2 +- .../published_apis/core/responses.py | 28 ++++++++++++++++++- .../core/serviceapidescriptions.py | 14 ++-------- .../published_apis/core/validate_user.py | 4 +++ .../published_apis/db/db.py | 2 +- .../published_apis/encoder.py | 2 +- .../published_apis/util.py | 3 +- .../published_apis/wsgi.py | 4 +++ .../requirements.txt | 2 ++ 13 files changed, 61 insertions(+), 40 deletions(-) rename services/TS29222_CAPIF_Publish_Service_API/published_apis/{__main__.py => app.py} (94%) create mode 100644 services/TS29222_CAPIF_Publish_Service_API/published_apis/wsgi.py diff --git a/services/TS29222_CAPIF_Publish_Service_API/Dockerfile b/services/TS29222_CAPIF_Publish_Service_API/Dockerfile index b2e6167..9f298a3 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/Dockerfile +++ b/services/TS29222_CAPIF_Publish_Service_API/Dockerfile @@ -12,6 +12,8 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["python3"] +# ENTRYPOINT ["python3"] +ENTRYPOINT ["gunicorn"] -CMD ["-m", "published_apis"] \ No newline at end of file +# CMD ["-m", "published_apis"] +CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/published_apis", "wsgi:app"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/__main__.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py similarity index 94% rename from services/TS29222_CAPIF_Publish_Service_API/published_apis/__main__.py rename to services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py index dfba9ef..f4cdc30 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/__main__.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py @@ -3,15 +3,15 @@ import connexion import logging -from published_apis import encoder +# from published_apis import encoder +import encoder -from flask import Flask, jsonify, request from flask_jwt_extended import JWTManager, jwt_required, create_access_token -from pymongo import MongoClient -from .config import Config +# from .config import Config +from config import Config from logging.handlers import RotatingFileHandler -from .core.consumer_messager import Subscriber -from flask_executor import Executor +from core.consumer_messager import Subscriber + import os from fluent import sender from flask_executor import Executor @@ -24,10 +24,9 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.instrumentation.redis import RedisInstrumentor - - NAME = "Publish-Service" + def configure_monitoring(app, config): resource = Resource(attributes={"service.name": NAME}) @@ -86,7 +85,6 @@ def configure_monitoring(app, config): l.addHandler(FluentBitHandler()) - def configure_logging(app): del app.logger.handlers[:] loggers = [app.logger, ] @@ -114,6 +112,7 @@ def verbose_formatter(): datefmt='%d/%m/%Y %H:%M:%S' ) + app = connexion.App(__name__, specification_dir='./openapi/') app.app.json_encoder = encoder.JSONEncoder app.add_api('openapi.yaml', @@ -133,10 +132,11 @@ if eval(os.environ.get("MONITORING").lower().capitalize()): executor = Executor(app.app) subscriber = Subscriber() + @app.app.before_first_request def up_listener(): executor.submit(subscriber.listen) -if __name__ == '__main__': - app.run(debug=True, port=8080) +# if __name__ == '__main__': +# app.run(debug=True, port=8080) diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/config.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/config.py index 11e1c4f..01f9914 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/config.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/config.py @@ -5,7 +5,7 @@ import os class Config: def __init__(self): self.cached = 0 - self.file="./config.yaml" + self.file="../config.yaml" self.my_config = {} stamp = os.stat(self.file).st_mtime diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py index 8fc2b62..ab80164 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/controllers/default_controller.py @@ -1,20 +1,14 @@ import connexion -from published_apis.models.service_api_description import ServiceAPIDescription # noqa: E501 -from ..core import serviceapidescriptions +from ..models.service_api_description import ServiceAPIDescription # noqa: E501 from ..core.serviceapidescriptions import PublishServiceOperations from ..core.publisher import Publisher -import json from flask import Response, request, current_app -from flask_jwt_extended import jwt_required, get_jwt_identity -from flask import current_app -from ..encoder import JSONEncoder -from ..models.problem_details import ProblemDetails + from cryptography import x509 from cryptography.hazmat.backends import default_backend from ..core.validate_user import ControlAccess from functools import wraps -import pymongo service_operations = PublishServiceOperations() diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/resources.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/resources.py index 94e29ec..efbe3c2 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/resources.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/resources.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from ..db.db import MongoDatabse +from db.db import MongoDatabse class Resource(ABC): diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/responses.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/responses.py index c1809ed..c61eae0 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/responses.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/responses.py @@ -1,37 +1,63 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response +from ..util import dict_to_camel_case, clean_empty import json -from bson import json_util + mimetype = "application/json" + def make_response(object, status): res = Response(json.dumps(object, cls=JSONEncoder), status=status, mimetype=mimetype) return res + def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) + def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) + def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) + def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) def unauthorized_error(detail, cause): prob = ProblemDetails(title="Unauthorized", status=401, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py index 5c903d1..9dd9beb 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py @@ -1,21 +1,11 @@ -import sys - -import pymongo from pymongo import ReturnDocument import secrets from flask import current_app, Flask, Response -import json -from pymongo import response -from ..db.db import MongoDatabse -from ..encoder import JSONEncoder -from ..models.problem_details import ProblemDetails from .resources import Resource -from published_apis.models.service_api_description import ServiceAPIDescription from datetime import datetime from ..util import dict_to_camel_case, clean_empty -from .responses import bad_request_error, internal_server_error, forbidden_error, not_found_error, unauthorized_error, make_response -from bson import json_util +from .responses import internal_server_error, forbidden_error, not_found_error, unauthorized_error, make_response from .auth_manager import AuthManager @@ -109,7 +99,7 @@ class PublishServiceOperations(Resource): self.auth_manager.add_auth_service(api_id, apf_id) current_app.logger.debug("Service inserted in database") - res = make_response(object=serviceapidescription, status=201) + res = make_response(object=dict_to_camel_case(clean_empty(serviceapidescription.to_dict())), status=201) res.headers['Location'] = "http://localhost:8080/published-apis/v1/" + str(apf_id) + "/service-apis/" + str(api_id) return res diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/validate_user.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/validate_user.py index 67d434d..5eed2c9 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/validate_user.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/validate_user.py @@ -4,6 +4,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource from .responses import internal_server_error +from ..util import dict_to_camel_case, clean_empty class ControlAccess(Resource): @@ -18,6 +19,9 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature or "services" not in cert_entry["resources"] or service_id not in cert_entry["resources"]["services"]: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") except Exception as e: diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py index 91f7b6b..114b50b 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py @@ -2,7 +2,7 @@ import atexit import time from pymongo import MongoClient from pymongo.errors import AutoReconnect -from ..config import Config +from config import Config from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/encoder.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/encoder.py index c753f45..80bad8f 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/encoder.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/encoder.py @@ -1,7 +1,7 @@ from connexion.apps.flask_app import FlaskJSONEncoder import six -from published_apis.models.base_model_ import Model +from models.base_model_ import Model class JSONEncoder(FlaskJSONEncoder): diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/util.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/util.py index 3f53de6..c43b274 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/util.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/util.py @@ -1,8 +1,7 @@ import datetime import six -import typing -from published_apis import typing_utils +import typing_utils def clean_empty(d): diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/wsgi.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/wsgi.py new file mode 100644 index 0000000..6026b0f --- /dev/null +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/wsgi.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == "__main__": + app.run() diff --git a/services/TS29222_CAPIF_Publish_Service_API/requirements.txt b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt index 0ba8fb5..cd12aa4 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt @@ -19,3 +19,5 @@ flask_executor == 1.0.0 pyopenssl == 23.0.0 redis == 4.5.4 flask_executor == 1.0.0 +gunicorn==22.0.0 +packaging==24.0 -- GitLab From c5a9a3adea80b9b5aa945a9698de8e813f0af030 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Mon, 10 Jun 2024 12:03:20 +0200 Subject: [PATCH 245/310] fix --- services/register/register_service/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/services/register/register_service/__main__.py b/services/register/register_service/__main__.py index 12f6ffd..dbbbb37 100644 --- a/services/register/register_service/__main__.py +++ b/services/register/register_service/__main__.py @@ -11,6 +11,7 @@ from .config import Config app = Flask(__name__) + jwt_manager = JWTManager(app) config = Config().get_config() -- GitLab From d140ad20bc5ae36d3784000373766f0be1fc2a1c Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Mon, 10 Jun 2024 15:00:49 +0300 Subject: [PATCH 246/310] Move Invoker Management API from testing to production server, remove unnecessary imports --- .../{__main__.py => app.py} | 21 ++++++------------- .../api_invoker_management/config.py | 2 +- .../controllers/default_controller.py | 5 +---- .../core/apiinvokerenrolmentdetails.py | 9 ++++---- .../core/auth_manager.py | 2 -- .../core/invoker_internal_ops.py | 1 - .../api_invoker_management/core/publisher.py | 2 -- .../api_invoker_management/core/resources.py | 2 +- .../api_invoker_management/core/responses.py | 17 +++++++++++++++ .../core/validate_user.py | 5 +++++ .../api_invoker_management/db/db.py | 3 +-- .../api_invoker_management/encoder.py | 2 +- .../api_invoker_management/util.py | 16 ++++++++++++-- .../api_invoker_management/wsgi.py | 4 ++++ .../prepare_invoker.sh | 6 ++++-- .../requirements.txt | 2 ++ .../api_provider_management/app.py | 1 - .../controllers/default_controller.py | 3 --- ...i_provider_enrolment_details_controller.py | 4 ---- .../core/auth_manager.py | 1 - .../core/provider_enrolment_details_api.py | 2 -- .../api_provider_management/core/publisher.py | 1 - .../api_provider_management/core/resources.py | 6 +++--- .../core/sign_certificate.py | 1 - .../core/validate_user.py | 4 ++++ .../api_provider_management/db/db.py | 3 +-- .../api_provider_management/util.py | 2 -- .../published_apis/app.py | 1 - .../published_apis/core/auth_manager.py | 2 -- .../published_apis/core/consumer_messager.py | 5 ----- .../published_apis/core/publisher.py | 1 - .../published_apis/db/db.py | 1 - services/register/register_service/app.py | 2 -- .../core/register_operations.py | 1 - services/register/register_service/db/db.py | 2 -- 35 files changed, 70 insertions(+), 72 deletions(-) rename services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/{__main__.py => app.py} (94%) create mode 100644 services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/wsgi.py diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/__main__.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py similarity index 94% rename from services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/__main__.py rename to services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py index 32ccc2e..5d8cc4b 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/__main__.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py @@ -2,13 +2,10 @@ import connexion import logging -from api_invoker_management import encoder - -from flask import Flask, jsonify, request +import encoder from flask_jwt_extended import JWTManager, jwt_required, create_access_token -from pymongo import MongoClient -from .config import Config -from .core.consumer_messager import Subscriber +from config import Config +from core.consumer_messager import Subscriber from logging.handlers import RotatingFileHandler import os from fluent import sender @@ -23,12 +20,6 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.instrumentation.redis import RedisInstrumentor - -import sys -import uuid - - - NAME = "Invoker-Service" def configure_monitoring(app, config): @@ -146,7 +137,7 @@ subscriber = Subscriber() def create_listener_message(): executor.submit(subscriber.listen) -if __name__ == '__main__': - import logging - app.run(debug=True, port=8080) +# if __name__ == '__main__': +# import logging +# app.run(debug=True, port=8080) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py index 11e1c4f..01f9914 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/config.py @@ -5,7 +5,7 @@ import os class Config: def __init__(self): self.cached = 0 - self.file="./config.yaml" + self.file="../config.yaml" self.my_config = {} stamp = os.stat(self.file).st_mtime diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py index 27eb1c8..9f94b19 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py @@ -1,14 +1,11 @@ import connexion -from api_invoker_management.models.api_invoker_enrolment_details import APIInvokerEnrolmentDetails # noqa: E501 +from ..models.api_invoker_enrolment_details import APIInvokerEnrolmentDetails # noqa: E501 from ..core.apiinvokerenrolmentdetails import InvokerManagementOperations from ..core.validate_user import ControlAccess -import json from flask import Response, request, current_app from flask_jwt_extended import jwt_required, get_jwt_identity -from ..encoder import JSONEncoder -from ..models.problem_details import ProblemDetails from cryptography import x509 from cryptography.hazmat.backends import default_backend from ..core.publisher import Publisher diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py index dc186e4..7becdca 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py @@ -6,7 +6,7 @@ from .responses import bad_request_error, not_found_error, forbidden_error, inte from flask import current_app, Flask, Response import json from datetime import datetime -from ..util import dict_to_camel_case +from ..util import dict_to_camel_case, clean_empty from .auth_manager import AuthManager from .resources import Resource from ..config import Config @@ -73,7 +73,6 @@ class InvokerManagementOperations(Resource): api_invoker_id = 'INV'+str(secrets.token_hex(15)) cert = self.__sign_cert(apiinvokerenrolmentdetail.onboarding_information.api_invoker_public_key, api_invoker_id) - apiinvokerenrolmentdetail.api_invoker_id = api_invoker_id current_app.logger.debug(cert) apiinvokerenrolmentdetail.onboarding_information.api_invoker_certificate = cert['data']['certificate'] @@ -91,7 +90,7 @@ class InvokerManagementOperations(Resource): self.auth_manager.add_auth_invoker(cert['data']['certificate'], api_invoker_id) - res = make_response(object=apiinvokerenrolmentdetail, status=201) + res = make_response(object=dict_to_camel_case(clean_empty(apiinvokerenrolmentdetail.to_dict())), status=201) res.headers['Location'] = "/api-invoker-management/v1/onboardedInvokers/" + str(api_invoker_id) return res @@ -129,7 +128,9 @@ class InvokerManagementOperations(Resource): current_app.logger.debug("Invoker Resource inserted in database") - res = make_response(object=APIInvokerEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) + invoker_updated = APIInvokerEnrolmentDetails().from_dict(dict_to_camel_case(result)) + + res = make_response(object=dict_to_camel_case(clean_empty(invoker_updated.to_dict())), status=200) return res except Exception as e: diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/auth_manager.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/auth_manager.py index cfab01e..c3736a6 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/auth_manager.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/auth_manager.py @@ -1,5 +1,3 @@ - -from flask import current_app from cryptography import x509 from cryptography.hazmat.backends import default_backend from .resources import Resource diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/invoker_internal_ops.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/invoker_internal_ops.py index a0aa138..a680d4d 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/invoker_internal_ops.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/invoker_internal_ops.py @@ -1,6 +1,5 @@ from .resources import Resource -from flask import current_app class InvokerInternalOperations(Resource): diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/publisher.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/publisher.py index 3898c4b..34fcdf4 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/publisher.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/publisher.py @@ -1,6 +1,4 @@ import redis -import sys -from flask import current_app class Publisher(): diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/resources.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/resources.py index 94e29ec..efbe3c2 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/resources.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/resources.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from ..db.db import MongoDatabse +from db.db import MongoDatabse class Resource(ABC): diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/responses.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/responses.py index 962c4b6..96ace13 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/responses.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/responses.py @@ -2,6 +2,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response import json +from ..util import dict_to_camel_case, clean_empty mimetype = "application/json" @@ -13,19 +14,35 @@ def make_response(object, status): def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/validate_user.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/validate_user.py index 5a2a5dc..bcf0d88 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/validate_user.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/validate_user.py @@ -4,6 +4,8 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource from .responses import internal_server_error +from ..util import dict_to_camel_case, clean_empty + class ControlAccess(Resource): @@ -18,6 +20,9 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") except Exception as e: diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py index 5bfcd0b..bb93393 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py @@ -1,8 +1,7 @@ -import atexit import time from pymongo import MongoClient from pymongo.errors import AutoReconnect -from ..config import Config +from config import Config from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/encoder.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/encoder.py index 04b06eb..80bad8f 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/encoder.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/encoder.py @@ -1,7 +1,7 @@ from connexion.apps.flask_app import FlaskJSONEncoder import six -from api_invoker_management.models.base_model_ import Model +from models.base_model_ import Model class JSONEncoder(FlaskJSONEncoder): diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/util.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/util.py index 6ce55d8..27ba971 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/util.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/util.py @@ -1,8 +1,20 @@ import datetime import six -import typing -from api_invoker_management import typing_utils +import typing_utils + + +def clean_empty(d): + if isinstance(d, dict): + return { + k: v + for k, v in ((k, clean_empty(v)) for k, v in d.items()) + if v + } + if isinstance(d, list): + return [v for v in map(clean_empty, d) if v] + return d + def dict_to_camel_case(my_dict): diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/wsgi.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/wsgi.py new file mode 100644 index 0000000..6026b0f --- /dev/null +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/wsgi.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == "__main__": + app.run() diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh b/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh index 90ce2be..76843be 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh @@ -14,5 +14,7 @@ curl -vv -k -retry 30 \ --request GET "$VAULT_ADDR/v1/secret/data/server_cert/pub" 2>/dev/null | jq -r '.data.data.pub_key' -j > /usr/src/app/api_invoker_management/pubkey.pem -cd /usr/src/app/ -python3 -m api_invoker_management \ No newline at end of file +#cd /usr/src/app/ +#python3 -m api_invoker_management +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/api_invoker_management wsgi:app \ No newline at end of file diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt index ce843e6..10f300b 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt @@ -19,3 +19,5 @@ fluent-logger == 0.10.0 opentelemetry-api == 1.19.0 opentelemetry-sdk == 1.19.0 flask_executor == 1.0.0 +gunicorn==22.0.0 +packaging==24.0 \ No newline at end of file diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py index 4caa391..7eefe68 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py @@ -10,7 +10,6 @@ from config import Config import os import sys from fluent import sender -from flask_executor import Executor from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry import trace from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py index 50745b8..d9a190c 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/default_controller.py @@ -1,7 +1,4 @@ import connexion -import six -import json - from flask import Response, request, current_app from ..core.provider_enrolment_details_api import ProviderManagementOperations diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/individual_api_provider_enrolment_details_controller.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/individual_api_provider_enrolment_details_controller.py index 50ccf0e..dee8d15 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/individual_api_provider_enrolment_details_controller.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/controllers/individual_api_provider_enrolment_details_controller.py @@ -1,8 +1,4 @@ -from email.quoprimime import body_decode import connexion -import six -import json - from flask import Response, request, current_app from ..core.provider_enrolment_details_api import ProviderManagementOperations from ..encoder import JSONEncoder diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/auth_manager.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/auth_manager.py index a4a2d24..cfa302a 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/auth_manager.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/auth_manager.py @@ -1,5 +1,4 @@ -from flask import current_app from cryptography import x509 from cryptography.hazmat.backends import default_backend from .resources import Resource diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py index 976cb32..61ab74d 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py @@ -138,7 +138,6 @@ class ProviderManagementOperations(Resource): current_app.logger.debug("Provider domain updated in database") provider_updated = APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)) - # return make_response(object=APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) return make_response(object=dict_to_camel_case(provider_updated.to_dict()), status=200) except Exception as e: @@ -165,7 +164,6 @@ class ProviderManagementOperations(Resource): current_app.logger.debug("Provider domain updated in database") provider_updated = APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)) - # return make_response(object=APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)), status=200) return make_response(object=dict_to_camel_case(provider_updated.to_dict()), status=200) except Exception as e: diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/publisher.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/publisher.py index 38d7259..acedb7e 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/publisher.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/publisher.py @@ -1,5 +1,4 @@ import redis -import sys class Publisher(): diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/resources.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/resources.py index 86e99d4..7b8092a 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/resources.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/resources.py @@ -1,6 +1,6 @@ -from abc import ABC, abstractmethod -from ..db.db import MongoDatabse -from .publisher import Publisher +from abc import ABC +from db.db import MongoDatabse +from core.publisher import Publisher class Resource(ABC): diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py index 6ec96cf..159947e 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py @@ -1,6 +1,5 @@ import requests import json -import sys from ..config import Config def sign_certificate(publick_key, provider_id): diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/validate_user.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/validate_user.py index 4d101ae..4a9445d 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/validate_user.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/validate_user.py @@ -4,6 +4,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource from .responses import internal_server_error +from ..util import dict_to_camel_case, clean_empty class ControlAccess(Resource): @@ -18,6 +19,9 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") except Exception as e: diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py index bb08025..e2aba93 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py @@ -1,8 +1,7 @@ -import atexit import time from pymongo import MongoClient from pymongo.errors import AutoReconnect -from ..config import Config +from config import Config from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py index 5ff8054..6fc44d5 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py @@ -1,7 +1,5 @@ import datetime - import six -import typing import typing_utils def clean_empty(d): diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py index f4cdc30..d3abb2d 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py @@ -7,7 +7,6 @@ import logging import encoder from flask_jwt_extended import JWTManager, jwt_required, create_access_token -# from .config import Config from config import Config from logging.handlers import RotatingFileHandler from core.consumer_messager import Subscriber diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/auth_manager.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/auth_manager.py index 6e329a4..cb2f0b9 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/auth_manager.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/auth_manager.py @@ -1,7 +1,5 @@ from flask import current_app -from cryptography import x509 -from cryptography.hazmat.backends import default_backend from .resources import Resource diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/consumer_messager.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/consumer_messager.py index 93d35e4..f0b3159 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/consumer_messager.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/consumer_messager.py @@ -1,10 +1,5 @@ # subscriber.py import redis -import time -import sys -import json -import asyncio -from threading import Thread from .internal_service_ops import InternalServiceOps from flask import current_app diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/publisher.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/publisher.py index f7b0c3c..34fcdf4 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/publisher.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/publisher.py @@ -1,5 +1,4 @@ import redis -import sys class Publisher(): diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py index 114b50b..e30cb66 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py @@ -1,4 +1,3 @@ -import atexit import time from pymongo import MongoClient from pymongo.errors import AutoReconnect diff --git a/services/register/register_service/app.py b/services/register/register_service/app.py index c92f175..cca3cfb 100644 --- a/services/register/register_service/app.py +++ b/services/register/register_service/app.py @@ -1,12 +1,10 @@ -import os from flask import Flask from controllers.register_controller import register_routes from flask_jwt_extended import JWTManager from OpenSSL.crypto import PKey, TYPE_RSA, X509Req, dump_certificate_request, FILETYPE_PEM, dump_privatekey import requests import json -import jwt from config import Config app = Flask(__name__) diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index 53f9806..cf5219d 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -3,7 +3,6 @@ from flask_jwt_extended import create_access_token from db.db import MongoDatabse from datetime import datetime from config import Config -import base64 import uuid class RegisterOperations: diff --git a/services/register/register_service/db/db.py b/services/register/register_service/db/db.py index 3f73712..9e64103 100644 --- a/services/register/register_service/db/db.py +++ b/services/register/register_service/db/db.py @@ -1,9 +1,7 @@ -import atexit import time from pymongo import MongoClient from pymongo.errors import AutoReconnect from config import Config -from bson.codec_options import CodecOptions class MongoDatabse(): -- GitLab From 7ae2a0e70d332a8cfc2d4b156edda5e583645ccf Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Mon, 10 Jun 2024 16:13:59 +0300 Subject: [PATCH 247/310] Move Discover API from testing to production server & remove unnecessary imports --- .../Dockerfile | 4 ++-- .../requirements.txt | 4 +++- .../service_apis/{__main__.py => app.py} | 16 +++++-------- .../service_apis/config.py | 2 +- .../controllers/default_controller.py | 9 +------ .../service_apis/core/discoveredapis.py | 24 +++++-------------- .../service_apis/core/resources.py | 2 +- .../service_apis/core/responses.py | 18 +++++++++++++- .../service_apis/db/db.py | 4 ++-- .../service_apis/encoder.py | 2 +- .../service_apis/util.py | 6 ++--- .../service_apis/wsgi.py | 4 ++++ .../Dockerfile | 2 -- 13 files changed, 47 insertions(+), 50 deletions(-) rename services/TS29222_CAPIF_Discover_Service_API/service_apis/{__main__.py => app.py} (94%) create mode 100644 services/TS29222_CAPIF_Discover_Service_API/service_apis/wsgi.py diff --git a/services/TS29222_CAPIF_Discover_Service_API/Dockerfile b/services/TS29222_CAPIF_Discover_Service_API/Dockerfile index efa70c9..9f1d46e 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/Dockerfile +++ b/services/TS29222_CAPIF_Discover_Service_API/Dockerfile @@ -12,6 +12,6 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["python3"] +ENTRYPOINT ["gunicorn"] -CMD ["-m", "service_apis"] \ No newline at end of file +CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/service_apis", "wsgi:app"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Discover_Service_API/requirements.txt b/services/TS29222_CAPIF_Discover_Service_API/requirements.txt index 33a22f4..1732b50 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Discover_Service_API/requirements.txt @@ -19,4 +19,6 @@ fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 -flask_executor == 1.0.0 \ No newline at end of file +flask_executor == 1.0.0 +gunicorn==22.0.0 +packaging==24.0 \ No newline at end of file diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/__main__.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py similarity index 94% rename from services/TS29222_CAPIF_Discover_Service_API/service_apis/__main__.py rename to services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py index 57326e9..4b4c169 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/__main__.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py @@ -2,18 +2,15 @@ import connexion -from service_apis import encoder +import encoder -import pymongo import logging -from flask import Flask, jsonify, request from flask_jwt_extended import JWTManager, jwt_required, create_access_token -from pymongo import MongoClient from logging.handlers import RotatingFileHandler -from .config import Config +from config import Config + import os from fluent import sender -from flask_executor import Executor from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter @@ -23,10 +20,9 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.instrumentation.redis import RedisInstrumentor - - NAME = "Discover-Service" + def configure_monitoring(app, config): resource = Resource(attributes={"service.name": NAME}) @@ -127,5 +123,5 @@ if eval(os.environ.get("MONITORING").lower().capitalize()): jwt = JWTManager(app.app) -if __name__ == '__main__': - app.run(debug=True, port=8080) +# if __name__ == '__main__': +# app.run(debug=True, port=8080) diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/config.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/config.py index d04bd1a..97ab831 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/config.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/config.py @@ -5,7 +5,7 @@ import os class Config: def __init__(self): self.cached = 0 - self.file="./config.yaml" + self.file="../config.yaml" self.my_config = {} stamp = os.stat(self.file).st_mtime diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/controllers/default_controller.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/controllers/default_controller.py index aef2022..2c81a78 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/controllers/default_controller.py @@ -1,13 +1,6 @@ -import sys -from service_apis.core.discoveredapis import DiscoverApisOperations -import json +from ..core.discoveredapis import DiscoverApisOperations from flask import Response, request, current_app -from service_apis.encoder import JSONEncoder -from service_apis.models.problem_details import ProblemDetails -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -import pymongo discover_apis = DiscoverApisOperations() diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py index f56b585..9ad5462 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py @@ -1,18 +1,9 @@ -import sys -import pymongo from flask import current_app, Flask, Response -import json -from service_apis.core.responses import internal_server_error, forbidden_error ,make_response, not_found_error -from service_apis.db.db import MongoDatabse -from service_apis.encoder import JSONEncoder -from service_apis.models.problem_details import ProblemDetails -from service_apis.models.service_api_description import ServiceAPIDescription -from service_apis.models.discovered_apis import DiscoveredAPIs -from service_apis.util import dict_to_camel_case, clean_empty -from service_apis.core.resources import Resource -from bson import json_util - +from ..core.responses import internal_server_error, forbidden_error ,make_response, not_found_error +from ..models.discovered_apis import DiscoveredAPIs +from ..util import dict_to_camel_case, clean_empty +from ..core.resources import Resource class DiscoverApisOperations(Resource): @@ -46,16 +37,13 @@ class DiscoverApisOperations(Resource): discoved_apis = services.find(my_query, {"_id":0, "api_name":1, "api_id":1, "aef_profiles":1, "description":1, "supported_features":1, "shareable_info":1, "service_api_category":1, "api_supp_feats":1, "pub_api_path":1, "ccf_id":1}) json_docs = [] for discoved_api in discoved_apis: - my_api = dict_to_camel_case(discoved_api) - my_api = clean_empty(my_api) - json_docs.append(my_api) + json_docs.append(discoved_api) if len(json_docs) == 0: return not_found_error(detail="API Invoker " + api_invoker_id + " has no API Published that accomplish filter conditions", cause="No API Published accomplish filter conditions") apis_discoveres = DiscoveredAPIs(service_api_descriptions=json_docs) - res = make_response(object=apis_discoveres, status=200) - current_app.logger.debug("Discovered APIs by: " + api_invoker_id) + res = make_response(object=dict_to_camel_case(clean_empty(apis_discoveres.to_dict())), status=200) return res except Exception as e: diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/resources.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/resources.py index 94e29ec..efbe3c2 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/resources.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/resources.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from ..db.db import MongoDatabse +from db.db import MongoDatabse class Resource(ABC): diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/responses.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/responses.py index 4f03578..df9905f 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/responses.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/responses.py @@ -1,7 +1,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response -from bson import json_util +from ..util import dict_to_camel_case, clean_empty import json mimetype = "application/json" @@ -14,19 +14,35 @@ def make_response(object, status): def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/db/db.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/db/db.py index 5445455..2b259ce 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/db/db.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/db/db.py @@ -1,8 +1,7 @@ -import atexit import time from pymongo import MongoClient from pymongo.errors import AutoReconnect -from ..config import Config +from config import Config from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor @@ -10,6 +9,7 @@ from opentelemetry.instrumentation.pymongo import PymongoInstrumentor if eval(os.environ.get("MONITORING").lower().capitalize()): PymongoInstrumentor().instrument() + class MongoDatabse(): def __init__(self): diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/encoder.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/encoder.py index c026759..80bad8f 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/encoder.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/encoder.py @@ -1,7 +1,7 @@ from connexion.apps.flask_app import FlaskJSONEncoder import six -from service_apis.models.base_model_ import Model +from models.base_model_ import Model class JSONEncoder(FlaskJSONEncoder): diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/util.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/util.py index b3f89d7..c39e5fa 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/util.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/util.py @@ -1,9 +1,7 @@ import datetime -import sys import six -import typing -from service_apis import typing_utils +import typing_utils def clean_empty(d): if isinstance(d, dict): @@ -27,6 +25,8 @@ def dict_to_camel_case(my_dict): my_key= ''.join([my_key[0].lower(), my_key[1:]]) if my_key == "serviceApiCategory": my_key = "serviceAPICategory" + elif my_key == "serviceApiDescriptions": + my_key = "serviceAPIDescriptions" if isinstance(value, list): result[my_key] = list(map( diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/wsgi.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/wsgi.py new file mode 100644 index 0000000..6026b0f --- /dev/null +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/wsgi.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == "__main__": + app.run() diff --git a/services/TS29222_CAPIF_Publish_Service_API/Dockerfile b/services/TS29222_CAPIF_Publish_Service_API/Dockerfile index 9f298a3..c11e2d6 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/Dockerfile +++ b/services/TS29222_CAPIF_Publish_Service_API/Dockerfile @@ -12,8 +12,6 @@ COPY . /usr/src/app EXPOSE 8080 -# ENTRYPOINT ["python3"] ENTRYPOINT ["gunicorn"] -# CMD ["-m", "published_apis"] CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/published_apis", "wsgi:app"] \ No newline at end of file -- GitLab From 2500a7d3e8a6ef1e5d3a078f13d1075286a82df7 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Tue, 11 Jun 2024 10:18:47 +0300 Subject: [PATCH 248/310] Move Security Context API from testing to production server and remove unnecessary imports --- .../capif_security/{__main__.py => app.py} | 58 +++++++++---------- .../capif_security/config.py | 2 +- .../controllers/default_controller.py | 24 +++----- .../capif_security/core/consumer_messager.py | 5 -- .../core/internal_security_ops.py | 1 - .../capif_security/core/notification.py | 4 +- .../capif_security/core/publisher.py | 2 +- .../capif_security/core/resources.py | 5 +- .../capif_security/core/responses.py | 28 ++++++++- .../capif_security/core/servicesecurity.py | 40 ++++++------- .../capif_security/db/db.py | 2 +- .../capif_security/encoder.py | 2 +- .../capif_security/util.py | 3 +- .../capif_security/wsgi.py | 4 ++ .../requirements.txt | 4 +- .../security_prepare.sh | 4 +- 16 files changed, 99 insertions(+), 89 deletions(-) rename services/TS29222_CAPIF_Security_API/capif_security/{__main__.py => app.py} (78%) create mode 100644 services/TS29222_CAPIF_Security_API/capif_security/wsgi.py diff --git a/services/TS29222_CAPIF_Security_API/capif_security/__main__.py b/services/TS29222_CAPIF_Security_API/capif_security/app.py similarity index 78% rename from services/TS29222_CAPIF_Security_API/capif_security/__main__.py rename to services/TS29222_CAPIF_Security_API/capif_security/app.py index 98d3964..30fc3a4 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/__main__.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/app.py @@ -2,14 +2,11 @@ import connexion import logging -from capif_security import encoder +import encoder from flask_jwt_extended import JWTManager -from .config import Config -from .core.consumer_messager import Subscriber -from threading import Thread -from flask_executor import Executor +from config import Config +from core.consumer_messager import Subscriber from logging.handlers import RotatingFileHandler -import sys import os from fluent import sender from flask_executor import Executor @@ -22,10 +19,9 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.instrumentation.redis import RedisInstrumentor - - NAME = "Security-Service" + def configure_monitoring(app, config): resource = Resource(attributes={"service.name": NAME}) @@ -110,39 +106,37 @@ def verbose_formatter(): datefmt='%d/%m/%Y %H:%M:%S' ) -def main(): - - with open("/usr/src/app/capif_security/server.key", "rb") as key_file: - key_data = key_file.read() - app = connexion.App(__name__, specification_dir='./openapi/') - app.app.json_encoder = encoder.JSONEncoder +with open("/usr/src/app/capif_security/server.key", "rb") as key_file: + key_data = key_file.read() +app = connexion.App(__name__, specification_dir='./openapi/') +app.app.json_encoder = encoder.JSONEncoder - app.app.config['JWT_ALGORITHM'] = 'RS256' - app.app.config['JWT_PRIVATE_KEY'] = key_data - app.add_api('openapi.yaml', - arguments={'title': 'CAPIF_Security_API'}, - pythonic_params=True) - JWTManager(app.app) - subscriber = Subscriber() +app.app.config['JWT_ALGORITHM'] = 'RS256' +app.app.config['JWT_PRIVATE_KEY'] = key_data +app.add_api('openapi.yaml', + arguments={'title': 'CAPIF_Security_API'}, + pythonic_params=True) - config = Config() - configure_logging(app.app) +JWTManager(app.app) +subscriber = Subscriber() - if eval(os.environ.get("MONITORING").lower().capitalize()): - configure_monitoring(app.app, config.get_config()) +config = Config() +configure_logging(app.app) - executor = Executor(app.app) +if eval(os.environ.get("MONITORING").lower().capitalize()): + configure_monitoring(app.app, config.get_config()) - @app.app.before_first_request - def up_listener(): - executor.submit(subscriber.listen) +executor = Executor(app.app) +@app.app.before_first_request +def up_listener(): + executor.submit(subscriber.listen) - app.run(port=8080, debug=True) -if __name__ == '__main__': - main() +# +# if __name__ == '__main__': +# main() diff --git a/services/TS29222_CAPIF_Security_API/capif_security/config.py b/services/TS29222_CAPIF_Security_API/capif_security/config.py index 11e1c4f..01f9914 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/config.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/config.py @@ -5,7 +5,7 @@ import os class Config: def __init__(self): self.cached = 0 - self.file="./config.yaml" + self.file="../config.yaml" self.my_config = {} stamp = os.stat(self.file).st_mtime diff --git a/services/TS29222_CAPIF_Security_API/capif_security/controllers/default_controller.py b/services/TS29222_CAPIF_Security_API/capif_security/controllers/default_controller.py index aee7098..6713db1 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/controllers/default_controller.py @@ -1,26 +1,20 @@ import connexion -import six - -from capif_security.models.access_token_err import AccessTokenErr # noqa: E501 -from capif_security.models.access_token_rsp import AccessTokenRsp # noqa: E501 -from capif_security.models.access_token_req import AccessTokenReq # noqa: E501 -from capif_security.models.security_notification import SecurityNotification # noqa: E501 -from capif_security.models.service_security import ServiceSecurity # noqa: E501 -from capif_security import util + +from ..models.access_token_req import AccessTokenReq # noqa: E501 +from ..models.security_notification import SecurityNotification # noqa: E501 +from ..models.service_security import ServiceSecurity # noqa: E501 + from ..core.servicesecurity import SecurityOperations -from ..core.consumer_messager import Subscriber + from ..core.publisher import Publisher -import json + from flask import Response, request, current_app -from flask_jwt_extended import jwt_required, get_jwt_identity -from ..encoder import JSONEncoder -from ..models.problem_details import ProblemDetails -import sys + from cryptography import x509 from cryptography.hazmat.backends import default_backend from ..core.validate_user import ControlAccess from functools import wraps -import pymongo + service_security_ops = SecurityOperations() publish_ops = Publisher() diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/consumer_messager.py b/services/TS29222_CAPIF_Security_API/capif_security/core/consumer_messager.py index fd9c328..749f57e 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/consumer_messager.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/consumer_messager.py @@ -1,10 +1,5 @@ # subscriber.py import redis -import time -import sys -import json -import asyncio -from threading import Thread from .internal_security_ops import InternalSecurityOps from flask import current_app diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/internal_security_ops.py b/services/TS29222_CAPIF_Security_API/capif_security/core/internal_security_ops.py index d5cbd96..d1b28d5 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/internal_security_ops.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/internal_security_ops.py @@ -1,5 +1,4 @@ -from flask import current_app from .resources import Resource class InternalSecurityOps(Resource): diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/notification.py b/services/TS29222_CAPIF_Security_API/capif_security/core/notification.py index 5d69adc..2efad5f 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/notification.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/notification.py @@ -1,7 +1,5 @@ import requests -from ..encoder import JSONEncoder -import sys -import json + class Notifications(): diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/publisher.py b/services/TS29222_CAPIF_Security_API/capif_security/core/publisher.py index f7b0c3c..8292de4 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/publisher.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/publisher.py @@ -1,5 +1,5 @@ import redis -import sys + class Publisher(): diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/resources.py b/services/TS29222_CAPIF_Security_API/capif_security/core/resources.py index 2ba2a0f..53a35e5 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/resources.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/resources.py @@ -1,7 +1,8 @@ -from abc import ABC, abstractmethod -from ..db.db import MongoDatabse +from abc import ABC +from db.db import MongoDatabse from .notification import Notifications + class Resource(ABC): def __init__(self): diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/responses.py b/services/TS29222_CAPIF_Security_API/capif_security/core/responses.py index 26e82b6..9c2020c 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/responses.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/responses.py @@ -2,36 +2,62 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response import json -from bson import json_util +from ..util import dict_to_camel_case, clean_empty mimetype = "application/json" + def make_response(object, status): res = Response(json.dumps(object, cls=JSONEncoder), status=status, mimetype=mimetype) return res + def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) + def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) + def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) + def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) + def unauthorized_error(detail, cause): prob = ProblemDetails(title="Unauthorized", status=401, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py index 69475ac..f553b4d 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py @@ -1,28 +1,19 @@ -import sys -import pymongo from pymongo import ReturnDocument -import secrets -import re + import rfc3987 from flask import current_app, Flask, Response from flask_jwt_extended import create_access_token from datetime import datetime, timedelta import json -from ..db.db import MongoDatabse -from ..encoder import JSONEncoder -from ..models.problem_details import ProblemDetails from ..models.access_token_rsp import AccessTokenRsp from ..models.access_token_claims import AccessTokenClaims from bson import json_util -import requests from ..core.publisher import Publisher from ..models.access_token_err import AccessTokenErr -from ..models.service_security import ServiceSecurity from ..util import dict_to_camel_case, clean_empty from .responses import not_found_error, make_response, bad_request_error, internal_server_error, forbidden_error -from .notification import Notifications from .resources import Resource import os @@ -53,7 +44,8 @@ class SecurityOperations(Resource): if header != "3gpp": current_app.logger.error("Bad format scope") token_error = AccessTokenErr(error="invalid_scope", error_description="The first characters must be '3gpp'") - return make_response(object=token_error, status=400) + # return make_response(object=dict_to_camel_case(clean_empty(token_error.to_dict())), status=400) + return make_response(object=clean_empty(token_error.to_dict()), status=400) _, body = scope.split("#") @@ -67,21 +59,24 @@ class SecurityOperations(Resource): if aef_id not in aef_security_context: current_app.logger.error("Bad format Scope, not valid aef id ") token_error = AccessTokenErr(error="invalid_scope", error_description="One of aef_id not belongs of your security context") - return make_response(object=token_error, status=400) + # return make_response(object=dict_to_camel_case(clean_empty(token_error.to_dict())), status=400) + return make_response(object=clean_empty(token_error.to_dict()), status=400) api_names = api_names.split(",") for api_name in api_names: service = capif_service_col.find_one({"$and": [{"api_name":api_name},{self.filter_aef_id:aef_id}]}) if service is None: current_app.logger.error("Bad format Scope, not valid api name") token_error = AccessTokenErr(error="invalid_scope", error_description="One of the api names does not exist or is not associated with the aef id provided") - return make_response(object=token_error, status=400) + # return make_response(object=dict_to_camel_case(clean_empty(token_error.to_dict())), status=400) + return make_response(object=clean_empty(token_error.to_dict()), status=400) return None except Exception as e: current_app.logger.error("Bad format Scope: " + e) token_error = AccessTokenErr(error="invalid_scope", error_description="malformed scope") - return make_response(object=token_error, status=400) + # return make_response(object=dict_to_camel_case(clean_empty(token_error.to_dict())), status=400) + return make_response(object=clean_empty(token_error.to_dict()), status=400) def __init__(self): Resource.__init__(self) @@ -182,7 +177,7 @@ class SecurityOperations(Resource): rec.update(service_security.to_dict()) mycol.insert_one(rec) - res = make_response(object=service_security, status=201) + res = make_response(object=dict_to_camel_case(clean_empty(service_security.to_dict())), status=201) res.headers['Location'] = "https://{}/capif-security/v1/trustedInvokers/{}".format(os.getenv('CAPIF_HOSTNAME'),str(api_invoker_id)) return res @@ -244,17 +239,19 @@ class SecurityOperations(Resource): invoker = invokers_col.find_one({"api_invoker_id": access_token_req["client_id"]}) if invoker is None: client_id_error = AccessTokenErr(error="invalid_client", error_description="Client Id not found") - return make_response(object=client_id_error, status=400) + # return make_response(object=dict_to_camel_case(clean_empty(client_id_error.to_dict())), status=400) + return make_response(object=clean_empty(client_id_error.to_dict()), status=400) if access_token_req["grant_type"] != "client_credentials": client_id_error = AccessTokenErr(error="unsupported_grant_type", error_description="Invalid value for `grant_type` ({0}), must be one of ['client_credentials'] - 'grant_type'" .format(access_token_req["grant_type"])) - return make_response(object=client_id_error, status=400) + # return make_response(object=dict_to_camel_case(clean_empty(client_id_error.to_dict())), status=400) + return make_response(object=clean_empty(client_id_error.to_dict()), status=400) service_security = mycol.find_one({"api_invoker_id": security_id}) if service_security is None: - current_app.logger.error("Not found securoty context with id: " + security_id) + current_app.logger.error("Not found security context with id: " + security_id) return not_found_error(detail= security_context_not_found_detail, cause=api_invoker_no_context_cause) result = self.__check_scope(access_token_req["scope"], service_security) @@ -271,7 +268,8 @@ class SecurityOperations(Resource): current_app.logger.debug("Created access token") - res = make_response(object=access_token_resp, status=200) + # res = make_response(object=dict_to_camel_case(clean_empty(access_token_resp.to_dict())), status=200) + res = make_response(object=clean_empty(access_token_resp.to_dict()), status=200) return res except Exception as e: exception = "An exception occurred in return token" @@ -318,11 +316,11 @@ class SecurityOperations(Resource): result = mycol.find_one_and_update(old_object, {"$set":service_security}, projection={'_id': 0, "api_invoker_id":0},return_document=ReturnDocument.AFTER ,upsert=False) - result = clean_empty(result) + # result = clean_empty(result) current_app.logger.debug("Updated security context") - res= make_response(object=dict_to_camel_case(result), status=200) + res= make_response(object=dict_to_camel_case(clean_empty(result)), status=200) res.headers['Location'] = "https://${CAPIF_HOSTNAME}/capif-security/v1/trustedInvokers/" + str( api_invoker_id) return res diff --git a/services/TS29222_CAPIF_Security_API/capif_security/db/db.py b/services/TS29222_CAPIF_Security_API/capif_security/db/db.py index dbbb99e..4a009db 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/db/db.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/db/db.py @@ -2,7 +2,7 @@ import atexit import time from pymongo import MongoClient from pymongo.errors import AutoReconnect -from ..config import Config +from config import Config from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor diff --git a/services/TS29222_CAPIF_Security_API/capif_security/encoder.py b/services/TS29222_CAPIF_Security_API/capif_security/encoder.py index 9d6964e..80bad8f 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/encoder.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/encoder.py @@ -1,7 +1,7 @@ from connexion.apps.flask_app import FlaskJSONEncoder import six -from capif_security.models.base_model_ import Model +from models.base_model_ import Model class JSONEncoder(FlaskJSONEncoder): diff --git a/services/TS29222_CAPIF_Security_API/capif_security/util.py b/services/TS29222_CAPIF_Security_API/capif_security/util.py index 873c290..00ceb15 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/util.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/util.py @@ -1,8 +1,7 @@ import datetime import six -import typing -from capif_security import typing_utils +import typing_utils def clean_empty(d): diff --git a/services/TS29222_CAPIF_Security_API/capif_security/wsgi.py b/services/TS29222_CAPIF_Security_API/capif_security/wsgi.py new file mode 100644 index 0000000..6026b0f --- /dev/null +++ b/services/TS29222_CAPIF_Security_API/capif_security/wsgi.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == "__main__": + app.run() diff --git a/services/TS29222_CAPIF_Security_API/requirements.txt b/services/TS29222_CAPIF_Security_API/requirements.txt index 220adaf..835b8ca 100644 --- a/services/TS29222_CAPIF_Security_API/requirements.txt +++ b/services/TS29222_CAPIF_Security_API/requirements.txt @@ -19,4 +19,6 @@ fluent == 0.10.0 fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 -flask_executor == 1.0.0 \ No newline at end of file +flask_executor == 1.0.0 +gunicorn==22.0.0 +packaging==24.0 \ No newline at end of file diff --git a/services/TS29222_CAPIF_Security_API/security_prepare.sh b/services/TS29222_CAPIF_Security_API/security_prepare.sh index c28b389..94bece0 100644 --- a/services/TS29222_CAPIF_Security_API/security_prepare.sh +++ b/services/TS29222_CAPIF_Security_API/security_prepare.sh @@ -15,5 +15,5 @@ curl -k -retry 30 \ --request GET "$VAULT_ADDR/v1/secret/data/server_cert/private" 2>/dev/null | jq -r '.data.data.key' -j > /usr/src/app/capif_security/server.key -cd /usr/src/app/ -python3 -m capif_security \ No newline at end of file +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/capif_security wsgi:app \ No newline at end of file -- GitLab From e4038e14581eb66e2c00b2a895d450096436ac48 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Tue, 11 Jun 2024 12:57:25 +0300 Subject: [PATCH 249/310] Move ACL API from testing to production server, rename basic folder and remove unnecessary imports --- .../Dockerfile | 4 ++-- .../{openapi_server => capif_acl}/__init__.py | 0 .../__main__.py => capif_acl/app.py} | 18 ++++++--------- .../{openapi_server => capif_acl}/config.py | 2 +- .../controllers/__init__.py | 0 .../controllers/default_controller.py | 3 ++- .../controllers/security_controller_.py | 0 .../core/accesscontrolpolicyapi.py | 3 ++- .../core/consumer_messager.py | 2 +- .../core/internal_service_ops.py | 8 ++++--- .../core/publisher.py | 3 +-- .../capif_acl/core/resources.py | 8 +++++++ .../core/responses.py | 22 +++++++++++++++++++ .../{openapi_server => capif_acl}/db/db.py | 4 ++-- .../{openapi_server => capif_acl}/encoder.py | 2 +- .../capif_acl/models/__init__.py | 10 +++++++++ .../models/access_control_policy_list.py | 8 +++---- .../models/api_invoker_policy.py | 8 +++---- .../models/base_model_.py | 2 +- .../models/invalid_param.py | 4 ++-- .../models/problem_details.py | 8 +++---- .../models/time_range_list.py | 4 ++-- .../openapi/openapi.yaml | 2 +- .../test/__init__.py | 2 +- .../test/test_default_controller.py | 7 +----- .../typing_utils.py | 0 .../{openapi_server => capif_acl}/util.py | 7 +++--- .../capif_acl/wsgi.py | 5 +++++ .../openapi_server/core/resources.py | 7 ------ .../openapi_server/models/__init__.py | 10 --------- .../requirements.txt | 2 ++ 31 files changed, 94 insertions(+), 71 deletions(-) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/__init__.py (100%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server/__main__.py => capif_acl/app.py} (94%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/config.py (92%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/controllers/__init__.py (100%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/controllers/default_controller.py (98%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/controllers/security_controller_.py (100%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/core/accesscontrolpolicyapi.py (98%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/core/consumer_messager.py (98%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/core/internal_service_ops.py (96%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/core/publisher.py (83%) create mode 100644 services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/resources.py rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/core/responses.py (72%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/db/db.py (97%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/encoder.py (91%) create mode 100644 services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/__init__.py rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/models/access_control_policy_list.py (90%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/models/api_invoker_policy.py (96%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/models/base_model_.py (98%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/models/invalid_param.py (96%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/models/problem_details.py (97%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/models/time_range_list.py (96%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/openapi/openapi.yaml (99%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/test/__init__.py (89%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/test/test_default_controller.py (79%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/typing_utils.py (100%) rename services/TS29222_CAPIF_Access_Control_Policy_API/{openapi_server => capif_acl}/util.py (98%) create mode 100644 services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/wsgi.py delete mode 100644 services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/resources.py delete mode 100644 services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/__init__.py diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/Dockerfile b/services/TS29222_CAPIF_Access_Control_Policy_API/Dockerfile index 92dcb33..c69bd56 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/Dockerfile +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/Dockerfile @@ -11,6 +11,6 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["python3"] +ENTRYPOINT ["gunicorn"] -CMD ["-m", "openapi_server"] \ No newline at end of file +CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/capif_acl", "wsgi:app"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/__init__.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/__init__.py similarity index 100% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/__init__.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/__init__.py diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/__main__.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py similarity index 94% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/__main__.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py index 215e85e..da28365 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/__main__.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py @@ -2,19 +2,17 @@ import connexion -from openapi_server import encoder +import encoder import logging -from flask import Flask, jsonify, request from flask_jwt_extended import JWTManager, jwt_required, create_access_token -from pymongo import MongoClient from logging.handlers import RotatingFileHandler -from .config import Config +from config import Config from datetime import datetime import os from fluent import sender -from .core.consumer_messager import Subscriber +from core.consumer_messager import Subscriber from flask_executor import Executor from flask_apscheduler import APScheduler from opentelemetry.instrumentation.flask import FlaskInstrumentor @@ -117,11 +115,8 @@ app.add_api('openapi.yaml', arguments={'title': 'CAPIF_Access_Control_policy_API'}, pythonic_params=True) - config = Config() - - jwt = JWTManager(app.app) configure_logging(app.app) @@ -138,12 +133,13 @@ subscriber = Subscriber() scheduler = APScheduler() scheduler.api_enabled = True scheduler.init_app(app.app) +scheduler.start() @scheduler.task('date', id='listener', next_run_time=datetime.now()) def up_listener(): with scheduler.app.app_context(): executor.submit(subscriber.listen()) -if __name__ == '__main__': - scheduler.start() - app.run(debug=True,port=8080, use_reloader=False) +# if __name__ == '__main__': +# scheduler.start() +# app.run(debug=True,port=8080, use_reloader=False) diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/config.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/config.py similarity index 92% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/config.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/config.py index 11e1c4f..01f9914 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/config.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/config.py @@ -5,7 +5,7 @@ import os class Config: def __init__(self): self.cached = 0 - self.file="./config.yaml" + self.file="../config.yaml" self.my_config = {} stamp = os.stat(self.file).st_mtime diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/controllers/__init__.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/__init__.py similarity index 100% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/controllers/__init__.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/__init__.py diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/controllers/default_controller.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/default_controller.py similarity index 98% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/controllers/default_controller.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/default_controller.py index c17e195..eb047f0 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/default_controller.py @@ -1,4 +1,4 @@ -from typing import Dict + from functools import wraps from flask import request, current_app from cryptography import x509 @@ -6,6 +6,7 @@ from cryptography.hazmat.backends import default_backend from ..core.accesscontrolpolicyapi import accessControlPolicyApi + def cert_validation(): def _cert_validation(f): @wraps(f) diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/controllers/security_controller_.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/security_controller_.py similarity index 100% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/controllers/security_controller_.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/security_controller_.py diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/accesscontrolpolicyapi.py similarity index 98% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/accesscontrolpolicyapi.py index fd14e80..4a7280d 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/accesscontrolpolicyapi.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/accesscontrolpolicyapi.py @@ -4,6 +4,7 @@ from .responses import make_response, not_found_error, internal_server_error from ..models.access_control_policy_list import AccessControlPolicyList from ..util import dict_to_camel_case, clean_empty + class accessControlPolicyApi(Resource): def get_acl(self, service_api_id, aef_id, api_invoker_id, supported_features): @@ -45,7 +46,7 @@ class accessControlPolicyApi(Resource): return not_found_error(f"No ACLs found for the requested service: {service_api_id}, aef_id: {aef_id}, invoker: {api_invoker_id} and supportedFeatures: {supported_features}", "Wrong id") acl = AccessControlPolicyList(api_invoker_policies) - response = acl.to_dict() + response = clean_empty(acl.to_dict()) return make_response(object=dict_to_camel_case(response), status=200) except Exception as e: diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/consumer_messager.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/consumer_messager.py similarity index 98% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/consumer_messager.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/consumer_messager.py index 72c51ee..0da2440 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/consumer_messager.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/consumer_messager.py @@ -1,6 +1,6 @@ # subscriber.py import redis -from ..config import Config +from config import Config from .internal_service_ops import InternalServiceOps from flask import current_app diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py similarity index 96% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py index e10986a..82d1f43 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/internal_service_ops.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py @@ -1,12 +1,14 @@ from flask import current_app from .resources import Resource -from ..models.api_invoker_policy import ApiInvokerPolicy -from ..models.time_range_list import TimeRangeList +from models.api_invoker_policy import ApiInvokerPolicy +from models.time_range_list import TimeRangeList from datetime import datetime, timedelta -from ..core.publisher import Publisher +from core.publisher import Publisher publisher_ops = Publisher() + + class InternalServiceOps(Resource): def create_acl(self, invoker_id, service_id, aef_id): diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/publisher.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/publisher.py similarity index 83% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/publisher.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/publisher.py index 3898c4b..8292de4 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/publisher.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/publisher.py @@ -1,6 +1,5 @@ import redis -import sys -from flask import current_app + class Publisher(): diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/resources.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/resources.py new file mode 100644 index 0000000..d5a3552 --- /dev/null +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/resources.py @@ -0,0 +1,8 @@ +from abc import ABC +from db.db import MongoDatabse + + +class Resource(ABC): + + def __init__(self): + self.db = MongoDatabse() \ No newline at end of file diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/responses.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/responses.py similarity index 72% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/responses.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/responses.py index 962c4b6..9d5ea09 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/responses.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/responses.py @@ -1,31 +1,53 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response +from ..util import dict_to_camel_case, clean_empty import json mimetype = "application/json" + def make_response(object, status): res = Response(json.dumps(object, cls=JSONEncoder), status=status, mimetype=mimetype) return res + def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) + def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) + def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) + def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/db/db.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/db/db.py similarity index 97% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/db/db.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/db/db.py index b25c794..d1185c2 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/db/db.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/db/db.py @@ -1,8 +1,8 @@ -import atexit + import time from pymongo import MongoClient from pymongo.errors import AutoReconnect -from ..config import Config +from config import Config from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/encoder.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/encoder.py similarity index 91% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/encoder.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/encoder.py index 3bbef85..80bad8f 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/encoder.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/encoder.py @@ -1,7 +1,7 @@ from connexion.apps.flask_app import FlaskJSONEncoder import six -from openapi_server.models.base_model_ import Model +from models.base_model_ import Model class JSONEncoder(FlaskJSONEncoder): diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/__init__.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/__init__.py new file mode 100644 index 0000000..4c4e60f --- /dev/null +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/__init__.py @@ -0,0 +1,10 @@ +# coding: utf-8 + +# flake8: noqa +from __future__ import absolute_import +# import models into model package +from capif_acl.models.access_control_policy_list import AccessControlPolicyList +from capif_acl.models.api_invoker_policy import ApiInvokerPolicy +from capif_acl.models.invalid_param import InvalidParam +from capif_acl.models.problem_details import ProblemDetails +from capif_acl.models.time_range_list import TimeRangeList diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/access_control_policy_list.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/access_control_policy_list.py similarity index 90% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/access_control_policy_list.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/access_control_policy_list.py index 98f3fc1..b3cacd4 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/access_control_policy_list.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/access_control_policy_list.py @@ -5,11 +5,11 @@ from datetime import date, datetime # noqa: F401 from typing import List, Dict # noqa: F401 -from openapi_server.models.base_model_ import Model -from openapi_server.models.api_invoker_policy import ApiInvokerPolicy -from openapi_server import util +from capif_acl.models.base_model_ import Model +from capif_acl.models.api_invoker_policy import ApiInvokerPolicy +from capif_acl import util -from openapi_server.models.api_invoker_policy import ApiInvokerPolicy # noqa: E501 +from capif_acl.models.api_invoker_policy import ApiInvokerPolicy # noqa: E501 class AccessControlPolicyList(Model): """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/api_invoker_policy.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/api_invoker_policy.py similarity index 96% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/api_invoker_policy.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/api_invoker_policy.py index a5f1b47..86ffec0 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/api_invoker_policy.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/api_invoker_policy.py @@ -5,11 +5,11 @@ from datetime import date, datetime # noqa: F401 from typing import List, Dict # noqa: F401 -from openapi_server.models.base_model_ import Model -from openapi_server.models.time_range_list import TimeRangeList -from openapi_server import util +from capif_acl.models.base_model_ import Model +from capif_acl.models.time_range_list import TimeRangeList +from capif_acl import util -from openapi_server.models.time_range_list import TimeRangeList # noqa: E501 +from capif_acl.models.time_range_list import TimeRangeList # noqa: E501 class ApiInvokerPolicy(Model): """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/base_model_.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/base_model_.py similarity index 98% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/base_model_.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/base_model_.py index 916e582..cce5379 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/base_model_.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/base_model_.py @@ -3,7 +3,7 @@ import pprint import six import typing -from openapi_server import util +from capif_acl import util T = typing.TypeVar('T') diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/invalid_param.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/invalid_param.py similarity index 96% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/invalid_param.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/invalid_param.py index 8d91e6c..88606c4 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/invalid_param.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/invalid_param.py @@ -5,8 +5,8 @@ from datetime import date, datetime # noqa: F401 from typing import List, Dict # noqa: F401 -from openapi_server.models.base_model_ import Model -from openapi_server import util +from capif_acl.models.base_model_ import Model +from capif_acl import util class InvalidParam(Model): diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/problem_details.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/problem_details.py similarity index 97% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/problem_details.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/problem_details.py index 08f2908..25caab4 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/problem_details.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/problem_details.py @@ -5,12 +5,12 @@ from datetime import date, datetime # noqa: F401 from typing import List, Dict # noqa: F401 -from openapi_server.models.base_model_ import Model -from openapi_server.models.invalid_param import InvalidParam +from capif_acl.models.base_model_ import Model +from capif_acl.models.invalid_param import InvalidParam import re -from openapi_server import util +from capif_acl import util -from openapi_server.models.invalid_param import InvalidParam # noqa: E501 +from capif_acl.models.invalid_param import InvalidParam # noqa: E501 import re # noqa: E501 class ProblemDetails(Model): diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/time_range_list.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/time_range_list.py similarity index 96% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/time_range_list.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/time_range_list.py index eb18a7e..eaacad6 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/time_range_list.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/models/time_range_list.py @@ -5,8 +5,8 @@ from datetime import date, datetime # noqa: F401 from typing import List, Dict # noqa: F401 -from openapi_server.models.base_model_ import Model -from openapi_server import util +from capif_acl.models.base_model_ import Model +from capif_acl import util class TimeRangeList(Model): diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/openapi/openapi.yaml b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/openapi/openapi.yaml similarity index 99% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/openapi/openapi.yaml rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/openapi/openapi.yaml index 6f8c99d..1621baa 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/openapi/openapi.yaml +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/openapi/openapi.yaml @@ -130,7 +130,7 @@ paths: description: Service Unavailable default: description: Generic Error - x-openapi-router-controller: openapi_server.controllers.default_controller + x-openapi-router-controller: capif_acl.controllers.default_controller components: responses: "307": diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/test/__init__.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/test/__init__.py similarity index 89% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/test/__init__.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/test/__init__.py index 364aba9..5d664f8 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/test/__init__.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/test/__init__.py @@ -3,7 +3,7 @@ import logging import connexion from flask_testing import TestCase -from openapi_server.encoder import JSONEncoder +from capif_acl.encoder import JSONEncoder class BaseTestCase(TestCase): diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/test/test_default_controller.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/test/test_default_controller.py similarity index 79% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/test/test_default_controller.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/test/test_default_controller.py index c850757..44ed0be 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/test/test_default_controller.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/test/test_default_controller.py @@ -3,12 +3,7 @@ from __future__ import absolute_import import unittest -from flask import json -from six import BytesIO - -from openapi_server.models.access_control_policy_list import AccessControlPolicyList # noqa: E501 -from openapi_server.models.problem_details import ProblemDetails # noqa: E501 -from openapi_server.test import BaseTestCase +from test import BaseTestCase class TestDefaultController(BaseTestCase): diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/typing_utils.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/typing_utils.py similarity index 100% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/typing_utils.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/typing_utils.py diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/util.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/util.py similarity index 98% rename from services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/util.py rename to services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/util.py index 672406d..2790390 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/util.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/util.py @@ -1,8 +1,8 @@ import datetime import six -import typing -from openapi_server import typing_utils +import typing_utils + def clean_empty(d): if isinstance(d, dict): @@ -15,6 +15,7 @@ def clean_empty(d): return [v for v in map(clean_empty, d) if v] return d + def dict_to_camel_case(my_dict): @@ -41,8 +42,6 @@ def dict_to_camel_case(my_dict): return result - - def _deserialize(data, klass): """Deserializes dict, list, str into an object. diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/wsgi.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/wsgi.py new file mode 100644 index 0000000..e72413c --- /dev/null +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/wsgi.py @@ -0,0 +1,5 @@ +from app import app + +if __name__ == "__main__": + # app.scheduler.start() + app.run() diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/resources.py b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/resources.py deleted file mode 100644 index 94e29ec..0000000 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/core/resources.py +++ /dev/null @@ -1,7 +0,0 @@ -from abc import ABC, abstractmethod -from ..db.db import MongoDatabse - -class Resource(ABC): - - def __init__(self): - self.db = MongoDatabse() \ No newline at end of file diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/__init__.py b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/__init__.py deleted file mode 100644 index a4269de..0000000 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/models/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# coding: utf-8 - -# flake8: noqa -from __future__ import absolute_import -# import models into model package -from openapi_server.models.access_control_policy_list import AccessControlPolicyList -from openapi_server.models.api_invoker_policy import ApiInvokerPolicy -from openapi_server.models.invalid_param import InvalidParam -from openapi_server.models.problem_details import ProblemDetails -from openapi_server.models.time_range_list import TimeRangeList diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt b/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt index 03d8aaf..087fc12 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt @@ -27,3 +27,5 @@ redis == 4.5.4 flask_executor == 1.0.0 Flask-APScheduler == 1.12.4 Flask-Script == 2.0.6 +gunicorn==22.0.0 +packaging==24.0 \ No newline at end of file -- GitLab From 8841e8062d8a97248ca9e04b546d14ca5c0dcfb6 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 11 Jun 2024 13:24:12 +0200 Subject: [PATCH 250/310] Update libraries --- .../requirements.txt | 6 +- .../requirements.txt | 7 +- .../requirements.txt | 13 +- .../requirements.txt | 7 +- .../requirements.txt | 9 +- .../TS29222_CAPIF_Events_API/requirements.txt | 19 +- .../requirements.txt | 7 +- .../requirements.txt | 7 +- .../requirements.txt | 3 +- .../requirements.txt | 9 +- services/helper/requirements.txt | 2 +- services/register/requirements.txt | 18 +- tools/robot/basicRequirements.txt | 204 +++++++++--------- 13 files changed, 158 insertions(+), 153 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt index ce843e6..49bdd94 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt @@ -1,11 +1,11 @@ connexion[swagger-ui] == 2.14.2; python_version>="3.6" swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 -setuptools >= 21.0.0 +setuptools == 68.2.2 Flask == 2.0.3 pymongo == 4.3.3 flask_jwt_extended == 4.4.4 -pyopenssl == 23.0.0 +cryptography == 42.0.8 rfc3987 == 1.3.8 redis == 4.5.4 opentelemetry-instrumentation == 0.40b0 @@ -19,3 +19,5 @@ fluent-logger == 0.10.0 opentelemetry-api == 1.19.0 opentelemetry-sdk == 1.19.0 flask_executor == 1.0.0 +Werkzeug == 2.2.3 + diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt index d45d1e5..b44401b 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt +++ b/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt @@ -1,12 +1,12 @@ connexion[swagger-ui] == 2.14.2; python_version>="3.6" swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 -setuptools >= 21.0.0 +setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.0.1 +pymongo == 4.3.3 redis == 4.5.4 flask_jwt_extended == 4.4.4 -pyopenssl == 23.0.0 +cryptography == 42.0.8 rfc3987 == 1.3.8 opentelemetry-instrumentation == 0.38b0 opentelemetry-instrumentation-flask == 0.38b0 @@ -18,4 +18,5 @@ fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 +Werkzeug == 2.2.3 diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt b/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt index 03d8aaf..1c80c12 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt @@ -1,14 +1,9 @@ connexion[swagger-ui] == 2.14.2; python_version>="3.6" -# 2.3 is the last version that supports python 3.4-3.5 -connexion[swagger-ui] <= 2.3.0; python_version=="3.5" or python_version=="3.4" -# connexion requires werkzeug but connexion < 2.4.0 does not install werkzeug -# we must peg werkzeug versions below to fix connexion -# https://github.com/zalando/connexion/pull/1044 -werkzeug == 0.16.1; python_version=="3.5" or python_version=="3.4" +Werkzeug == 2.2.3 swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 -setuptools >= 21.0.0 -Flask == 2.1.1 +setuptools == 68.2.2 +Flask == 2.0.3 pymongo == 4.3.3 flask_jwt_extended == 4.4.4 opentelemetry-instrumentation == 0.38b0 @@ -22,7 +17,7 @@ fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 -pyopenssl == 23.0.0 +cryptography == 42.0.8 redis == 4.5.4 flask_executor == 1.0.0 Flask-APScheduler == 1.12.4 diff --git a/services/TS29222_CAPIF_Auditing_API/requirements.txt b/services/TS29222_CAPIF_Auditing_API/requirements.txt index 197399f..18fd92f 100644 --- a/services/TS29222_CAPIF_Auditing_API/requirements.txt +++ b/services/TS29222_CAPIF_Auditing_API/requirements.txt @@ -1,9 +1,9 @@ connexion[swagger-ui] == 2.14.2; python_version>="3.6" swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 -setuptools >= 21.0.0 +setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.0.1 +pymongo == 4.3.3 elasticsearch == 8.4.3 flask_jwt_extended == 4.4.4 opentelemetry-instrumentation == 0.38b0 @@ -17,4 +17,5 @@ fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 -pyopenssl == 23.0.0 +cryptography == 42.0.8 +Werkzeug == 2.2.3 diff --git a/services/TS29222_CAPIF_Discover_Service_API/requirements.txt b/services/TS29222_CAPIF_Discover_Service_API/requirements.txt index 33a22f4..8e4d2e2 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Discover_Service_API/requirements.txt @@ -1,11 +1,11 @@ connexion[swagger-ui] == 2.14.2; python_version>="3.6" swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 -setuptools >= 21.0.0 +setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.0.1 +pymongo == 4.3.3 flask_jwt_extended == 4.4.4 -pyopenssl == 23.0.0 +cryptography == 42.0.8 rfc3987 == 1.3.8 redis == 4.5.4 opentelemetry-instrumentation == 0.38b0 @@ -19,4 +19,5 @@ fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 -flask_executor == 1.0.0 \ No newline at end of file +flask_executor == 1.0.0 +Werkzeug == 2.2.3 \ No newline at end of file diff --git a/services/TS29222_CAPIF_Events_API/requirements.txt b/services/TS29222_CAPIF_Events_API/requirements.txt index 743f814..e75e20a 100644 --- a/services/TS29222_CAPIF_Events_API/requirements.txt +++ b/services/TS29222_CAPIF_Events_API/requirements.txt @@ -1,9 +1,9 @@ connexion[swagger-ui] == 2.14.2; python_version>="3.6" swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 -setuptools >= 21.0.0 +setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.0.1 +pymongo == 4.3.3 opentelemetry-instrumentation == 0.38b0 opentelemetry-instrumentation-flask == 0.38b0 opentelemetry-instrumentation-redis == 0.38b0 @@ -16,10 +16,11 @@ opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 flask_jwt_extended -pyopenssl -rfc3987 -redis -flask_executor -Flask-APScheduler -aiohttp==3.9.5 -async-timeout==4.0.3 +cryptography == 42.0.8 +rfc3987 == 1.3.8 +redis == 4.5.4 +flask_executor == 1.0.0 +Flask-APScheduler == 1.12.4 +aiohttp == 3.9.5 +async-timeout == 4.0.3 +Werkzeug == 2.2.3 diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt index 0dfa8b6..af0caa0 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt @@ -1,9 +1,9 @@ connexion[swagger-ui] == 2.14.2; python_version>="3.6" swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 -setuptools >= 21.0.0 +setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.0.1 +pymongo == 4.3.3 elasticsearch == 8.4.3 flask_jwt_extended == 4.4.4 redis == 4.5.4 @@ -18,4 +18,5 @@ fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 -pyopenssl == 23.0.0 +cryptography == 42.0.8 +Werkzeug == 2.2.3 diff --git a/services/TS29222_CAPIF_Publish_Service_API/requirements.txt b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt index 0ba8fb5..076261e 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt @@ -1,9 +1,9 @@ connexion[swagger-ui] == 2.14.2; python_version>="3.6" swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 -setuptools >= 21.0.0 +setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.0.1 +pymongo == 4.3.3 flask_jwt_extended == 4.4.4 opentelemetry-instrumentation == 0.38b0 opentelemetry-instrumentation-flask == 0.38b0 @@ -16,6 +16,7 @@ fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 -pyopenssl == 23.0.0 +cryptography == 42.0.8 redis == 4.5.4 flask_executor == 1.0.0 +Werkzeug == 2.2.3 diff --git a/services/TS29222_CAPIF_Routing_Info_API/requirements.txt b/services/TS29222_CAPIF_Routing_Info_API/requirements.txt index 1be0346..47ce074 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/requirements.txt +++ b/services/TS29222_CAPIF_Routing_Info_API/requirements.txt @@ -1,5 +1,6 @@ connexion[swagger-ui] == 2.14.2; python_version>="3.6" swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 -setuptools >= 21.0.0 +setuptools == 68.2.2 Flask == 2.0.3 +Werkzeug == 2.2.3 diff --git a/services/TS29222_CAPIF_Security_API/requirements.txt b/services/TS29222_CAPIF_Security_API/requirements.txt index 220adaf..2fde469 100644 --- a/services/TS29222_CAPIF_Security_API/requirements.txt +++ b/services/TS29222_CAPIF_Security_API/requirements.txt @@ -1,11 +1,11 @@ connexion[swagger-ui] == 2.14.2; python_version>="3.6" swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 -setuptools >= 21.0.0 +setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.0.1 +pymongo == 4.3.3 flask_jwt_extended == 4.4.4 -pyopenssl == 23.0.0 +cryptography == 42.0.8 rfc3987 == 1.3.8 redis == 4.5.4 flask_executor == 1.0.0 @@ -19,4 +19,5 @@ fluent == 0.10.0 fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 -flask_executor == 1.0.0 \ No newline at end of file +flask_executor == 1.0.0 +Werkzeug == 2.2.3 \ No newline at end of file diff --git a/services/helper/requirements.txt b/services/helper/requirements.txt index 68d82b2..563f6a3 100644 --- a/services/helper/requirements.txt +++ b/services/helper/requirements.txt @@ -1,7 +1,7 @@ python_dateutil == 2.9.0.post0 setuptools == 68.2.2 Flask == 3.0.3 -pymongo == 4.0.1 +pymongo == 4.3.3 flask_jwt_extended == 4.6.0 pyopenssl == 24.1.0 pyyaml == 6.0.1 diff --git a/services/register/requirements.txt b/services/register/requirements.txt index c5446a6..acd7136 100644 --- a/services/register/requirements.txt +++ b/services/register/requirements.txt @@ -1,10 +1,10 @@ python_dateutil >= 2.6.0 -setuptools >= 21.0.0 -Flask >= 2.0.3 -pymongo == 4.0.1 -flask_jwt_extended -pyopenssl -pyyaml -requests -bcrypt -flask_httpauth \ No newline at end of file +setuptools == 68.2.2 +Flask == 3.0.3 +pymongo == 4.3.3 +flask_jwt_extended == 4.6.0 +pyopenssl == 24.1.0 +pyyaml == 6.0.1 +requests == 2.32.2 +bcrypt == 4.0.1 +flask_httpauth == 4.8.0 diff --git a/tools/robot/basicRequirements.txt b/tools/robot/basicRequirements.txt index aa3e60e..8b56d92 100644 --- a/tools/robot/basicRequirements.txt +++ b/tools/robot/basicRequirements.txt @@ -1,103 +1,103 @@ # Requirements needed when generating releases. See BUILD.rst for details. -appdirs==1.4.4 -argh==0.26.2 -arrow==1.2.3 -async-generator==1.10 -async-timeout==4.0.2 -attrs==22.1.0 -bcrypt==4.0.1 -beautifulsoup4==4.11.1 -binaryornot==0.4.4 -bson==0.5.10 -certifi==2021.10.8 -cffi==1.15.1 -chardet==5.0.0 -charset-normalizer==2.0.12 -click==8.1.7 -configparser==5.3.0 -cookiecutter==2.1.1 -coverage==4.5.4 -cryptography==38.0.1 -Deprecated==1.2.13 -distlib==0.3.6 -dnspython==2.2.1 -docutils==0.19 -exceptiongroup==1.0.0rc9 -filelock==3.8.0 -flake8==3.9.2 -flask==3.0.3 -h11==0.14.0 -idna==3.4 -iniconfig==1.1.1 -invoke==1.6.0 -ipaddress==1.0.23 -Jinja2==3.1.2 -jinja2-time==0.2.0 -lxml==4.9.1 -MarkupSafe==2.1.1 -mccabe==0.6.1 -numpy==1.23.4 -outcome==1.2.0 -packaging==21.3 -pandas==1.5.1 -paramiko==2.11.0 -pathtools==0.1.2 -platformdirs==2.5.2 -pluggy==0.13.1 -psutil==5.9.3 -py==1.11.0 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pycodestyle==2.7.0 -pycparser==2.21 -pyflakes==2.3.1 -PyGithub==1.56 -PyJWT==2.6.0 -pymongo==4.3.2 -PyNaCl==1.5.0 -pyOpenSSL==22.1.0 -pyparsing==3.0.9 -PySocks==1.7.1 -pytest==6.2.4 -python-dateutil==2.8.2 -python-ldap==3.4.3 -python-slugify==6.1.2 -pythonping==1.1.3 -pytz==2022.5 -PyYAML==6.0 -redis==4.3.4 -rellu==0.7 -requests==2.28.1 -rfc3987==1.3.8 -robotframework==7.0 -robotframework-archivelibrary == 0.4.2 -robotframework-httpctrl==0.3.1 -robotframework-lint==1.1 -robotframework-mongodb-library==3.2 -robotframework-pythonlibcore==4.4.1 -robotframework-requests==0.9.3 -robotframework-seleniumlibrary==6.0.0 -robotframework-sshlibrary==3.8.0 -robotremoteserver==1.1 -scp==0.14.4 -selenium==4.5.0 -six==1.16.0 -sniffio==1.3.0 -sortedcontainers==2.4.0 -soupsieve==2.3.2.post1 -sshconf==0.2.5 -text-unidecode==1.3 -toml==0.10.2 -tomli==2.0.1 -tox==3.26.0 -tqdm==4.64.1 -trio==0.22.0 -trio-websocket==0.9.2 -typing-extensions==4.11.0 -urllib3==1.26.12 -virtualenv==20.16.5 -watchdog==4.0.0 -webdrivermanager==0.10.0 -wrapt==1.15.0 -wsproto==1.2.0 -xlrd==2.0.1 +appdirs == 1.4.4 +argh == 0.26.2 +arrow == 1.2.3 +async-generator == 1.10 +async-timeout == 4.0.2 +attrs == 22.1.0 +bcrypt == 4.0.1 +beautifulsoup4 == 4.11.1 +binaryornot == 0.4.4 +bson == 0.5.10 +certifi == 2021.10.8 +cffi == 1.15.1 +chardet == 5.0.0 +charset-normalizer == 2.0.12 +click == 8.1.7 +configparser == 5.3.0 +cookiecutter == 2.1.1 +coverage == 4.5.4 +cryptography == 42.0.8 +Deprecated == 1.2.13 +distlib == 0.3.6 +dnspython == 2.2.1 +docutils == 0.19 +exceptiongroup == 1.0.0rc9 +filelock == 3.8.0 +flake8 == 3.9.2 +flask == 3.0.3 +h11 == 0.14.0 +idna == 3.4 +iniconfig == 1.1.1 +invoke == 1.6.0 +ipaddress == 1.0.23 +Jinja2 == 3.1.2 +jinja2-time == 0.2.0 +lxml == 4.9.1 +MarkupSafe == 2.1.1 +mccabe == 0.6.1 +numpy == 1.23.4 +outcome == 1.2.0 +packaging == 21.3 +pandas == 1.5.1 +paramiko == 2.11.0 +pathtools == 0.1.2 +platformdirs == 2.5.2 +pluggy == 0.13.1 +psutil == 5.9.3 +py == 1.11.0 +pyasn1 == 0.4.8 +pyasn1-modules == 0.2.8 +pycodestyle == 2.7.0 +pycparser == 2.21 +pyflakes == 2.3.1 +PyGithub == 1.56 +PyJWT == 2.6.0 +pymongo == 4.3.3 +PyNaCl == 1.5.0 +pyOpenSSL == 24.1.0 +pyparsing == 3.0.9 +PySocks == 1.7.1 +pytest == 6.2.4 +python-dateutil == 2.8.2 +python-ldap == 3.4.3 +python-slugify == 6.1.2 +pythonping == 1.1.3 +pytz == 2022.5 +PyYAML == 6.0 +redis == 4.5.4 +rellu == 0.7 +requests == 2.28.1 +rfc3987 == 1.3.8 +robotframework == 7.0 +robotframework-archivelibrary == 0.4.2 +robotframework-httpctrl == 0.3.1 +robotframework-lint == 1.1 +robotframework-mongodb-library == 3.2 +robotframework-pythonlibcore == 4.4.1 +robotframework-requests == 0.9.3 +robotframework-seleniumlibrary == 6.0.0 +robotframework-sshlibrary == 3.8.0 +robotremoteserver == 1.1 +scp == 0.14.4 +selenium == 4.5.0 +six == 1.16.0 +sniffio == 1.3.0 +sortedcontainers == 2.4.0 +soupsieve == 2.3.2.post1 +sshconf == 0.2.5 +text-unidecode == 1.3 +toml == 0.10.2 +tomli == 2.0.1 +tox == 3.26.0 +tqdm == 4.64.1 +trio == 0.22.0 +trio-websocket == 0.9.2 +typing-extensions == 4.11.0 +urllib3 == 1.26.12 +virtualenv == 20.16.5 +watchdog == 4.0.0 +webdrivermanager == 0.10.0 +wrapt == 1.15.0 +wsproto == 1.2.0 +xlrd == 2.0.1 -- GitLab From 05998bf22b3e4db817c9cb34e8e5f6e4c1ff9649 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Tue, 11 Jun 2024 14:27:18 +0300 Subject: [PATCH 251/310] Move Audit API from testing to production server --- .../TS29222_CAPIF_Auditing_API/Dockerfile | 4 ++-- .../logs/{__main__.py => app.py} | 8 +++---- .../TS29222_CAPIF_Auditing_API/logs/config.py | 2 +- .../logs/controllers/default_controller.py | 21 ++++--------------- .../logs/core/auditoperations.py | 14 +++++-------- .../logs/core/resources.py | 4 ++-- .../logs/core/responses.py | 17 +++++++++++++++ .../TS29222_CAPIF_Auditing_API/logs/db/db.py | 5 ++--- .../logs/encoder.py | 2 +- .../TS29222_CAPIF_Auditing_API/logs/util.py | 5 +++-- .../TS29222_CAPIF_Auditing_API/logs/wsgi.py | 4 ++++ .../requirements.txt | 2 ++ 12 files changed, 47 insertions(+), 41 deletions(-) rename services/TS29222_CAPIF_Auditing_API/logs/{__main__.py => app.py} (97%) create mode 100644 services/TS29222_CAPIF_Auditing_API/logs/wsgi.py diff --git a/services/TS29222_CAPIF_Auditing_API/Dockerfile b/services/TS29222_CAPIF_Auditing_API/Dockerfile index d7030b6..4908c19 100644 --- a/services/TS29222_CAPIF_Auditing_API/Dockerfile +++ b/services/TS29222_CAPIF_Auditing_API/Dockerfile @@ -12,6 +12,6 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["python3"] +ENTRYPOINT ["gunicorn"] -CMD ["-m", "logs"] \ No newline at end of file +CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/logs", "wsgi:app"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Auditing_API/logs/__main__.py b/services/TS29222_CAPIF_Auditing_API/logs/app.py similarity index 97% rename from services/TS29222_CAPIF_Auditing_API/logs/__main__.py rename to services/TS29222_CAPIF_Auditing_API/logs/app.py index 385f0b5..b89c1a6 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/__main__.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/app.py @@ -3,8 +3,8 @@ import connexion import logging from logging.handlers import RotatingFileHandler -from logs import encoder -from .config import Config +import encoder +from config import Config import os from fluent import sender from opentelemetry.instrumentation.flask import FlaskInstrumentor @@ -112,5 +112,5 @@ config = Config() if eval(os.environ.get("MONITORING").lower().capitalize()): configure_monitoring(app.app, config.get_config()) -if __name__ == '__main__': - app.run(port=8080) +# if __name__ == '__main__': +# app.run(port=8080) diff --git a/services/TS29222_CAPIF_Auditing_API/logs/config.py b/services/TS29222_CAPIF_Auditing_API/logs/config.py index d04bd1a..97ab831 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/config.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/config.py @@ -5,7 +5,7 @@ import os class Config: def __init__(self): self.cached = 0 - self.file="./config.yaml" + self.file="../config.yaml" self.my_config = {} stamp = os.stat(self.file).st_mtime diff --git a/services/TS29222_CAPIF_Auditing_API/logs/controllers/default_controller.py b/services/TS29222_CAPIF_Auditing_API/logs/controllers/default_controller.py index c330f47..ead9ec5 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/controllers/default_controller.py @@ -1,21 +1,8 @@ -import connexion -import six -import sys - -from logs.models.interface_description import InterfaceDescription # noqa: E501 -from logs.models.invocation_log import InvocationLog # noqa: E501 -from logs.models.operation import Operation # noqa: E501 -from logs.models.problem_details import ProblemDetails # noqa: E501 -from logs.models.protocol import Protocol # noqa: E501 -from logs import util +from ..util import deserialize_datetime from ..core.auditoperations import AuditOperations -import json from flask import Response, request, current_app -from ..encoder import JSONEncoder -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -import pymongo + from ..core.responses import bad_request_error audit_operations = AuditOperations() @@ -65,8 +52,8 @@ def api_invocation_logs_get(aef_id=None, api_invoker_id=None, time_range_start=N cause="Mandatory parameters missing", invalid_params=[ {"param": "aef_id or api_invoker_id", "reason": "missing"}]) - time_range_start = util.deserialize_datetime(time_range_start) - time_range_end = util.deserialize_datetime(time_range_end) + time_range_start = deserialize_datetime(time_range_start) + time_range_end = deserialize_datetime(time_range_end) query_params = {"aef_id": aef_id, "api_invoker_id": api_invoker_id, diff --git a/services/TS29222_CAPIF_Auditing_API/logs/core/auditoperations.py b/services/TS29222_CAPIF_Auditing_API/logs/core/auditoperations.py index ac83701..6632d28 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/core/auditoperations.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/core/auditoperations.py @@ -1,11 +1,9 @@ -import sys from flask import current_app, Flask, Response import json -import sys -from datetime import datetime + from .resources import Resource -from bson import json_util + from ..util import dict_to_camel_case, clean_empty from .responses import bad_request_error, not_found_error, forbidden_error, internal_server_error, make_response from ..models.invocation_log import InvocationLog @@ -57,11 +55,9 @@ class AuditOperations (Resource): if not result['logs']: return not_found_error(detail="Parameters do not match any log entry", cause="No logs found") - - result = dict_to_camel_case(clean_empty(result)) - invocation_log = InvocationLog(result['aefId'], result['apiInvokerId'], result['logs'], - result['supportedFeatures']) - res = make_response(object=invocation_log, status=200) + invocation_log = InvocationLog(result['aef_id'], result['api_invoker_id'], result['logs'], + result['supported_features']) + res = make_response(object=dict_to_camel_case(clean_empty(invocation_log.to_dict())), status=200) current_app.logger.debug("Found invocation logs") return res diff --git a/services/TS29222_CAPIF_Auditing_API/logs/core/resources.py b/services/TS29222_CAPIF_Auditing_API/logs/core/resources.py index d55b30c..d5a3552 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/core/resources.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/core/resources.py @@ -1,5 +1,5 @@ -from abc import ABC, abstractmethod -from ..db.db import MongoDatabse +from abc import ABC +from db.db import MongoDatabse class Resource(ABC): diff --git a/services/TS29222_CAPIF_Auditing_API/logs/core/responses.py b/services/TS29222_CAPIF_Auditing_API/logs/core/responses.py index 5986c50..9d5ea09 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/core/responses.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/core/responses.py @@ -1,6 +1,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response +from ..util import dict_to_camel_case, clean_empty import json mimetype = "application/json" @@ -15,22 +16,38 @@ def make_response(object, status): def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Auditing_API/logs/db/db.py b/services/TS29222_CAPIF_Auditing_API/logs/db/db.py index f3286d2..17a93fe 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/db/db.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/db/db.py @@ -1,9 +1,8 @@ -import atexit + import time from pymongo import MongoClient from pymongo.errors import AutoReconnect -from ..config import Config -from bson.codec_options import CodecOptions +from config import Config import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor diff --git a/services/TS29222_CAPIF_Auditing_API/logs/encoder.py b/services/TS29222_CAPIF_Auditing_API/logs/encoder.py index 55259f5..80bad8f 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/encoder.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/encoder.py @@ -1,7 +1,7 @@ from connexion.apps.flask_app import FlaskJSONEncoder import six -from logs.models.base_model_ import Model +from models.base_model_ import Model class JSONEncoder(FlaskJSONEncoder): diff --git a/services/TS29222_CAPIF_Auditing_API/logs/util.py b/services/TS29222_CAPIF_Auditing_API/logs/util.py index ff81257..ec14301 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/util.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/util.py @@ -1,8 +1,8 @@ import datetime import six -import typing -from logs import typing_utils +import typing_utils + def clean_empty(d): if isinstance(d, dict): @@ -15,6 +15,7 @@ def clean_empty(d): return [v for v in map(clean_empty, d) if v] return d + def dict_to_camel_case(my_dict): diff --git a/services/TS29222_CAPIF_Auditing_API/logs/wsgi.py b/services/TS29222_CAPIF_Auditing_API/logs/wsgi.py new file mode 100644 index 0000000..6026b0f --- /dev/null +++ b/services/TS29222_CAPIF_Auditing_API/logs/wsgi.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == "__main__": + app.run() diff --git a/services/TS29222_CAPIF_Auditing_API/requirements.txt b/services/TS29222_CAPIF_Auditing_API/requirements.txt index 197399f..cb1e439 100644 --- a/services/TS29222_CAPIF_Auditing_API/requirements.txt +++ b/services/TS29222_CAPIF_Auditing_API/requirements.txt @@ -18,3 +18,5 @@ opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 pyopenssl == 23.0.0 +gunicorn==22.0.0 +packaging==24.0 \ No newline at end of file -- GitLab From c7f153130470e2a2f6284ba9ec7876cef8131647 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Tue, 11 Jun 2024 14:46:18 +0300 Subject: [PATCH 252/310] Move Logging API from testing to production server --- .../Dockerfile | 4 ++-- .../{__main__.py => app.py} | 11 +++++----- .../api_invocation_logs/config.py | 2 +- .../controllers/default_controller.py | 6 ++--- .../core/invocationlogs.py | 13 ++++------- .../api_invocation_logs/core/resources.py | 4 ++-- .../api_invocation_logs/core/responses.py | 22 ++++++++++++++++++- .../api_invocation_logs/core/validate_user.py | 5 +++++ .../api_invocation_logs/db/db.py | 5 ++--- .../api_invocation_logs/encoder.py | 2 +- .../api_invocation_logs/util.py | 5 +++-- .../api_invocation_logs/wsgi.py | 4 ++++ .../requirements.txt | 2 ++ 13 files changed, 54 insertions(+), 31 deletions(-) rename services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/{__main__.py => app.py} (97%) create mode 100644 services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/wsgi.py diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/Dockerfile b/services/TS29222_CAPIF_Logging_API_Invocation_API/Dockerfile index 7339cec..4248907 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/Dockerfile +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/Dockerfile @@ -12,6 +12,6 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["python3"] +ENTRYPOINT ["gunicorn"] -CMD ["-m", "api_invocation_logs"] \ No newline at end of file +CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/api_invocation_logs", "wsgi:app"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/__main__.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py similarity index 97% rename from services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/__main__.py rename to services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py index f5323c9..519cc15 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/__main__.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py @@ -3,8 +3,8 @@ import connexion import logging from logging.handlers import RotatingFileHandler -from api_invocation_logs import encoder -from .config import Config +import encoder +from config import Config import os from fluent import sender from opentelemetry.instrumentation.flask import FlaskInstrumentor @@ -16,10 +16,9 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor - - NAME = "Logging-Service" + def configure_monitoring(app, config): resource = Resource(attributes={"service.name": NAME}) @@ -116,5 +115,5 @@ config = Config() if eval(os.environ.get("MONITORING").lower().capitalize()): configure_monitoring(app.app, config.get_config()) -if __name__ == '__main__': - app.run(port=8080) +# if __name__ == '__main__': +# app.run(port=8080) diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/config.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/config.py index d04bd1a..97ab831 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/config.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/config.py @@ -5,7 +5,7 @@ import os class Config: def __init__(self): self.cached = 0 - self.file="./config.yaml" + self.file="../config.yaml" self.my_config = {} stamp = os.stat(self.file).st_mtime diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/controllers/default_controller.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/controllers/default_controller.py index eb0cd98..7ce9972 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/controllers/default_controller.py @@ -1,13 +1,11 @@ import connexion -from api_invocation_logs.models.invocation_log import InvocationLog # noqa: E501 +from ..models.invocation_log import InvocationLog # noqa: E501 from ..core.invocationlogs import LoggingInvocationOperations -import json from flask import Response, request, current_app -from ..encoder import JSONEncoder -from ..models.problem_details import ProblemDetails + from ..core.validate_user import ControlAccess from cryptography import x509 from cryptography.hazmat.backends import default_backend diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py index dde0c64..fe4fc9f 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py @@ -1,18 +1,13 @@ -import sys + import os -import pymongo + import secrets from flask import current_app, Flask, Response -import json -from ..db.db import MongoDatabse -from ..encoder import JSONEncoder -from ..models.problem_details import ProblemDetails from pymongo import ReturnDocument from ..util import dict_to_camel_case, clean_empty from .resources import Resource -from .responses import bad_request_error, internal_server_error, forbidden_error, not_found_error, unauthorized_error, make_response -from ..models.invocation_log import InvocationLog +from .responses import internal_server_error, not_found_error, unauthorized_error, make_response class LoggingInvocationOperations(Resource): @@ -104,7 +99,7 @@ class LoggingInvocationOperations(Resource): existing_invocationlog['logs'].append(updated_invocation_log) mycol.find_one_and_update(my_query, {"$set": existing_invocationlog}, projection={'_id': 0, 'log_id': 0}, return_document=ReturnDocument.AFTER, upsert=False) - res = make_response(object=invocationlog, status=201) + res = make_response(object=dict_to_camel_case(clean_empty(invocationlog.to_dict())), status=201) current_app.logger.debug("Invocation Logs response ready") apis_added = {log.api_id:log.api_name for log in invocationlog.logs} diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/resources.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/resources.py index d55b30c..d5a3552 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/resources.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/resources.py @@ -1,5 +1,5 @@ -from abc import ABC, abstractmethod -from ..db.db import MongoDatabse +from abc import ABC +from db.db import MongoDatabse class Resource(ABC): diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/responses.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/responses.py index 3a136b1..6940f64 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/responses.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/responses.py @@ -1,8 +1,8 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response +from ..util import dict_to_camel_case, clean_empty import json -from bson import json_util mimetype = "application/json" @@ -16,28 +16,48 @@ def make_response(object, status): def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) def unauthorized_error(detail, cause): prob = ProblemDetails(title="Unauthorized", status=401, detail=detail, cause=cause) + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/validate_user.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/validate_user.py index 2a89437..13d8b6a 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/validate_user.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/validate_user.py @@ -4,6 +4,8 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource from .responses import internal_server_error +from ..util import dict_to_camel_case, clean_empty + class ControlAccess(Resource): @@ -18,6 +20,9 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") except Exception as e: diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/db/db.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/db/db.py index 96a39f3..f706e5f 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/db/db.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/db/db.py @@ -1,9 +1,8 @@ -import atexit + import time from pymongo import MongoClient from pymongo.errors import AutoReconnect -from ..config import Config -from bson.codec_options import CodecOptions +from config import Config import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/encoder.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/encoder.py index efa1154..80bad8f 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/encoder.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/encoder.py @@ -1,7 +1,7 @@ from connexion.apps.flask_app import FlaskJSONEncoder import six -from api_invocation_logs.models.base_model_ import Model +from models.base_model_ import Model class JSONEncoder(FlaskJSONEncoder): diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/util.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/util.py index af55567..ec14301 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/util.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/util.py @@ -1,8 +1,8 @@ import datetime import six -import typing -from api_invocation_logs import typing_utils +import typing_utils + def clean_empty(d): if isinstance(d, dict): @@ -15,6 +15,7 @@ def clean_empty(d): return [v for v in map(clean_empty, d) if v] return d + def dict_to_camel_case(my_dict): diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/wsgi.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/wsgi.py new file mode 100644 index 0000000..6026b0f --- /dev/null +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/wsgi.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == "__main__": + app.run() diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt index 197399f..cb1e439 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt @@ -18,3 +18,5 @@ opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 pyopenssl == 23.0.0 +gunicorn==22.0.0 +packaging==24.0 \ No newline at end of file -- GitLab From be36f960614becf726f2a8851bf0798a499862ed Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Tue, 11 Jun 2024 16:13:08 +0200 Subject: [PATCH 253/310] Fix how MONITOTING Environment variable is checked at __main___ code on each service --- .../api_invoker_management/__main__.py | 3 ++- .../api_provider_management/__main__.py | 3 ++- .../openapi_server/__main__.py | 4 ++-- services/TS29222_CAPIF_Auditing_API/logs/__main__.py | 3 ++- .../service_apis/__main__.py | 3 ++- services/TS29222_CAPIF_Events_API/capif_events/__main__.py | 3 ++- .../capif_events/models/routing_rule.py | 4 ---- .../api_invocation_logs/__main__.py | 3 ++- .../published_apis/__main__.py | 3 ++- .../capif_routing_info/models/ipv4_address_range.py | 4 ++-- .../capif_routing_info/models/routing_rule.py | 4 ---- .../TS29222_CAPIF_Security_API/capif_security/__main__.py | 3 ++- services/clean_capif_docker_services.sh | 2 +- services/run.sh | 2 +- 14 files changed, 22 insertions(+), 22 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/__main__.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/__main__.py index 32ccc2e..84f93cd 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/__main__.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/__main__.py @@ -136,7 +136,8 @@ config = Config() jwt = JWTManager(app.app) configure_logging(app.app) -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": configure_monitoring(app.app, config.get_config()) executor = Executor(app.app) diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/__main__.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/__main__.py index f52d69c..ee71416 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/__main__.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/__main__.py @@ -120,7 +120,8 @@ def main(): config = Config() configure_logging(app.app) - if eval(os.environ.get("MONITORING").lower().capitalize()): + monitoring_value = os.environ.get("MONITORING", "").lower() + if monitoring_value == "true": configure_monitoring(app.app, config.get_config()) app.app.config['JWT_ALGORITHM'] = 'RS256' diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/__main__.py b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/__main__.py index 215e85e..ae9ad7e 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/__main__.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/__main__.py @@ -125,8 +125,8 @@ config = Config() jwt = JWTManager(app.app) configure_logging(app.app) - -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": configure_monitoring(app.app, config.get_config()) app.app.config["invocations"]=config.get_config()["invocations"] diff --git a/services/TS29222_CAPIF_Auditing_API/logs/__main__.py b/services/TS29222_CAPIF_Auditing_API/logs/__main__.py index 385f0b5..ac6df6d 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/__main__.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/__main__.py @@ -109,7 +109,8 @@ app.add_api('openapi.yaml', configure_logging(app.app) config = Config() -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": configure_monitoring(app.app, config.get_config()) if __name__ == '__main__': diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/__main__.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/__main__.py index 57326e9..84523da 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/__main__.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/__main__.py @@ -121,7 +121,8 @@ configure_logging(app.app) config = Config() -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": configure_monitoring(app.app, config.get_config()) jwt = JWTManager(app.app) diff --git a/services/TS29222_CAPIF_Events_API/capif_events/__main__.py b/services/TS29222_CAPIF_Events_API/capif_events/__main__.py index 1e602ef..b566f66 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/__main__.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/__main__.py @@ -134,7 +134,8 @@ notifications = Notifications() jwt = JWTManager(app.app) configure_logging(app.app) -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": configure_monitoring(app.app, config.get_config()) config = Config() diff --git a/services/TS29222_CAPIF_Events_API/capif_events/models/routing_rule.py b/services/TS29222_CAPIF_Events_API/capif_events/models/routing_rule.py index 7ff1d4e..a4e4d07 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/models/routing_rule.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/models/routing_rule.py @@ -11,10 +11,6 @@ from capif_events.models.ipv4_address_range import Ipv4AddressRange from capif_events.models.ipv6_address_range import Ipv6AddressRange from capif_events import util -from capif_events.models.aef_profile import AefProfile # noqa: E501 -from capif_events.models.ipv4_address_range import Ipv4AddressRange # noqa: E501 -from capif_events.models.ipv6_address_range import Ipv6AddressRange # noqa: E501 - class RoutingRule(Model): """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/__main__.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/__main__.py index f5323c9..b992231 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/__main__.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/__main__.py @@ -113,7 +113,8 @@ configure_logging(app.app) config = Config() -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": configure_monitoring(app.app, config.get_config()) if __name__ == '__main__': diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/__main__.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/__main__.py index dfba9ef..aa5f60c 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/__main__.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/__main__.py @@ -127,7 +127,8 @@ config = Config() jwt = JWTManager(app.app) configure_logging(app.app) -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": configure_monitoring(app.app, config.get_config()) executor = Executor(app.app) diff --git a/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/models/ipv4_address_range.py b/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/models/ipv4_address_range.py index 8bf4532..5f8e7c0 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/models/ipv4_address_range.py +++ b/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/models/ipv4_address_range.py @@ -70,7 +70,7 @@ class Ipv4AddressRange(Model): :type start: str """ if start is not None and not re.search(r'^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$', start): # noqa: E501 - raise ValueError("Invalid value for `start`, must be a follow pattern or equal to `/^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/`") # noqa: E501 + raise ValueError("Invalid value for `start`, must be a follow pattern or equal to `/^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/`") # noqa: E501 self._start = start @@ -95,6 +95,6 @@ class Ipv4AddressRange(Model): :type end: str """ if end is not None and not re.search(r'^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$', end): # noqa: E501 - raise ValueError("Invalid value for `end`, must be a follow pattern or equal to `/^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/`") # noqa: E501 + raise ValueError("Invalid value for `end`, must be a follow pattern or equal to `/^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/`") # noqa: E501 self._end = end diff --git a/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/models/routing_rule.py b/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/models/routing_rule.py index f177670..29c0930 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/models/routing_rule.py +++ b/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/models/routing_rule.py @@ -11,10 +11,6 @@ from capif_routing_info.models.ipv4_address_range import Ipv4AddressRange from capif_routing_info.models.ipv6_address_range import Ipv6AddressRange from capif_routing_info import util -from capif_routing_info.models.aef_profile import AefProfile # noqa: E501 -from capif_routing_info.models.ipv4_address_range import Ipv4AddressRange # noqa: E501 -from capif_routing_info.models.ipv6_address_range import Ipv6AddressRange # noqa: E501 - class RoutingRule(Model): """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/services/TS29222_CAPIF_Security_API/capif_security/__main__.py b/services/TS29222_CAPIF_Security_API/capif_security/__main__.py index 98d3964..26eec19 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/__main__.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/__main__.py @@ -131,7 +131,8 @@ def main(): config = Config() configure_logging(app.app) - if eval(os.environ.get("MONITORING").lower().capitalize()): + monitoring_value = os.environ.get("MONITORING", "").lower() + if monitoring_value == "true": configure_monitoring(app.app, config.get_config()) executor = Executor(app.app) diff --git a/services/clean_capif_docker_services.sh b/services/clean_capif_docker_services.sh index cfc5b71..617ffed 100755 --- a/services/clean_capif_docker_services.sh +++ b/services/clean_capif_docker_services.sh @@ -68,7 +68,7 @@ echo "${FILES[@]}" for FILE in "${FILES[@]}"; do echo "Executing 'docker compose down' for file $FILE" - docker compose -f "$FILE" down --rmi all + CAPIF_PRIV_KEY=$CAPIF_PRIV_KEY_BASE_64 DUID=$DUID DGID=$DGID MONITORING=$MONITORING_STATE docker compose -f "$FILE" down --rmi all status=$? if [ $status -eq 0 ]; then echo "*** Removed Service from $FILE ***" diff --git a/services/run.sh b/services/run.sh index 4465aaa..3053019 100755 --- a/services/run.sh +++ b/services/run.sh @@ -62,7 +62,7 @@ echo Nginx hostname will be $HOSTNAME, deploy $DEPLOY, monitoring $MONITORING_ST if [ "$MONITORING_STATE" == "true" ] ; then echo '***Monitoring set as true***' - echo '***Creating Monitoging stack***' + echo '***Creating Monitoring stack***' DUID=$DUID DGID=$DGID docker compose -f "../monitoring/docker-compose.yml" up --detach status=$? -- GitLab From 695e3535ca6c078a4b6973d821e0de88013936ce Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 12 Jun 2024 10:23:08 +0200 Subject: [PATCH 254/310] Fix some medium issues --- .../core/apiinvokerenrolmentdetails.py | 2 +- .../api_invoker_management/db/db.py | 3 ++- .../TS29222_CAPIF_API_Invoker_Management_API/config.yaml | 3 ++- .../api_provider_management/core/sign_certificate.py | 2 +- .../api_provider_management/db/db.py | 3 ++- .../TS29222_CAPIF_API_Provider_Management_API/config.yaml | 3 ++- .../openapi_server/db/db.py | 3 ++- services/TS29222_CAPIF_Auditing_API/logs/db/db.py | 3 ++- .../service_apis/db/db.py | 3 ++- services/TS29222_CAPIF_Events_API/capif_events/db/db.py | 3 ++- .../api_invocation_logs/db/db.py | 3 ++- .../published_apis/db/db.py | 3 ++- services/TS29222_CAPIF_Security_API/capif_security/db/db.py | 3 ++- services/helper/config.yaml | 3 ++- services/helper/helper_service/__main__.py | 4 ++-- services/register/config.yaml | 3 ++- services/register/register_service/__main__.py | 6 +++--- 17 files changed, 33 insertions(+), 20 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py index 3a8ec3f..9bad92e 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py @@ -41,7 +41,7 @@ class InvokerManagementOperations(Resource): 'common_name': invoker_id } - response = requests.request("POST", url, headers=headers, data=data, verify = False) + response = requests.request("POST", url, headers=headers, data=data, verify = self.config["ca_factory"].get("verify", False)) print(response) response_payload = json.loads(response.text) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py index 638cc80..b92280a 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/db/db.py @@ -7,7 +7,8 @@ from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": PymongoInstrumentor().instrument() class MongoDatabse(): diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml b/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml index 2a14561..3107e41 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/config.yaml @@ -12,7 +12,8 @@ mongo: { ca_factory: { "url": "vault", "port": "8200", - "token": "dev-only-token" + "token": "dev-only-token", + "verify": False } monitoring: { diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py index 6ec96cf..dff8006 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/sign_certificate.py @@ -16,7 +16,7 @@ def sign_certificate(publick_key, provider_id): 'common_name': provider_id } - response = requests.request("POST", url, headers=headers, data=json.dumps(data), verify = False) + response = requests.request("POST", url, headers=headers, data=json.dumps(data), verify = config["ca_factory"].get("verify", False)) response_payload = json.loads(response.text) return response_payload["data"]["certificate"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py index a67e2ea..2eea8cc 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/db/db.py @@ -7,7 +7,8 @@ from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": PymongoInstrumentor().instrument() diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml b/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml index 7d1899a..ce684f3 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml +++ b/services/TS29222_CAPIF_API_Provider_Management_API/config.yaml @@ -12,7 +12,8 @@ mongo: { ca_factory: { "url": "vault", "port": "8200", - "token": "dev-only-token" + "token": "dev-only-token", + "verify": False } diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/db/db.py b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/db/db.py index b25c794..843a421 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/db/db.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/openapi_server/db/db.py @@ -7,7 +7,8 @@ from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": PymongoInstrumentor().instrument() class MongoDatabse(): diff --git a/services/TS29222_CAPIF_Auditing_API/logs/db/db.py b/services/TS29222_CAPIF_Auditing_API/logs/db/db.py index f3286d2..b2fc38c 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/db/db.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/db/db.py @@ -7,7 +7,8 @@ from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": PymongoInstrumentor().instrument() diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/db/db.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/db/db.py index 5445455..bd3b28c 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/db/db.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/db/db.py @@ -7,7 +7,8 @@ from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": PymongoInstrumentor().instrument() class MongoDatabse(): diff --git a/services/TS29222_CAPIF_Events_API/capif_events/db/db.py b/services/TS29222_CAPIF_Events_API/capif_events/db/db.py index 2f4d119..be7bf39 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/db/db.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/db/db.py @@ -7,7 +7,8 @@ from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": PymongoInstrumentor().instrument() diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/db/db.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/db/db.py index 96a39f3..17daff1 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/db/db.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/db/db.py @@ -7,7 +7,8 @@ from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": PymongoInstrumentor().instrument() diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py index 91f7b6b..23ee4a2 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/db/db.py @@ -7,7 +7,8 @@ from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": PymongoInstrumentor().instrument() diff --git a/services/TS29222_CAPIF_Security_API/capif_security/db/db.py b/services/TS29222_CAPIF_Security_API/capif_security/db/db.py index dbbb99e..d537657 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/db/db.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/db/db.py @@ -7,7 +7,8 @@ from bson.codec_options import CodecOptions import os from opentelemetry.instrumentation.pymongo import PymongoInstrumentor -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": PymongoInstrumentor().instrument() diff --git a/services/helper/config.yaml b/services/helper/config.yaml index 46e5801..bb090f0 100644 --- a/services/helper/config.yaml +++ b/services/helper/config.yaml @@ -14,5 +14,6 @@ mongo: { ca_factory: { "url": "vault", "port": "8200", - "token": "dev-only-token" + "token": "dev-only-token", + "verify": False } diff --git a/services/helper/helper_service/__main__.py b/services/helper/helper_service/__main__.py index 69411aa..c2f6e39 100644 --- a/services/helper/helper_service/__main__.py +++ b/services/helper/helper_service/__main__.py @@ -40,7 +40,7 @@ data = { 'common_name': "superadmin" } -response = requests.request("POST", url, headers=headers, data=data, verify = False) +response = requests.request("POST", url, headers=headers, data=data, verify = config["ca_factory"].get("verify", False)) superadmin_cert = json.loads(response.text)['data']['certificate'] # Save the superadmin certificate @@ -53,7 +53,7 @@ headers = { 'X-Vault-Token': config['ca_factory']['token'] } -response = requests.request("GET", url, headers=headers, verify = False) +response = requests.request("GET", url, headers=headers, verify = config["ca_factory"].get("verify", False)) ca_root = json.loads(response.text)['data']['data']['ca'] cert_file = open("helper_service/certs/ca_root.crt", 'wb') diff --git a/services/register/config.yaml b/services/register/config.yaml index f63df9f..d44e09f 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -10,7 +10,8 @@ mongo: { ca_factory: { "url": "vault", "port": "8200", - "token": "dev-only-token" + "token": "dev-only-token", + "verify": False } register: { diff --git a/services/register/register_service/__main__.py b/services/register/register_service/__main__.py index 7554fa2..612b031 100644 --- a/services/register/register_service/__main__.py +++ b/services/register/register_service/__main__.py @@ -44,7 +44,7 @@ data = { 'common_name': "superadmin" } -response = requests.request("POST", url, headers=headers, data=data, verify = False) +response = requests.request("POST", url, headers=headers, data=data, verify = config["ca_factory"].get("verify", False)) superadmin_cert = json.loads(response.text)['data']['certificate'] # Save the superadmin certificate @@ -57,7 +57,7 @@ headers = { 'X-Vault-Token': config['ca_factory']['token'] } -response = requests.request("GET", url, headers=headers, verify = False) +response = requests.request("GET", url, headers=headers, verify = config["ca_factory"].get("verify", False)) ca_root = json.loads(response.text)['data']['data']['ca'] cert_file = open("register_service/certs/ca_root.crt", 'wb') @@ -67,7 +67,7 @@ cert_file.close() # Request CAPIF private key to encode the CAPIF token url = 'http://{}:{}/v1/secret/data/server_cert/private'.format(config["ca_factory"]["url"], config["ca_factory"]["port"]) headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} -response = requests.request("GET", url, headers=headers, verify = False) +response = requests.request("GET", url, headers=headers, verify = config["ca_factory"].get("verify", False)) key_data = json.loads(response.text)["data"]["data"]["key"] -- GitLab From 7af7c1802dbd8f0dde849539c4c53351f794d066 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 12 Jun 2024 10:46:26 +0200 Subject: [PATCH 255/310] Upgrade pymongo library to solve some security risks --- .../TS29222_CAPIF_API_Invoker_Management_API/requirements.txt | 2 +- .../TS29222_CAPIF_API_Provider_Management_API/requirements.txt | 2 +- .../TS29222_CAPIF_Access_Control_Policy_API/requirements.txt | 2 +- services/TS29222_CAPIF_Auditing_API/requirements.txt | 2 +- services/TS29222_CAPIF_Discover_Service_API/requirements.txt | 2 +- services/TS29222_CAPIF_Events_API/requirements.txt | 2 +- .../TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt | 2 +- services/TS29222_CAPIF_Publish_Service_API/requirements.txt | 2 +- services/TS29222_CAPIF_Security_API/requirements.txt | 2 +- services/helper/requirements.txt | 2 +- services/register/requirements.txt | 2 +- tools/robot/basicRequirements.txt | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt index 49bdd94..d6cb4ae 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt @@ -3,7 +3,7 @@ swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 flask_jwt_extended == 4.4.4 cryptography == 42.0.8 rfc3987 == 1.3.8 diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt index b44401b..03fc57f 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt +++ b/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt @@ -3,7 +3,7 @@ swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 redis == 4.5.4 flask_jwt_extended == 4.4.4 cryptography == 42.0.8 diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt b/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt index 1c80c12..e336fd8 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt @@ -4,7 +4,7 @@ swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 flask_jwt_extended == 4.4.4 opentelemetry-instrumentation == 0.38b0 opentelemetry-instrumentation-flask == 0.38b0 diff --git a/services/TS29222_CAPIF_Auditing_API/requirements.txt b/services/TS29222_CAPIF_Auditing_API/requirements.txt index 18fd92f..d6dacc0 100644 --- a/services/TS29222_CAPIF_Auditing_API/requirements.txt +++ b/services/TS29222_CAPIF_Auditing_API/requirements.txt @@ -3,7 +3,7 @@ swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 elasticsearch == 8.4.3 flask_jwt_extended == 4.4.4 opentelemetry-instrumentation == 0.38b0 diff --git a/services/TS29222_CAPIF_Discover_Service_API/requirements.txt b/services/TS29222_CAPIF_Discover_Service_API/requirements.txt index 8e4d2e2..61747f2 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Discover_Service_API/requirements.txt @@ -3,7 +3,7 @@ swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 flask_jwt_extended == 4.4.4 cryptography == 42.0.8 rfc3987 == 1.3.8 diff --git a/services/TS29222_CAPIF_Events_API/requirements.txt b/services/TS29222_CAPIF_Events_API/requirements.txt index e75e20a..efc0c6f 100644 --- a/services/TS29222_CAPIF_Events_API/requirements.txt +++ b/services/TS29222_CAPIF_Events_API/requirements.txt @@ -3,7 +3,7 @@ swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 opentelemetry-instrumentation == 0.38b0 opentelemetry-instrumentation-flask == 0.38b0 opentelemetry-instrumentation-redis == 0.38b0 diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt index af0caa0..2de683c 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt @@ -3,7 +3,7 @@ swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 elasticsearch == 8.4.3 flask_jwt_extended == 4.4.4 redis == 4.5.4 diff --git a/services/TS29222_CAPIF_Publish_Service_API/requirements.txt b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt index 076261e..1690fde 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt @@ -3,7 +3,7 @@ swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 flask_jwt_extended == 4.4.4 opentelemetry-instrumentation == 0.38b0 opentelemetry-instrumentation-flask == 0.38b0 diff --git a/services/TS29222_CAPIF_Security_API/requirements.txt b/services/TS29222_CAPIF_Security_API/requirements.txt index 2fde469..45a43d6 100644 --- a/services/TS29222_CAPIF_Security_API/requirements.txt +++ b/services/TS29222_CAPIF_Security_API/requirements.txt @@ -3,7 +3,7 @@ swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 2.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 flask_jwt_extended == 4.4.4 cryptography == 42.0.8 rfc3987 == 1.3.8 diff --git a/services/helper/requirements.txt b/services/helper/requirements.txt index 563f6a3..dbe8a48 100644 --- a/services/helper/requirements.txt +++ b/services/helper/requirements.txt @@ -1,7 +1,7 @@ python_dateutil == 2.9.0.post0 setuptools == 68.2.2 Flask == 3.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 flask_jwt_extended == 4.6.0 pyopenssl == 24.1.0 pyyaml == 6.0.1 diff --git a/services/register/requirements.txt b/services/register/requirements.txt index acd7136..e9b85df 100644 --- a/services/register/requirements.txt +++ b/services/register/requirements.txt @@ -1,7 +1,7 @@ python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 3.0.3 -pymongo == 4.3.3 +pymongo == 4.7.3 flask_jwt_extended == 4.6.0 pyopenssl == 24.1.0 pyyaml == 6.0.1 diff --git a/tools/robot/basicRequirements.txt b/tools/robot/basicRequirements.txt index 8b56d92..06e5cc2 100644 --- a/tools/robot/basicRequirements.txt +++ b/tools/robot/basicRequirements.txt @@ -53,7 +53,7 @@ pycparser == 2.21 pyflakes == 2.3.1 PyGithub == 1.56 PyJWT == 2.6.0 -pymongo == 4.3.3 +pymongo == 4.7.3 PyNaCl == 1.5.0 pyOpenSSL == 24.1.0 pyparsing == 3.0.9 -- GitLab From fd5925bd2ac5dd93fee01b3445b367343ad5dca8 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Wed, 12 Jun 2024 17:16:50 +0300 Subject: [PATCH 256/310] [Mock server tests failing] Fix Events testing --- .../capif_events/core/notifications.py | 5 +++-- .../capif_events/core/validate_user.py | 6 ++++++ .../api_invocation_logs/core/invocationlogs.py | 4 ++-- .../register/register_service/core/register_operations.py | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py index 1ca9f2f..3b32d11 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py @@ -10,6 +10,7 @@ import json from flask import current_app import asyncio import aiohttp +from util import dict_to_camel_case, clean_empty class Notifications(): @@ -33,9 +34,9 @@ class Notifications(): event_detail={redis_event.get('key'):redis_event.get('information')} current_app.logger.debug(event_detail) data = EventNotification(sub["subscription_id"], events=redis_event.get('event'), event_detail=event_detail) - current_app.logger.debug(json.dumps(data,cls=JSONEncoder)) + current_app.logger.debug(json.dumps(data.to_dict(),cls=JSONEncoder)) - asyncio.run(self.send(url, json.loads(json.dumps(data,cls=JSONEncoder)))) + asyncio.run(self.send(url, dict_to_camel_case(clean_empty(data.to_dict())))) except Exception as e: current_app.logger.error("An exception occurred ::" + str(e)) diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/validate_user.py b/services/TS29222_CAPIF_Events_API/capif_events/core/validate_user.py index 18d54a2..be87def 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/validate_user.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/validate_user.py @@ -4,6 +4,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource from .responses import internal_server_error +from ..util import dict_to_camel_case, clean_empty class ControlAccess(Resource): @@ -18,6 +19,11 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature or "event_subscriptions" not in cert_entry["resources"] or event_id not in cert_entry["resources"]["event_subscriptions"]: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") + + prob = prob.to_dict() + prob = clean_empty(prob) + prob = dict_to_camel_case(prob) + return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") except Exception as e: diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py index 752b214..185c056 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py @@ -80,7 +80,7 @@ class LoggingInvocationOperations(Resource): current_app.logger.debug("Check service apis") event=None - invocation_log_base=json.loads(json.dumps(invocationlog, cls=JSONEncoder)) + invocation_log_base=json.loads(json.dumps(invocationlog.to_dict(), cls=JSONEncoder)) for log in invocationlog.logs: result = self.__check_service_apis(log.api_id, log.api_name) @@ -97,7 +97,7 @@ class LoggingInvocationOperations(Resource): event="SERVICE_API_INVOCATION_FAILURE" current_app.logger.info(event) - invocation_log_base['logs']=[log] + invocation_log_base['logs']=[log.to_dict()] invocationLogs=[invocation_log_base] RedisEvent(event,"invocationLogs",invocationLogs).send_event() diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index a420fb8..060beca 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -65,7 +65,7 @@ class RegisterOperations: try: url = f"https://capifcore/helper/deleteEntities/{uuid}" - requests.delete(url, cert=("register_service/certs/superadmin.crt", "register_service/certs/superadmin.key"), verify="register_service/certs/ca_root.crt") + requests.delete(url, cert=("certs/superadmin.crt", "certs/superadmin.key"), verify="certs/ca_root.crt") mycol.delete_one({"uuid": uuid}) -- GitLab From 07c8d687ae186c9e5736787fc1c57032c6ed43d7 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Thu, 13 Jun 2024 12:31:38 +0200 Subject: [PATCH 257/310] fix --- services/register/config.yaml | 12 ++++++++---- .../register_service/core/register_operations.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/services/register/config.yaml b/services/register/config.yaml index f63df9f..182818b 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -12,11 +12,15 @@ ca_factory: { "port": "8200", "token": "dev-only-token" } +ccf: { + "url": "capifcore", + "helper_remove_user": "/helper/deleteEntities/" +} register: { - register_uuid: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', - refresh_expiration: 30, #days - token_expiration: 10, #mins - admin_users: {admin_user: "admin", + "register_uuid": '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + "refresh_expiration": 30, #days + "token_expiration": 10, #mins + "admin_users": {admin_user: "admin", admin_pass: "password123"} } \ No newline at end of file diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index b76d092..5082d51 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -1,4 +1,4 @@ -from flask import Flask, jsonify, request, Response +from flask import Flask, jsonify, request, Response, current_app from flask_jwt_extended import create_access_token from ..db.db import MongoDatabse from datetime import datetime @@ -63,7 +63,7 @@ class RegisterOperations: try: - url = f"https://capifcore/helper/deleteEntities/{uuid}" + url = f"https://{self.config["ccf"]["url"]}{self.config["ccf"]["helper_remove_user"]}{uuid}" requests.delete(url, cert=("register_service/certs/superadmin.crt", "register_service/certs/superadmin.key"), verify="register_service/certs/ca_root.crt") mycol.delete_one({"uuid": uuid}) -- GitLab From 552781bf1e9c2b1900c977d3980c55b0bcc9873b Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Thu, 13 Jun 2024 13:41:19 +0300 Subject: [PATCH 258/310] Fix bugs in Events API. All tests succeed --- .../capif_events/util.py | 19 +++++++------------ .../core/invocationlogs.py | 2 +- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/services/TS29222_CAPIF_Events_API/capif_events/util.py b/services/TS29222_CAPIF_Events_API/capif_events/util.py index 5a99e67..f067fde 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/util.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/util.py @@ -10,36 +10,31 @@ def clean_empty(d): return { k: v for k, v in ((k, clean_empty(v)) for k, v in d.items()) - if v + if v is not None or v == 0 } if isinstance(d, list): - return [v for v in map(clean_empty, d) if v] + return [v for v in map(clean_empty, d) if v is not None or v == 0] return d def dict_to_camel_case(my_dict): - - result = {} - for attr, value in my_dict.items(): - - my_key = ''.join(word.title() for word in attr.split('_')) - my_key= ''.join([my_key[0].lower(), my_key[1:]]) - + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key= ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr if isinstance(value, list): result[my_key] = list(map( lambda x: dict_to_camel_case(x) if isinstance(x, dict) else x, value )) - elif hasattr(value, "to_dict"): result[my_key] = dict_to_camel_case(value) - elif isinstance(value, dict): value = dict_to_camel_case(value) result[my_key] = value else: result[my_key] = value - return result diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py index 185c056..06e597c 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py @@ -99,7 +99,7 @@ class LoggingInvocationOperations(Resource): current_app.logger.info(event) invocation_log_base['logs']=[log.to_dict()] invocationLogs=[invocation_log_base] - RedisEvent(event,"invocationLogs",invocationLogs).send_event() + RedisEvent(event,"invocation_logs",invocationLogs).send_event() current_app.logger.debug("After log check") -- GitLab From baa05607efe3ced382cc266773b84ed0396121e8 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 13 Jun 2024 17:48:36 +0200 Subject: [PATCH 259/310] refactoring ocf helm --- helm/capif/Chart.yaml | 38 +- .../templates/tests/test-connection.yaml | 15 - .../.helmignore | 0 .../Chart.yaml | 2 +- .../charts/mock-server/templates/NOTES.txt | 22 + .../charts/mock-server/templates/_helpers.tpl | 62 ++ .../mock-server/templates/deployment.yaml | 68 ++ .../templates/hpa.yaml | 6 +- .../charts/mock-server/templates/ingress.yaml | 61 ++ .../charts/mock-server/templates/service.yaml | 15 + .../mock-server/templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 6 +- helm/capif/charts/mock-server/values.yaml | 108 ++ .../{helper => mongo-express}/.helmignore | 0 helm/capif/charts/mongo-express/Chart.yaml | 24 + .../charts/mongo-express/templates/NOTES.txt | 22 + .../mongo-express/templates/_helpers.tpl | 62 ++ .../mongo-express/templates/deployment.yaml | 73 ++ .../charts/mongo-express/templates/hpa.yaml | 32 + .../templates/ingress.yaml | 4 +- .../mongo-express/templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + helm/capif/charts/mongo-express/values.yaml | 112 ++ .../charts/mongo-register-express/.helmignore | 23 + .../charts/mongo-register-express/Chart.yaml | 24 + .../templates/NOTES.txt | 22 + .../templates/_helpers.tpl | 62 ++ .../templates/deployment.yaml | 73 ++ .../mongo-register-express/templates/hpa.yaml | 32 + .../templates/ingress.yaml | 61 ++ .../templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + .../charts/mongo-register-express/values.yaml | 113 ++ helm/capif/charts/mongo-register/.helmignore | 23 + helm/capif/charts/mongo-register/Chart.yaml | 24 + .../charts/mongo-register/templates/NOTES.txt | 22 + .../mongo-register/templates/_helpers.tpl | 62 ++ .../mongo-register/templates/deployment.yaml | 73 ++ .../charts/mongo-register/templates/hpa.yaml | 32 + .../mongo-register/templates/ingress.yaml | 61 ++ .../charts/mongo-register/templates/pvc.yaml | 13 + .../mongo-register/templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + helm/capif/charts/mongo-register/values.yaml | 113 ++ helm/capif/charts/mongo/.helmignore | 23 + .../capif/charts/{helper => mongo}/Chart.yaml | 2 +- .../{helper => mongo}/templates/NOTES.txt | 8 +- .../{helper => mongo}/templates/_helpers.tpl | 20 +- .../charts/mongo/templates/deployment.yaml | 83 ++ .../{helper => mongo}/templates/hpa.yaml | 6 +- .../{helper => mongo}/templates/ingress.yaml | 4 +- helm/capif/charts/mongo/templates/pvc.yaml | 13 + .../capif/charts/mongo/templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 4 +- .../templates/tests/test-connection.yaml | 15 + helm/capif/charts/mongo/values.yaml | 116 +++ helm/capif/charts/nginx/.helmignore | 23 + helm/capif/charts/nginx/Chart.yaml | 24 + helm/capif/charts/nginx/templates/NOTES.txt | 22 + .../capif/charts/nginx/templates/_helpers.tpl | 62 ++ .../charts/nginx/templates/deployment.yaml | 76 ++ helm/capif/charts/nginx/templates/hpa.yaml | 32 + .../charts/nginx/templates/ingress-route.yaml | 25 + .../capif/charts/nginx/templates/ingress.yaml | 59 ++ .../capif/charts/nginx/templates/service.yaml | 16 + .../nginx/templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 17 + helm/capif/charts/nginx/values.yaml | 122 +++ .../ocf-access-control-policy/.helmignore | 23 + .../ocf-access-control-policy/Chart.yaml | 24 + .../templates/NOTES.txt | 22 + .../templates/_helpers.tpl | 62 ++ .../templates/deployment.yaml | 10 +- .../templates/hpa.yaml | 32 + .../templates/ingress.yaml | 61 ++ .../templates/service.yaml | 4 +- .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + .../values.yaml | 4 +- .../ocf-api-invocation-logs/.helmignore | 23 + .../charts/ocf-api-invocation-logs/Chart.yaml | 24 + .../templates/NOTES.txt | 22 + .../templates/_helpers.tpl | 62 ++ .../templates/configmap.yaml} | 4 +- .../templates/deployment.yaml | 78 ++ .../templates/hpa.yaml | 32 + .../templates/ingress.yaml | 61 ++ .../templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + .../ocf-api-invocation-logs/values.yaml | 119 +++ .../ocf-api-invoker-management/.helmignore | 23 + .../ocf-api-invoker-management/Chart.yaml | 24 + .../templates/NOTES.txt | 22 + .../templates/_helpers.tpl | 62 ++ .../templates/configmap.yaml} | 14 +- .../templates/deployment.yaml | 76 ++ .../templates/hpa.yaml | 32 + .../templates/ingress.yaml | 61 ++ .../templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + .../ocf-api-invoker-management/values.yaml | 119 +++ .../ocf-api-provider-management/.helmignore | 23 + .../ocf-api-provider-management/Chart.yaml | 24 + .../templates/NOTES.txt | 22 + .../templates/_helpers.tpl | 62 ++ .../templates/configmap.yaml} | 14 +- .../templates/deployment.yaml | 76 ++ .../templates/hpa.yaml | 32 + .../templates/ingress.yaml | 61 ++ .../templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + .../ocf-api-provider-management/values.yaml | 119 +++ .../charts/ocf-auditing-api-logs/.helmignore | 23 + .../charts/ocf-auditing-api-logs/Chart.yaml | 24 + .../ocf-auditing-api-logs/configmap.yaml} | 4 +- .../templates/NOTES.txt | 8 +- .../templates/_helpers.tpl | 20 +- .../templates/configmap.yaml | 27 + .../templates/deployment.yaml | 70 ++ .../ocf-auditing-api-logs/templates/hpa.yaml | 32 + .../templates/ingress.yaml | 61 ++ .../templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 4 +- .../templates/tests/test-connection.yaml | 15 + .../charts/ocf-auditing-api-logs/values.yaml | 115 +++ .../ocf-discover-service-api/.helmignore | 23 + .../ocf-discover-service-api/Chart.yaml | 24 + .../templates/NOTES.txt | 22 + .../templates/_helpers.tpl | 62 ++ .../templates/configmap.yaml} | 4 +- .../templates/deployment.yaml | 70 ++ .../templates/hpa.yaml | 32 + .../templates/ingress.yaml | 61 ++ .../templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + .../ocf-discover-service-api/values.yaml | 116 +++ helm/capif/charts/ocf-events/.helmignore | 23 + helm/capif/charts/ocf-events/Chart.yaml | 24 + .../charts/ocf-events/templates/NOTES.txt | 22 + .../charts/ocf-events/templates/_helpers.tpl | 62 ++ .../ocf-events/templates/configmap.yaml} | 6 +- .../ocf-events/templates/deployment.yaml | 70 ++ .../charts/ocf-events/templates/hpa.yaml | 32 + .../charts/ocf-events/templates/ingress.yaml | 61 ++ .../charts/ocf-events/templates/service.yaml | 15 + .../ocf-events/templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + helm/capif/charts/ocf-events/values.yaml | 115 +++ helm/capif/charts/ocf-helper/.helmignore | 23 + helm/capif/charts/ocf-helper/Chart.yaml | 24 + .../charts/ocf-helper/templates/NOTES.txt | 22 + .../charts/ocf-helper/templates/_helpers.tpl | 62 ++ .../templates/deployment.yaml | 10 +- .../charts/ocf-helper/templates/hpa.yaml | 32 + .../charts/ocf-helper/templates/ingress.yaml | 61 ++ .../templates/ocf-helper-configmap.yaml | 0 .../templates/service.yaml | 4 +- .../ocf-helper/templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + .../charts/{helper => ocf-helper}/values.yaml | 6 +- .../ocf-publish-service-api/.helmignore | 23 + .../charts/ocf-publish-service-api/Chart.yaml | 24 + .../templates/NOTES.txt | 22 + .../templates/_helpers.tpl | 62 ++ .../templates/configmap.yaml} | 4 +- .../templates/deployment.yaml | 70 ++ .../templates/hpa.yaml | 32 + .../templates/ingress.yaml | 61 ++ .../templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + .../ocf-publish-service-api/values.yaml | 115 +++ helm/capif/charts/ocf-register/.helmignore | 23 + helm/capif/charts/ocf-register/Chart.yaml | 24 + .../charts/ocf-register/templates/NOTES.txt | 22 + .../ocf-register/templates/_helpers.tpl | 62 ++ .../ocf-register/templates/configmap.yaml} | 12 +- .../ocf-register/templates/deployment.yaml | 74 ++ .../charts/ocf-register/templates/hpa.yaml | 32 + .../ocf-register/templates/ingress.yaml | 60 ++ .../ocf-register/templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + helm/capif/charts/ocf-register/values.yaml | 118 +++ .../capif/charts/ocf-routing-info/.helmignore | 23 + helm/capif/charts/ocf-routing-info/Chart.yaml | 24 + .../ocf-routing-info/templates/NOTES.txt | 22 + .../ocf-routing-info/templates/_helpers.tpl | 62 ++ .../templates/deployment.yaml | 69 ++ .../ocf-routing-info/templates/hpa.yaml | 32 + .../ocf-routing-info/templates/ingress.yaml | 61 ++ .../ocf-routing-info/templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + .../capif/charts/ocf-routing-info/values.yaml | 111 ++ helm/capif/charts/ocf-security/.helmignore | 23 + helm/capif/charts/ocf-security/Chart.yaml | 24 + .../charts/ocf-security/templates/NOTES.txt | 22 + .../ocf-security/templates/_helpers.tpl | 62 ++ .../ocf-security/templates/configmap.yaml} | 4 +- .../ocf-security/templates/deployment.yaml | 78 ++ .../charts/ocf-security/templates/hpa.yaml | 32 + .../ocf-security/templates/ingress.yaml | 61 ++ .../ocf-security/templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + helm/capif/charts/ocf-security/values.yaml | 118 +++ helm/capif/charts/redis/.helmignore | 23 + helm/capif/charts/redis/Chart.yaml | 24 + helm/capif/charts/redis/templates/NOTES.txt | 22 + .../capif/charts/redis/templates/_helpers.tpl | 62 ++ .../charts/redis/templates/deployment.yaml | 69 ++ helm/capif/charts/redis/templates/hpa.yaml | 32 + .../capif/charts/redis/templates/ingress.yaml | 61 ++ .../capif/charts/redis/templates/service.yaml | 15 + .../redis/templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + helm/capif/charts/redis/values.yaml | 111 ++ helm/capif/templates/api-invocation-logs.yaml | 17 - .../templates/api-invoker-management.yaml | 17 - .../templates/api-provider-management.yaml | 17 - helm/capif/templates/capif-events.yaml | 17 - helm/capif/templates/capif-routing-info.yaml | 17 - helm/capif/templates/capif-security.yaml | 17 - helm/capif/templates/deployment.yaml | 973 ------------------ helm/capif/templates/logs.yaml | 17 - helm/capif/templates/mongo-express.yaml | 17 - helm/capif/templates/mongo-pvc.yaml | 17 - .../templates/mongo-register-express.yaml | 17 - helm/capif/templates/mongo-register-pvc.yaml | 17 - helm/capif/templates/mongo-register.yaml | 17 - helm/capif/templates/mongo.yaml | 17 - helm/capif/templates/nginx-ingress-route.yaml | 17 - .../templates/nginx-ssl-ingress-route.yaml | 18 - helm/capif/templates/nginx-ssl-route.yaml | 22 - helm/capif/templates/nginx-ssl.yaml | 32 - helm/capif/templates/nginx.yaml | 48 - helm/capif/templates/published-apis.yaml | 17 - helm/capif/templates/redis.yaml | 17 - helm/capif/templates/register.yaml | 19 - helm/capif/templates/service-apis.yaml | 17 - helm/capif/values.yaml | 509 --------- 249 files changed, 7972 insertions(+), 2027 deletions(-) delete mode 100644 helm/capif/charts/access-control-policy/templates/tests/test-connection.yaml rename helm/capif/charts/{access-control-policy => mock-server}/.helmignore (100%) rename helm/capif/charts/{access-control-policy => mock-server}/Chart.yaml (97%) create mode 100644 helm/capif/charts/mock-server/templates/NOTES.txt create mode 100644 helm/capif/charts/mock-server/templates/_helpers.tpl create mode 100644 helm/capif/charts/mock-server/templates/deployment.yaml rename helm/capif/charts/{access-control-policy => mock-server}/templates/hpa.yaml (82%) create mode 100644 helm/capif/charts/mock-server/templates/ingress.yaml create mode 100644 helm/capif/charts/mock-server/templates/service.yaml create mode 100644 helm/capif/charts/mock-server/templates/serviceaccount.yaml rename helm/capif/charts/{helper => mock-server}/templates/tests/test-connection.yaml (50%) create mode 100644 helm/capif/charts/mock-server/values.yaml rename helm/capif/charts/{helper => mongo-express}/.helmignore (100%) create mode 100644 helm/capif/charts/mongo-express/Chart.yaml create mode 100644 helm/capif/charts/mongo-express/templates/NOTES.txt create mode 100644 helm/capif/charts/mongo-express/templates/_helpers.tpl create mode 100644 helm/capif/charts/mongo-express/templates/deployment.yaml create mode 100644 helm/capif/charts/mongo-express/templates/hpa.yaml rename helm/capif/charts/{access-control-policy => mongo-express}/templates/ingress.yaml (93%) create mode 100644 helm/capif/charts/mongo-express/templates/service.yaml create mode 100644 helm/capif/charts/mongo-express/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/mongo-express/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/mongo-express/values.yaml create mode 100644 helm/capif/charts/mongo-register-express/.helmignore create mode 100644 helm/capif/charts/mongo-register-express/Chart.yaml create mode 100644 helm/capif/charts/mongo-register-express/templates/NOTES.txt create mode 100644 helm/capif/charts/mongo-register-express/templates/_helpers.tpl create mode 100644 helm/capif/charts/mongo-register-express/templates/deployment.yaml create mode 100644 helm/capif/charts/mongo-register-express/templates/hpa.yaml create mode 100644 helm/capif/charts/mongo-register-express/templates/ingress.yaml create mode 100644 helm/capif/charts/mongo-register-express/templates/service.yaml create mode 100644 helm/capif/charts/mongo-register-express/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/mongo-register-express/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/mongo-register-express/values.yaml create mode 100644 helm/capif/charts/mongo-register/.helmignore create mode 100644 helm/capif/charts/mongo-register/Chart.yaml create mode 100644 helm/capif/charts/mongo-register/templates/NOTES.txt create mode 100644 helm/capif/charts/mongo-register/templates/_helpers.tpl create mode 100644 helm/capif/charts/mongo-register/templates/deployment.yaml create mode 100644 helm/capif/charts/mongo-register/templates/hpa.yaml create mode 100644 helm/capif/charts/mongo-register/templates/ingress.yaml create mode 100644 helm/capif/charts/mongo-register/templates/pvc.yaml create mode 100644 helm/capif/charts/mongo-register/templates/service.yaml create mode 100644 helm/capif/charts/mongo-register/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/mongo-register/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/mongo-register/values.yaml create mode 100644 helm/capif/charts/mongo/.helmignore rename helm/capif/charts/{helper => mongo}/Chart.yaml (98%) rename helm/capif/charts/{helper => mongo}/templates/NOTES.txt (77%) rename helm/capif/charts/{helper => mongo}/templates/_helpers.tpl (76%) create mode 100644 helm/capif/charts/mongo/templates/deployment.yaml rename helm/capif/charts/{helper => mongo}/templates/hpa.yaml (86%) rename helm/capif/charts/{helper => mongo}/templates/ingress.yaml (95%) create mode 100644 helm/capif/charts/mongo/templates/pvc.yaml create mode 100644 helm/capif/charts/mongo/templates/service.yaml rename helm/capif/charts/{helper => mongo}/templates/serviceaccount.yaml (73%) create mode 100644 helm/capif/charts/mongo/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/mongo/values.yaml create mode 100644 helm/capif/charts/nginx/.helmignore create mode 100644 helm/capif/charts/nginx/Chart.yaml create mode 100644 helm/capif/charts/nginx/templates/NOTES.txt create mode 100644 helm/capif/charts/nginx/templates/_helpers.tpl create mode 100644 helm/capif/charts/nginx/templates/deployment.yaml create mode 100644 helm/capif/charts/nginx/templates/hpa.yaml create mode 100644 helm/capif/charts/nginx/templates/ingress-route.yaml create mode 100644 helm/capif/charts/nginx/templates/ingress.yaml create mode 100644 helm/capif/charts/nginx/templates/service.yaml create mode 100644 helm/capif/charts/nginx/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/nginx/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/nginx/values.yaml create mode 100644 helm/capif/charts/ocf-access-control-policy/.helmignore create mode 100644 helm/capif/charts/ocf-access-control-policy/Chart.yaml create mode 100644 helm/capif/charts/ocf-access-control-policy/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-access-control-policy/templates/_helpers.tpl rename helm/capif/charts/{access-control-policy => ocf-access-control-policy}/templates/deployment.yaml (84%) create mode 100644 helm/capif/charts/ocf-access-control-policy/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-access-control-policy/templates/ingress.yaml rename helm/capif/charts/{access-control-policy => ocf-access-control-policy}/templates/service.yaml (62%) create mode 100644 helm/capif/charts/ocf-access-control-policy/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-access-control-policy/templates/tests/test-connection.yaml rename helm/capif/charts/{access-control-policy => ocf-access-control-policy}/values.yaml (96%) create mode 100644 helm/capif/charts/ocf-api-invocation-logs/.helmignore create mode 100644 helm/capif/charts/ocf-api-invocation-logs/Chart.yaml create mode 100644 helm/capif/charts/ocf-api-invocation-logs/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-api-invocation-logs/templates/_helpers.tpl rename helm/capif/{templates/capif-invocation-configmap.yaml => charts/ocf-api-invocation-logs/templates/configmap.yaml} (83%) create mode 100644 helm/capif/charts/ocf-api-invocation-logs/templates/deployment.yaml create mode 100644 helm/capif/charts/ocf-api-invocation-logs/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-api-invocation-logs/templates/ingress.yaml create mode 100644 helm/capif/charts/ocf-api-invocation-logs/templates/service.yaml create mode 100644 helm/capif/charts/ocf-api-invocation-logs/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-api-invocation-logs/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/ocf-api-invocation-logs/values.yaml create mode 100644 helm/capif/charts/ocf-api-invoker-management/.helmignore create mode 100644 helm/capif/charts/ocf-api-invoker-management/Chart.yaml create mode 100644 helm/capif/charts/ocf-api-invoker-management/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-api-invoker-management/templates/_helpers.tpl rename helm/capif/{templates/capif-invoker-configmap.yaml => charts/ocf-api-invoker-management/templates/configmap.yaml} (61%) create mode 100644 helm/capif/charts/ocf-api-invoker-management/templates/deployment.yaml create mode 100644 helm/capif/charts/ocf-api-invoker-management/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-api-invoker-management/templates/ingress.yaml create mode 100644 helm/capif/charts/ocf-api-invoker-management/templates/service.yaml create mode 100644 helm/capif/charts/ocf-api-invoker-management/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-api-invoker-management/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/ocf-api-invoker-management/values.yaml create mode 100644 helm/capif/charts/ocf-api-provider-management/.helmignore create mode 100644 helm/capif/charts/ocf-api-provider-management/Chart.yaml create mode 100644 helm/capif/charts/ocf-api-provider-management/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-api-provider-management/templates/_helpers.tpl rename helm/capif/{templates/capif-provider-configmap.yaml => charts/ocf-api-provider-management/templates/configmap.yaml} (60%) create mode 100644 helm/capif/charts/ocf-api-provider-management/templates/deployment.yaml create mode 100644 helm/capif/charts/ocf-api-provider-management/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-api-provider-management/templates/ingress.yaml create mode 100644 helm/capif/charts/ocf-api-provider-management/templates/service.yaml create mode 100644 helm/capif/charts/ocf-api-provider-management/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-api-provider-management/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/ocf-api-provider-management/values.yaml create mode 100644 helm/capif/charts/ocf-auditing-api-logs/.helmignore create mode 100644 helm/capif/charts/ocf-auditing-api-logs/Chart.yaml rename helm/capif/{templates/capif-logs-configmap.yaml => charts/ocf-auditing-api-logs/configmap.yaml} (80%) rename helm/capif/charts/{access-control-policy => ocf-auditing-api-logs}/templates/NOTES.txt (81%) rename helm/capif/charts/{access-control-policy => ocf-auditing-api-logs}/templates/_helpers.tpl (71%) create mode 100644 helm/capif/charts/ocf-auditing-api-logs/templates/configmap.yaml create mode 100644 helm/capif/charts/ocf-auditing-api-logs/templates/deployment.yaml create mode 100644 helm/capif/charts/ocf-auditing-api-logs/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-auditing-api-logs/templates/ingress.yaml create mode 100644 helm/capif/charts/ocf-auditing-api-logs/templates/service.yaml rename helm/capif/charts/{access-control-policy => ocf-auditing-api-logs}/templates/serviceaccount.yaml (69%) create mode 100644 helm/capif/charts/ocf-auditing-api-logs/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/ocf-auditing-api-logs/values.yaml create mode 100644 helm/capif/charts/ocf-discover-service-api/.helmignore create mode 100644 helm/capif/charts/ocf-discover-service-api/Chart.yaml create mode 100644 helm/capif/charts/ocf-discover-service-api/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-discover-service-api/templates/_helpers.tpl rename helm/capif/{templates/capif-service-configmap.yaml => charts/ocf-discover-service-api/templates/configmap.yaml} (81%) create mode 100644 helm/capif/charts/ocf-discover-service-api/templates/deployment.yaml create mode 100644 helm/capif/charts/ocf-discover-service-api/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-discover-service-api/templates/ingress.yaml create mode 100644 helm/capif/charts/ocf-discover-service-api/templates/service.yaml create mode 100644 helm/capif/charts/ocf-discover-service-api/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-discover-service-api/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/ocf-discover-service-api/values.yaml create mode 100644 helm/capif/charts/ocf-events/.helmignore create mode 100644 helm/capif/charts/ocf-events/Chart.yaml create mode 100644 helm/capif/charts/ocf-events/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-events/templates/_helpers.tpl rename helm/capif/{templates/capif-events-configmap.yaml => charts/ocf-events/templates/configmap.yaml} (82%) create mode 100644 helm/capif/charts/ocf-events/templates/deployment.yaml create mode 100644 helm/capif/charts/ocf-events/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-events/templates/ingress.yaml create mode 100644 helm/capif/charts/ocf-events/templates/service.yaml create mode 100644 helm/capif/charts/ocf-events/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-events/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/ocf-events/values.yaml create mode 100644 helm/capif/charts/ocf-helper/.helmignore create mode 100644 helm/capif/charts/ocf-helper/Chart.yaml create mode 100644 helm/capif/charts/ocf-helper/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-helper/templates/_helpers.tpl rename helm/capif/charts/{helper => ocf-helper}/templates/deployment.yaml (88%) create mode 100644 helm/capif/charts/ocf-helper/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-helper/templates/ingress.yaml rename helm/capif/charts/{helper => ocf-helper}/templates/ocf-helper-configmap.yaml (100%) rename helm/capif/charts/{helper => ocf-helper}/templates/service.yaml (66%) create mode 100644 helm/capif/charts/ocf-helper/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-helper/templates/tests/test-connection.yaml rename helm/capif/charts/{helper => ocf-helper}/values.yaml (96%) create mode 100644 helm/capif/charts/ocf-publish-service-api/.helmignore create mode 100644 helm/capif/charts/ocf-publish-service-api/Chart.yaml create mode 100644 helm/capif/charts/ocf-publish-service-api/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-publish-service-api/templates/_helpers.tpl rename helm/capif/{templates/capif-published-configmap.yaml => charts/ocf-publish-service-api/templates/configmap.yaml} (81%) create mode 100644 helm/capif/charts/ocf-publish-service-api/templates/deployment.yaml create mode 100644 helm/capif/charts/ocf-publish-service-api/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-publish-service-api/templates/ingress.yaml create mode 100644 helm/capif/charts/ocf-publish-service-api/templates/service.yaml create mode 100644 helm/capif/charts/ocf-publish-service-api/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-publish-service-api/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/ocf-publish-service-api/values.yaml create mode 100644 helm/capif/charts/ocf-register/.helmignore create mode 100644 helm/capif/charts/ocf-register/Chart.yaml create mode 100644 helm/capif/charts/ocf-register/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-register/templates/_helpers.tpl rename helm/capif/{templates/register-configmap.yaml => charts/ocf-register/templates/configmap.yaml} (52%) create mode 100644 helm/capif/charts/ocf-register/templates/deployment.yaml create mode 100644 helm/capif/charts/ocf-register/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-register/templates/ingress.yaml create mode 100644 helm/capif/charts/ocf-register/templates/service.yaml create mode 100644 helm/capif/charts/ocf-register/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-register/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/ocf-register/values.yaml create mode 100644 helm/capif/charts/ocf-routing-info/.helmignore create mode 100644 helm/capif/charts/ocf-routing-info/Chart.yaml create mode 100644 helm/capif/charts/ocf-routing-info/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-routing-info/templates/_helpers.tpl create mode 100644 helm/capif/charts/ocf-routing-info/templates/deployment.yaml create mode 100644 helm/capif/charts/ocf-routing-info/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-routing-info/templates/ingress.yaml create mode 100644 helm/capif/charts/ocf-routing-info/templates/service.yaml create mode 100644 helm/capif/charts/ocf-routing-info/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-routing-info/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/ocf-routing-info/values.yaml create mode 100644 helm/capif/charts/ocf-security/.helmignore create mode 100644 helm/capif/charts/ocf-security/Chart.yaml create mode 100644 helm/capif/charts/ocf-security/templates/NOTES.txt create mode 100644 helm/capif/charts/ocf-security/templates/_helpers.tpl rename helm/capif/{templates/capif-security-configmap.yaml => charts/ocf-security/templates/configmap.yaml} (82%) create mode 100644 helm/capif/charts/ocf-security/templates/deployment.yaml create mode 100644 helm/capif/charts/ocf-security/templates/hpa.yaml create mode 100644 helm/capif/charts/ocf-security/templates/ingress.yaml create mode 100644 helm/capif/charts/ocf-security/templates/service.yaml create mode 100644 helm/capif/charts/ocf-security/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/ocf-security/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/ocf-security/values.yaml create mode 100644 helm/capif/charts/redis/.helmignore create mode 100644 helm/capif/charts/redis/Chart.yaml create mode 100644 helm/capif/charts/redis/templates/NOTES.txt create mode 100644 helm/capif/charts/redis/templates/_helpers.tpl create mode 100644 helm/capif/charts/redis/templates/deployment.yaml create mode 100644 helm/capif/charts/redis/templates/hpa.yaml create mode 100644 helm/capif/charts/redis/templates/ingress.yaml create mode 100644 helm/capif/charts/redis/templates/service.yaml create mode 100644 helm/capif/charts/redis/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/redis/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/redis/values.yaml delete mode 100644 helm/capif/templates/api-invocation-logs.yaml delete mode 100644 helm/capif/templates/api-invoker-management.yaml delete mode 100644 helm/capif/templates/api-provider-management.yaml delete mode 100644 helm/capif/templates/capif-events.yaml delete mode 100644 helm/capif/templates/capif-routing-info.yaml delete mode 100644 helm/capif/templates/capif-security.yaml delete mode 100644 helm/capif/templates/logs.yaml delete mode 100644 helm/capif/templates/mongo-express.yaml delete mode 100644 helm/capif/templates/mongo-pvc.yaml delete mode 100644 helm/capif/templates/mongo-register-express.yaml delete mode 100644 helm/capif/templates/mongo-register-pvc.yaml delete mode 100644 helm/capif/templates/mongo-register.yaml delete mode 100644 helm/capif/templates/mongo.yaml delete mode 100644 helm/capif/templates/nginx-ingress-route.yaml delete mode 100644 helm/capif/templates/nginx-ssl-ingress-route.yaml delete mode 100644 helm/capif/templates/nginx-ssl-route.yaml delete mode 100644 helm/capif/templates/nginx-ssl.yaml delete mode 100644 helm/capif/templates/nginx.yaml delete mode 100644 helm/capif/templates/published-apis.yaml delete mode 100644 helm/capif/templates/redis.yaml delete mode 100644 helm/capif/templates/register.yaml delete mode 100644 helm/capif/templates/service-apis.yaml diff --git a/helm/capif/Chart.yaml b/helm/capif/Chart.yaml index b76f468..3cdcb3e 100644 --- a/helm/capif/Chart.yaml +++ b/helm/capif/Chart.yaml @@ -20,9 +20,43 @@ version: v3.1.6 # It is recommended to use it with quotes. appVersion: "v3.1.6" dependencies: - - name: access-control-policy + - name: ocf-access-control-policy version: "*" - - name: helper + - name: ocf-api-invoker-management + version: "*" + - name: ocf-api-provider-management + version: "*" + - name: ocf-api-invocation-logs + version: "*" + - name: ocf-events + version: "*" + - name: ocf-helper + version: "*" + - name: ocf-routing-info + version: "*" + - name: ocf-security + version: "*" + - name: ocf-register + version: "*" + - name: mongo-register + version: "*" + - name: ocf-auditing-api-logs + version: "*" + - name: ocf-publish-service-api + version: "*" + - name: ocf-discover-service-api + version: "*" + - name: mongo + version: "*" + - name: mongo-express + version: "*" + - name: mongo-register-express + version: "*" + - name: nginx + version: "*" + - name: mock-server + version: "*" + - name: redis version: "*" - name: "tempo" condition: tempo.enabled diff --git a/helm/capif/charts/access-control-policy/templates/tests/test-connection.yaml b/helm/capif/charts/access-control-policy/templates/tests/test-connection.yaml deleted file mode 100644 index 0e67abf..0000000 --- a/helm/capif/charts/access-control-policy/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "access-control-policy.fullname" . }}-test-connection" - labels: - {{- include "access-control-policy.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "access-control-policy.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/helm/capif/charts/access-control-policy/.helmignore b/helm/capif/charts/mock-server/.helmignore similarity index 100% rename from helm/capif/charts/access-control-policy/.helmignore rename to helm/capif/charts/mock-server/.helmignore diff --git a/helm/capif/charts/access-control-policy/Chart.yaml b/helm/capif/charts/mock-server/Chart.yaml similarity index 97% rename from helm/capif/charts/access-control-policy/Chart.yaml rename to helm/capif/charts/mock-server/Chart.yaml index b13bbf0..9ca2cda 100644 --- a/helm/capif/charts/access-control-policy/Chart.yaml +++ b/helm/capif/charts/mock-server/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -name: access-control-policy +name: mock-server description: A Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart. diff --git a/helm/capif/charts/mock-server/templates/NOTES.txt b/helm/capif/charts/mock-server/templates/NOTES.txt new file mode 100644 index 0000000..4e3d056 --- /dev/null +++ b/helm/capif/charts/mock-server/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mock-server.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "mock-server.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mock-server.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mock-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/mock-server/templates/_helpers.tpl b/helm/capif/charts/mock-server/templates/_helpers.tpl new file mode 100644 index 0000000..4c9fe56 --- /dev/null +++ b/helm/capif/charts/mock-server/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "mock-server.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "mock-server.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "mock-server.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "mock-server.labels" -}} +helm.sh/chart: {{ include "mock-server.chart" . }} +{{ include "mock-server.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "mock-server.selectorLabels" -}} +app.kubernetes.io/name: {{ include "mock-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "mock-server.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "mock-server.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/mock-server/templates/deployment.yaml b/helm/capif/charts/mock-server/templates/deployment.yaml new file mode 100644 index 0000000..270411b --- /dev/null +++ b/helm/capif/charts/mock-server/templates/deployment.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "mock-server.fullname" . }} + labels: + {{- include "mock-server.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "mock-server.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "mock-server.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "mock-server.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/access-control-policy/templates/hpa.yaml b/helm/capif/charts/mock-server/templates/hpa.yaml similarity index 82% rename from helm/capif/charts/access-control-policy/templates/hpa.yaml rename to helm/capif/charts/mock-server/templates/hpa.yaml index 67eb195..fbdd9bc 100644 --- a/helm/capif/charts/access-control-policy/templates/hpa.yaml +++ b/helm/capif/charts/mock-server/templates/hpa.yaml @@ -2,14 +2,14 @@ apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: - name: {{ include "access-control-policy.fullname" . }} + name: {{ include "mock-server.fullname" . }} labels: - {{- include "access-control-policy.labels" . | nindent 4 }} + {{- include "mock-server.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment - name: {{ include "access-control-policy.fullname" . }} + name: {{ include "mock-server.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: diff --git a/helm/capif/charts/mock-server/templates/ingress.yaml b/helm/capif/charts/mock-server/templates/ingress.yaml new file mode 100644 index 0000000..68406e7 --- /dev/null +++ b/helm/capif/charts/mock-server/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "mock-server.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "mock-server.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: mock-server + port: + number: {{ $svcPort }} + {{- else }} + serviceName: mock-server + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/mock-server/templates/service.yaml b/helm/capif/charts/mock-server/templates/service.yaml new file mode 100644 index 0000000..f160730 --- /dev/null +++ b/helm/capif/charts/mock-server/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: mock-server + labels: + {{- include "mock-server.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "mock-server.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/mock-server/templates/serviceaccount.yaml b/helm/capif/charts/mock-server/templates/serviceaccount.yaml new file mode 100644 index 0000000..004803d --- /dev/null +++ b/helm/capif/charts/mock-server/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "mock-server.serviceAccountName" . }} + labels: + {{- include "mock-server.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/helper/templates/tests/test-connection.yaml b/helm/capif/charts/mock-server/templates/tests/test-connection.yaml similarity index 50% rename from helm/capif/charts/helper/templates/tests/test-connection.yaml rename to helm/capif/charts/mock-server/templates/tests/test-connection.yaml index f3959cc..796d72b 100644 --- a/helm/capif/charts/helper/templates/tests/test-connection.yaml +++ b/helm/capif/charts/mock-server/templates/tests/test-connection.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "helper.fullname" . }}-test-connection" + name: "{{ include "mock-server.fullname" . }}-test-connection" labels: - {{- include "helper.labels" . | nindent 4 }} + {{- include "mock-server.labels" . | nindent 4 }} annotations: "helm.sh/hook": test spec: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "helper.fullname" . }}:{{ .Values.service.port }}'] + args: ['mock-server:{{ .Values.service.port }}'] restartPolicy: Never diff --git a/helm/capif/charts/mock-server/values.yaml b/helm/capif/charts/mock-server/values.yaml new file mode 100644 index 0000000..f005d9f --- /dev/null +++ b/helm/capif/charts/mock-server/values.yaml @@ -0,0 +1,108 @@ +# Default values for mock-server. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: mock-server + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 9090 + +ingress: + enabled: true + className: "nginx" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: mock-server.example.local + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: + tcpSocket: + port: 9090 + initialDelaySeconds: 20 + periodSeconds: 5 +readinessProbe: +# httpGet: +# path: / +# port: http + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/helper/.helmignore b/helm/capif/charts/mongo-express/.helmignore similarity index 100% rename from helm/capif/charts/helper/.helmignore rename to helm/capif/charts/mongo-express/.helmignore diff --git a/helm/capif/charts/mongo-express/Chart.yaml b/helm/capif/charts/mongo-express/Chart.yaml new file mode 100644 index 0000000..7150bbc --- /dev/null +++ b/helm/capif/charts/mongo-express/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: mongo-express +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/mongo-express/templates/NOTES.txt b/helm/capif/charts/mongo-express/templates/NOTES.txt new file mode 100644 index 0000000..b93d9e5 --- /dev/null +++ b/helm/capif/charts/mongo-express/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mongo-express.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "mongo-express.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mongo-express.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mongo-express.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/mongo-express/templates/_helpers.tpl b/helm/capif/charts/mongo-express/templates/_helpers.tpl new file mode 100644 index 0000000..15950b2 --- /dev/null +++ b/helm/capif/charts/mongo-express/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "mongo-express.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "mongo-express.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "mongo-express.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "mongo-express.labels" -}} +helm.sh/chart: {{ include "mongo-express.chart" . }} +{{ include "mongo-express.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "mongo-express.selectorLabels" -}} +app.kubernetes.io/name: {{ include "mongo-express.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "mongo-express.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "mongo-express.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/mongo-express/templates/deployment.yaml b/helm/capif/charts/mongo-express/templates/deployment.yaml new file mode 100644 index 0000000..175a045 --- /dev/null +++ b/helm/capif/charts/mongo-express/templates/deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "mongo-express.fullname" . }} + labels: + {{- include "mongo-express.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "mongo-express.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + labels: + {{- include "mongo-express.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "mongo-express.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + env: + - name: ME_CONFIG_MONGODB_ADMINUSERNAME + value: {{ quote .Values.env.meConfigMongodbAdminusername }} + - name: ME_CONFIG_MONGODB_ADMINPASSWORD + value: {{ quote .Values.env.meConfigMongodbAdminpassword }} + - name: ME_CONFIG_MONGODB_URL + value: {{ quote .Values.env.meConfigMongodbUrl }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/mongo-express/templates/hpa.yaml b/helm/capif/charts/mongo-express/templates/hpa.yaml new file mode 100644 index 0000000..2b7ca92 --- /dev/null +++ b/helm/capif/charts/mongo-express/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "mongo-express.fullname" . }} + labels: + {{- include "mongo-express.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "mongo-express.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/access-control-policy/templates/ingress.yaml b/helm/capif/charts/mongo-express/templates/ingress.yaml similarity index 93% rename from helm/capif/charts/access-control-policy/templates/ingress.yaml rename to helm/capif/charts/mongo-express/templates/ingress.yaml index dcafedb..9a0f710 100644 --- a/helm/capif/charts/access-control-policy/templates/ingress.yaml +++ b/helm/capif/charts/mongo-express/templates/ingress.yaml @@ -1,5 +1,5 @@ {{- if .Values.ingress.enabled -}} -{{- $fullName := include "access-control-policy.fullname" . -}} +{{- $fullName := include "mongo-express.fullname" . -}} {{- $svcPort := .Values.service.port -}} {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} @@ -17,7 +17,7 @@ kind: Ingress metadata: name: {{ $fullName }} labels: - {{- include "access-control-policy.labels" . | nindent 4 }} + {{- include "mongo-express.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} diff --git a/helm/capif/charts/mongo-express/templates/service.yaml b/helm/capif/charts/mongo-express/templates/service.yaml new file mode 100644 index 0000000..888a03f --- /dev/null +++ b/helm/capif/charts/mongo-express/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongo-express + labels: + {{- include "mongo-express.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http + selector: + {{- include "mongo-express.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/mongo-express/templates/serviceaccount.yaml b/helm/capif/charts/mongo-express/templates/serviceaccount.yaml new file mode 100644 index 0000000..4a6a666 --- /dev/null +++ b/helm/capif/charts/mongo-express/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "mongo-express.serviceAccountName" . }} + labels: + {{- include "mongo-express.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/mongo-express/templates/tests/test-connection.yaml b/helm/capif/charts/mongo-express/templates/tests/test-connection.yaml new file mode 100644 index 0000000..666e36f --- /dev/null +++ b/helm/capif/charts/mongo-express/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "mongo-express.fullname" . }}-test-connection" + labels: + {{- include "mongo-express.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['mongo-express:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/mongo-express/values.yaml b/helm/capif/charts/mongo-express/values.yaml new file mode 100644 index 0000000..447ec98 --- /dev/null +++ b/helm/capif/charts/mongo-express/values.yaml @@ -0,0 +1,112 @@ +# Default values for mongo-express. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: mongo-express + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "1.0.0-alpha.4" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + meConfigMongodbAdminusername: root + meConfigMongodbAdminpassword: example + meConfigMongodbUrl: mongodb://root:example@mongo:27017/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8082 + targetPort: 8081 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8081 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/mongo-register-express/.helmignore b/helm/capif/charts/mongo-register-express/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/mongo-register-express/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/mongo-register-express/Chart.yaml b/helm/capif/charts/mongo-register-express/Chart.yaml new file mode 100644 index 0000000..b4e246c --- /dev/null +++ b/helm/capif/charts/mongo-register-express/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: mongo-register-express +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/mongo-register-express/templates/NOTES.txt b/helm/capif/charts/mongo-register-express/templates/NOTES.txt new file mode 100644 index 0000000..60013f0 --- /dev/null +++ b/helm/capif/charts/mongo-register-express/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mongo-register-express.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "mongo-register-express.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mongo-register-express.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mongo-register-express.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/mongo-register-express/templates/_helpers.tpl b/helm/capif/charts/mongo-register-express/templates/_helpers.tpl new file mode 100644 index 0000000..accb35f --- /dev/null +++ b/helm/capif/charts/mongo-register-express/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "mongo-register-express.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "mongo-register-express.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "mongo-register-express.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "mongo-register-express.labels" -}} +helm.sh/chart: {{ include "mongo-register-express.chart" . }} +{{ include "mongo-register-express.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "mongo-register-express.selectorLabels" -}} +app.kubernetes.io/name: {{ include "mongo-register-express.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "mongo-register-express.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "mongo-register-express.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/mongo-register-express/templates/deployment.yaml b/helm/capif/charts/mongo-register-express/templates/deployment.yaml new file mode 100644 index 0000000..3e86c2d --- /dev/null +++ b/helm/capif/charts/mongo-register-express/templates/deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "mongo-register-express.fullname" . }} + labels: + {{- include "mongo-register-express.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "mongo-register-express.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + labels: + {{- include "mongo-register-express.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "mongo-register-express.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + env: + - name: ME_CONFIG_MONGODB_ADMINPASSWORD + value: {{ quote .Values.env.meConfigMongodbAdminpassword }} + - name: ME_CONFIG_MONGODB_ADMINUSERNAME + value: {{ quote .Values.env.meConfigMongodbAdminusername }} + - name: ME_CONFIG_MONGODB_URL + value: {{ quote .Values.env.meConfigMongodbUrl }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/mongo-register-express/templates/hpa.yaml b/helm/capif/charts/mongo-register-express/templates/hpa.yaml new file mode 100644 index 0000000..7f0a835 --- /dev/null +++ b/helm/capif/charts/mongo-register-express/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "mongo-register-express.fullname" . }} + labels: + {{- include "mongo-register-express.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "mongo-register-express.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/mongo-register-express/templates/ingress.yaml b/helm/capif/charts/mongo-register-express/templates/ingress.yaml new file mode 100644 index 0000000..02c99e5 --- /dev/null +++ b/helm/capif/charts/mongo-register-express/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "mongo-register-express.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "mongo-register-express.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/mongo-register-express/templates/service.yaml b/helm/capif/charts/mongo-register-express/templates/service.yaml new file mode 100644 index 0000000..eed599c --- /dev/null +++ b/helm/capif/charts/mongo-register-express/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongo-register-express + labels: + {{- include "mongo-register-express.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http + selector: + {{- include "mongo-register-express.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/mongo-register-express/templates/serviceaccount.yaml b/helm/capif/charts/mongo-register-express/templates/serviceaccount.yaml new file mode 100644 index 0000000..21c6862 --- /dev/null +++ b/helm/capif/charts/mongo-register-express/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "mongo-register-express.serviceAccountName" . }} + labels: + {{- include "mongo-register-express.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/mongo-register-express/templates/tests/test-connection.yaml b/helm/capif/charts/mongo-register-express/templates/tests/test-connection.yaml new file mode 100644 index 0000000..240abe3 --- /dev/null +++ b/helm/capif/charts/mongo-register-express/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "mongo-register-express.fullname" . }}-test-connection" + labels: + {{- include "mongo-register-express.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "mongo-register-express.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/mongo-register-express/values.yaml b/helm/capif/charts/mongo-register-express/values.yaml new file mode 100644 index 0000000..d36bf6c --- /dev/null +++ b/helm/capif/charts/mongo-register-express/values.yaml @@ -0,0 +1,113 @@ +# Default values for mongo-register-express. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: mongo-express + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "1.0.0-alpha.4" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + meConfigMongodbAdminusername: root + meConfigMongodbAdminpassword: example + meConfigMongodbUrl: mongodb://root:example@mongo-register:27017/ + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8082 + targetPort: 8081 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8081 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/mongo-register/.helmignore b/helm/capif/charts/mongo-register/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/mongo-register/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/mongo-register/Chart.yaml b/helm/capif/charts/mongo-register/Chart.yaml new file mode 100644 index 0000000..08a4264 --- /dev/null +++ b/helm/capif/charts/mongo-register/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: mongo-register +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/mongo-register/templates/NOTES.txt b/helm/capif/charts/mongo-register/templates/NOTES.txt new file mode 100644 index 0000000..4cbdf59 --- /dev/null +++ b/helm/capif/charts/mongo-register/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mongo-register.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "mongo-register.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mongo-register.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mongo-register.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/mongo-register/templates/_helpers.tpl b/helm/capif/charts/mongo-register/templates/_helpers.tpl new file mode 100644 index 0000000..8ade944 --- /dev/null +++ b/helm/capif/charts/mongo-register/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "mongo-register.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "mongo-register.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "mongo-register.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "mongo-register.labels" -}} +helm.sh/chart: {{ include "mongo-register.chart" . }} +{{ include "mongo-register.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "mongo-register.selectorLabels" -}} +app.kubernetes.io/name: {{ include "mongo-register.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "mongo-register.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "mongo-register.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/mongo-register/templates/deployment.yaml b/helm/capif/charts/mongo-register/templates/deployment.yaml new file mode 100644 index 0000000..442eee6 --- /dev/null +++ b/helm/capif/charts/mongo-register/templates/deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "mongo-register.fullname" . }} + labels: + {{- include "mongo-register.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "mongo-register.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + labels: + {{- include "mongo-register.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "mongo-register.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: MONGO_INITDB_ROOT_PASSWORD + value: {{ quote .Values.env.mongoInitdbRootPassword }} + - name: MONGO_INITDB_ROOT_USERNAME + value: {{ quote .Values.env.mongoInitdbRootUsername }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/mongo-register/templates/hpa.yaml b/helm/capif/charts/mongo-register/templates/hpa.yaml new file mode 100644 index 0000000..7dd1b59 --- /dev/null +++ b/helm/capif/charts/mongo-register/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "mongo-register.fullname" . }} + labels: + {{- include "mongo-register.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "mongo-register.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/mongo-register/templates/ingress.yaml b/helm/capif/charts/mongo-register/templates/ingress.yaml new file mode 100644 index 0000000..d4897ec --- /dev/null +++ b/helm/capif/charts/mongo-register/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "mongo-register.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "mongo-register.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/mongo-register/templates/pvc.yaml b/helm/capif/charts/mongo-register/templates/pvc.yaml new file mode 100644 index 0000000..13f1733 --- /dev/null +++ b/helm/capif/charts/mongo-register/templates/pvc.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + {{- include "mongo-register.labels" . | nindent 8 }} + name: mongo-register-pvc +spec: + storageClassName: {{ .Values.persistence.storageClass }} + accessModes: + - ReadWriteMany + resources: + requests: + storage: {{ .Values.persistence.storage }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-register/templates/service.yaml b/helm/capif/charts/mongo-register/templates/service.yaml new file mode 100644 index 0000000..46ea110 --- /dev/null +++ b/helm/capif/charts/mongo-register/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongo-register + labels: + {{- include "mongo-register.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "mongo-register.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/mongo-register/templates/serviceaccount.yaml b/helm/capif/charts/mongo-register/templates/serviceaccount.yaml new file mode 100644 index 0000000..56e94e6 --- /dev/null +++ b/helm/capif/charts/mongo-register/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "mongo-register.serviceAccountName" . }} + labels: + {{- include "mongo-register.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/mongo-register/templates/tests/test-connection.yaml b/helm/capif/charts/mongo-register/templates/tests/test-connection.yaml new file mode 100644 index 0000000..4d0e00a --- /dev/null +++ b/helm/capif/charts/mongo-register/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "mongo-register.fullname" . }}-test-connection" + labels: + {{- include "mongo-register.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['mongo-register:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/mongo-register/values.yaml b/helm/capif/charts/mongo-register/values.yaml new file mode 100644 index 0000000..dcb783f --- /dev/null +++ b/helm/capif/charts/mongo-register/values.yaml @@ -0,0 +1,113 @@ +# Default values for mongo-register. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: mongo + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "6.0.2" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + mongoInitdbRootPassword: example + mongoInitdbRootUsername: root + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + runAsUser: 999 + +service: + type: ClusterIP + port: 27017 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 27017 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +persistence: + storage: 8Gi + storageClass: nfs-01 + +# Additional volumes on the output Deployment definition. +volumes: + - name: mongo-register-pvc + persistentVolumeClaim: + claimName: mongo-register-pvc + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: mongo-register-pvc + mountPath: /data/db + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/mongo/.helmignore b/helm/capif/charts/mongo/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/mongo/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/helper/Chart.yaml b/helm/capif/charts/mongo/Chart.yaml similarity index 98% rename from helm/capif/charts/helper/Chart.yaml rename to helm/capif/charts/mongo/Chart.yaml index 4ddfbf3..638bb45 100644 --- a/helm/capif/charts/helper/Chart.yaml +++ b/helm/capif/charts/mongo/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -name: helper +name: mongo description: A Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart. diff --git a/helm/capif/charts/helper/templates/NOTES.txt b/helm/capif/charts/mongo/templates/NOTES.txt similarity index 77% rename from helm/capif/charts/helper/templates/NOTES.txt rename to helm/capif/charts/mongo/templates/NOTES.txt index f8f6f77..1fcc593 100644 --- a/helm/capif/charts/helper/templates/NOTES.txt +++ b/helm/capif/charts/mongo/templates/NOTES.txt @@ -6,16 +6,16 @@ {{- end }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "helper.fullname" . }}) + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mongo.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "helper.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "helper.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "mongo.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mongo.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "helper.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mongo.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT diff --git a/helm/capif/charts/helper/templates/_helpers.tpl b/helm/capif/charts/mongo/templates/_helpers.tpl similarity index 76% rename from helm/capif/charts/helper/templates/_helpers.tpl rename to helm/capif/charts/mongo/templates/_helpers.tpl index f4a197b..cce4e1b 100644 --- a/helm/capif/charts/helper/templates/_helpers.tpl +++ b/helm/capif/charts/mongo/templates/_helpers.tpl @@ -1,7 +1,7 @@ {{/* Expand the name of the chart. */}} -{{- define "helper.name" -}} +{{- define "mongo.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} @@ -10,7 +10,7 @@ Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} -{{- define "helper.fullname" -}} +{{- define "mongo.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} @@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "helper.chart" -}} +{{- define "mongo.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "helper.labels" -}} -helm.sh/chart: {{ include "helper.chart" . }} -{{ include "helper.selectorLabels" . }} +{{- define "mongo.labels" -}} +helm.sh/chart: {{ include "mongo.chart" . }} +{{ include "mongo.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -45,17 +45,17 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Selector labels */}} -{{- define "helper.selectorLabels" -}} -app.kubernetes.io/name: {{ include "helper.name" . }} +{{- define "mongo.selectorLabels" -}} +app.kubernetes.io/name: {{ include "mongo.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} -{{- define "helper.serviceAccountName" -}} +{{- define "mongo.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} -{{- default (include "helper.fullname" .) .Values.serviceAccount.name }} +{{- default (include "mongo.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} diff --git a/helm/capif/charts/mongo/templates/deployment.yaml b/helm/capif/charts/mongo/templates/deployment.yaml new file mode 100644 index 0000000..80cedad --- /dev/null +++ b/helm/capif/charts/mongo/templates/deployment.yaml @@ -0,0 +1,83 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "mongo.fullname" . }} + labels: + {{- include "mongo.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "mongo.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + labels: + {{- include "mongo.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "mongo.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: MONGO_INITDB_ROOT_PASSWORD + value: {{ quote .Values.env.mongoInitdbRootPassword }} + - name: MONGO_INITDB_ROOT_USERNAME + value: {{ quote .Values.env.mongoInitdbRootUsername }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + - name: mongo-helper + image: busybox + command: + - sh + - -c + - while true ; do echo alive ; sleep 10 ; done + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/helper/templates/hpa.yaml b/helm/capif/charts/mongo/templates/hpa.yaml similarity index 86% rename from helm/capif/charts/helper/templates/hpa.yaml rename to helm/capif/charts/mongo/templates/hpa.yaml index 046148d..ae64b40 100644 --- a/helm/capif/charts/helper/templates/hpa.yaml +++ b/helm/capif/charts/mongo/templates/hpa.yaml @@ -2,14 +2,14 @@ apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: - name: {{ include "helper.fullname" . }} + name: {{ include "mongo.fullname" . }} labels: - {{- include "helper.labels" . | nindent 4 }} + {{- include "mongo.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment - name: {{ include "helper.fullname" . }} + name: {{ include "mongo.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: diff --git a/helm/capif/charts/helper/templates/ingress.yaml b/helm/capif/charts/mongo/templates/ingress.yaml similarity index 95% rename from helm/capif/charts/helper/templates/ingress.yaml rename to helm/capif/charts/mongo/templates/ingress.yaml index b3817bf..b173c90 100644 --- a/helm/capif/charts/helper/templates/ingress.yaml +++ b/helm/capif/charts/mongo/templates/ingress.yaml @@ -1,5 +1,5 @@ {{- if .Values.ingress.enabled -}} -{{- $fullName := include "helper.fullname" . -}} +{{- $fullName := include "mongo.fullname" . -}} {{- $svcPort := .Values.service.port -}} {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} @@ -17,7 +17,7 @@ kind: Ingress metadata: name: {{ $fullName }} labels: - {{- include "helper.labels" . | nindent 4 }} + {{- include "mongo.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} diff --git a/helm/capif/charts/mongo/templates/pvc.yaml b/helm/capif/charts/mongo/templates/pvc.yaml new file mode 100644 index 0000000..c0ceafd --- /dev/null +++ b/helm/capif/charts/mongo/templates/pvc.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + {{- include "mongo.labels" . | nindent 8 }} + name: mongo-pvc +spec: + storageClassName: {{ .Values.persistence.storageClass }} + accessModes: + - ReadWriteMany + resources: + requests: + storage: {{ .Values.persistence.storage }} \ No newline at end of file diff --git a/helm/capif/charts/mongo/templates/service.yaml b/helm/capif/charts/mongo/templates/service.yaml new file mode 100644 index 0000000..be5b5f4 --- /dev/null +++ b/helm/capif/charts/mongo/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongo + labels: + {{- include "mongo.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "mongo.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/helper/templates/serviceaccount.yaml b/helm/capif/charts/mongo/templates/serviceaccount.yaml similarity index 73% rename from helm/capif/charts/helper/templates/serviceaccount.yaml rename to helm/capif/charts/mongo/templates/serviceaccount.yaml index e0e6d79..95b6769 100644 --- a/helm/capif/charts/helper/templates/serviceaccount.yaml +++ b/helm/capif/charts/mongo/templates/serviceaccount.yaml @@ -2,9 +2,9 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ include "helper.serviceAccountName" . }} + name: {{ include "mongo.serviceAccountName" . }} labels: - {{- include "helper.labels" . | nindent 4 }} + {{- include "mongo.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} diff --git a/helm/capif/charts/mongo/templates/tests/test-connection.yaml b/helm/capif/charts/mongo/templates/tests/test-connection.yaml new file mode 100644 index 0000000..4cf0b7f --- /dev/null +++ b/helm/capif/charts/mongo/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "mongo.fullname" . }}-test-connection" + labels: + {{- include "mongo.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['mongo:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/mongo/values.yaml b/helm/capif/charts/mongo/values.yaml new file mode 100644 index 0000000..53b8cf5 --- /dev/null +++ b/helm/capif/charts/mongo/values.yaml @@ -0,0 +1,116 @@ +# Default values for mongo. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: mongo + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "6.0.2" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + mongoInitdbRootPassword: example + mongoInitdbRootUsername: root + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + runAsUser: 999 + +service: + type: ClusterIP + port: 27017 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: + tcpSocket: + port: 27017 + initialDelaySeconds: 20 + periodSeconds: 5 + +readinessProbe: + tcpSocket: + port: 27017 +# initialDelaySeconds: 5 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +persistence: + storage: 8Gi + storageClass: nfs-01 + +# Additional volumes on the output Deployment definition. +volumes: + - name: mongo-pvc + persistentVolumeClaim: + claimName: mongo-pvc + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: mongo-pvc + mountPath: /data/db + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/nginx/.helmignore b/helm/capif/charts/nginx/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/nginx/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/nginx/Chart.yaml b/helm/capif/charts/nginx/Chart.yaml new file mode 100644 index 0000000..3464a39 --- /dev/null +++ b/helm/capif/charts/nginx/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: nginx +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/nginx/templates/NOTES.txt b/helm/capif/charts/nginx/templates/NOTES.txt new file mode 100644 index 0000000..918bb64 --- /dev/null +++ b/helm/capif/charts/nginx/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "nginx.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "nginx.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "nginx.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "nginx.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/nginx/templates/_helpers.tpl b/helm/capif/charts/nginx/templates/_helpers.tpl new file mode 100644 index 0000000..ad9f432 --- /dev/null +++ b/helm/capif/charts/nginx/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nginx.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "nginx.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nginx.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nginx.labels" -}} +helm.sh/chart: {{ include "nginx.chart" . }} +{{ include "nginx.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nginx.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nginx.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "nginx.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "nginx.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/nginx/templates/deployment.yaml b/helm/capif/charts/nginx/templates/deployment.yaml new file mode 100644 index 0000000..a5cd26c --- /dev/null +++ b/helm/capif/charts/nginx/templates/deployment.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nginx.fullname" . }} + labels: + {{- include "nginx.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "nginx.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + labels: + {{- include "nginx.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nginx.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + {{- range .Values.services }} + - name: {{ .name }} + containerPort: {{ .port }} + {{- end }} + env: + - name: CAPIF_HOSTNAME + value: {{ quote .Values.env.capifHostname }} + - name: VAULT_HOSTNAME + value: {{ quote .Values.env.vaultHostname }} + - name: VAULT_PORT + value: {{ quote .Values.env.vaultPort }} + - name: VAULT_ACCESS_TOKEN + value: {{ quote .Values.env.vaultAccessToken }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/nginx/templates/hpa.yaml b/helm/capif/charts/nginx/templates/hpa.yaml new file mode 100644 index 0000000..b664f71 --- /dev/null +++ b/helm/capif/charts/nginx/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "nginx.fullname" . }} + labels: + {{- include "nginx.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "nginx.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/nginx/templates/ingress-route.yaml b/helm/capif/charts/nginx/templates/ingress-route.yaml new file mode 100644 index 0000000..5fec096 --- /dev/null +++ b/helm/capif/charts/nginx/templates/ingress-route.yaml @@ -0,0 +1,25 @@ +{{- if eq .Values.ingress.className "IngressRoute" }} +{{- $fullName := include "nginx.fullname" . -}} +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: {{ $fullName }}-route +spec: + entryPoints: [web] + routes: + - kind: Rule + {{- range .Values.ingress.hosts }} + match: Host(`{{ .host | quote }} && Path(`/ca-root`, `/sign-csr`, `/certdata`, `/register`, `/testdata`, `/getauth`, `/test`)`) + services: + - kind: Service + name: nginx + port: 8080 + scheme: http + - kind: Service + name: nginx + port: 443 + tls: + passthrough: true + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/nginx/templates/ingress.yaml b/helm/capif/charts/nginx/templates/ingress.yaml new file mode 100644 index 0000000..82f373e --- /dev/null +++ b/helm/capif/charts/nginx/templates/ingress.yaml @@ -0,0 +1,59 @@ +{{- if .Values.ingress.enabled -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: nginx-ingress + labels: + {{- include "nginx.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: nginx + port: + number: 443 + {{- else }} + serviceName: nginx + servicePort: 443 + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/nginx/templates/service.yaml b/helm/capif/charts/nginx/templates/service.yaml new file mode 100644 index 0000000..6482161 --- /dev/null +++ b/helm/capif/charts/nginx/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + {{- include "nginx.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + {{- range .Values.service.ports }} + - name: {{ .name }} + port: {{ .port }} + targetPort: {{ .targetPort }} + {{- end }} + selector: + {{- include "nginx.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/helm/capif/charts/nginx/templates/serviceaccount.yaml b/helm/capif/charts/nginx/templates/serviceaccount.yaml new file mode 100644 index 0000000..02c08bb --- /dev/null +++ b/helm/capif/charts/nginx/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "nginx.serviceAccountName" . }} + labels: + {{- include "nginx.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/nginx/templates/tests/test-connection.yaml b/helm/capif/charts/nginx/templates/tests/test-connection.yaml new file mode 100644 index 0000000..bdbc141 --- /dev/null +++ b/helm/capif/charts/nginx/templates/tests/test-connection.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "test-connection" + labels: + {{- include "nginx.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + {{- range .Values.service.ports }} + - name: wget-{{ .name }} + image: busybox + command: ['wget'] + args: ['nginx:{{ .port }}'] + {{- end }} + restartPolicy: Never diff --git a/helm/capif/charts/nginx/values.yaml b/helm/capif/charts/nginx/values.yaml new file mode 100644 index 0000000..e3ba001 --- /dev/null +++ b/helm/capif/charts/nginx/values.yaml @@ -0,0 +1,122 @@ +# Default values for nginx. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + capifHostname: my-capif.apps.ocp-epg.hi.inet + vaultHostname: vault-internal.mon.svc.cluster.local + vaultPort: 8200 + vaultAccessToken: dev-only-token + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + ports: + - name: "http" + port: 8080 + targetPort: 8080 + - name: "https" + port: 443 + targetPort: 443 + type: ClusterIP + + +ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + nginx.ingress.kubernetes.io/ssl-passthrough: "true" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + hosts: + - host: nginx-example.local + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 20 + periodSeconds: 5 +readinessProbe: +# httpGet: +# path: / +# port: http + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/ocf-access-control-policy/.helmignore b/helm/capif/charts/ocf-access-control-policy/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-access-control-policy/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-access-control-policy/Chart.yaml b/helm/capif/charts/ocf-access-control-policy/Chart.yaml new file mode 100644 index 0000000..bed9447 --- /dev/null +++ b/helm/capif/charts/ocf-access-control-policy/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-access-control-policy +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-access-control-policy/templates/NOTES.txt b/helm/capif/charts/ocf-access-control-policy/templates/NOTES.txt new file mode 100644 index 0000000..9928abe --- /dev/null +++ b/helm/capif/charts/ocf-access-control-policy/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-access-control-policy.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-access-control-policy.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-access-control-policy.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-access-control-policy.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-access-control-policy/templates/_helpers.tpl b/helm/capif/charts/ocf-access-control-policy/templates/_helpers.tpl new file mode 100644 index 0000000..d446320 --- /dev/null +++ b/helm/capif/charts/ocf-access-control-policy/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-access-control-policy.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-access-control-policy.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-access-control-policy.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-access-control-policy.labels" -}} +helm.sh/chart: {{ include "ocf-access-control-policy.chart" . }} +{{ include "ocf-access-control-policy.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-access-control-policy.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-access-control-policy.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-access-control-policy.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-access-control-policy.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/access-control-policy/templates/deployment.yaml b/helm/capif/charts/ocf-access-control-policy/templates/deployment.yaml similarity index 84% rename from helm/capif/charts/access-control-policy/templates/deployment.yaml rename to helm/capif/charts/ocf-access-control-policy/templates/deployment.yaml index 2e4d15f..3a8000f 100644 --- a/helm/capif/charts/access-control-policy/templates/deployment.yaml +++ b/helm/capif/charts/ocf-access-control-policy/templates/deployment.yaml @@ -1,22 +1,22 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "access-control-policy.fullname" . }} + name: {{ include "ocf-access-control-policy.fullname" . }} labels: - {{- include "access-control-policy.labels" . | nindent 4 }} + {{- include "ocf-access-control-policy.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: - {{- include "access-control-policy.selectorLabels" . | nindent 6 }} + {{- include "ocf-access-control-policy.selectorLabels" . | nindent 6 }} template: metadata: annotations: date: "{{ now | unixEpoch }}" labels: - {{- include "access-control-policy.labels" . | nindent 8 }} + {{- include "ocf-access-control-policy.labels" . | nindent 8 }} {{- with .Values.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} @@ -25,7 +25,7 @@ spec: imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} - serviceAccountName: {{ include "access-control-policy.serviceAccountName" . }} + serviceAccountName: {{ include "ocf-access-control-policy.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: diff --git a/helm/capif/charts/ocf-access-control-policy/templates/hpa.yaml b/helm/capif/charts/ocf-access-control-policy/templates/hpa.yaml new file mode 100644 index 0000000..3e5774e --- /dev/null +++ b/helm/capif/charts/ocf-access-control-policy/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-access-control-policy.fullname" . }} + labels: + {{- include "ocf-access-control-policy.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-access-control-policy.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-access-control-policy/templates/ingress.yaml b/helm/capif/charts/ocf-access-control-policy/templates/ingress.yaml new file mode 100644 index 0000000..7aa1dc7 --- /dev/null +++ b/helm/capif/charts/ocf-access-control-policy/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-access-control-policy.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-access-control-policy.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/access-control-policy/templates/service.yaml b/helm/capif/charts/ocf-access-control-policy/templates/service.yaml similarity index 62% rename from helm/capif/charts/access-control-policy/templates/service.yaml rename to helm/capif/charts/ocf-access-control-policy/templates/service.yaml index c10293a..16616bd 100644 --- a/helm/capif/charts/access-control-policy/templates/service.yaml +++ b/helm/capif/charts/ocf-access-control-policy/templates/service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: name: access-control-policy labels: - {{- include "access-control-policy.labels" . | nindent 4 }} + {{- include "ocf-access-control-policy.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: @@ -12,4 +12,4 @@ spec: protocol: TCP name: http selector: - {{- include "access-control-policy.selectorLabels" . | nindent 4 }} + {{- include "ocf-access-control-policy.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-access-control-policy/templates/serviceaccount.yaml b/helm/capif/charts/ocf-access-control-policy/templates/serviceaccount.yaml new file mode 100644 index 0000000..0f513f2 --- /dev/null +++ b/helm/capif/charts/ocf-access-control-policy/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-access-control-policy.serviceAccountName" . }} + labels: + {{- include "ocf-access-control-policy.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-access-control-policy/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-access-control-policy/templates/tests/test-connection.yaml new file mode 100644 index 0000000..b19566f --- /dev/null +++ b/helm/capif/charts/ocf-access-control-policy/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-access-control-policy.fullname" . }}-test-connection" + labels: + {{- include "ocf-access-control-policy.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['access-control-policy:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/access-control-policy/values.yaml b/helm/capif/charts/ocf-access-control-policy/values.yaml similarity index 96% rename from helm/capif/charts/access-control-policy/values.yaml rename to helm/capif/charts/ocf-access-control-policy/values.yaml index 61aba34..2912e09 100644 --- a/helm/capif/charts/access-control-policy/values.yaml +++ b/helm/capif/charts/ocf-access-control-policy/values.yaml @@ -1,11 +1,11 @@ -# Default values for access-control-policy. +# Default values for ocf-access-control-policy. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 image: - repository: access-control-policy + repository: ocf-access-control-policy pullPolicy: Always # Overrides the image tag whose default is the chart appVersion. tag: "" diff --git a/helm/capif/charts/ocf-api-invocation-logs/.helmignore b/helm/capif/charts/ocf-api-invocation-logs/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-api-invocation-logs/Chart.yaml b/helm/capif/charts/ocf-api-invocation-logs/Chart.yaml new file mode 100644 index 0000000..d735b16 --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-api-invocation-logs +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-api-invocation-logs/templates/NOTES.txt b/helm/capif/charts/ocf-api-invocation-logs/templates/NOTES.txt new file mode 100644 index 0000000..8c7da80 --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-api-invocation-logs.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-api-invocation-logs.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-api-invocation-logs.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-api-invocation-logs.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-api-invocation-logs/templates/_helpers.tpl b/helm/capif/charts/ocf-api-invocation-logs/templates/_helpers.tpl new file mode 100644 index 0000000..f2a83cf --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-api-invocation-logs.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-api-invocation-logs.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-api-invocation-logs.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-api-invocation-logs.labels" -}} +helm.sh/chart: {{ include "ocf-api-invocation-logs.chart" . }} +{{ include "ocf-api-invocation-logs.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-api-invocation-logs.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-api-invocation-logs.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-api-invocation-logs.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-api-invocation-logs.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/templates/capif-invocation-configmap.yaml b/helm/capif/charts/ocf-api-invocation-logs/templates/configmap.yaml similarity index 83% rename from helm/capif/templates/capif-invocation-configmap.yaml rename to helm/capif/charts/ocf-api-invocation-logs/templates/configmap.yaml index 68fc1f1..bd13f9f 100644 --- a/helm/capif/templates/capif-invocation-configmap.yaml +++ b/helm/capif/charts/ocf-api-invocation-logs/templates/configmap.yaml @@ -5,8 +5,8 @@ metadata: data: config.yaml: | mongo: { - 'user': '{{ .Values.mongo.mongo.env.mongoInitdbRootUsername }}', - 'password': '{{ .Values.mongo.mongo.env.mongoInitdbRootPassword }}', + 'user': '{{ .Values.env.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoInitdbRootPassword }}', 'db': 'capif', 'logs_col': 'invocationlogs', 'invoker_col': 'invokerdetails', diff --git a/helm/capif/charts/ocf-api-invocation-logs/templates/deployment.yaml b/helm/capif/charts/ocf-api-invocation-logs/templates/deployment.yaml new file mode 100644 index 0000000..fc3ce11 --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ocf-api-invocation-logs.fullname" . }} + labels: + {{- include "ocf-api-invocation-logs.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ocf-api-invocation-logs.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "ocf-api-invocation-logs.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ocf-api-invocation-logs.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: CAPIF_HOSTNAME + value: {{ quote .Values.env.capifHostname }} + - name: MONITORING + value: {{ quote .Values.env.monitoring }} + - name: VAULT_HOSTNAME + value: {{ quote .Values.env.vaultHostname }} + - name: VAULT_PORT + value: {{ quote .Values.env.vaultPort }} + - name: VAULT_ACCESS_TOKEN + value: {{ quote .Values.env.vaultAccessToken }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/ocf-api-invocation-logs/templates/hpa.yaml b/helm/capif/charts/ocf-api-invocation-logs/templates/hpa.yaml new file mode 100644 index 0000000..71df2ef --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-api-invocation-logs.fullname" . }} + labels: + {{- include "ocf-api-invocation-logs.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-api-invocation-logs.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-api-invocation-logs/templates/ingress.yaml b/helm/capif/charts/ocf-api-invocation-logs/templates/ingress.yaml new file mode 100644 index 0000000..47f0f46 --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-api-invocation-logs.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-api-invocation-logs.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-api-invocation-logs/templates/service.yaml b/helm/capif/charts/ocf-api-invocation-logs/templates/service.yaml new file mode 100644 index 0000000..48614fd --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: api-invocation-logs + labels: + {{- include "ocf-api-invocation-logs.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ocf-api-invocation-logs.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-api-invocation-logs/templates/serviceaccount.yaml b/helm/capif/charts/ocf-api-invocation-logs/templates/serviceaccount.yaml new file mode 100644 index 0000000..c08cfed --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-api-invocation-logs.serviceAccountName" . }} + labels: + {{- include "ocf-api-invocation-logs.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-api-invocation-logs/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-api-invocation-logs/templates/tests/test-connection.yaml new file mode 100644 index 0000000..3c3098b --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-api-invocation-logs.fullname" . }}-test-connection" + labels: + {{- include "ocf-api-invocation-logs.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['api-invocation-logs:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/ocf-api-invocation-logs/values.yaml b/helm/capif/charts/ocf-api-invocation-logs/values.yaml new file mode 100644 index 0000000..4c4431b --- /dev/null +++ b/helm/capif/charts/ocf-api-invocation-logs/values.yaml @@ -0,0 +1,119 @@ +# Default values for ocf-api-invocation-logs. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: ocf-api-invocation-logs-api + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + monitoring: "true" + capifHostname: capif + vaultHostname: vault + vaultPort: 8200 + vaultAccessToken: dev-only-token + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: capif-invocation-config + configMap: + name: capif-invocation-configmap + items: + - key: "config.yaml" + path: "config.yaml" + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: capif-invocation-config + mountPath: /usr/src/app/config.yaml + subPath: config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/ocf-api-invoker-management/.helmignore b/helm/capif/charts/ocf-api-invoker-management/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-api-invoker-management/Chart.yaml b/helm/capif/charts/ocf-api-invoker-management/Chart.yaml new file mode 100644 index 0000000..2a9ef72 --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-api-invoker-management +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-api-invoker-management/templates/NOTES.txt b/helm/capif/charts/ocf-api-invoker-management/templates/NOTES.txt new file mode 100644 index 0000000..cfccd3a --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-api-invoker-management.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-api-invoker-management.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-api-invoker-management.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-api-invoker-management.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-api-invoker-management/templates/_helpers.tpl b/helm/capif/charts/ocf-api-invoker-management/templates/_helpers.tpl new file mode 100644 index 0000000..35e9994 --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-api-invoker-management.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-api-invoker-management.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-api-invoker-management.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-api-invoker-management.labels" -}} +helm.sh/chart: {{ include "ocf-api-invoker-management.chart" . }} +{{ include "ocf-api-invoker-management.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-api-invoker-management.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-api-invoker-management.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-api-invoker-management.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-api-invoker-management.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/templates/capif-invoker-configmap.yaml b/helm/capif/charts/ocf-api-invoker-management/templates/configmap.yaml similarity index 61% rename from helm/capif/templates/capif-invoker-configmap.yaml rename to helm/capif/charts/ocf-api-invoker-management/templates/configmap.yaml index 32bab3c..4d83c98 100644 --- a/helm/capif/templates/capif-invoker-configmap.yaml +++ b/helm/capif/charts/ocf-api-invoker-management/templates/configmap.yaml @@ -5,8 +5,8 @@ metadata: data: config.yaml: | mongo: { - 'user': '{{ .Values.mongo.mongo.env.mongoInitdbRootUsername }}', - 'password': '{{ .Values.mongo.mongo.env.mongoInitdbRootPassword }}', + 'user': '{{ .Values.env.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoInitdbRootPassword }}', 'db': 'capif', 'col': 'invokerdetails', 'capif_users_col': "user", @@ -16,17 +16,17 @@ data: 'port': "27017" } mongo_register: { - 'user': '{{ .Values.mongoRegister.mongo.env.mongoInitdbRootUsername }}', - 'password': '{{ .Values.mongoRegister.mongo.env.mongoInitdbRootPassword }}', + 'user': '{{ .Values.env.mongoRegister.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoRegister.mongoInitdbRootPassword }}', 'db': 'capif_users', 'col': 'user', 'host': 'mongo-register', 'port': '27017' } ca_factory: { - "url": {{ quote .Values.parametersVault.env.vaultHostname }}, - "port": {{ quote .Values.parametersVault.env.vaultPort }}, - "token": {{ quote .Values.parametersVault.env.vaultAccessToken }} + "url": {{ quote .Values.env.vaultHostname }}, + "port": {{ quote .Values.env.vaultPort }}, + "token": {{ quote .Values.env.vaultAccessToken }} } monitoring: { diff --git a/helm/capif/charts/ocf-api-invoker-management/templates/deployment.yaml b/helm/capif/charts/ocf-api-invoker-management/templates/deployment.yaml new file mode 100644 index 0000000..c4fd0c9 --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/templates/deployment.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ocf-api-invoker-management.fullname" . }} + labels: + {{- include "ocf-api-invoker-management.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ocf-api-invoker-management.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "ocf-api-invoker-management.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ocf-api-invoker-management.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: MONITORING + value: {{ quote .Values.env.monitoring }} + - name: VAULT_HOSTNAME + value: {{ quote .Values.env.vaultHostname }} + - name: VAULT_PORT + value: {{ quote .Values.env.vaultPort }} + - name: VAULT_ACCESS_TOKEN + value: {{ quote .Values.env.vaultAccessToken }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/ocf-api-invoker-management/templates/hpa.yaml b/helm/capif/charts/ocf-api-invoker-management/templates/hpa.yaml new file mode 100644 index 0000000..44d58a7 --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-api-invoker-management.fullname" . }} + labels: + {{- include "ocf-api-invoker-management.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-api-invoker-management.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-api-invoker-management/templates/ingress.yaml b/helm/capif/charts/ocf-api-invoker-management/templates/ingress.yaml new file mode 100644 index 0000000..aee54f4 --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-api-invoker-management.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-api-invoker-management.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-api-invoker-management/templates/service.yaml b/helm/capif/charts/ocf-api-invoker-management/templates/service.yaml new file mode 100644 index 0000000..f62fb9b --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: api-invoker-management + labels: + {{- include "ocf-api-invoker-management.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ocf-api-invoker-management.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-api-invoker-management/templates/serviceaccount.yaml b/helm/capif/charts/ocf-api-invoker-management/templates/serviceaccount.yaml new file mode 100644 index 0000000..c328517 --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-api-invoker-management.serviceAccountName" . }} + labels: + {{- include "ocf-api-invoker-management.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-api-invoker-management/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-api-invoker-management/templates/tests/test-connection.yaml new file mode 100644 index 0000000..f194ec6 --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-api-invoker-management.fullname" . }}-test-connection" + labels: + {{- include "ocf-api-invoker-management.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['api-invoker-management:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/ocf-api-invoker-management/values.yaml b/helm/capif/charts/ocf-api-invoker-management/values.yaml new file mode 100644 index 0000000..a296a41 --- /dev/null +++ b/helm/capif/charts/ocf-api-invoker-management/values.yaml @@ -0,0 +1,119 @@ +# Default values for ocf-api-invoker-management. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: ocf-api-invoker-management-api + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + monitoring: "true" + vaultHostname: vault + vaultPort: 8200 + vaultAccessToken: dev-only-token + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + mongoRegister: + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: capif-invoker-config + configMap: + name: capif-invoker-configmap + items: + - key: "config.yaml" + path: "config.yaml" + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: capif-invoker-config + mountPath: /usr/src/app/config.yaml + subPath: config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/ocf-api-provider-management/.helmignore b/helm/capif/charts/ocf-api-provider-management/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-api-provider-management/Chart.yaml b/helm/capif/charts/ocf-api-provider-management/Chart.yaml new file mode 100644 index 0000000..773f014 --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-api-provider-management +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-api-provider-management/templates/NOTES.txt b/helm/capif/charts/ocf-api-provider-management/templates/NOTES.txt new file mode 100644 index 0000000..d65d7c1 --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-api-provider-management.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-api-provider-management.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-api-provider-management.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-api-provider-management.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-api-provider-management/templates/_helpers.tpl b/helm/capif/charts/ocf-api-provider-management/templates/_helpers.tpl new file mode 100644 index 0000000..eb706b4 --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-api-provider-management.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-api-provider-management.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-api-provider-management.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-api-provider-management.labels" -}} +helm.sh/chart: {{ include "ocf-api-provider-management.chart" . }} +{{ include "ocf-api-provider-management.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-api-provider-management.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-api-provider-management.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-api-provider-management.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-api-provider-management.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/templates/capif-provider-configmap.yaml b/helm/capif/charts/ocf-api-provider-management/templates/configmap.yaml similarity index 60% rename from helm/capif/templates/capif-provider-configmap.yaml rename to helm/capif/charts/ocf-api-provider-management/templates/configmap.yaml index 28e530f..e59cfe1 100644 --- a/helm/capif/templates/capif-provider-configmap.yaml +++ b/helm/capif/charts/ocf-api-provider-management/templates/configmap.yaml @@ -5,8 +5,8 @@ metadata: data: config.yaml: | mongo: { - 'user': '{{ .Values.mongo.mongo.env.mongoInitdbRootUsername }}', - 'password': '{{ .Values.mongo.mongo.env.mongoInitdbRootPassword }}', + 'user': '{{ .Values.env.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoInitdbRootPassword }}', 'db': 'capif', 'col': 'providerenrolmentdetails', 'certs_col': "certs", @@ -15,17 +15,17 @@ data: 'port': "27017" } mongo_register: { - 'user': '{{ .Values.mongoRegister.mongo.env.mongoInitdbRootUsername }}', - 'password': '{{ .Values.mongoRegister.mongo.env.mongoInitdbRootPassword }}', + 'user': '{{ .Values.env.mongoRegister.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoRegister.mongoInitdbRootPassword }}', 'db': 'capif_users', 'col': 'user', 'host': 'mongo-register', 'port': '27017' } ca_factory: { - "url": {{ quote .Values.parametersVault.env.vaultHostname }}, - "port": {{ quote .Values.parametersVault.env.vaultPort }}, - "token": {{ quote .Values.parametersVault.env.vaultAccessToken }} + "url": {{ quote .Values.env.vaultHostname }}, + "port": {{ quote .Values.env.vaultPort }}, + "token": {{ quote .Values.env.vaultAccessToken }} } diff --git a/helm/capif/charts/ocf-api-provider-management/templates/deployment.yaml b/helm/capif/charts/ocf-api-provider-management/templates/deployment.yaml new file mode 100644 index 0000000..c5ff215 --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/templates/deployment.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ocf-api-provider-management.fullname" . }} + labels: + {{- include "ocf-api-provider-management.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ocf-api-provider-management.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "ocf-api-provider-management.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ocf-api-provider-management.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: MONITORING + value: {{ quote .Values.env.monitoring }} + - name: VAULT_HOSTNAME + value: {{ quote .Values.env.vaultHostname }} + - name: VAULT_PORT + value: {{ quote .Values.env.vaultPort }} + - name: VAULT_ACCESS_TOKEN + value: {{ quote .Values.env.vaultAccessToken }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/ocf-api-provider-management/templates/hpa.yaml b/helm/capif/charts/ocf-api-provider-management/templates/hpa.yaml new file mode 100644 index 0000000..f3453cc --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-api-provider-management.fullname" . }} + labels: + {{- include "ocf-api-provider-management.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-api-provider-management.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-api-provider-management/templates/ingress.yaml b/helm/capif/charts/ocf-api-provider-management/templates/ingress.yaml new file mode 100644 index 0000000..2edad45 --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-api-provider-management.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-api-provider-management.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-api-provider-management/templates/service.yaml b/helm/capif/charts/ocf-api-provider-management/templates/service.yaml new file mode 100644 index 0000000..cf51d10 --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: api-provider-management + labels: + {{- include "ocf-api-provider-management.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ocf-api-provider-management.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-api-provider-management/templates/serviceaccount.yaml b/helm/capif/charts/ocf-api-provider-management/templates/serviceaccount.yaml new file mode 100644 index 0000000..39bbc5c --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-api-provider-management.serviceAccountName" . }} + labels: + {{- include "ocf-api-provider-management.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-api-provider-management/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-api-provider-management/templates/tests/test-connection.yaml new file mode 100644 index 0000000..c8ca529 --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-api-provider-management.fullname" . }}-test-connection" + labels: + {{- include "ocf-api-provider-management.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['api-provider-management:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/ocf-api-provider-management/values.yaml b/helm/capif/charts/ocf-api-provider-management/values.yaml new file mode 100644 index 0000000..019b214 --- /dev/null +++ b/helm/capif/charts/ocf-api-provider-management/values.yaml @@ -0,0 +1,119 @@ +# Default values for ocf-api-provider-management. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: ocf-api-provider-management-api + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + monitoring: "true" + vaultHostname: vault + vaultPort: 8200 + vaultAccessToken: dev-only-token + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + mongoRegister: + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: capif-provider-config + configMap: + name: capif-provider-configmap + items: + - key: "config.yaml" + path: "config.yaml" + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: capif-provider-config + mountPath: /usr/src/app/config.yaml + subPath: config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/ocf-auditing-api-logs/.helmignore b/helm/capif/charts/ocf-auditing-api-logs/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-auditing-api-logs/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-auditing-api-logs/Chart.yaml b/helm/capif/charts/ocf-auditing-api-logs/Chart.yaml new file mode 100644 index 0000000..dd3585c --- /dev/null +++ b/helm/capif/charts/ocf-auditing-api-logs/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-auditing-api-logs +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/templates/capif-logs-configmap.yaml b/helm/capif/charts/ocf-auditing-api-logs/configmap.yaml similarity index 80% rename from helm/capif/templates/capif-logs-configmap.yaml rename to helm/capif/charts/ocf-auditing-api-logs/configmap.yaml index 53cae6e..729d751 100644 --- a/helm/capif/templates/capif-logs-configmap.yaml +++ b/helm/capif/charts/ocf-auditing-api-logs/configmap.yaml @@ -5,8 +5,8 @@ metadata: data: config.yaml: | mongo: { - 'user': '{{ .Values.mongo.mongo.env.mongoInitdbRootUsername }}', - 'password': '{{ .Values.mongo.mongo.env.mongoInitdbRootPassword }}', + 'user': '{{ .Values.env.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoInitdbRootPassword }}', 'db': 'capif', 'logs_col': 'invocationlogs', 'capif_users_col': "user", diff --git a/helm/capif/charts/access-control-policy/templates/NOTES.txt b/helm/capif/charts/ocf-auditing-api-logs/templates/NOTES.txt similarity index 81% rename from helm/capif/charts/access-control-policy/templates/NOTES.txt rename to helm/capif/charts/ocf-auditing-api-logs/templates/NOTES.txt index 2c54f9d..639b668 100644 --- a/helm/capif/charts/access-control-policy/templates/NOTES.txt +++ b/helm/capif/charts/ocf-auditing-api-logs/templates/NOTES.txt @@ -6,16 +6,16 @@ {{- end }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "access-control-policy.fullname" . }}) + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-auditing-api-logs.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "access-control-policy.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "access-control-policy.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-auditing-api-logs.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-auditing-api-logs.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "access-control-policy.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-auditing-api-logs.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT diff --git a/helm/capif/charts/access-control-policy/templates/_helpers.tpl b/helm/capif/charts/ocf-auditing-api-logs/templates/_helpers.tpl similarity index 71% rename from helm/capif/charts/access-control-policy/templates/_helpers.tpl rename to helm/capif/charts/ocf-auditing-api-logs/templates/_helpers.tpl index 4b87b90..6f69487 100644 --- a/helm/capif/charts/access-control-policy/templates/_helpers.tpl +++ b/helm/capif/charts/ocf-auditing-api-logs/templates/_helpers.tpl @@ -1,7 +1,7 @@ {{/* Expand the name of the chart. */}} -{{- define "access-control-policy.name" -}} +{{- define "ocf-auditing-api-logs.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} @@ -10,7 +10,7 @@ Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} -{{- define "access-control-policy.fullname" -}} +{{- define "ocf-auditing-api-logs.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} @@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "access-control-policy.chart" -}} +{{- define "ocf-auditing-api-logs.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "access-control-policy.labels" -}} -helm.sh/chart: {{ include "access-control-policy.chart" . }} -{{ include "access-control-policy.selectorLabels" . }} +{{- define "ocf-auditing-api-logs.labels" -}} +helm.sh/chart: {{ include "ocf-auditing-api-logs.chart" . }} +{{ include "ocf-auditing-api-logs.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -45,17 +45,17 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Selector labels */}} -{{- define "access-control-policy.selectorLabels" -}} -app.kubernetes.io/name: {{ include "access-control-policy.name" . }} +{{- define "ocf-auditing-api-logs.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-auditing-api-logs.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} -{{- define "access-control-policy.serviceAccountName" -}} +{{- define "ocf-auditing-api-logs.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} -{{- default (include "access-control-policy.fullname" .) .Values.serviceAccount.name }} +{{- default (include "ocf-auditing-api-logs.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} diff --git a/helm/capif/charts/ocf-auditing-api-logs/templates/configmap.yaml b/helm/capif/charts/ocf-auditing-api-logs/templates/configmap.yaml new file mode 100644 index 0000000..729d751 --- /dev/null +++ b/helm/capif/charts/ocf-auditing-api-logs/templates/configmap.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: capif-logs-configmap +data: + config.yaml: | + mongo: { + 'user': '{{ .Values.env.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoInitdbRootPassword }}', + 'db': 'capif', + 'logs_col': 'invocationlogs', + 'capif_users_col': "user", + 'host': 'mongo', + 'port': "27017" + } + + monitoring: { + "fluent_bit_host": fluent-bit, + "fluent_bit_port": 24224, + "opentelemetry_url": "otel-collector", + "opentelemetry_port": "55680", + "opentelemetry_max_queue_size": 8192, + "opentelemetry_schedule_delay_millis": 20000, + "opentelemetry_max_export_batch_size": 2048, + "opentelemetry_export_timeout_millis": 60000 + } + diff --git a/helm/capif/charts/ocf-auditing-api-logs/templates/deployment.yaml b/helm/capif/charts/ocf-auditing-api-logs/templates/deployment.yaml new file mode 100644 index 0000000..62cbf03 --- /dev/null +++ b/helm/capif/charts/ocf-auditing-api-logs/templates/deployment.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ocf-auditing-api-logs.fullname" . }} + labels: + {{- include "ocf-auditing-api-logs.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ocf-auditing-api-logs.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "ocf-auditing-api-logs.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ocf-auditing-api-logs.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: MONITORING + value: {{ quote .Values.env.monitoring }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/ocf-auditing-api-logs/templates/hpa.yaml b/helm/capif/charts/ocf-auditing-api-logs/templates/hpa.yaml new file mode 100644 index 0000000..4133851 --- /dev/null +++ b/helm/capif/charts/ocf-auditing-api-logs/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-auditing-api-logs.fullname" . }} + labels: + {{- include "ocf-auditing-api-logs.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-auditing-api-logs.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-auditing-api-logs/templates/ingress.yaml b/helm/capif/charts/ocf-auditing-api-logs/templates/ingress.yaml new file mode 100644 index 0000000..1a63f2f --- /dev/null +++ b/helm/capif/charts/ocf-auditing-api-logs/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-auditing-api-logs.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-auditing-api-logs.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-auditing-api-logs/templates/service.yaml b/helm/capif/charts/ocf-auditing-api-logs/templates/service.yaml new file mode 100644 index 0000000..cc0a685 --- /dev/null +++ b/helm/capif/charts/ocf-auditing-api-logs/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: logs + labels: + {{- include "ocf-auditing-api-logs.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ocf-auditing-api-logs.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/access-control-policy/templates/serviceaccount.yaml b/helm/capif/charts/ocf-auditing-api-logs/templates/serviceaccount.yaml similarity index 69% rename from helm/capif/charts/access-control-policy/templates/serviceaccount.yaml rename to helm/capif/charts/ocf-auditing-api-logs/templates/serviceaccount.yaml index fc12b54..7cc126f 100644 --- a/helm/capif/charts/access-control-policy/templates/serviceaccount.yaml +++ b/helm/capif/charts/ocf-auditing-api-logs/templates/serviceaccount.yaml @@ -2,9 +2,9 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ include "access-control-policy.serviceAccountName" . }} + name: {{ include "ocf-auditing-api-logs.serviceAccountName" . }} labels: - {{- include "access-control-policy.labels" . | nindent 4 }} + {{- include "ocf-auditing-api-logs.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} diff --git a/helm/capif/charts/ocf-auditing-api-logs/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-auditing-api-logs/templates/tests/test-connection.yaml new file mode 100644 index 0000000..ddd02e7 --- /dev/null +++ b/helm/capif/charts/ocf-auditing-api-logs/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-auditing-api-logs.fullname" . }}-test-connection" + labels: + {{- include "ocf-auditing-api-logs.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['logs:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/ocf-auditing-api-logs/values.yaml b/helm/capif/charts/ocf-auditing-api-logs/values.yaml new file mode 100644 index 0000000..41e3d1f --- /dev/null +++ b/helm/capif/charts/ocf-auditing-api-logs/values.yaml @@ -0,0 +1,115 @@ +# Default values for ocf-auditing-api-logs. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: auditing-api + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + monitoring: "true" + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: capif-logs-config + configMap: + name: capif-logs-configmap + items: + - key: "config.yaml" + path: "config.yaml" + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: capif-logs-config + mountPath: /usr/src/app/config.yaml + subPath: config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/ocf-discover-service-api/.helmignore b/helm/capif/charts/ocf-discover-service-api/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-discover-service-api/Chart.yaml b/helm/capif/charts/ocf-discover-service-api/Chart.yaml new file mode 100644 index 0000000..3bffbb8 --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-discover-service-api +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-discover-service-api/templates/NOTES.txt b/helm/capif/charts/ocf-discover-service-api/templates/NOTES.txt new file mode 100644 index 0000000..64d2230 --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-discover-service-api.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-discover-service-api.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-discover-service-api.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-discover-service-api.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-discover-service-api/templates/_helpers.tpl b/helm/capif/charts/ocf-discover-service-api/templates/_helpers.tpl new file mode 100644 index 0000000..2c42280 --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-discover-service-api.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-discover-service-api.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-discover-service-api.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-discover-service-api.labels" -}} +helm.sh/chart: {{ include "ocf-discover-service-api.chart" . }} +{{ include "ocf-discover-service-api.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-discover-service-api.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-discover-service-api.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-discover-service-api.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-discover-service-api.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/templates/capif-service-configmap.yaml b/helm/capif/charts/ocf-discover-service-api/templates/configmap.yaml similarity index 81% rename from helm/capif/templates/capif-service-configmap.yaml rename to helm/capif/charts/ocf-discover-service-api/templates/configmap.yaml index 1cd3d66..96d0c36 100644 --- a/helm/capif/templates/capif-service-configmap.yaml +++ b/helm/capif/charts/ocf-discover-service-api/templates/configmap.yaml @@ -5,8 +5,8 @@ metadata: data: config.yaml: | mongo: { - 'user': '{{ .Values.mongo.mongo.env.mongoInitdbRootUsername }}', - 'password': '{{ .Values.mongo.mongo.env.mongoInitdbRootPassword }}', + 'user': '{{ .Values.env.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoInitdbRootPassword }}', 'db': 'capif', 'col': 'serviceapidescriptions', 'invokers_col': 'invokerdetails', diff --git a/helm/capif/charts/ocf-discover-service-api/templates/deployment.yaml b/helm/capif/charts/ocf-discover-service-api/templates/deployment.yaml new file mode 100644 index 0000000..438b986 --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/templates/deployment.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ocf-discover-service-api.fullname" . }} + labels: + {{- include "ocf-discover-service-api.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ocf-discover-service-api.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "ocf-discover-service-api.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ocf-discover-service-api.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: MONITORING + value: {{ quote .Values.env.monitoring }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/ocf-discover-service-api/templates/hpa.yaml b/helm/capif/charts/ocf-discover-service-api/templates/hpa.yaml new file mode 100644 index 0000000..bb4c301 --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-discover-service-api.fullname" . }} + labels: + {{- include "ocf-discover-service-api.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-discover-service-api.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-discover-service-api/templates/ingress.yaml b/helm/capif/charts/ocf-discover-service-api/templates/ingress.yaml new file mode 100644 index 0000000..b518729 --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-discover-service-api.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-discover-service-api.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-discover-service-api/templates/service.yaml b/helm/capif/charts/ocf-discover-service-api/templates/service.yaml new file mode 100644 index 0000000..fda46c3 --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: service-apis + labels: + {{- include "ocf-discover-service-api.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ocf-discover-service-api.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-discover-service-api/templates/serviceaccount.yaml b/helm/capif/charts/ocf-discover-service-api/templates/serviceaccount.yaml new file mode 100644 index 0000000..be78919 --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-discover-service-api.serviceAccountName" . }} + labels: + {{- include "ocf-discover-service-api.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-discover-service-api/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-discover-service-api/templates/tests/test-connection.yaml new file mode 100644 index 0000000..3551457 --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-discover-service-api.fullname" . }}-test-connection" + labels: + {{- include "ocf-discover-service-api.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['service-apis:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/ocf-discover-service-api/values.yaml b/helm/capif/charts/ocf-discover-service-api/values.yaml new file mode 100644 index 0000000..b69232d --- /dev/null +++ b/helm/capif/charts/ocf-discover-service-api/values.yaml @@ -0,0 +1,116 @@ +# Default values for ocf-discover-service-api. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: discover-service-api + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + monitoring: "true" + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: capif-service-config + configMap: + name: capif-service-configmap + items: + - key: "config.yaml" + path: "config.yaml" + + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: capif-service-config + mountPath: /usr/src/app/config.yaml + subPath: config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/ocf-events/.helmignore b/helm/capif/charts/ocf-events/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-events/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-events/Chart.yaml b/helm/capif/charts/ocf-events/Chart.yaml new file mode 100644 index 0000000..9e9667f --- /dev/null +++ b/helm/capif/charts/ocf-events/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-events +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-events/templates/NOTES.txt b/helm/capif/charts/ocf-events/templates/NOTES.txt new file mode 100644 index 0000000..2fe0746 --- /dev/null +++ b/helm/capif/charts/ocf-events/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-events.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-events.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-events.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-events.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-events/templates/_helpers.tpl b/helm/capif/charts/ocf-events/templates/_helpers.tpl new file mode 100644 index 0000000..1c0caa6 --- /dev/null +++ b/helm/capif/charts/ocf-events/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-events.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-events.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-events.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-events.labels" -}} +helm.sh/chart: {{ include "ocf-events.chart" . }} +{{ include "ocf-events.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-events.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-events.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-events.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-events.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/templates/capif-events-configmap.yaml b/helm/capif/charts/ocf-events/templates/configmap.yaml similarity index 82% rename from helm/capif/templates/capif-events-configmap.yaml rename to helm/capif/charts/ocf-events/templates/configmap.yaml index ca31c23..a928cac 100644 --- a/helm/capif/templates/capif-events-configmap.yaml +++ b/helm/capif/charts/ocf-events/templates/configmap.yaml @@ -5,8 +5,8 @@ metadata: data: config.yaml: | mongo: { - 'user': '{{ .Values.mongo.mongo.env.mongoInitdbRootUsername }}', - 'password': '{{ .Values.mongo.mongo.env.mongoInitdbRootPassword }}', + 'user': '{{ .Values.env.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoInitdbRootPassword }}', 'db': 'capif', 'col': 'eventsdetails', 'certs_col': "certs", @@ -25,4 +25,4 @@ data: "opentelemetry_schedule_delay_millis": 20000, "opentelemetry_max_export_batch_size": 2048, "opentelemetry_export_timeout_millis": 60000 - } + } \ No newline at end of file diff --git a/helm/capif/charts/ocf-events/templates/deployment.yaml b/helm/capif/charts/ocf-events/templates/deployment.yaml new file mode 100644 index 0000000..f94cc7b --- /dev/null +++ b/helm/capif/charts/ocf-events/templates/deployment.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ocf-events.fullname" . }} + labels: + {{- include "ocf-events.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ocf-events.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "ocf-events.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ocf-events.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: MONITORING + value: {{ quote .Values.env.monitoring }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/ocf-events/templates/hpa.yaml b/helm/capif/charts/ocf-events/templates/hpa.yaml new file mode 100644 index 0000000..f494b03 --- /dev/null +++ b/helm/capif/charts/ocf-events/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-events.fullname" . }} + labels: + {{- include "ocf-events.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-events.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-events/templates/ingress.yaml b/helm/capif/charts/ocf-events/templates/ingress.yaml new file mode 100644 index 0000000..9844a7f --- /dev/null +++ b/helm/capif/charts/ocf-events/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-events.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-events.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-events/templates/service.yaml b/helm/capif/charts/ocf-events/templates/service.yaml new file mode 100644 index 0000000..dd54aca --- /dev/null +++ b/helm/capif/charts/ocf-events/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: capif-events + labels: + {{- include "ocf-events.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ocf-events.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-events/templates/serviceaccount.yaml b/helm/capif/charts/ocf-events/templates/serviceaccount.yaml new file mode 100644 index 0000000..f29121a --- /dev/null +++ b/helm/capif/charts/ocf-events/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-events.serviceAccountName" . }} + labels: + {{- include "ocf-events.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-events/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-events/templates/tests/test-connection.yaml new file mode 100644 index 0000000..fc22433 --- /dev/null +++ b/helm/capif/charts/ocf-events/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-events.fullname" . }}-test-connection" + labels: + {{- include "ocf-events.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['capif-events:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/ocf-events/values.yaml b/helm/capif/charts/ocf-events/values.yaml new file mode 100644 index 0000000..c600141 --- /dev/null +++ b/helm/capif/charts/ocf-events/values.yaml @@ -0,0 +1,115 @@ +# Default values for ocf-events. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: events-api + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + monitoring: "true" + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: capif-events-config + configMap: + name: capif-events-configmap + items: + - key: "config.yaml" + path: "config.yaml" + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: capif-events-config + mountPath: /usr/src/app/config.yaml + subPath: config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/ocf-helper/.helmignore b/helm/capif/charts/ocf-helper/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-helper/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-helper/Chart.yaml b/helm/capif/charts/ocf-helper/Chart.yaml new file mode 100644 index 0000000..ac740bc --- /dev/null +++ b/helm/capif/charts/ocf-helper/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-helper +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-helper/templates/NOTES.txt b/helm/capif/charts/ocf-helper/templates/NOTES.txt new file mode 100644 index 0000000..abd1318 --- /dev/null +++ b/helm/capif/charts/ocf-helper/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-helper.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-helper.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-helper.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-helper.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-helper/templates/_helpers.tpl b/helm/capif/charts/ocf-helper/templates/_helpers.tpl new file mode 100644 index 0000000..df679e3 --- /dev/null +++ b/helm/capif/charts/ocf-helper/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-helper.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-helper.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-helper.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-helper.labels" -}} +helm.sh/chart: {{ include "ocf-helper.chart" . }} +{{ include "ocf-helper.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-helper.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-helper.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-helper.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-helper.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/helper/templates/deployment.yaml b/helm/capif/charts/ocf-helper/templates/deployment.yaml similarity index 88% rename from helm/capif/charts/helper/templates/deployment.yaml rename to helm/capif/charts/ocf-helper/templates/deployment.yaml index a3f43d3..7c55930 100644 --- a/helm/capif/charts/helper/templates/deployment.yaml +++ b/helm/capif/charts/ocf-helper/templates/deployment.yaml @@ -1,23 +1,23 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "helper.fullname" . }} + name: {{ include "ocf-helper.fullname" . }} labels: - {{- include "helper.labels" . | nindent 4 }} + {{- include "ocf-helper.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: - {{- include "helper.selectorLabels" . | nindent 6 }} + {{- include "ocf-helper.selectorLabels" . | nindent 6 }} template: metadata: annotations: date: "{{ now | unixEpoch }}" checksum/config: {{ include (print $.Template.BasePath "/ocf-helper-configmap.yaml") . | sha256sum }} labels: - {{- include "helper.labels" . | nindent 8 }} + {{- include "ocf-helper.labels" . | nindent 8 }} {{- with .Values.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} @@ -26,7 +26,7 @@ spec: imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} - serviceAccountName: {{ include "helper.serviceAccountName" . }} + serviceAccountName: {{ include "ocf-helper.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: diff --git a/helm/capif/charts/ocf-helper/templates/hpa.yaml b/helm/capif/charts/ocf-helper/templates/hpa.yaml new file mode 100644 index 0000000..670686a --- /dev/null +++ b/helm/capif/charts/ocf-helper/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-helper.fullname" . }} + labels: + {{- include "ocf-helper.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-helper.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-helper/templates/ingress.yaml b/helm/capif/charts/ocf-helper/templates/ingress.yaml new file mode 100644 index 0000000..76d37c8 --- /dev/null +++ b/helm/capif/charts/ocf-helper/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-helper.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-helper.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/helper/templates/ocf-helper-configmap.yaml b/helm/capif/charts/ocf-helper/templates/ocf-helper-configmap.yaml similarity index 100% rename from helm/capif/charts/helper/templates/ocf-helper-configmap.yaml rename to helm/capif/charts/ocf-helper/templates/ocf-helper-configmap.yaml diff --git a/helm/capif/charts/helper/templates/service.yaml b/helm/capif/charts/ocf-helper/templates/service.yaml similarity index 66% rename from helm/capif/charts/helper/templates/service.yaml rename to helm/capif/charts/ocf-helper/templates/service.yaml index 4a74370..87006eb 100644 --- a/helm/capif/charts/helper/templates/service.yaml +++ b/helm/capif/charts/ocf-helper/templates/service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: name: helper labels: - {{- include "helper.labels" . | nindent 4 }} + {{- include "ocf-helper.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: @@ -12,4 +12,4 @@ spec: protocol: TCP name: http selector: - {{- include "helper.selectorLabels" . | nindent 4 }} + {{- include "ocf-helper.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-helper/templates/serviceaccount.yaml b/helm/capif/charts/ocf-helper/templates/serviceaccount.yaml new file mode 100644 index 0000000..a3da059 --- /dev/null +++ b/helm/capif/charts/ocf-helper/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-helper.serviceAccountName" . }} + labels: + {{- include "ocf-helper.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-helper/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-helper/templates/tests/test-connection.yaml new file mode 100644 index 0000000..5776042 --- /dev/null +++ b/helm/capif/charts/ocf-helper/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-helper.fullname" . }}-test-connection" + labels: + {{- include "ocf-helper.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['helper:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/helper/values.yaml b/helm/capif/charts/ocf-helper/values.yaml similarity index 96% rename from helm/capif/charts/helper/values.yaml rename to helm/capif/charts/ocf-helper/values.yaml index 147c003..36e0989 100644 --- a/helm/capif/charts/helper/values.yaml +++ b/helm/capif/charts/ocf-helper/values.yaml @@ -1,11 +1,11 @@ -# Default values for helper. +# Default values for ocf-helper. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 image: - repository: "helper" + repository: "ocf-helper" pullPolicy: Always # Overrides the image tag whose default is the chart appVersion. tag: "" @@ -36,7 +36,7 @@ serviceAccount: name: "" podAnnotations: - app: ocf-helper + app: ocf-ocf-helper podLabels: {} diff --git a/helm/capif/charts/ocf-publish-service-api/.helmignore b/helm/capif/charts/ocf-publish-service-api/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-publish-service-api/Chart.yaml b/helm/capif/charts/ocf-publish-service-api/Chart.yaml new file mode 100644 index 0000000..166d252 --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-publish-service-api +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-publish-service-api/templates/NOTES.txt b/helm/capif/charts/ocf-publish-service-api/templates/NOTES.txt new file mode 100644 index 0000000..deaaa58 --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-publish-service-api.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-publish-service-api.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-publish-service-api.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-publish-service-api.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-publish-service-api/templates/_helpers.tpl b/helm/capif/charts/ocf-publish-service-api/templates/_helpers.tpl new file mode 100644 index 0000000..9ca28fa --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-publish-service-api.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-publish-service-api.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-publish-service-api.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-publish-service-api.labels" -}} +helm.sh/chart: {{ include "ocf-publish-service-api.chart" . }} +{{ include "ocf-publish-service-api.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-publish-service-api.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-publish-service-api.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-publish-service-api.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-publish-service-api.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/templates/capif-published-configmap.yaml b/helm/capif/charts/ocf-publish-service-api/templates/configmap.yaml similarity index 81% rename from helm/capif/templates/capif-published-configmap.yaml rename to helm/capif/charts/ocf-publish-service-api/templates/configmap.yaml index 507afd4..a76b2f2 100644 --- a/helm/capif/templates/capif-published-configmap.yaml +++ b/helm/capif/charts/ocf-publish-service-api/templates/configmap.yaml @@ -5,8 +5,8 @@ metadata: data: config.yaml: | mongo: { - 'user': '{{ .Values.mongo.mongo.env.mongoInitdbRootUsername }}', - 'password': '{{ .Values.mongo.mongo.env.mongoInitdbRootPassword }}', + 'user': '{{ .Values.env.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoInitdbRootPassword }}', 'db': 'capif', 'col': 'serviceapidescriptions', 'certs_col': "certs", diff --git a/helm/capif/charts/ocf-publish-service-api/templates/deployment.yaml b/helm/capif/charts/ocf-publish-service-api/templates/deployment.yaml new file mode 100644 index 0000000..49d9b2c --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/templates/deployment.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ocf-publish-service-api.fullname" . }} + labels: + {{- include "ocf-publish-service-api.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ocf-publish-service-api.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "ocf-publish-service-api.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ocf-publish-service-api.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: MONITORING + value: {{ quote .Values.env.monitoring }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/ocf-publish-service-api/templates/hpa.yaml b/helm/capif/charts/ocf-publish-service-api/templates/hpa.yaml new file mode 100644 index 0000000..34c2368 --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-publish-service-api.fullname" . }} + labels: + {{- include "ocf-publish-service-api.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-publish-service-api.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-publish-service-api/templates/ingress.yaml b/helm/capif/charts/ocf-publish-service-api/templates/ingress.yaml new file mode 100644 index 0000000..22cd9f3 --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-publish-service-api.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-publish-service-api.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-publish-service-api/templates/service.yaml b/helm/capif/charts/ocf-publish-service-api/templates/service.yaml new file mode 100644 index 0000000..2412215 --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: published-apis + labels: + {{- include "ocf-publish-service-api.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ocf-publish-service-api.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-publish-service-api/templates/serviceaccount.yaml b/helm/capif/charts/ocf-publish-service-api/templates/serviceaccount.yaml new file mode 100644 index 0000000..22ca36f --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-publish-service-api.serviceAccountName" . }} + labels: + {{- include "ocf-publish-service-api.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-publish-service-api/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-publish-service-api/templates/tests/test-connection.yaml new file mode 100644 index 0000000..edbd061 --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-publish-service-api.fullname" . }}-test-connection" + labels: + {{- include "ocf-publish-service-api.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['published-apis:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/ocf-publish-service-api/values.yaml b/helm/capif/charts/ocf-publish-service-api/values.yaml new file mode 100644 index 0000000..4ab3c9c --- /dev/null +++ b/helm/capif/charts/ocf-publish-service-api/values.yaml @@ -0,0 +1,115 @@ +# Default values for ocf-publish-service-api. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: publish-service-api + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + monitoring: "true" + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: capif-published-config + configMap: + name: capif-published-configmap + items: + - key: "config.yaml" + path: "config.yaml" + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: capif-published-config + mountPath: /usr/src/app/config.yaml + subPath: config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/ocf-register/.helmignore b/helm/capif/charts/ocf-register/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-register/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-register/Chart.yaml b/helm/capif/charts/ocf-register/Chart.yaml new file mode 100644 index 0000000..06dee80 --- /dev/null +++ b/helm/capif/charts/ocf-register/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-register +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-register/templates/NOTES.txt b/helm/capif/charts/ocf-register/templates/NOTES.txt new file mode 100644 index 0000000..f323bac --- /dev/null +++ b/helm/capif/charts/ocf-register/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-register.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-register.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-register.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-register.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-register/templates/_helpers.tpl b/helm/capif/charts/ocf-register/templates/_helpers.tpl new file mode 100644 index 0000000..c1d5489 --- /dev/null +++ b/helm/capif/charts/ocf-register/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-register.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-register.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-register.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-register.labels" -}} +helm.sh/chart: {{ include "ocf-register.chart" . }} +{{ include "ocf-register.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-register.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-register.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-register.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-register.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/templates/register-configmap.yaml b/helm/capif/charts/ocf-register/templates/configmap.yaml similarity index 52% rename from helm/capif/templates/register-configmap.yaml rename to helm/capif/charts/ocf-register/templates/configmap.yaml index 7dcc300..d927eba 100644 --- a/helm/capif/templates/register-configmap.yaml +++ b/helm/capif/charts/ocf-register/templates/configmap.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: ConfigMap metadata: name: register-configmap - labels: - {{- include "capif.labels" . | nindent 4 }} data: config.yaml: |- mongo: { @@ -11,13 +9,13 @@ data: 'password': 'example', 'db': 'capif_users', 'col': 'user', - 'host': '{{ .Values.register.register.env.mongoHost }}', - 'port': '{{ .Values.register.register.env.mongoPort }}' + 'host': '{{ .Values.env.mongoHost }}', + 'port': '{{ .Values.env.mongoPort }}' } ca_factory: { - "url": "{{ .Values.parametersVault.env.vaultHostname }}", - "port": "{{ .Values.parametersVault.env.vaultPort }}", - "token": "{{ .Values.parametersVault.env.vaultAccessToken }}" + "url": "{{ .Values.env.vaultHostname }}", + "port": "{{ .Values.env.vaultPort }}", + "token": "{{ .Values.env.vaultAccessToken }}" } register: { register_uuid: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', diff --git a/helm/capif/charts/ocf-register/templates/deployment.yaml b/helm/capif/charts/ocf-register/templates/deployment.yaml new file mode 100644 index 0000000..5437dfc --- /dev/null +++ b/helm/capif/charts/ocf-register/templates/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ocf-register.fullname" . }} + labels: + {{- include "ocf-register.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ocf-register.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "ocf-register.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ocf-register.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: VAULT_HOSTNAME + value: {{ quote .Values.env.vaultHostname }} + - name: VAULT_PORT + value: {{ quote .Values.env.vaultPort }} + - name: VAULT_ACCESS_TOKEN + value: {{ quote .Values.env.vaultAccessToken }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/ocf-register/templates/hpa.yaml b/helm/capif/charts/ocf-register/templates/hpa.yaml new file mode 100644 index 0000000..936dbb4 --- /dev/null +++ b/helm/capif/charts/ocf-register/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-register.fullname" . }} + labels: + {{- include "ocf-register.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-register.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-register/templates/ingress.yaml b/helm/capif/charts/ocf-register/templates/ingress.yaml new file mode 100644 index 0000000..c5911eb --- /dev/null +++ b/helm/capif/charts/ocf-register/templates/ingress.yaml @@ -0,0 +1,60 @@ +{{- if .Values.ingress.enabled -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: nginx-register + labels: + {{- include "ocf-register.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: register + port: + number: {{ $svcPort }} + {{- else }} + serviceName: register + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-register/templates/service.yaml b/helm/capif/charts/ocf-register/templates/service.yaml new file mode 100644 index 0000000..ae6f7f6 --- /dev/null +++ b/helm/capif/charts/ocf-register/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: register + labels: + {{- include "ocf-register.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: 8080 + protocol: TCP + name: http + selector: + {{- include "ocf-register.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-register/templates/serviceaccount.yaml b/helm/capif/charts/ocf-register/templates/serviceaccount.yaml new file mode 100644 index 0000000..d295456 --- /dev/null +++ b/helm/capif/charts/ocf-register/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-register.serviceAccountName" . }} + labels: + {{- include "ocf-register.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-register/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-register/templates/tests/test-connection.yaml new file mode 100644 index 0000000..40831f6 --- /dev/null +++ b/helm/capif/charts/ocf-register/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-register.fullname" . }}-test-connection" + labels: + {{- include "ocf-register.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['register:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/ocf-register/values.yaml b/helm/capif/charts/ocf-register/values.yaml new file mode 100644 index 0000000..b9e9e49 --- /dev/null +++ b/helm/capif/charts/ocf-register/values.yaml @@ -0,0 +1,118 @@ +# Default values for ocf-register. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: register + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + mongoHost: mongo-register + mongoPort: 27017 + vaultHostname: vault + vaultPort: 8200 + vaultAccessToken: dev-only-token + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8084 + +ingress: + enabled: true + className: "nginx" + annotations: + #cert-manager.io/issuer: letsencrypt-issuer + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + nginx.ingress.kubernetes.io/ssl-passthrough: "true" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + hosts: + - host: register.app.ocp-epg.hi.inet + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: register-configmap + configMap: + name: register-configmap + items: + - key: "config.yaml" + path: "config.yaml" + + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: register-configmap + mountPath: /usr/src/app/config.yaml + subPath: config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/ocf-routing-info/.helmignore b/helm/capif/charts/ocf-routing-info/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-routing-info/Chart.yaml b/helm/capif/charts/ocf-routing-info/Chart.yaml new file mode 100644 index 0000000..bf109e2 --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-routing-info +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-routing-info/templates/NOTES.txt b/helm/capif/charts/ocf-routing-info/templates/NOTES.txt new file mode 100644 index 0000000..f08d1a0 --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-routing-info.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-routing-info.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-routing-info.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-routing-info.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-routing-info/templates/_helpers.tpl b/helm/capif/charts/ocf-routing-info/templates/_helpers.tpl new file mode 100644 index 0000000..58bef4e --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-routing-info.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-routing-info.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-routing-info.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-routing-info.labels" -}} +helm.sh/chart: {{ include "ocf-routing-info.chart" . }} +{{ include "ocf-routing-info.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-routing-info.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-routing-info.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-routing-info.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-routing-info.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-routing-info/templates/deployment.yaml b/helm/capif/charts/ocf-routing-info/templates/deployment.yaml new file mode 100644 index 0000000..2e1abf1 --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/templates/deployment.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ocf-routing-info.fullname" . }} + labels: + {{- include "ocf-routing-info.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ocf-routing-info.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + labels: + {{- include "ocf-routing-info.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ocf-routing-info.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: MONITORING + value: {{ quote .Values.env.monitoring }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/ocf-routing-info/templates/hpa.yaml b/helm/capif/charts/ocf-routing-info/templates/hpa.yaml new file mode 100644 index 0000000..6172807 --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-routing-info.fullname" . }} + labels: + {{- include "ocf-routing-info.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-routing-info.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-routing-info/templates/ingress.yaml b/helm/capif/charts/ocf-routing-info/templates/ingress.yaml new file mode 100644 index 0000000..cf74fa9 --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-routing-info.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-routing-info.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-routing-info/templates/service.yaml b/helm/capif/charts/ocf-routing-info/templates/service.yaml new file mode 100644 index 0000000..125bf08 --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: capif-routing-info + labels: + {{- include "ocf-routing-info.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ocf-routing-info.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-routing-info/templates/serviceaccount.yaml b/helm/capif/charts/ocf-routing-info/templates/serviceaccount.yaml new file mode 100644 index 0000000..6d1721d --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-routing-info.serviceAccountName" . }} + labels: + {{- include "ocf-routing-info.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-routing-info/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-routing-info/templates/tests/test-connection.yaml new file mode 100644 index 0000000..26169c1 --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-routing-info.fullname" . }}-test-connection" + labels: + {{- include "ocf-routing-info.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['capif-routing-info:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/ocf-routing-info/values.yaml b/helm/capif/charts/ocf-routing-info/values.yaml new file mode 100644 index 0000000..8ba779c --- /dev/null +++ b/helm/capif/charts/ocf-routing-info/values.yaml @@ -0,0 +1,111 @@ +# Default values for ocf-routing-info. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: routing-info-api + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + monitoring: "true" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/ocf-security/.helmignore b/helm/capif/charts/ocf-security/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/ocf-security/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/ocf-security/Chart.yaml b/helm/capif/charts/ocf-security/Chart.yaml new file mode 100644 index 0000000..93606cd --- /dev/null +++ b/helm/capif/charts/ocf-security/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ocf-security +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/ocf-security/templates/NOTES.txt b/helm/capif/charts/ocf-security/templates/NOTES.txt new file mode 100644 index 0000000..3654878 --- /dev/null +++ b/helm/capif/charts/ocf-security/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ocf-security.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ocf-security.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ocf-security.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ocf-security.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/ocf-security/templates/_helpers.tpl b/helm/capif/charts/ocf-security/templates/_helpers.tpl new file mode 100644 index 0000000..11c7d3f --- /dev/null +++ b/helm/capif/charts/ocf-security/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ocf-security.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ocf-security.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ocf-security.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ocf-security.labels" -}} +helm.sh/chart: {{ include "ocf-security.chart" . }} +{{ include "ocf-security.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ocf-security.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ocf-security.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ocf-security.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ocf-security.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/templates/capif-security-configmap.yaml b/helm/capif/charts/ocf-security/templates/configmap.yaml similarity index 82% rename from helm/capif/templates/capif-security-configmap.yaml rename to helm/capif/charts/ocf-security/templates/configmap.yaml index ade6a59..5d099d1 100644 --- a/helm/capif/templates/capif-security-configmap.yaml +++ b/helm/capif/charts/ocf-security/templates/configmap.yaml @@ -5,8 +5,8 @@ metadata: data: config.yaml: | mongo: { - 'user': '{{ .Values.mongo.mongo.env.mongoInitdbRootUsername }}', - 'password': '{{ .Values.mongo.mongo.env.mongoInitdbRootPassword }}', + 'user': '{{ .Values.env.mongoInitdbRootUsername }}', + 'password': '{{ .Values.env.mongoInitdbRootPassword }}', 'db': 'capif', 'col': 'security', 'capif_service_col': 'serviceapidescriptions', diff --git a/helm/capif/charts/ocf-security/templates/deployment.yaml b/helm/capif/charts/ocf-security/templates/deployment.yaml new file mode 100644 index 0000000..44bd7fa --- /dev/null +++ b/helm/capif/charts/ocf-security/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ocf-security.fullname" . }} + labels: + {{- include "ocf-security.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ocf-security.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "ocf-security.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ocf-security.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: CAPIF_HOSTNAME + value: {{ quote .Values.env.capifHostname }} + - name: MONITORING + value: {{ quote .Values.env.monitoring }} + - name: VAULT_HOSTNAME + value: {{ quote .Values.env.vaultHostname }} + - name: VAULT_PORT + value: {{ quote .Values.env.vaultPort }} + - name: VAULT_ACCESS_TOKEN + value: {{ quote .Values.env.vaultAccessToken }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/ocf-security/templates/hpa.yaml b/helm/capif/charts/ocf-security/templates/hpa.yaml new file mode 100644 index 0000000..7b4759b --- /dev/null +++ b/helm/capif/charts/ocf-security/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ocf-security.fullname" . }} + labels: + {{- include "ocf-security.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ocf-security.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-security/templates/ingress.yaml b/helm/capif/charts/ocf-security/templates/ingress.yaml new file mode 100644 index 0000000..5656fea --- /dev/null +++ b/helm/capif/charts/ocf-security/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ocf-security.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ocf-security.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/ocf-security/templates/service.yaml b/helm/capif/charts/ocf-security/templates/service.yaml new file mode 100644 index 0000000..553dc57 --- /dev/null +++ b/helm/capif/charts/ocf-security/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: capif-security + labels: + {{- include "ocf-security.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ocf-security.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/ocf-security/templates/serviceaccount.yaml b/helm/capif/charts/ocf-security/templates/serviceaccount.yaml new file mode 100644 index 0000000..29faf43 --- /dev/null +++ b/helm/capif/charts/ocf-security/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ocf-security.serviceAccountName" . }} + labels: + {{- include "ocf-security.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/ocf-security/templates/tests/test-connection.yaml b/helm/capif/charts/ocf-security/templates/tests/test-connection.yaml new file mode 100644 index 0000000..08b3752 --- /dev/null +++ b/helm/capif/charts/ocf-security/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ocf-security.fullname" . }}-test-connection" + labels: + {{- include "ocf-security.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['capif-security:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/ocf-security/values.yaml b/helm/capif/charts/ocf-security/values.yaml new file mode 100644 index 0000000..37b57c7 --- /dev/null +++ b/helm/capif/charts/ocf-security/values.yaml @@ -0,0 +1,118 @@ +# Default values for ocf-security. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: security-api + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + monitoring: "true" + capifHostname: capif + vaultHostname: vault + vaultPort: 8200 + vaultAccessToken: dev-only-token + mongoInitdbRootUsername: root + mongoInitdbRootPassword: example + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: + tcpSocket: + port: 8080 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: capif-security-config + configMap: + name: capif-security-configmap + items: + - key: "config.yaml" + path: "config.yaml" + + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: capif-security-config + mountPath: /usr/src/app/config.yaml + subPath: config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/redis/.helmignore b/helm/capif/charts/redis/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/redis/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/redis/Chart.yaml b/helm/capif/charts/redis/Chart.yaml new file mode 100644 index 0000000..165c196 --- /dev/null +++ b/helm/capif/charts/redis/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: redis +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/redis/templates/NOTES.txt b/helm/capif/charts/redis/templates/NOTES.txt new file mode 100644 index 0000000..0735ad4 --- /dev/null +++ b/helm/capif/charts/redis/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "redis.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "redis.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "redis.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "redis.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/redis/templates/_helpers.tpl b/helm/capif/charts/redis/templates/_helpers.tpl new file mode 100644 index 0000000..f6a718b --- /dev/null +++ b/helm/capif/charts/redis/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "redis.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "redis.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "redis.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "redis.labels" -}} +helm.sh/chart: {{ include "redis.chart" . }} +{{ include "redis.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "redis.selectorLabels" -}} +app.kubernetes.io/name: {{ include "redis.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "redis.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "redis.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/redis/templates/deployment.yaml b/helm/capif/charts/redis/templates/deployment.yaml new file mode 100644 index 0000000..2350429 --- /dev/null +++ b/helm/capif/charts/redis/templates/deployment.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "redis.fullname" . }} + labels: + {{- include "redis.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "redis.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + date: "{{ now | unixEpoch }}" + labels: + {{- include "redis.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "redis.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: REDIS_REPLICATION_MODE + value: {{ quote .Values.env.redisReplicationMode }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/capif/charts/redis/templates/hpa.yaml b/helm/capif/charts/redis/templates/hpa.yaml new file mode 100644 index 0000000..db46f1b --- /dev/null +++ b/helm/capif/charts/redis/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "redis.fullname" . }} + labels: + {{- include "redis.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "redis.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/redis/templates/ingress.yaml b/helm/capif/charts/redis/templates/ingress.yaml new file mode 100644 index 0000000..f5674cb --- /dev/null +++ b/helm/capif/charts/redis/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "redis.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "redis.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/redis/templates/service.yaml b/helm/capif/charts/redis/templates/service.yaml new file mode 100644 index 0000000..6482b83 --- /dev/null +++ b/helm/capif/charts/redis/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis + labels: + {{- include "redis.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "redis.selectorLabels" . | nindent 4 }} diff --git a/helm/capif/charts/redis/templates/serviceaccount.yaml b/helm/capif/charts/redis/templates/serviceaccount.yaml new file mode 100644 index 0000000..8f21aeb --- /dev/null +++ b/helm/capif/charts/redis/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "redis.serviceAccountName" . }} + labels: + {{- include "redis.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/capif/charts/redis/templates/tests/test-connection.yaml b/helm/capif/charts/redis/templates/tests/test-connection.yaml new file mode 100644 index 0000000..998be8e --- /dev/null +++ b/helm/capif/charts/redis/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "redis.fullname" . }}-test-connection" + labels: + {{- include "redis.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "redis.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/capif/charts/redis/values.yaml b/helm/capif/charts/redis/values.yaml new file mode 100644 index 0000000..4011e97 --- /dev/null +++ b/helm/capif/charts/redis/values.yaml @@ -0,0 +1,111 @@ +# Default values for redis. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: redis + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "alpine" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + redisReplicationMode: master + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 6379 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + +livenessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 5 + periodSeconds: 5 +readinessProbe: + tcpSocket: + port: 6379 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/templates/api-invocation-logs.yaml b/helm/capif/templates/api-invocation-logs.yaml deleted file mode 100644 index a9b4d8f..0000000 --- a/helm/capif/templates/api-invocation-logs.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: api-invocation-logs - labels: - io.kompose.service: api-invocation-logs - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.apiInvocationLogs.type }} - selector: - io.kompose.service: api-invocation-logs - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.apiInvocationLogs.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/api-invoker-management.yaml b/helm/capif/templates/api-invoker-management.yaml deleted file mode 100644 index 3eaeda4..0000000 --- a/helm/capif/templates/api-invoker-management.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: api-invoker-management - labels: - io.kompose.service: api-invoker-management - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.apiInvokerManagement.type }} - selector: - io.kompose.service: api-invoker-management - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.apiInvokerManagement.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/api-provider-management.yaml b/helm/capif/templates/api-provider-management.yaml deleted file mode 100644 index 4237986..0000000 --- a/helm/capif/templates/api-provider-management.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: api-provider-management - labels: - io.kompose.service: api-provider-management - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.apiProviderManagement.type }} - selector: - io.kompose.service: api-provider-management - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.apiProviderManagement.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/capif-events.yaml b/helm/capif/templates/capif-events.yaml deleted file mode 100644 index 40b3d7b..0000000 --- a/helm/capif/templates/capif-events.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: capif-events - labels: - io.kompose.service: capif-events - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.capifEvents.type }} - selector: - io.kompose.service: capif-events - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.capifEvents.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/capif-routing-info.yaml b/helm/capif/templates/capif-routing-info.yaml deleted file mode 100644 index 6de48aa..0000000 --- a/helm/capif/templates/capif-routing-info.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: capif-routing-info - labels: - io.kompose.service: capif-routing-info - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.capifRoutingInfo.type }} - selector: - io.kompose.service: capif-routing-info - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.capifRoutingInfo.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/capif-security.yaml b/helm/capif/templates/capif-security.yaml deleted file mode 100644 index e0bf7d8..0000000 --- a/helm/capif/templates/capif-security.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: capif-security - labels: - io.kompose.service: capif-security - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.capifSecurity.type }} - selector: - io.kompose.service: capif-security - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.capifSecurity.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/deployment.yaml b/helm/capif/templates/deployment.yaml index 4f58100..8b13789 100644 --- a/helm/capif/templates/deployment.yaml +++ b/helm/capif/templates/deployment.yaml @@ -1,974 +1 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: api-invocation-logs - labels: - io.kompose.service: api-invocation-logs - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.apiInvocationLogs.replicas }} - selector: - matchLabels: - io.kompose.service: api-invocation-logs - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: api-invocation-logs - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - checksum/config: {{ include (print $.Template.BasePath "/capif-invocation-configmap.yaml") . | sha256sum }} - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: CAPIF_HOSTNAME - value: {{ quote .Values.nginx.nginx.env.capifHostname }} - - name: MONITORING - value: {{ quote .Values.apiInvocationLogs.apiInvocationLogs.env.monitoring }} - - name: VAULT_HOSTNAME - value: {{ quote .Values.parametersVault.env.vaultHostname }} - - name: VAULT_PORT - value: {{ quote .Values.parametersVault.env.vaultPort }} - - name: VAULT_ACCESS_TOKEN - value: {{ quote .Values.parametersVault.env.vaultAccessToken }} - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - image: {{ .Values.apiInvocationLogs.apiInvocationLogs.image.repository }}:{{ .Values.apiInvocationLogs.apiInvocationLogs.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.apiInvocationLogs.apiInvocationLogs.image.imagePullPolicy }} - name: api-invocation-logs - ports: - - containerPort: 8080 - volumeMounts: - - name: capif-invocation-config - mountPath: /usr/src/app/config.yaml - subPath: config.yaml - resources: - {{- toYaml .Values.apiInvocationLogs.apiInvocationLogs.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 8080 - initialDelaySeconds: 10 - periodSeconds: 5 - volumes: - - name: capif-invocation-config - configMap: - name: capif-invocation-configmap - items: - - key: "config.yaml" - path: "config.yaml" - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: api-invoker-management - labels: - io.kompose.service: api-invoker-management - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.apiInvokerManagement.replicas }} - selector: - matchLabels: - io.kompose.service: api-invoker-management - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: api-invoker-management - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - checksum/config: {{ include (print $.Template.BasePath "/capif-invoker-configmap.yaml") . | sha256sum }} - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - - name: MONITORING - value: {{ quote .Values.apiInvokerManagement.apiInvokerManagement.env.monitoring }} - - name: VAULT_HOSTNAME - value: {{ quote .Values.parametersVault.env.vaultHostname }} - - name: VAULT_PORT - value: {{ quote .Values.parametersVault.env.vaultPort }} - - name: VAULT_ACCESS_TOKEN - value: {{ quote .Values.parametersVault.env.vaultAccessToken }} - image: {{ .Values.apiInvokerManagement.apiInvokerManagement.image.repository }}:{{ - .Values.apiInvokerManagement.apiInvokerManagement.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.apiInvokerManagement.apiInvokerManagement.image.imagePullPolicy }} - name: api-invoker-management - ports: - - containerPort: 8080 - volumeMounts: - - name: capif-invoker-config - mountPath: /usr/src/app/config.yaml - subPath: config.yaml - resources: - {{- toYaml .Values.apiInvokerManagement.apiInvokerManagement.resources | nindent 12 }} - volumes: - - name: capif-invoker-config - configMap: - name: capif-invoker-configmap - items: - - key: "config.yaml" - path: "config.yaml" - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: api-provider-management - labels: - io.kompose.service: api-provider-management - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.apiProviderManagement.replicas }} - selector: - matchLabels: - io.kompose.service: api-provider-management - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: api-provider-management - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - checksum/config: {{ include (print $.Template.BasePath "/capif-provider-configmap.yaml") . | sha256sum }} - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - - name: MONITORING - value: {{ quote .Values.apiProviderManagement.apiProviderManagement.env.monitoring }} - - name: VAULT_HOSTNAME - value: {{ quote .Values.parametersVault.env.vaultHostname }} - - name: VAULT_PORT - value: {{ quote .Values.parametersVault.env.vaultPort }} - - name: VAULT_ACCESS_TOKEN - value: {{ quote .Values.parametersVault.env.vaultAccessToken }} - image: {{ .Values.apiProviderManagement.apiProviderManagement.image.repository - }}:{{ .Values.apiProviderManagement.apiProviderManagement.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.apiProviderManagement.apiProviderManagement.image.imagePullPolicy }} - name: api-provider-management - ports: - - containerPort: 8080 - volumeMounts: - - name: capif-provider-config - mountPath: /usr/src/app/config.yaml - subPath: config.yaml - resources: - {{- toYaml .Values.apiProviderManagement.apiProviderManagement.resources | nindent 12 }} - volumes: - - name: capif-provider-config - configMap: - name: capif-provider-configmap - items: - - key: "config.yaml" - path: "config.yaml" - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: capif-events - labels: - io.kompose.service: capif-events - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.capifEvents.replicas }} - selector: - matchLabels: - io.kompose.service: capif-events - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: capif-events - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - checksum/config: {{ include (print $.Template.BasePath "/capif-events-configmap.yaml") . | sha256sum }} - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - - name: MONITORING - value: {{ quote .Values.capifEvents.capifEvents.env.monitoring }} - image: {{ .Values.capifEvents.capifEvents.image.repository }}:{{ .Values.capifEvents.capifEvents.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.capifEvents.capifEvents.image.imagePullPolicy }} - name: capif-events - ports: - - containerPort: 8080 - volumeMounts: - - name: capif-events-config - mountPath: /usr/src/app/config.yaml - subPath: config.yaml - resources: - {{- toYaml .Values.capifEvents.capifEvents.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 5 - volumes: - - name: capif-events-config - configMap: - name: capif-events-configmap - items: - - key: "config.yaml" - path: "config.yaml" - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: capif-routing-info - labels: - io.kompose.service: capif-routing-info - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.capifRoutingInfo.replicas }} - selector: - matchLabels: - io.kompose.service: capif-routing-info - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: capif-routing-info - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - - name: MONITORING - value: {{ quote .Values.capifRoutingInfo.capifRoutingInfo.env.monitoring }} - image: {{ .Values.capifRoutingInfo.capifRoutingInfo.image.repository }}:{{ .Values.capifRoutingInfo.capifRoutingInfo.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.capifRoutingInfo.capifRoutingInfo.image.imagePullPolicy }} - name: capif-routing-info - ports: - - containerPort: 8080 - resources: - {{- toYaml .Values.capifRoutingInfo.capifRoutingInfo.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 5 - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: capif-security - labels: - io.kompose.service: capif-security - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.capifSecurity.replicas }} - selector: - matchLabels: - io.kompose.service: capif-security - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: capif-security - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - checksum/config: {{ include (print $.Template.BasePath "/capif-security-configmap.yaml") . | sha256sum }} - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: CAPIF_HOSTNAME - value: {{ quote .Values.nginx.nginx.env.capifHostname }} - - name: MONITORING - value: {{ quote .Values.capifSecurity.capifSecurity.env.monitoring }} - - name: VAULT_HOSTNAME - value: {{ quote .Values.parametersVault.env.vaultHostname }} - - name: VAULT_PORT - value: {{ quote .Values.parametersVault.env.vaultPort }} - - name: VAULT_ACCESS_TOKEN - value: {{ quote .Values.parametersVault.env.vaultAccessToken }} - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - image: {{ .Values.capifSecurity.capifSecurity.image.repository }}:{{ .Values.capifSecurity.capifSecurity.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.capifSecurity.capifSecurity.image.imagePullPolicy }} - name: capif-security - ports: - - containerPort: 8080 - volumeMounts: - - name: capif-security-config - mountPath: /usr/src/app/config.yaml - subPath: config.yaml - resources: - {{- toYaml .Values.capifSecurity.capifSecurity.resources | nindent 12 }} - volumes: - - name: capif-security-config - configMap: - name: capif-security-configmap - items: - - key: "config.yaml" - path: "config.yaml" - restartPolicy: Always - restartPolicy: Always -{{- if eq .Values.register.enable "true" }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: register - labels: - io.kompose.service: register - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.register.replicas }} - selector: - matchLabels: - io.kompose.service: register - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: register - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - checksum/config: {{ include (print $.Template.BasePath "/register-configmap.yaml") . | sha256sum }} - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - - name: VAULT_HOSTNAME - value: {{ quote .Values.parametersVault.env.vaultHostname }} - - name: VAULT_PORT - value: {{ quote .Values.parametersVault.env.vaultPort }} - - name: VAULT_ACCESS_TOKEN - value: {{ quote .Values.parametersVault.env.vaultAccessToken }} - image: {{ .Values.register.register.image.repository }}:{{ .Values.register.register.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.register.register.image.imagePullPolicy }} - name: register - ports: - - containerPort: 8080 - resources: - {{- toYaml .Values.register.register.resources | nindent 12 }} - volumeMounts: - - name: register-configmap - mountPath: /usr/src/app/config.yaml - subPath: config.yaml - volumes: - - name: register-configmap - configMap: - name: register-configmap - items: - - key: "config.yaml" - path: "config.yaml" - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mongo-register - labels: - io.kompose.service: mongo-register - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.mongoRegister.replicas }} - selector: - matchLabels: - io.kompose.service: mongo-register - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: mongo-register - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: MONGO_INITDB_ROOT_PASSWORD - value: {{ quote .Values.mongoRegister.mongo.env.mongoInitdbRootPassword }} - - name: MONGO_INITDB_ROOT_USERNAME - value: {{ quote .Values.mongoRegister.mongo.env.mongoInitdbRootUsername }} - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - image: {{ .Values.mongoRegister.mongo.image.repository }}:{{ .Values.mongoRegister.mongo.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.mongoRegister.mongo.image.imagePullPolicy }} - name: mongo-register - {{- if .Values.mongoRegister.mongo.persistence.enable }} - volumeMounts: - - name: mongo-register-pvc - mountPath: /data/db - {{- end }} - ports: - - containerPort: 27017 - securityContext: - runAsUser: 999 - resources: - {{- toYaml .Values.mongoRegister.mongo.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 27017 -# initialDelaySeconds: 5 - periodSeconds: 5 - {{- if .Values.mongoRegister.mongo.persistence.enable }} - volumes: - - name: mongo-register-pvc - persistentVolumeClaim: - claimName: mongo-register-pvc - {{- end }} - restartPolicy: Always -{{- end }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: logs - labels: - io.kompose.service: logs - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.logs.replicas }} - selector: - matchLabels: - io.kompose.service: logs - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: logs - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - checksum/config: {{ include (print $.Template.BasePath "/capif-logs-configmap.yaml") . | sha256sum }} - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - - name: MONITORING - value: {{ quote .Values.logs.logs.env.monitoring }} - image: {{ .Values.logs.logs.image.repository }}:{{ .Values.logs.logs.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.logs.logs.image.imagePullPolicy }} - name: logs - ports: - - containerPort: 8080 - volumeMounts: - - name: capif-logs-config - mountPath: /usr/src/app/config.yaml - subPath: config.yaml - resources: - {{- toYaml .Values.logs.logs.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 5 - volumes: - - name: capif-logs-config - configMap: - name: capif-logs-configmap - items: - - key: "config.yaml" - path: "config.yaml" - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mongo - labels: - io.kompose.service: mongo - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.mongo.replicas }} - strategy: - type: Recreate - selector: - matchLabels: - io.kompose.service: mongo - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: mongo - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: MONGO_INITDB_ROOT_PASSWORD - value: {{ quote .Values.mongo.mongo.env.mongoInitdbRootPassword }} - - name: MONGO_INITDB_ROOT_USERNAME - value: {{ quote .Values.mongo.mongo.env.mongoInitdbRootUsername }} - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - image: {{ .Values.mongo.mongo.image.repository }}:{{ .Values.mongo.mongo.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.mongo.mongo.image.imagePullPolicy }} - name: mongo - ports: - - containerPort: 27017 - securityContext: - runAsUser: 999 - {{- if eq .Values.mongo.persistence.enable "true" }} - volumeMounts: - - name: mongo-pvc - mountPath: /data/db - {{- end }} - resources: - {{- toYaml .Values.mongo.mongo.resources | nindent 12 }} - livenessProbe: - tcpSocket: - port: 27017 - initialDelaySeconds: 20 - periodSeconds: 5 - readinessProbe: - tcpSocket: - port: 27017 -# initialDelaySeconds: 5 - periodSeconds: 5 - - name: mongo-helper - image: busybox - command: - - sh - - -c - - while true ; do echo alive ; sleep 10 ; done - {{- if eq .Values.mongo.persistence.enable "true" }} - volumeMounts: - - mountPath: /mongodata - name: mongo-pvc - {{- end }} - {{- if eq .Values.mongo.persistence.enable "true" }} - volumes: - - name: mongo-pvc - persistentVolumeClaim: - claimName: mongo-pvc - {{- end }} - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mongo-express - labels: - io.kompose.service: mongo-express - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.mongoExpress.replicas }} - selector: - matchLabels: - io.kompose.service: mongo-express - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: mongo-express - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: ME_CONFIG_MONGODB_ADMINPASSWORD - value: {{ quote .Values.mongoExpress.mongoExpress.env.meConfigMongodbAdminpassword - }} - - name: ME_CONFIG_MONGODB_ADMINUSERNAME - value: {{ quote .Values.mongoExpress.mongoExpress.env.meConfigMongodbAdminusername - }} - - name: ME_CONFIG_MONGODB_URL - value: {{ quote .Values.mongoExpress.mongoExpress.env.meConfigMongodbUrl }} - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - image: {{ .Values.mongoExpress.mongoExpress.image.repository }}:{{ .Values.mongoExpress.mongoExpress.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.mongoExpress.mongoExpress.image.imagePullPolicy }} - name: mongo-express - ports: - - containerPort: 8081 - resources: - {{- toYaml .Values.mongoExpress.mongoExpress.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 8081 -# initialDelaySeconds: 0 - periodSeconds: 5 - restartPolicy: Always ---- - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mongo-register-express - labels: - io.kompose.service: mongo-register-express - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.mongoRegisterExpress.replicas }} - selector: - matchLabels: - io.kompose.service: mongo-register-express - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: mongo-register-express - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: ME_CONFIG_MONGODB_ADMINPASSWORD - value: {{ quote .Values.mongoRegisterExpress.mongoRegisterExpress.env.meConfigMongodbAdminpassword - }} - - name: ME_CONFIG_MONGODB_ADMINUSERNAME - value: {{ quote .Values.mongoRegisterExpress.mongoRegisterExpress.env.meConfigMongodbAdminusername - }} - - name: ME_CONFIG_MONGODB_URL - value: {{ quote .Values.mongoRegisterExpress.mongoRegisterExpress.env.meConfigMongodbUrl }} - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - image: {{ .Values.mongoRegisterExpress.mongoRegisterExpress.image.repository }}:{{ .Values.mongoRegisterExpress.mongoRegisterExpress.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.mongoRegisterExpress.mongoRegisterExpress.image.imagePullPolicy }} - name: mongo-register-express - ports: - - containerPort: 8081 - resources: - {{- toYaml .Values.mongoRegisterExpress.mongoRegisterExpress.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 8081 -# initialDelaySeconds: 0 - periodSeconds: 5 - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx - labels: - io.kompose.service: nginx - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.nginx.replicas }} - selector: - matchLabels: - io.kompose.service: nginx - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: nginx - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: CAPIF_HOSTNAME - value: {{ quote .Values.nginx.nginx.env.capifHostname }} - - name: VAULT_HOSTNAME - value: {{ quote .Values.parametersVault.env.vaultHostname }} - - name: VAULT_PORT - value: {{ quote .Values.parametersVault.env.vaultPort }} - - name: VAULT_ACCESS_TOKEN - value: {{ quote .Values.parametersVault.env.vaultAccessToken }} - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - image: {{ .Values.nginx.nginx.image.repository }}:{{ .Values.nginx.nginx.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.nginx.nginx.image.imagePullPolicy }} - name: nginx - ports: - - containerPort: 8080 - - containerPort: 443 - livenessProbe: - tcpSocket: - port: 8080 - initialDelaySeconds: 20 - periodSeconds: 5 -# readinessProbe: -# tcpSocket: -# port: 8080 -# initialDelaySeconds: 60 -# periodSeconds: 5 - resources: - {{- toYaml .Values.nginx.nginx.resources | nindent 12 }} - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: published-apis - labels: - io.kompose.service: published-apis - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.publishedApis.replicas }} - selector: - matchLabels: - io.kompose.service: published-apis - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: published-apis - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - checksum/config: {{ include (print $.Template.BasePath "/capif-published-configmap.yaml") . | sha256sum }} - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - - name: MONITORING - value: {{ quote .Values.publishedApis.publishedApis.env.monitoring }} - image: {{ .Values.publishedApis.publishedApis.image.repository }}:{{ .Values.publishedApis.publishedApis.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.publishedApis.publishedApis.image.imagePullPolicy }} - name: published-apis - ports: - - containerPort: 8080 - volumeMounts: - - name: capif-published-config - mountPath: /usr/src/app/config.yaml - subPath: config.yaml - resources: - {{- toYaml .Values.publishedApis.publishedApis.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 5 - volumes: - - name: capif-published-config - configMap: - name: capif-published-configmap - items: - - key: "config.yaml" - path: "config.yaml" - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: redis - labels: - io.kompose.service: redis - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.redis.replicas }} - selector: - matchLabels: - io.kompose.service: redis - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: redis - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - args: - - redis-server - env: - - name: REDIS_REPLICATION_MODE - value: {{ quote .Values.redis.redis.env.redisReplicationMode }} - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - image: {{ .Values.redis.redis.image.repository }}:{{ .Values.redis.redis.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.redis.redis.image.imagePullPolicy }} - name: redis - ports: - - containerPort: 6379 - resources: - {{- toYaml .Values.redis.redis.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 6379 -# initialDelaySeconds: 5 - periodSeconds: 5 - livenessProbe: - tcpSocket: - port: 6379 - initialDelaySeconds: 5 - periodSeconds: 5 - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: service-apis - labels: - io.kompose.service: service-apis - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert -spec: - replicas: {{ .Values.serviceApis.replicas }} - selector: - matchLabels: - io.kompose.service: service-apis - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - io.kompose.network/services-default: "true" - io.kompose.service: service-apis - {{- include "capif.selectorLabels" . | nindent 8 }} - annotations: - date: "{{ now | unixEpoch }}" - checksum/config: {{ include (print $.Template.BasePath "/capif-service-configmap.yaml") . | sha256sum }} - spec: - hostAliases: - - ip: "{{ .Values.ingress.ip }}" - hostnames: - - "{{ .Values.nginx.nginx.env.capifHostname }}" - containers: - - env: - - name: KUBERNETES_CLUSTER_DOMAIN - value: {{ quote .Values.kubernetesClusterDomain }} - - name: MONITORING - value: {{ quote .Values.serviceApis.serviceApis.env.monitoring }} - image: {{ .Values.serviceApis.serviceApis.image.repository }}:{{ .Values.serviceApis.serviceApis.image.tag | default .Chart.AppVersion }} - imagePullPolicy: {{ .Values.serviceApis.serviceApis.image.imagePullPolicy }} - name: service-apis - ports: - - containerPort: 8080 - volumeMounts: - - name: capif-service-config - mountPath: /usr/src/app/config.yaml - subPath: config.yaml - resources: - {{- toYaml .Values.serviceApis.serviceApis.resources | nindent 12 }} - readinessProbe: - tcpSocket: - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 5 - volumes: - - name: capif-service-config - configMap: - name: capif-service-configmap - items: - - key: "config.yaml" - path: "config.yaml" - restartPolicy: Always diff --git a/helm/capif/templates/logs.yaml b/helm/capif/templates/logs.yaml deleted file mode 100644 index 7382eff..0000000 --- a/helm/capif/templates/logs.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: logs - labels: - io.kompose.service: logs - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.logs.type }} - selector: - io.kompose.service: logs - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.logs.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/mongo-express.yaml b/helm/capif/templates/mongo-express.yaml deleted file mode 100644 index 28d553b..0000000 --- a/helm/capif/templates/mongo-express.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: mongo-express - labels: - io.kompose.service: mongo-express - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.mongoExpress.type }} - selector: - io.kompose.service: mongo-express - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.mongoExpress.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/mongo-pvc.yaml b/helm/capif/templates/mongo-pvc.yaml deleted file mode 100644 index 3c80c14..0000000 --- a/helm/capif/templates/mongo-pvc.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.mongo.persistence.enable "true" }} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - labels: - io.kompose.service: mongo-pvc - name: mongo-pvc -spec: - storageClassName: {{ .Values.mongo.persistence.storageClass }} - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.mongo.persistence.storage }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/mongo-register-express.yaml b/helm/capif/templates/mongo-register-express.yaml deleted file mode 100644 index 5de4b22..0000000 --- a/helm/capif/templates/mongo-register-express.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: mongo-register-express - labels: - io.kompose.service: mongo-register-express - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.mongoRegisterExpress.type }} - selector: - io.kompose.service: mongo-register-express - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.mongoRegisterExpress.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/mongo-register-pvc.yaml b/helm/capif/templates/mongo-register-pvc.yaml deleted file mode 100644 index b5a11d6..0000000 --- a/helm/capif/templates/mongo-register-pvc.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if .Values.mongoRegister.mongo.persistence.enable }} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - labels: - io.kompose.service: mongo-register - name: mongo-register-pvc -spec: - storageClassName: {{ .Values.mongoRegister.mongo.persistence.storageClass }} - accessModes: - - ReadWriteMany - resources: - requests: - storage: {{ .Values.mongoRegister.mongo.persistence.storage }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/mongo-register.yaml b/helm/capif/templates/mongo-register.yaml deleted file mode 100644 index 82b307f..0000000 --- a/helm/capif/templates/mongo-register.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: mongo-register - labels: - io.kompose.service: mongo-register - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.mongoRegister.type }} - selector: - io.kompose.service: mongo-register - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.mongoRegister.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/mongo.yaml b/helm/capif/templates/mongo.yaml deleted file mode 100644 index 8642764..0000000 --- a/helm/capif/templates/mongo.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: mongo - labels: - io.kompose.service: mongo - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.mongo.type }} - selector: - io.kompose.service: mongo - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.mongo.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/nginx-ingress-route.yaml b/helm/capif/templates/nginx-ingress-route.yaml deleted file mode 100644 index 57ca0be..0000000 --- a/helm/capif/templates/nginx-ingress-route.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if eq .Values.nginx.ingressType "IngressRoute" }} ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: nginx-capif-ingress-route -spec: - entryPoints: [web] - routes: - - kind: Rule - match: Host(`{{ .Values.nginx.nginx.env.capifHostname }} && Path(`/ca-root`, `/sign-csr`, `/certdata`, `/register`, `/testdata`, `/getauth`, `/test`)`) - services: - - kind: Service - name: nginx - port: 8080 - scheme: http -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/nginx-ssl-ingress-route.yaml b/helm/capif/templates/nginx-ssl-ingress-route.yaml deleted file mode 100644 index 8c806b6..0000000 --- a/helm/capif/templates/nginx-ssl-ingress-route.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if eq .Values.nginx.ingressType "IngressRoute" }} ---- -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: nginx-ssl-capif-ingress-route -spec: - entryPoints: [web] - routes: - - kind: Rule - match: Host(`{{ .Values.nginx.nginx.env.capifHostname }}`) - services: - - kind: Service - name: nginx - port: 443 - tls: - passthrough: true -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/nginx-ssl-route.yaml b/helm/capif/templates/nginx-ssl-route.yaml deleted file mode 100644 index 3e24b72..0000000 --- a/helm/capif/templates/nginx-ssl-route.yaml +++ /dev/null @@ -1,22 +0,0 @@ -{{- if eq .Values.env "openshift" }} -apiVersion: route.openshift.io/v1 -kind: Route -metadata: - labels: - name: nginx-ssl -spec: - host: {{ .Values.nginx.nginx.env.capifHostname }} - port: - targetPort: "443" - tls: - termination: passthrough - to: - kind: Service - name: nginx - weight: 100 -status: - ingress: - - conditions: - host: {{ .Values.nginx.nginx.env.capifHostname }} - routerCanonicalHostname: router-default.apps.ocp-epg.hi.inet -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/nginx-ssl.yaml b/helm/capif/templates/nginx-ssl.yaml deleted file mode 100644 index 39487d5..0000000 --- a/helm/capif/templates/nginx-ssl.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if eq .Values.nginx.ingressType "Ingress" }} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: nginx-register - labels: - {{- include "capif.labels" . | nindent 4 }} - {{- with .Values.nginx.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - cert-manager.io/issuer: letsencrypt-issuer - {{- end }} -spec: -{{- if .Values.nginx.ingressClassName }} - ingressClassName: {{ .Values.nginx.ingressClassName }} -{{- end }} - rules: - - host: "{{ .Values.nginx.nginx.env.registerHostname }}" - http: - paths: - - backend: - service: - name: 'register' - port: - number: 8084 - path: / - pathType: Prefix - tls: - - hosts: - - "{{ .Values.nginx.nginx.env.registerHostname }}" - secretName: letsencrypt-secret -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/nginx.yaml b/helm/capif/templates/nginx.yaml deleted file mode 100644 index 61856f5..0000000 --- a/helm/capif/templates/nginx.yaml +++ /dev/null @@ -1,48 +0,0 @@ -{{- if eq .Values.nginx.ingressType "Ingress" }} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: nginx - labels: - {{- include "capif.labels" . | nindent 4 }} - {{- with .Values.nginx.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" - nginx.ingress.kubernetes.io/ssl-passthrough: "true" - nginx.ingress.kubernetes.io/ssl-redirect: "true" - {{- end }} -spec: -{{- if .Values.nginx.ingressClassName }} - ingressClassName: {{ .Values.nginx.ingressClassName }} -{{- end }} - rules: - - host: "{{ .Values.nginx.nginx.env.capifHostname }}" - http: - paths: - - backend: - service: - name: 'nginx' - port: - number: 443 - path: / - pathType: Prefix -{{- end }} ---- -apiVersion: v1 -kind: Service -metadata: - name: nginx - labels: - io.kompose.service: nginx - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.nginx.type }} - selector: - io.kompose.service: nginx - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.nginx.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/published-apis.yaml b/helm/capif/templates/published-apis.yaml deleted file mode 100644 index a5444f1..0000000 --- a/helm/capif/templates/published-apis.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: published-apis - labels: - io.kompose.service: published-apis - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.publishedApis.type }} - selector: - io.kompose.service: published-apis - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.publishedApis.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/redis.yaml b/helm/capif/templates/redis.yaml deleted file mode 100644 index 3254a95..0000000 --- a/helm/capif/templates/redis.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: redis - labels: - io.kompose.service: redis - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.redis.type }} - selector: - io.kompose.service: redis - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.redis.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/templates/register.yaml b/helm/capif/templates/register.yaml deleted file mode 100644 index 2de1d64..0000000 --- a/helm/capif/templates/register.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if eq .Values.register.enable "true" }} -apiVersion: v1 -kind: Service -metadata: - name: register - labels: - io.kompose.service: register - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.register.type }} - selector: - io.kompose.service: register - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.register.ports | toYaml | nindent 2 -}} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/service-apis.yaml b/helm/capif/templates/service-apis.yaml deleted file mode 100644 index bff1af5..0000000 --- a/helm/capif/templates/service-apis.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: service-apis - labels: - io.kompose.service: service-apis - {{- include "capif.labels" . | nindent 4 }} - annotations: - kompose.cmd: kompose -f ../services/docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) -spec: - type: {{ .Values.serviceApis.type }} - selector: - io.kompose.service: service-apis - {{- include "capif.selectorLabels" . | nindent 4 }} - ports: - {{- .Values.serviceApis.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index 0a50782..67c9e42 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -1,514 +1,5 @@ -# -- The Environment variable. Use openshift if you are deploying in Openshift cluster. anotherwise use the field empty -env: "" - -# Use the Ip address dude for the kubernetes to your Ingress Controller ej: kubectl -n NAMESPACE_CAPIF get ing -ingress: - ip: "10.17.173.127" - monitoring: enable: "true" - -accessControlPolicy: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/access-control-policy" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If env.monitoring: true. Setup monitoring.enable: true - env: - monitoring: "true" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - replicas: 1 - type: ClusterIP - -apiInvocationLogs: - apiInvocationLogs: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/api-invocation-logs-api" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If env.monitoring: true. Setup monitoring.enable: true - env: - monitoring: "true" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - replicas: 1 - type: ClusterIP -apiInvokerManagement: - apiInvokerManagement: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/api-invoker-management-api" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If env.monitoring: true. Setup monitoring.enable: true - env: - monitoring: "true" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - replicas: 1 - type: ClusterIP -apiProviderManagement: - apiProviderManagement: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/api-provider-management-api" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If env.monitoring: true. Setup monitoring.enable: true - env: - monitoring: "true" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - replicas: 1 - type: ClusterIP -capifEvents: - capifEvents: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/events-api" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If env.monitoring: true. Setup monitoring.enable: true - env: - monitoring: "true" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - replicas: 1 - type: ClusterIP -capifRoutingInfo: - capifRoutingInfo: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/routing-info-api" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If env.monitoring: true. Setup monitoring.enable: true - env: - monitoring: "true" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - replicas: 1 - type: ClusterIP -capifSecurity: - capifSecurity: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/security-api" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If env.monitoring: true. Setup monitoring.enable: true - env: - monitoring: "true" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - replicas: 1 - type: ClusterIP -register: - # -- If register enabled. enable: true, enable: "" = not enabled - enable: "true" - register: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/register" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - env: - mongoHost: mongo-register - mongoPort: 27017 - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8084 - targetPort: 8080 - replicas: 1 - type: ClusterIP -mongoRegister: - mongo: - env: - # User's password MongoDB - mongoInitdbRootPassword: example - # Name of User's mongodb - mongoInitdbRootUsername: root - image: - # -- The docker image repository to use - repository: "mongo" - # -- The docker image tag to use - # @default Chart version - tag: "6.0.2" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If mongoRegister.mongo.persistence enabled. enable: true, enable: false is = not enabled - persistence: - enable: true - storage: 8Gi - storageClass: nfs-01 - resources: {} -# limits: -# cpu: 100m -# memory: 128Mi -# requests: -# cpu: 100m -# memory: 128Mi - ports: - - name: "27017" - port: 27017 - targetPort: 27017 - replicas: 1 - type: ClusterIP - -kubernetesClusterDomain: cluster.local -logs: - # -- If register enabled. enable: true, enable: "" = not enabled - enable: "true" - logs: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/auditing-api" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If env.monitoring: true. Setup monitoring.enable: true - env: - monitoring: "true" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - replicas: 1 - type: ClusterIP -mongo: - mongo: - env: - # User's password MongoDB - mongoInitdbRootPassword: example - # Name of User's mongodb - mongoInitdbRootUsername: root - image: - # -- The docker image repository to use - repository: "mongo" - # -- The docker image tag to use - # @default Chart version - tag: "6.0.2" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - resources: {} -# limits: -# cpu: 100m -# memory: 128Mi -# requests: -# cpu: 100m -# memory: 128Mi - ports: - - name: "27017" - port: 27017 - targetPort: 27017 - replicas: 1 - type: ClusterIP - # -- If mongo.persistence enabled. enable: true, enable: "" = not enabled - persistence: - enable: "true" - storage: 8Gi - storageClass: nfs-01 -mongoExpress: - mongoExpress: - env: - # User's password MongoDB - meConfigMongodbAdminpassword: example - # Name of User's mongodb - meConfigMongodbAdminusername: root - # URI for connecting MongoDB - meConfigMongodbUrl: mongodb://root:example@mongo:27017/ - image: - # -- The docker image repository to use - repository: "mongo-express" - # -- The docker image tag to use - # @default Chart version - tag: "1.0.0-alpha.4" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8082" - port: 8082 - targetPort: 8081 - replicas: 1 - type: ClusterIP -mongoRegisterExpress: - mongoRegisterExpress: - env: - # User's password MongoDB - meConfigMongodbAdminpassword: example - # Name of User's mongodb - meConfigMongodbAdminusername: root - # URI for connecting MongoDB - meConfigMongodbUrl: mongodb://root:example@mongo-register:27017/ - image: - # -- The docker image repository to use - repository: "mongo-express" - # -- The docker image tag to use - # @default Chart version - tag: "1.0.0-alpha.4" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8082" - port: 8082 - targetPort: 8081 - replicas: 1 - type: ClusterIP -nginx: - # -- if nginx.ingressType: "Ingress". set up monitoring.prometheus.ingress: true - # and monitoring.grafana.ingress: true - # Use IngressRoute if you want to use Gateway API. ex traefix - ingressType: "Ingress" - ingressClassName: nginx - annotations: - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" - nginx.ingress.kubernetes.io/ssl-passthrough: "true" - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx: - env: - # -- Ingress's host to Capif - capifHostname: "my-capif.apps.ocp-epg.hi.inet" - registerHostname: "register.app.ocp-epg.hi.inet" - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/nginx" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - - name: "443" - port: 443 - targetPort: 443 - replicas: 1 - type: ClusterIP -publishedApis: - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - publishedApis: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/publish-service-api" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If env.monitoring: true. Setup monitoring.enable: true - env: - monitoring: "true" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - replicas: 1 - type: ClusterIP -redis: - ports: - - name: "6379" - port: 6379 - targetPort: 6379 - redis: - env: - # Mode of replication - redisReplicationMode: master - image: - # -- The docker image repository to use - repository: "redis" - # -- The docker image tag to use - # @default Chart version - tag: "alpine" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - replicas: 1 - type: ClusterIP -serviceApis: - ports: - - name: "8080" - port: 8080 - targetPort: 8080 - replicas: 1 - serviceApis: - image: - # -- The docker image repository to use - repository: "public.ecr.aws/o2v4a8t6/opencapif/discover-service-api" - # -- The docker image tag to use - # @default Chart version - tag: "" - # -- Image pull policy: Always, IfNotPresent - imagePullPolicy: Always - # -- If env.monitoring: true. Setup monitoring.enable: true - env: - monitoring: "true" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - type: ClusterIP -parametersVault: - env: - vaultHostname: vault-internal.mon.svc.cluster.local - vaultPort: 8200 - vaultAccessToken: dev-only-token - -helper: - env: - vaultHostname: vault-internal.mon.svc.cluster.local - vaultPort: 8200 - vaultAccessToken: dev-only-token - mongoHost: mongo - mongoPort: 27017 - capifHostname: my-capif.apps.ocp-epg.hi.inet - mongoInitdbRootUsername: root - mongoInitdbRootPassword: example # -- With tempo.enabled: false. It won't be deployed # -- If monitoring.enable: "true". Also enable tempo.enabled: true -- GitLab From 16606cf85dbb4d26182e6232e5ff18429cd6522b Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 13 Jun 2024 18:01:35 +0200 Subject: [PATCH 260/310] mock_server --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index 67c9e42..5fcf7f2 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -1,6 +1,6 @@ monitoring: enable: "true" - + # -- With tempo.enabled: false. It won't be deployed # -- If monitoring.enable: "true". Also enable tempo.enabled: true tempo: -- GitLab From 7fd449ba7267d0fccefc34c572467847f28bb2c2 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 13 Jun 2024 18:18:04 +0200 Subject: [PATCH 261/310] appVersion --- helm/capif/values.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index 5fcf7f2..16b1068 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -1,5 +1,6 @@ monitoring: enable: "true" + # -- With tempo.enabled: false. It won't be deployed # -- If monitoring.enable: "true". Also enable tempo.enabled: true -- GitLab From 9609a2ffbc943ea3b107ad9db0ea0da71fcb38ff Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 13 Jun 2024 18:20:03 +0200 Subject: [PATCH 262/310] ocf-helper --- helm/capif/values.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index 16b1068..7d5802b 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -1,7 +1,6 @@ monitoring: enable: "true" - # -- With tempo.enabled: false. It won't be deployed # -- If monitoring.enable: "true". Also enable tempo.enabled: true tempo: -- GitLab From 407ee3259add938b5977e717ead90c7a72a7e17d Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Thu, 13 Jun 2024 18:23:51 +0200 Subject: [PATCH 263/310] monitoring.grafana --- helm/capif/values.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index 7d5802b..9a98669 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -12,6 +12,7 @@ tempo: persistence: enabled: true size: 3Gi + monitoring: # -- If monitoring enabled. enable: true, enable: "" = not enabled enable: "true" -- GitLab From 02603ca5e049837477faa7f9280de539916c0eff Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 14 Jun 2024 10:21:28 +0200 Subject: [PATCH 264/310] helm upgrade --- helm/capif/values.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index 9a98669..a2c7347 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -177,4 +177,3 @@ monitoring: ingressRoute: enable: "" host: grafana.5gnacar.int - -- GitLab From 58b38bde93d0e9827661594a48234d19211ef5c3 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 14 Jun 2024 10:29:28 +0200 Subject: [PATCH 265/310] confimap.yaml ocf-register --- helm/capif/charts/ocf-register/templates/configmap.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helm/capif/charts/ocf-register/templates/configmap.yaml b/helm/capif/charts/ocf-register/templates/configmap.yaml index d927eba..cf801f4 100644 --- a/helm/capif/charts/ocf-register/templates/configmap.yaml +++ b/helm/capif/charts/ocf-register/templates/configmap.yaml @@ -9,6 +9,7 @@ data: 'password': 'example', 'db': 'capif_users', 'col': 'user', + 'admins': 'admins', 'host': '{{ .Values.env.mongoHost }}', 'port': '{{ .Values.env.mongoPort }}' } @@ -21,5 +22,6 @@ data: register_uuid: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', refresh_expiration: 30, #days token_expiration: 10, #mins - admin_users: {admin: "password123"} + admin_users: {admin_user: "admin", + admin_pass: "password123"} } \ No newline at end of file -- GitLab From 2bb3356f0fe836ada2cc115b0c6d79e4c9f65cfa Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 14 Jun 2024 10:37:39 +0200 Subject: [PATCH 266/310] <<: *dev_common --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index a2c7347..d9c8a80 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -176,4 +176,4 @@ monitoring: # -- If ingressRoute enable=true, use monitoring.grafana.ingress.enabled="" ingressRoute: enable: "" - host: grafana.5gnacar.int + host: grafana.5gnacar.int \ No newline at end of file -- GitLab From a383dbcbded9c418a3bffb133e760468851361d6 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 14 Jun 2024 10:49:32 +0200 Subject: [PATCH 267/310] ocf-pre-staging --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index d9c8a80..a2c7347 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -176,4 +176,4 @@ monitoring: # -- If ingressRoute enable=true, use monitoring.grafana.ingress.enabled="" ingressRoute: enable: "" - host: grafana.5gnacar.int \ No newline at end of file + host: grafana.5gnacar.int -- GitLab From 60e6c3d46f164759842958519375b433fe43b0ca Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Fri, 14 Jun 2024 11:40:12 +0200 Subject: [PATCH 268/310] fix --- services/register/config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/services/register/config.yaml b/services/register/config.yaml index 182818b..258821f 100644 --- a/services/register/config.yaml +++ b/services/register/config.yaml @@ -12,6 +12,7 @@ ca_factory: { "port": "8200", "token": "dev-only-token" } + ccf: { "url": "capifcore", "helper_remove_user": "/helper/deleteEntities/" -- GitLab From 40c0ed052fc15c88a4be17ffb52575584de5fceb Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 14 Jun 2024 11:54:11 +0200 Subject: [PATCH 269/310] ocf-register.env.capifHostname --- helm/capif/charts/ocf-register/templates/configmap.yaml | 4 ++++ helm/capif/charts/ocf-register/values.yaml | 1 + 2 files changed, 5 insertions(+) diff --git a/helm/capif/charts/ocf-register/templates/configmap.yaml b/helm/capif/charts/ocf-register/templates/configmap.yaml index cf801f4..22214a8 100644 --- a/helm/capif/charts/ocf-register/templates/configmap.yaml +++ b/helm/capif/charts/ocf-register/templates/configmap.yaml @@ -18,6 +18,10 @@ data: "port": "{{ .Values.env.vaultPort }}", "token": "{{ .Values.env.vaultAccessToken }}" } + ccf: { + "url": "{{ .Values.env.capifHostname }}", + "helper_remove_user": "/helper/deleteEntities/" + } register: { register_uuid: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', refresh_expiration: 30, #days diff --git a/helm/capif/charts/ocf-register/values.yaml b/helm/capif/charts/ocf-register/values.yaml index b9e9e49..5605ef8 100644 --- a/helm/capif/charts/ocf-register/values.yaml +++ b/helm/capif/charts/ocf-register/values.yaml @@ -20,6 +20,7 @@ env: vaultHostname: vault vaultPort: 8200 vaultAccessToken: dev-only-token + capifHostname: capif-test.example.int serviceAccount: # Specifies whether a service account should be created -- GitLab From d1d7f7eae34064f5c452a824bed6f7d968181000 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 14 Jun 2024 12:09:27 +0200 Subject: [PATCH 270/310] ocf-mon- --- helm/capif/charts/ocf-register/templates/configmap.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/charts/ocf-register/templates/configmap.yaml b/helm/capif/charts/ocf-register/templates/configmap.yaml index 22214a8..0c01aed 100644 --- a/helm/capif/charts/ocf-register/templates/configmap.yaml +++ b/helm/capif/charts/ocf-register/templates/configmap.yaml @@ -28,4 +28,4 @@ data: token_expiration: 10, #mins admin_users: {admin_user: "admin", admin_pass: "password123"} - } \ No newline at end of file + } -- GitLab From 203841f7e8d12e6a0e7cc740551f4f166329cb42 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 14 Jun 2024 12:15:25 +0200 Subject: [PATCH 271/310] ocf-pre-staging env ocf-register --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index a2c7347..d9c8a80 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -176,4 +176,4 @@ monitoring: # -- If ingressRoute enable=true, use monitoring.grafana.ingress.enabled="" ingressRoute: enable: "" - host: grafana.5gnacar.int + host: grafana.5gnacar.int \ No newline at end of file -- GitLab From fa88d4a4983f5d661acb4f30b3e751a4115086bc Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 14 Jun 2024 12:21:37 +0200 Subject: [PATCH 272/310] ocf-pre-staging --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index d9c8a80..a2c7347 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -176,4 +176,4 @@ monitoring: # -- If ingressRoute enable=true, use monitoring.grafana.ingress.enabled="" ingressRoute: enable: "" - host: grafana.5gnacar.int \ No newline at end of file + host: grafana.5gnacar.int -- GitLab From 06fceaa4d657c921f98d6f9ff1a8f6be2177d727 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Wed, 19 Jun 2024 08:50:10 +0200 Subject: [PATCH 273/310] Register Logs --- services/helper/helper_service/__main__.py | 10 +++++++++ .../register/register_service/__main__.py | 16 ++++++++++++++ .../controllers/register_controller.py | 22 +++++++++++++++++-- .../core/register_operations.py | 10 ++++++++- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/services/helper/helper_service/__main__.py b/services/helper/helper_service/__main__.py index 69411aa..3712422 100644 --- a/services/helper/helper_service/__main__.py +++ b/services/helper/helper_service/__main__.py @@ -9,6 +9,8 @@ import requests app = Flask(__name__) config = Config().get_config() +print(f"Creating superadmin CSR and keys...") + # Create a superadmin CSR and keys key = PKey() key.generate_key(TYPE_RSA, 2048) @@ -30,6 +32,8 @@ key_file = open("helper_service/certs/superadmin.key", 'wb+') key_file.write(bytes(private_key)) key_file.close() +print(f"Requesting superadmin certificate...") + # Request superadmin certificate url = 'http://{}:{}/v1/pki_int/sign/my-ca'.format(config["ca_factory"]["url"], config["ca_factory"]["port"]) headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} @@ -48,6 +52,8 @@ cert_file = open("helper_service/certs/superadmin.crt", 'wb') cert_file.write(bytes(superadmin_cert, 'utf-8')) cert_file.close() +print(f"Requesting ca_root certificate...") + url = f"http://{config['ca_factory']['url']}:{config['ca_factory']['port']}/v1/secret/data/ca" headers = { @@ -60,8 +66,12 @@ cert_file = open("helper_service/certs/ca_root.crt", 'wb') cert_file.write(bytes(ca_root, 'utf-8')) cert_file.close() +print(f"Registering blueprint...") + app.register_blueprint(helper_routes) app.logger.setLevel(logging.DEBUG) +print(f"Starting helper!") + if __name__ == '__main__': app.run(host='0.0.0.0', port=8080, debug=True) \ No newline at end of file diff --git a/services/register/register_service/__main__.py b/services/register/register_service/__main__.py index aeab955..0ccd973 100644 --- a/services/register/register_service/__main__.py +++ b/services/register/register_service/__main__.py @@ -3,6 +3,7 @@ from .controllers.register_controller import register_routes from flask_jwt_extended import JWTManager from OpenSSL.crypto import PKey, TYPE_RSA, X509Req, dump_certificate_request, FILETYPE_PEM, dump_privatekey import requests +import logging import json from .config import Config from .db.db import MongoDatabse @@ -14,6 +15,8 @@ jwt_manager = JWTManager(app) config = Config().get_config() +print(f"Creating superadmin CSR and keys...") + # Create a superadmin CSR and keys key = PKey() key.generate_key(TYPE_RSA, 2048) @@ -35,6 +38,8 @@ key_file = open("register_service/certs/superadmin.key", 'wb+') key_file.write(bytes(private_key)) key_file.close() +print(f"Requesting superadmin certificate...") + # Request superadmin certificate url = 'http://{}:{}/v1/pki_int/sign/my-ca'.format(config["ca_factory"]["url"], config["ca_factory"]["port"]) headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} @@ -53,6 +58,8 @@ cert_file = open("register_service/certs/superadmin.crt", 'wb') cert_file.write(bytes(superadmin_cert, 'utf-8')) cert_file.close() +print(f"Requesting ca_root certificate...") + url = f"http://{config['ca_factory']['url']}:{config['ca_factory']['port']}/v1/secret/data/ca" headers = { @@ -65,6 +72,8 @@ cert_file = open("register_service/certs/ca_root.crt", 'wb') cert_file.write(bytes(ca_root, 'utf-8')) cert_file.close() +print(f"Requesting CAPIF private key...") + # Request CAPIF private key to encode the CAPIF token url = 'http://{}:{}/v1/secret/data/server_cert/private'.format(config["ca_factory"]["url"], config["ca_factory"]["port"]) headers = {'X-Vault-Token': f"{config["ca_factory"]["token"]}"} @@ -72,9 +81,11 @@ response = requests.request("GET", url, headers=headers, verify = False) key_data = json.loads(response.text)["data"]["data"]["key"] +print(f"Checking initial administrator...") # Create an Admin in the Admin Collection client = MongoDatabse() if not client.get_col_by_name(client.capif_admins).find_one({"admin_name": config["register"]["admin_users"]["admin_user"], "admin_pass": config["register"]["admin_users"]["admin_pass"]}): + print(f"\nInitial administrato not found, registering: ({config["register"]["admin_users"]["admin_user"]}, {config["register"]["admin_users"]["admin_pass"]})\n") client.get_col_by_name(client.capif_admins).insert_one({"admin_name": config["register"]["admin_users"]["admin_user"], "admin_pass": config["register"]["admin_users"]["admin_pass"]}) @@ -82,7 +93,12 @@ app.config['JWT_ALGORITHM'] = 'RS256' app.config['JWT_PRIVATE_KEY'] = key_data app.config['REGISTRE_SECRET_KEY'] = config["register"]["register_uuid"] +print(f"Registering blueprint...") + app.register_blueprint(register_routes) +app.logger.setLevel(logging.DEBUG) + +print(f"Starting Register!") #---------------------------------------- # launch diff --git a/services/register/register_service/controllers/register_controller.py b/services/register/register_service/controllers/register_controller.py index ded8e8b..29cad57 100644 --- a/services/register/register_service/controllers/register_controller.py +++ b/services/register/register_service/controllers/register_controller.py @@ -19,6 +19,7 @@ register_operation = RegisterOperations() # Function to generate access tokens and refresh tokens def generate_tokens(username): + current_app.logger.debug(f"generating admin tokens...") access_payload = { 'username': username, 'exp': datetime.now() + timedelta(minutes=config["register"]["token_expiration"]) @@ -29,18 +30,22 @@ def generate_tokens(username): } access_token = jwt.encode(access_payload, current_app.config['REGISTRE_SECRET_KEY'], algorithm='HS256') refresh_token = jwt.encode(refresh_payload, current_app.config['REGISTRE_SECRET_KEY'], algorithm='HS256') + current_app.logger.debug(f"Access token : {access_token}\nRefresh token : {refresh_token}") return access_token, refresh_token # Function in charge of verifying the basic auth @auth.verify_password def verify_password(username, password): + current_app.logger.debug("Checking user credentials...") users = register_operation.get_users()[0].json["users"] client = MongoDatabse() admin = client.get_col_by_name(client.capif_admins).find_one({"admin_name": username, "admin_pass": password}) if admin: + current_app.logger.debug(f"Verified admin {username}") return username, "admin" for user in users: if user["username"] == username and user["password"]==password: + current_app.logger.debug(f"Verified user {username}") return username, "client" # Function responsible for verifying the token @@ -48,15 +53,18 @@ def admin_required(): def decorator(f): @wraps(f) def decorated(*args, **kwargs): - + current_app.logger.debug("Checking admin token...") token = request.headers.get('Authorization') if not token: + current_app.logger.debug("Token is missing.") return jsonify({'message': 'Token is missing'}), 401 if token.startswith('Bearer '): + current_app.logger.debug("Token is missing.") token = token.split('Bearer ')[1] if not token: + current_app.logger.debug("Token is missing.") return jsonify({'message': 'Token is missing'}), 401 try: @@ -64,6 +72,7 @@ def admin_required(): username = data['username'] return f(username, *args, **kwargs) except Exception as e: + current_app.logger.debug(f"Error: {str(e)}.") return jsonify({'message': str(e)}), 401 return decorated @@ -74,6 +83,7 @@ def admin_required(): def login(): username, rol = auth.current_user() if rol != "admin": + current_app.logger.debug(f"User {username} trying to log in as admin") return jsonify(message="Unauthorized. Administrator privileges required."), 401 access_token, refresh_token = generate_tokens(username) return jsonify({'access_token': access_token, 'refresh_token': refresh_token}) @@ -81,12 +91,14 @@ def login(): @register_routes.route('/refresh', methods=['POST']) @admin_required() def refresh_token(username): + current_app.logger.debug(f"Refreshing token for admin {username}") access_token, _ = generate_tokens(username) return jsonify({'access_token': access_token}) @register_routes.route("/createUser", methods=["POST"]) @admin_required() def register(username): + current_app.logger.debug(f"Admin {username} creating a user...") required_fields = { "username": str, "password": str, @@ -103,21 +115,24 @@ def register(username): } user_info = request.get_json() - + current_app.logger.debug(f"User Info: {user_info}") missing_fields = [] for field, field_type in required_fields.items(): if field not in user_info: missing_fields.append(field) elif not isinstance(user_info[field], field_type): + current_app.logger.debug(f"Error: Field {field} must be of type {field_type.__name__}") return jsonify({"error": f"Field '{field}' must be of type {field_type.__name__}"}), 400 for field, field_type in optional_fields.items(): if field in user_info and not isinstance(user_info[field], field_type): + current_app.logger.debug(f"Error: Field {field} must be of type {field_type.__name__}") return jsonify({"error": f"Optional field '{field}' must be of type {field_type.__name__}"}), 400 if field not in user_info: user_info[field] = None if missing_fields: + current_app.logger.debug(f"Error: missing requuired fields : {missing_fields}") return jsonify({"error": "Missing required fields", "fields": missing_fields}), 400 return register_operation.register_user(user_info) @@ -126,15 +141,18 @@ def register(username): @auth.login_required def getauth(): username, _ = auth.current_user() + current_app.logger.debug(f"Obtaining authorization for the user {username}") return register_operation.get_auth(username) @register_routes.route("/deleteUser/", methods=["DELETE"]) @admin_required() def remove(username, uuid): + current_app.logger.debug(f"Deleting user with id {uuid} by admin {username}") return register_operation.remove_user(uuid) @register_routes.route("/getUsers", methods=["GET"]) @admin_required() def getUsers(username): + current_app.logger.debug(f"Returning list of users to admin {username}") return register_operation.get_users() diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index 5082d51..edf201e 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -18,15 +18,19 @@ class RegisterOperations: mycol = self.db.get_col_by_name(self.db.capif_users) exist_user = mycol.find_one({"username": user_info["username"]}) if exist_user: + current_app.logger.debug(f"User already exists : {user_info["username"]}") return jsonify("user already exists"), 409 name_space = uuid.UUID(self.config["register"]["register_uuid"]) user_uuid = str(uuid.uuid5(name_space,user_info["username"])) + current_app.logger.debug(f"User uuid : {user_uuid}") user_info["uuid"] = user_uuid user_info["onboarding_date"]=datetime.now() obj = mycol.insert_one(user_info) + current_app.logger.debug(f"User with uuid {user_uuid} and username {user_info["username"]} registered successfully") + return jsonify(message="User registered successfully", uuid=user_uuid), 201 def get_auth(self, username): @@ -38,14 +42,18 @@ class RegisterOperations: exist_user = mycol.find_one({"username": username}) if exist_user is None: + current_app.logger.debug(f"Not exister user with this credentials : {username}") return jsonify("Not exister user with this credentials"), 400 access_token = create_access_token(identity=(username + " " + exist_user["uuid"])) + current_app.logger.debug(f"Access token generated for user {username} : {access_token}") cert_file = open("register_service/certs/ca_root.crt", 'rb') ca_root = cert_file.read() cert_file.close() + current_app.logger.debug(f"Returning the requested information...") + return jsonify(message="Token and CA root returned successfully", access_token=access_token, ca_root=ca_root.decode("utf-8"), @@ -67,7 +75,7 @@ class RegisterOperations: requests.delete(url, cert=("register_service/certs/superadmin.crt", "register_service/certs/superadmin.key"), verify="register_service/certs/ca_root.crt") mycol.delete_one({"uuid": uuid}) - + current_app.logger.debug(f"User with uuid {uuid} removed successfully") return jsonify(message="User removed successfully"), 204 except Exception as e: return jsonify(message=f"Errors when try remove user: {e}"), 500 -- GitLab From 14b221464902f7146f7d3c6d3542a0be9190efb6 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Thu, 20 Jun 2024 12:51:55 +0300 Subject: [PATCH 274/310] Resolve review comment: Remove commented code --- .../api_invoker_management/app.py | 7 +------ .../prepare_invoker.sh | 2 -- .../api_provider_management/core/responses.py | 4 ---- .../capif_acl/app.py | 4 ---- .../capif_acl/core/internal_service_ops.py | 3 --- .../capif_acl/wsgi.py | 1 - services/TS29222_CAPIF_Auditing_API/logs/app.py | 3 --- .../TS29222_CAPIF_Discover_Service_API/service_apis/app.py | 4 ---- services/TS29222_CAPIF_Events_API/capif_events/app.py | 3 --- .../api_invocation_logs/app.py | 3 --- .../published_apis/app.py | 4 ---- .../capif_routing_info/app.py | 7 ------- .../capif_routing_info/controllers/default_controller.py | 5 ----- services/TS29222_CAPIF_Security_API/capif_security/app.py | 6 ------ services/helper/helper_service/app.py | 2 -- 15 files changed, 1 insertion(+), 57 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py index 5d8cc4b..a85597d 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py @@ -135,9 +135,4 @@ subscriber = Subscriber() @app.app.before_first_request def create_listener_message(): - executor.submit(subscriber.listen) - -# if __name__ == '__main__': -# import logging -# app.run(debug=True, port=8080) - + executor.submit(subscriber.listen) \ No newline at end of file diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh b/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh index 76843be..df73fa6 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/prepare_invoker.sh @@ -14,7 +14,5 @@ curl -vv -k -retry 30 \ --request GET "$VAULT_ADDR/v1/secret/data/server_cert/pub" 2>/dev/null | jq -r '.data.data.pub_key' -j > /usr/src/app/api_invoker_management/pubkey.pem -#cd /usr/src/app/ -#python3 -m api_invoker_management gunicorn --bind 0.0.0.0:8080 \ --chdir /usr/src/app/api_invoker_management wsgi:app \ No newline at end of file diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py index f047297..4ad6bb8 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py @@ -14,7 +14,6 @@ def make_response(object, status): def internal_server_error(detail, cause): - # prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Internal Server Error", status=500, detail=detail, cause=cause, invalid_params=[{"param":"","reason":""}], supported_features="fffff") prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) prob = prob.to_dict() @@ -29,12 +28,10 @@ def forbidden_error(detail, cause): prob = prob.to_dict() prob = clean_empty(prob) - # return Response(json.dumps(dict_to_camel_case(prob.to_dict()), cls=JSONEncoder), status=403, mimetype=mimetype) return Response(json.dumps(dict_to_camel_case(prob), cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): - # prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params, supported_features="fffff") prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) prob = prob.to_dict() @@ -44,7 +41,6 @@ def bad_request_error(detail, cause, invalid_params): def not_found_error(detail, cause): - # prob = ProblemDetails(type="http://www.ietf.org/rfc/rfc2396.txt", instance="http://www.ietf.org/rfc/rfc2396.txt", title="Not Found", status=404, detail=detail, cause=cause, invalid_params=[{"param":"","reason":""}], supported_features="fffff") prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) prob = prob.to_dict() diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py index da28365..344df6b 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py @@ -139,7 +139,3 @@ scheduler.start() def up_listener(): with scheduler.app.app_context(): executor.submit(subscriber.listen()) - -# if __name__ == '__main__': -# scheduler.start() -# app.run(debug=True,port=8080, use_reloader=False) diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py index 68c7ef5..1e5aa20 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/internal_service_ops.py @@ -5,12 +5,9 @@ from models.api_invoker_policy import ApiInvokerPolicy from models.time_range_list import TimeRangeList from datetime import datetime, timedelta -from core.publisher import Publisher from .redis_event import RedisEvent from util import dict_to_camel_case, clean_empty -publisher_ops = Publisher() - class InternalServiceOps(Resource): diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/wsgi.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/wsgi.py index e72413c..6026b0f 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/wsgi.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/wsgi.py @@ -1,5 +1,4 @@ from app import app if __name__ == "__main__": - # app.scheduler.start() app.run() diff --git a/services/TS29222_CAPIF_Auditing_API/logs/app.py b/services/TS29222_CAPIF_Auditing_API/logs/app.py index b89c1a6..9a94d5a 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/app.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/app.py @@ -111,6 +111,3 @@ configure_logging(app.app) config = Config() if eval(os.environ.get("MONITORING").lower().capitalize()): configure_monitoring(app.app, config.get_config()) - -# if __name__ == '__main__': -# app.run(port=8080) diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py index 4b4c169..d3cbec7 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py @@ -121,7 +121,3 @@ if eval(os.environ.get("MONITORING").lower().capitalize()): configure_monitoring(app.app, config.get_config()) jwt = JWTManager(app.app) - - -# if __name__ == '__main__': -# app.run(debug=True, port=8080) diff --git a/services/TS29222_CAPIF_Events_API/capif_events/app.py b/services/TS29222_CAPIF_Events_API/capif_events/app.py index d72ecc6..ead8b52 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/app.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/app.py @@ -145,6 +145,3 @@ subscriber = Subscriber() @app.app.before_first_request def create_listener_message(): executor.submit(subscriber.listen) - -# if __name__ == '__main__': -# app.run(debug=True, port=8080) diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py index 519cc15..e7b8397 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py @@ -114,6 +114,3 @@ config = Config() if eval(os.environ.get("MONITORING").lower().capitalize()): configure_monitoring(app.app, config.get_config()) - -# if __name__ == '__main__': -# app.run(port=8080) diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py index d3abb2d..d8c3fc5 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py @@ -135,7 +135,3 @@ subscriber = Subscriber() @app.app.before_first_request def up_listener(): executor.submit(subscriber.listen) - - -# if __name__ == '__main__': -# app.run(debug=True, port=8080) diff --git a/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/app.py b/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/app.py index e669f46..8913580 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/app.py +++ b/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/app.py @@ -4,15 +4,8 @@ import connexion import encoder -# def main(): app = connexion.App(__name__, specification_dir='./openapi/') app.app.json_encoder = encoder.JSONEncoder app.add_api('openapi.yaml', arguments={'title': 'CAPIF_Routing_Info_API'}, pythonic_params=True) - -# app.run(port=8080) - - -# if __name__ == '__main__': -# main() diff --git a/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/controllers/default_controller.py b/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/controllers/default_controller.py index 442fa80..13c24a3 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/controllers/default_controller.py @@ -1,9 +1,4 @@ -import connexion -import six - -from ..models.problem_details import ProblemDetails # noqa: E501 from ..models.routing_info import RoutingInfo # noqa: E501 -# from capif_routing_info import util def service_apis_service_api_id_get(service_api_id, aef_id, supp_feat=None): # noqa: E501 diff --git a/services/TS29222_CAPIF_Security_API/capif_security/app.py b/services/TS29222_CAPIF_Security_API/capif_security/app.py index 30fc3a4..975488e 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/app.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/app.py @@ -134,9 +134,3 @@ executor = Executor(app.app) @app.app.before_first_request def up_listener(): executor.submit(subscriber.listen) - - -# -# if __name__ == '__main__': -# main() - diff --git a/services/helper/helper_service/app.py b/services/helper/helper_service/app.py index 2534195..bdc0007 100644 --- a/services/helper/helper_service/app.py +++ b/services/helper/helper_service/app.py @@ -63,5 +63,3 @@ cert_file.close() app.register_blueprint(helper_routes) app.logger.setLevel(logging.DEBUG) -# if __name__ == '__main__': -# app.run(host='0.0.0.0', port=8080, debug=True) \ No newline at end of file -- GitLab From bce1415cf15d090989031764abdf0a50d9744741 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Thu, 20 Jun 2024 15:27:58 +0300 Subject: [PATCH 275/310] Some more commented code --- .../api_provider_management/app.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py index 7eefe68..3769205 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py @@ -105,8 +105,6 @@ def verbose_formatter(): datefmt='%d/%m/%Y %H:%M:%S' ) -# def main(): - with open("/usr/src/app/api_provider_management/pubkey.pem", "rb") as pub_file: pub_data = pub_file.read() @@ -127,10 +125,3 @@ app.app.config['JWT_ALGORITHM'] = 'RS256' app.app.config['JWT_PUBLIC_KEY'] = pub_data JWTManager(app.app) - - -# app.run(port=8080, debug=True) - - -# if __name__ == '__main__': -# main() -- GitLab From 4fd63f005ff3a18dca0feb0606628d556316a3a3 Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Fri, 21 Jun 2024 11:49:36 +0300 Subject: [PATCH 276/310] Add serialize_clean_camel_case function, prepare script for all APIs and fix bug in dict_to_camel_case --- .../core/apiinvokerenrolmentdetails.py | 11 ++------ .../api_invoker_management/core/responses.py | 27 +++++++----------- .../core/validate_user.py | 6 ++-- .../api_invoker_management/util.py | 15 ++++++++-- .../core/provider_enrolment_details_api.py | 10 ++++--- .../api_provider_management/core/responses.py | 26 ++++++----------- .../core/validate_user.py | 7 ++--- .../api_provider_management/util.py | 18 ++++++++++-- .../Dockerfile | 4 +-- .../capif_acl/core/accesscontrolpolicyapi.py | 7 ++--- .../capif_acl/core/responses.py | 22 ++++----------- .../capif_acl/util.py | 15 ++++++++-- .../prepare_capif_acl.sh | 4 +++ .../TS29222_CAPIF_Auditing_API/Dockerfile | 4 +-- .../logs/core/auditoperations.py | 4 +-- .../logs/core/responses.py | 22 ++++----------- .../TS29222_CAPIF_Auditing_API/logs/util.py | 15 ++++++++-- .../prepare_audit.sh | 6 ++++ .../Dockerfile | 4 +-- .../prepare_discover.sh | 6 ++++ .../service_apis/core/discoveredapis.py | 6 ++-- .../service_apis/core/responses.py | 27 +++++++----------- .../service_apis/util.py | 18 ++++++++++-- services/TS29222_CAPIF_Events_API/Dockerfile | 4 +-- .../capif_events/core/events_apis.py | 5 ++-- .../capif_events/core/notifications.py | 4 +-- .../capif_events/core/responses.py | 27 +++++++----------- .../capif_events/core/validate_user.py | 8 ++---- .../capif_events/util.py | 10 +++++-- .../prepare_events.sh | 4 +++ .../Dockerfile | 4 +-- .../core/invocationlogs.py | 4 +-- .../api_invocation_logs/core/responses.py | 27 ++++-------------- .../api_invocation_logs/core/validate_user.py | 6 ++-- .../api_invocation_logs/util.py | 16 +++++++++-- .../prepare_logging.sh | 4 +++ .../Dockerfile | 4 +-- .../prepare_publish.sh | 4 +++ .../published_apis/core/responses.py | 28 +++++-------------- .../core/serviceapidescriptions.py | 4 +-- .../published_apis/core/validate_user.py | 7 ++--- .../published_apis/util.py | 16 +++++++++-- .../TS29222_CAPIF_Routing_Info_API/Dockerfile | 4 +-- .../capif_routing_info/util.py | 1 - .../prepare_routing_info.sh | 4 +++ .../TS29222_CAPIF_Security_API/Dockerfile | 2 +- .../capif_security/core/responses.py | 27 ++++-------------- .../capif_security/core/servicesecurity.py | 8 ++---- .../capif_security/util.py | 17 +++++++++-- ...ecurity_prepare.sh => prepare_security.sh} | 0 services/helper/Dockerfile | 4 +-- services/helper/prepare_helper.sh | 5 ++++ 52 files changed, 278 insertions(+), 264 deletions(-) create mode 100644 services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh create mode 100644 services/TS29222_CAPIF_Auditing_API/prepare_audit.sh create mode 100644 services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh create mode 100644 services/TS29222_CAPIF_Events_API/prepare_events.sh create mode 100644 services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh create mode 100644 services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh create mode 100644 services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh rename services/TS29222_CAPIF_Security_API/{security_prepare.sh => prepare_security.sh} (100%) create mode 100644 services/helper/prepare_helper.sh diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py index 3c3c38b..ee052a4 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py @@ -6,7 +6,7 @@ from .responses import bad_request_error, not_found_error, forbidden_error, inte from flask import current_app, Flask, Response import json from datetime import datetime -from ..util import dict_to_camel_case, clean_empty +from ..util import dict_to_camel_case, clean_empty, serialize_clean_camel_case from .auth_manager import AuthManager from .resources import Resource from ..config import Config @@ -91,7 +91,7 @@ class InvokerManagementOperations(Resource): self.auth_manager.add_auth_invoker(cert['data']['certificate'], api_invoker_id) - res = make_response(object=dict_to_camel_case(clean_empty(apiinvokerenrolmentdetail.to_dict())), status=201) + res = make_response(object=serialize_clean_camel_case(apiinvokerenrolmentdetail), status=201) res.headers['Location'] = "/api-invoker-management/v1/onboardedInvokers/" + str(api_invoker_id) if res.status_code == 201: @@ -99,11 +99,6 @@ class InvokerManagementOperations(Resource): RedisEvent("API_INVOKER_ONBOARDED", "apiInvokerIds", [str(api_invoker_id)]).send_event() return res - # except Exception as e: - # exception = "An exception occurred in create invoker" - # current_app.logger.error(exception + "::" + str(e)) - # return internal_server_error(detail=exception, cause=str(e)) - def update_apiinvokerenrolmentdetail(self, onboard_id, apiinvokerenrolmentdetail): mycol = self.db.get_col_by_name(self.db.invoker_enrolment_details) @@ -135,7 +130,7 @@ class InvokerManagementOperations(Resource): invoker_updated = APIInvokerEnrolmentDetails().from_dict(dict_to_camel_case(result)) - res = make_response(object=dict_to_camel_case(clean_empty(invoker_updated.to_dict())), status=200) + res = make_response(object=serialize_clean_camel_case(invoker_updated), status=200) if res.status_code == 200: current_app.logger.info("Invoker Updated") RedisEvent("API_INVOKER_UPDATED", "apiInvokerIds", [onboard_id]).send_event() diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/responses.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/responses.py index 96ace13..f647dbf 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/responses.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/responses.py @@ -2,47 +2,40 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response import json -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case mimetype = "application/json" + def make_response(object, status): res = Response(json.dumps(object, cls=JSONEncoder), status=status, mimetype=mimetype) return res + def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) + def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) + def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) + def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/validate_user.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/validate_user.py index bcf0d88..b1e7a9b 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/validate_user.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/validate_user.py @@ -4,7 +4,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource from .responses import internal_server_error -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case class ControlAccess(Resource): @@ -20,9 +20,7 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") except Exception as e: diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/util.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/util.py index 27ba971..31af7ab 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/util.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/util.py @@ -4,6 +4,14 @@ import six import typing_utils +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + def clean_empty(d): if isinstance(d, dict): return { @@ -23,8 +31,11 @@ def dict_to_camel_case(my_dict): for attr, value in my_dict.items(): - my_key = ''.join(word.title() for word in attr.split('_')) - my_key= ''.join([my_key[0].lower(), my_key[1:]]) + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key = ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr if isinstance(value, list): result[my_key] = list(map( diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py index 0b54ee8..30760b9 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/provider_enrolment_details_api.py @@ -5,7 +5,7 @@ from flask import current_app, Flask, Response from ..core.sign_certificate import sign_certificate from .responses import internal_server_error, not_found_error, forbidden_error, make_response, bad_request_error from datetime import datetime -from ..util import dict_to_camel_case, clean_empty +from ..util import dict_to_camel_case, clean_empty, serialize_clean_camel_case from .resources import Resource from .auth_manager import AuthManager @@ -64,7 +64,7 @@ class ProviderManagementOperations(Resource): current_app.logger.debug("Provider inserted in database") - res = make_response(object=dict_to_camel_case(api_provider_enrolment_details.to_dict()), status=201) + res = make_response(object=serialize_clean_camel_case(api_provider_enrolment_details), status=201) res.headers['Location'] = "/api-provider-management/v1/registrations/" + str(api_provider_enrolment_details.api_prov_dom_id) return res @@ -138,7 +138,8 @@ class ProviderManagementOperations(Resource): current_app.logger.debug("Provider domain updated in database") provider_updated = APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)) - return make_response(object=dict_to_camel_case(provider_updated.to_dict()), status=200) + + return make_response(object=serialize_clean_camel_case(provider_updated), status=200) except Exception as e: exception = "An exception occurred in update provider" @@ -164,7 +165,8 @@ class ProviderManagementOperations(Resource): current_app.logger.debug("Provider domain updated in database") provider_updated = APIProviderEnrolmentDetails().from_dict(dict_to_camel_case(result)) - return make_response(object=dict_to_camel_case(provider_updated.to_dict()), status=200) + + return make_response(object=serialize_clean_camel_case(provider_updated), status=200) except Exception as e: exception = "An exception occurred in patch provider" diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py index 4ad6bb8..98eca0c 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/responses.py @@ -1,6 +1,6 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case from flask import Response, current_app import json @@ -15,35 +15,27 @@ def make_response(object, status): def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) - prob = prob.to_dict() - prob = clean_empty(prob) - - return Response(json.dumps(dict_to_camel_case(prob), cls=JSONEncoder), status=500, mimetype=mimetype) + return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) - prob = prob.to_dict() - prob = clean_empty(prob) - - return Response(json.dumps(dict_to_camel_case(prob), cls=JSONEncoder), status=403, mimetype=mimetype) + return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) + prob = serialize_clean_camel_case(prob) - prob = prob.to_dict() - prob = clean_empty(prob) - - return Response(json.dumps(dict_to_camel_case(prob), cls=JSONEncoder), status=400, mimetype=cause) + return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) + prob = serialize_clean_camel_case(prob) - prob = prob.to_dict() - prob = clean_empty(prob) - - return Response(json.dumps(dict_to_camel_case(prob), cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file + return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/validate_user.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/validate_user.py index 4a9445d..29043d5 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/validate_user.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/core/validate_user.py @@ -4,7 +4,8 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource from .responses import internal_server_error -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case + class ControlAccess(Resource): @@ -19,9 +20,7 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") except Exception as e: diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py index 6fc44d5..431a28c 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/util.py @@ -2,6 +2,15 @@ import datetime import six import typing_utils + +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + def clean_empty(d): if isinstance(d, dict): return { @@ -13,6 +22,7 @@ def clean_empty(d): return [v for v in map(clean_empty, d) if v] return d + def dict_to_camel_case(my_dict): @@ -20,8 +30,11 @@ def dict_to_camel_case(my_dict): for attr, value in my_dict.items(): - my_key = ''.join(word.title() for word in attr.split('_')) - my_key= ''.join([my_key[0].lower(), my_key[1:]]) + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key = ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr if isinstance(value, list): result[my_key] = list(map( @@ -38,6 +51,7 @@ def dict_to_camel_case(my_dict): return result + def _deserialize(data, klass): """Deserializes dict, list, str into an object. diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/Dockerfile b/services/TS29222_CAPIF_Access_Control_Policy_API/Dockerfile index c69bd56..9c13825 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/Dockerfile +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/Dockerfile @@ -11,6 +11,4 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["gunicorn"] - -CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/capif_acl", "wsgi:app"] \ No newline at end of file +CMD ["sh", "prepare_capif_acl.sh"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/accesscontrolpolicyapi.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/accesscontrolpolicyapi.py index d82ac98..48bf04e 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/accesscontrolpolicyapi.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/accesscontrolpolicyapi.py @@ -2,7 +2,7 @@ from ..core.resources import Resource from flask import current_app from .responses import make_response, not_found_error, internal_server_error from ..models.access_control_policy_list import AccessControlPolicyList -from ..util import dict_to_camel_case, clean_empty +from ..util import dict_to_camel_case, clean_empty, serialize_clean_camel_case class accessControlPolicyApi(Resource): @@ -46,9 +46,8 @@ class accessControlPolicyApi(Resource): return not_found_error(f"No ACLs found for the requested service: {service_api_id}, aef_id: {aef_id}, invoker: {api_invoker_id} and supportedFeatures: {supported_features}", "Wrong id") acl = AccessControlPolicyList(api_invoker_policies) - response = clean_empty(acl.to_dict()) - return make_response(object=dict_to_camel_case(response), status=200) - + return make_response(object=serialize_clean_camel_case(acl), status=200) + except Exception as e: exception = "An exception occurred in get acl" current_app.logger.error(exception + "::" + str(e)) diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/responses.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/responses.py index 9d5ea09..9f8c3e6 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/responses.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/core/responses.py @@ -1,7 +1,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response -from ..util import dict_to_camel_case, clean_empty +from ..util import dict_to_camel_case, clean_empty, serialize_clean_camel_case import json mimetype = "application/json" @@ -15,39 +15,27 @@ def make_response(object, status): def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/util.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/util.py index 2790390..72d18d9 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/util.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/util.py @@ -4,6 +4,14 @@ import six import typing_utils +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + def clean_empty(d): if isinstance(d, dict): return { @@ -23,8 +31,11 @@ def dict_to_camel_case(my_dict): for attr, value in my_dict.items(): - my_key = ''.join(word.title() for word in attr.split('_')) - my_key= ''.join([my_key[0].lower(), my_key[1:]]) + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key = ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr if isinstance(value, list): result[my_key] = list(map( diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh b/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh new file mode 100644 index 0000000..fd0077f --- /dev/null +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/prepare_capif_acl.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/capif_acl wsgi:app \ No newline at end of file diff --git a/services/TS29222_CAPIF_Auditing_API/Dockerfile b/services/TS29222_CAPIF_Auditing_API/Dockerfile index 4908c19..1d1de32 100644 --- a/services/TS29222_CAPIF_Auditing_API/Dockerfile +++ b/services/TS29222_CAPIF_Auditing_API/Dockerfile @@ -12,6 +12,4 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["gunicorn"] - -CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/logs", "wsgi:app"] \ No newline at end of file +CMD ["sh", "prepare_audit.sh"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Auditing_API/logs/core/auditoperations.py b/services/TS29222_CAPIF_Auditing_API/logs/core/auditoperations.py index 6632d28..f74ec11 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/core/auditoperations.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/core/auditoperations.py @@ -4,7 +4,7 @@ import json from .resources import Resource -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case from .responses import bad_request_error, not_found_error, forbidden_error, internal_server_error, make_response from ..models.invocation_log import InvocationLog @@ -57,7 +57,7 @@ class AuditOperations (Resource): invocation_log = InvocationLog(result['aef_id'], result['api_invoker_id'], result['logs'], result['supported_features']) - res = make_response(object=dict_to_camel_case(clean_empty(invocation_log.to_dict())), status=200) + res = make_response(object=serialize_clean_camel_case(invocation_log), status=200) current_app.logger.debug("Found invocation logs") return res diff --git a/services/TS29222_CAPIF_Auditing_API/logs/core/responses.py b/services/TS29222_CAPIF_Auditing_API/logs/core/responses.py index 9d5ea09..89689a9 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/core/responses.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/core/responses.py @@ -1,7 +1,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case import json mimetype = "application/json" @@ -15,39 +15,27 @@ def make_response(object, status): def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Auditing_API/logs/util.py b/services/TS29222_CAPIF_Auditing_API/logs/util.py index ec14301..d5deea1 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/util.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/util.py @@ -4,6 +4,14 @@ import six import typing_utils +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + def clean_empty(d): if isinstance(d, dict): return { @@ -23,8 +31,11 @@ def dict_to_camel_case(my_dict): for attr, value in my_dict.items(): - my_key = ''.join(word.title() for word in attr.split('_')) - my_key= ''.join([my_key[0].lower(), my_key[1:]]) + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key = ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr if my_key == "serviceApiCategory": my_key = "serviceAPICategory" diff --git a/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh b/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh new file mode 100644 index 0000000..e47912b --- /dev/null +++ b/services/TS29222_CAPIF_Auditing_API/prepare_audit.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/logs wsgi:app + + diff --git a/services/TS29222_CAPIF_Discover_Service_API/Dockerfile b/services/TS29222_CAPIF_Discover_Service_API/Dockerfile index 9f1d46e..1478af8 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/Dockerfile +++ b/services/TS29222_CAPIF_Discover_Service_API/Dockerfile @@ -12,6 +12,4 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["gunicorn"] - -CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/service_apis", "wsgi:app"] \ No newline at end of file +CMD ["sh", "prepare_discover.sh"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh b/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh new file mode 100644 index 0000000..dd9ad7d --- /dev/null +++ b/services/TS29222_CAPIF_Discover_Service_API/prepare_discover.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/service_apis wsgi:app + + diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py index 9ad5462..487ceb1 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/discoveredapis.py @@ -2,7 +2,7 @@ from flask import current_app, Flask, Response from ..core.responses import internal_server_error, forbidden_error ,make_response, not_found_error from ..models.discovered_apis import DiscoveredAPIs -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case from ..core.resources import Resource @@ -42,8 +42,8 @@ class DiscoverApisOperations(Resource): if len(json_docs) == 0: return not_found_error(detail="API Invoker " + api_invoker_id + " has no API Published that accomplish filter conditions", cause="No API Published accomplish filter conditions") - apis_discoveres = DiscoveredAPIs(service_api_descriptions=json_docs) - res = make_response(object=dict_to_camel_case(clean_empty(apis_discoveres.to_dict())), status=200) + apis_discovered = DiscoveredAPIs(service_api_descriptions=json_docs) + res = make_response(object=serialize_clean_camel_case(apis_discovered), status=200) return res except Exception as e: diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/responses.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/responses.py index df9905f..b54555b 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/responses.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/core/responses.py @@ -1,48 +1,41 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case import json mimetype = "application/json" + def make_response(object, status): res = Response(json.dumps(object, cls=JSONEncoder), status=status, mimetype=mimetype) return res + def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) + def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) + def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) + def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/util.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/util.py index c39e5fa..cfa4391 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/util.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/util.py @@ -3,6 +3,15 @@ import datetime import six import typing_utils + +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + def clean_empty(d): if isinstance(d, dict): return { @@ -14,6 +23,7 @@ def clean_empty(d): return [v for v in map(clean_empty, d) if v] return d + def dict_to_camel_case(my_dict): @@ -21,8 +31,12 @@ def dict_to_camel_case(my_dict): for attr, value in my_dict.items(): - my_key = ''.join(word.title() for word in attr.split('_')) - my_key= ''.join([my_key[0].lower(), my_key[1:]]) + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key = ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr + if my_key == "serviceApiCategory": my_key = "serviceAPICategory" elif my_key == "serviceApiDescriptions": diff --git a/services/TS29222_CAPIF_Events_API/Dockerfile b/services/TS29222_CAPIF_Events_API/Dockerfile index 1fef3f2..b8ba685 100644 --- a/services/TS29222_CAPIF_Events_API/Dockerfile +++ b/services/TS29222_CAPIF_Events_API/Dockerfile @@ -13,6 +13,4 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["gunicorn"] - -CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/capif_events", "wsgi:app"] \ No newline at end of file +CMD ["sh", "prepare_events.sh"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/events_apis.py b/services/TS29222_CAPIF_Events_API/capif_events/core/events_apis.py index d6b5bdf..14cddb0 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/events_apis.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/events_apis.py @@ -12,9 +12,10 @@ from .resources import Resource from bson import json_util from .responses import internal_server_error, not_found_error, make_response, bad_request_error from ..db.db import MongoDatabse -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case from .auth_manager import AuthManager + class EventSubscriptionsOperations(Resource): def __check_subscriber_id(self, subscriber_id): @@ -73,7 +74,7 @@ class EventSubscriptionsOperations(Resource): self.auth_manager.add_auth_event(subscription_id, subscriber_id) - res = make_response(object=dict_to_camel_case(clean_empty(event_subscription.to_dict())), status=201) + res = make_response(object=serialize_clean_camel_case(event_subscription), status=201) res.headers['Location'] = "http://localhost:8080/capif-events/v1/" + \ str(subscriber_id) + "/subscriptions/" + str(subscription_id) diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py index 3b32d11..e005e7a 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/notifications.py @@ -10,7 +10,7 @@ import json from flask import current_app import asyncio import aiohttp -from util import dict_to_camel_case, clean_empty +from util import dict_to_camel_case, clean_empty, serialize_clean_camel_case class Notifications(): @@ -36,7 +36,7 @@ class Notifications(): data = EventNotification(sub["subscription_id"], events=redis_event.get('event'), event_detail=event_detail) current_app.logger.debug(json.dumps(data.to_dict(),cls=JSONEncoder)) - asyncio.run(self.send(url, dict_to_camel_case(clean_empty(data.to_dict())))) + asyncio.run(self.send(url, serialize_clean_camel_case(data))) except Exception as e: current_app.logger.error("An exception occurred ::" + str(e)) diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/responses.py b/services/TS29222_CAPIF_Events_API/capif_events/core/responses.py index 7862390..89689a9 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/responses.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/responses.py @@ -1,48 +1,41 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case import json mimetype = "application/json" + def make_response(object, status): res = Response(json.dumps(object, cls=JSONEncoder), status=status, mimetype=mimetype) return res + def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) + def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) + def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) + def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Events_API/capif_events/core/validate_user.py b/services/TS29222_CAPIF_Events_API/capif_events/core/validate_user.py index be87def..e7ddee1 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/core/validate_user.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/core/validate_user.py @@ -4,7 +4,8 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource from .responses import internal_server_error -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case + class ControlAccess(Resource): @@ -19,10 +20,7 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature or "event_subscriptions" not in cert_entry["resources"] or event_id not in cert_entry["resources"]["event_subscriptions"]: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") diff --git a/services/TS29222_CAPIF_Events_API/capif_events/util.py b/services/TS29222_CAPIF_Events_API/capif_events/util.py index f067fde..28a9e46 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/util.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/util.py @@ -1,10 +1,17 @@ import datetime import six -import typing import typing_utils +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + def clean_empty(d): if isinstance(d, dict): return { @@ -38,7 +45,6 @@ def dict_to_camel_case(my_dict): return result - def _deserialize(data, klass): """Deserializes dict, list, str into an object. diff --git a/services/TS29222_CAPIF_Events_API/prepare_events.sh b/services/TS29222_CAPIF_Events_API/prepare_events.sh new file mode 100644 index 0000000..62bd7ee --- /dev/null +++ b/services/TS29222_CAPIF_Events_API/prepare_events.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/capif_events wsgi:app diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/Dockerfile b/services/TS29222_CAPIF_Logging_API_Invocation_API/Dockerfile index 4248907..f7cd99b 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/Dockerfile +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/Dockerfile @@ -12,6 +12,4 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["gunicorn"] - -CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/api_invocation_logs", "wsgi:app"] \ No newline at end of file +CMD ["sh", "prepare_logging.sh"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py index 06e597c..bae9c5a 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/invocationlogs.py @@ -5,7 +5,7 @@ import secrets from flask import current_app, Flask, Response from pymongo import ReturnDocument -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case from ..encoder import JSONEncoder from .resources import Resource @@ -122,7 +122,7 @@ class LoggingInvocationOperations(Resource): existing_invocationlog['logs'].append(updated_invocation_log) mycol.find_one_and_update(my_query, {"$set": existing_invocationlog}, projection={'_id': 0, 'log_id': 0}, return_document=ReturnDocument.AFTER, upsert=False) - res = make_response(object=dict_to_camel_case(clean_empty(invocationlog.to_dict())), status=201) + res = make_response(object=serialize_clean_camel_case(invocationlog), status=201) current_app.logger.debug("Invocation Logs response ready") apis_added = {log.api_id:log.api_name for log in invocationlog.logs} diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/responses.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/responses.py index 6940f64..5c9803e 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/responses.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/responses.py @@ -1,7 +1,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case import json mimetype = "application/json" @@ -15,49 +15,34 @@ def make_response(object, status): def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) def unauthorized_error(detail, cause): prob = ProblemDetails(title="Unauthorized", status=401, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/validate_user.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/validate_user.py index 13d8b6a..d9f7d5d 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/validate_user.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/core/validate_user.py @@ -4,7 +4,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource from .responses import internal_server_error -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case class ControlAccess(Resource): @@ -20,9 +20,7 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") except Exception as e: diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/util.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/util.py index ec14301..baaea56 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/util.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/util.py @@ -4,6 +4,14 @@ import six import typing_utils +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + def clean_empty(d): if isinstance(d, dict): return { @@ -23,8 +31,12 @@ def dict_to_camel_case(my_dict): for attr, value in my_dict.items(): - my_key = ''.join(word.title() for word in attr.split('_')) - my_key= ''.join([my_key[0].lower(), my_key[1:]]) + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key = ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr + if my_key == "serviceApiCategory": my_key = "serviceAPICategory" diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh b/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh new file mode 100644 index 0000000..4cd01a6 --- /dev/null +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/prepare_logging.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/api_invocation_logs wsgi:app diff --git a/services/TS29222_CAPIF_Publish_Service_API/Dockerfile b/services/TS29222_CAPIF_Publish_Service_API/Dockerfile index c11e2d6..eca4402 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/Dockerfile +++ b/services/TS29222_CAPIF_Publish_Service_API/Dockerfile @@ -12,6 +12,4 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["gunicorn"] - -CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/published_apis", "wsgi:app"] \ No newline at end of file +CMD ["sh", "prepare_publish.sh"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh b/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh new file mode 100644 index 0000000..d0eed0b --- /dev/null +++ b/services/TS29222_CAPIF_Publish_Service_API/prepare_publish.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/published_apis wsgi:app diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/responses.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/responses.py index c61eae0..3d10ff5 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/responses.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/responses.py @@ -1,7 +1,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case import json @@ -16,48 +16,34 @@ def make_response(object, status): def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) + def unauthorized_error(detail, cause): prob = ProblemDetails(title="Unauthorized", status=401, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py index 41ce03d..f32b7b2 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/serviceapidescriptions.py @@ -4,7 +4,7 @@ from flask import current_app, Flask, Response from .resources import Resource from datetime import datetime -from ..util import dict_to_camel_case, clean_empty +from ..util import dict_to_camel_case, clean_empty, serialize_clean_camel_case from .responses import internal_server_error, forbidden_error, not_found_error, unauthorized_error, make_response from .auth_manager import AuthManager from .redis_event import RedisEvent @@ -110,7 +110,7 @@ class PublishServiceOperations(Resource): current_app.logger.debug("Service inserted in database") - res = make_response(object=dict_to_camel_case(clean_empty(serviceapidescription.to_dict())), status=201) + res = make_response(object=serialize_clean_camel_case(serviceapidescription), status=201) res.headers['Location'] = "http://localhost:8080/published-apis/v1/" + \ str(apf_id) + "/service-apis/" + str(api_id) diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/validate_user.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/validate_user.py index 5eed2c9..f484149 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/validate_user.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/core/validate_user.py @@ -4,7 +4,8 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource from .responses import internal_server_error -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case + class ControlAccess(Resource): @@ -19,9 +20,7 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature or "services" not in cert_entry["resources"] or service_id not in cert_entry["resources"]["services"]: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") except Exception as e: diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/util.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/util.py index c43b274..3d85256 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/util.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/util.py @@ -4,6 +4,14 @@ import six import typing_utils +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + def clean_empty(d): if isinstance(d, dict): return { @@ -15,15 +23,19 @@ def clean_empty(d): return [v for v in map(clean_empty, d) if v] return d + def dict_to_camel_case(my_dict): result = {} for attr, value in my_dict.items(): + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key= ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr - my_key = ''.join(word.title() for word in attr.split('_')) - my_key= ''.join([my_key[0].lower(), my_key[1:]]) if my_key == "serviceApiCategory": my_key = "serviceAPICategory" diff --git a/services/TS29222_CAPIF_Routing_Info_API/Dockerfile b/services/TS29222_CAPIF_Routing_Info_API/Dockerfile index cfa4b5d..1ea2ba9 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/Dockerfile +++ b/services/TS29222_CAPIF_Routing_Info_API/Dockerfile @@ -11,6 +11,6 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["gunicorn"] +EXPOSE 8080 -CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/capif_routing_info", "wsgi:app"] \ No newline at end of file +CMD ["sh", "prepare_routing_info.sh"] \ No newline at end of file diff --git a/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/util.py b/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/util.py index 910388b..54a1301 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/util.py +++ b/services/TS29222_CAPIF_Routing_Info_API/capif_routing_info/util.py @@ -1,7 +1,6 @@ import datetime import six -import typing import typing_utils diff --git a/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh b/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh new file mode 100644 index 0000000..53eaa2b --- /dev/null +++ b/services/TS29222_CAPIF_Routing_Info_API/prepare_routing_info.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/capif_routing_info wsgi:app diff --git a/services/TS29222_CAPIF_Security_API/Dockerfile b/services/TS29222_CAPIF_Security_API/Dockerfile index 9566c75..9f73be9 100644 --- a/services/TS29222_CAPIF_Security_API/Dockerfile +++ b/services/TS29222_CAPIF_Security_API/Dockerfile @@ -15,4 +15,4 @@ COPY . /usr/src/app EXPOSE 8080 -CMD ["sh", "security_prepare.sh"] +CMD ["sh", "prepare_security.sh"] diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/responses.py b/services/TS29222_CAPIF_Security_API/capif_security/core/responses.py index 9c2020c..4e1eb35 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/responses.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/responses.py @@ -2,7 +2,7 @@ from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from flask import Response import json -from ..util import dict_to_camel_case, clean_empty +from ..util import serialize_clean_camel_case mimetype = "application/json" @@ -15,49 +15,34 @@ def make_response(object, status): def internal_server_error(detail, cause): prob = ProblemDetails(title="Internal Server Error", status=500, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=500, mimetype=mimetype) def forbidden_error(detail, cause): prob = ProblemDetails(title="Forbidden", status=403, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=403, mimetype=mimetype) def bad_request_error(detail, cause, invalid_params): prob = ProblemDetails(title="Bad Request", status=400, detail=detail, cause=cause, invalid_params=invalid_params) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=400, mimetype=cause) def not_found_error(detail, cause): prob = ProblemDetails(title="Not Found", status=404, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=404, mimetype=mimetype) def unauthorized_error(detail, cause): prob = ProblemDetails(title="Unauthorized", status=401, detail=detail, cause=cause) - - prob = prob.to_dict() - prob = clean_empty(prob) - prob = dict_to_camel_case(prob) + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype=mimetype) \ No newline at end of file diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py index 9fe7838..5daa097 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py @@ -12,7 +12,7 @@ from ..core.publisher import Publisher from ..models.access_token_err import AccessTokenErr from ..models.access_token_rsp import AccessTokenRsp from ..models.access_token_claims import AccessTokenClaims -from ..util import dict_to_camel_case, clean_empty +from ..util import dict_to_camel_case, clean_empty, serialize_clean_camel_case from .responses import not_found_error, make_response, bad_request_error, internal_server_error, forbidden_error from .resources import Resource from .redis_event import RedisEvent @@ -196,7 +196,7 @@ class SecurityOperations(Resource): rec.update(service_security.to_dict()) mycol.insert_one(rec) - res = make_response(object=dict_to_camel_case(clean_empty(service_security.to_dict())), status=201) + res = make_response(object=serialize_clean_camel_case(service_security), status=201) res.headers['Location'] = "https://{}/capif-security/v1/trustedInvokers/{}".format( os.getenv('CAPIF_HOSTNAME'), str(api_invoker_id)) @@ -263,10 +263,9 @@ class SecurityOperations(Resource): invoker = invokers_col.find_one( {"api_invoker_id": access_token_req["client_id"]}) if invoker is None: - client_id_error = AccessTokenErr(error="invalid_client", error_description="Client Id not found") + client_id_error = AccessTokenErr(error="invalid_client", error_description="Client Id not found") return make_response(object=clean_empty(client_id_error.to_dict()), status=400) - if access_token_req["grant_type"] != "client_credentials": client_id_error = AccessTokenErr(error="unsupported_grant_type", error_description="Invalid value for `grant_type` ({0}), must be one of ['client_credentials'] - 'grant_type'" @@ -296,7 +295,6 @@ class SecurityOperations(Resource): current_app.logger.debug("Created access token") - # res = make_response(object=dict_to_camel_case(clean_empty(access_token_resp.to_dict())), status=200) res = make_response(object=clean_empty(access_token_resp.to_dict()), status=200) return res except Exception as e: diff --git a/services/TS29222_CAPIF_Security_API/capif_security/util.py b/services/TS29222_CAPIF_Security_API/capif_security/util.py index 00ceb15..72d18d9 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/util.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/util.py @@ -4,6 +4,14 @@ import six import typing_utils +def serialize_clean_camel_case(obj): + res = obj.to_dict() + res = clean_empty(res) + res = dict_to_camel_case(res) + + return res + + def clean_empty(d): if isinstance(d, dict): return { @@ -15,6 +23,7 @@ def clean_empty(d): return [v for v in map(clean_empty, d) if v] return d + def dict_to_camel_case(my_dict): @@ -22,8 +31,11 @@ def dict_to_camel_case(my_dict): for attr, value in my_dict.items(): - my_key = ''.join(word.title() for word in attr.split('_')) - my_key= ''.join([my_key[0].lower(), my_key[1:]]) + if len(attr.split('_')) != 1: + my_key = ''.join(word.title() for word in attr.split('_')) + my_key = ''.join([my_key[0].lower(), my_key[1:]]) + else: + my_key = attr if isinstance(value, list): result[my_key] = list(map( @@ -41,7 +53,6 @@ def dict_to_camel_case(my_dict): return result - def _deserialize(data, klass): """Deserializes dict, list, str into an object. diff --git a/services/TS29222_CAPIF_Security_API/security_prepare.sh b/services/TS29222_CAPIF_Security_API/prepare_security.sh similarity index 100% rename from services/TS29222_CAPIF_Security_API/security_prepare.sh rename to services/TS29222_CAPIF_Security_API/prepare_security.sh diff --git a/services/helper/Dockerfile b/services/helper/Dockerfile index 2c521b7..3643956 100644 --- a/services/helper/Dockerfile +++ b/services/helper/Dockerfile @@ -13,6 +13,4 @@ COPY . /usr/src/app EXPOSE 8080 -ENTRYPOINT ["gunicorn"] - -CMD ["--bind", "0.0.0.0:8080", "--chdir", "/usr/src/app/helper_service", "wsgi:app"] +CMD ["sh", "prepare_helper.sh"] diff --git a/services/helper/prepare_helper.sh b/services/helper/prepare_helper.sh new file mode 100644 index 0000000..d4297f6 --- /dev/null +++ b/services/helper/prepare_helper.sh @@ -0,0 +1,5 @@ +#!/bin/bash + + +gunicorn --bind 0.0.0.0:8080 \ + --chdir /usr/src/app/helper_service wsgi:app \ No newline at end of file -- GitLab From 8be103004396ba2080a4d6e6e1939a97ecd70b22 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Fri, 21 Jun 2024 11:04:51 +0200 Subject: [PATCH 277/310] upgrade some versions and requirements refactor --- .../requirements.txt | 6 +++--- .../test-requirements.txt | 4 ++-- .../requirements.txt | 4 ++-- .../test-requirements.txt | 4 ++-- .../requirements.txt | 6 +++--- .../test-requirements.txt | 2 +- services/TS29222_CAPIF_Auditing_API/requirements.txt | 6 +++--- services/TS29222_CAPIF_Auditing_API/test-requirements.txt | 4 ++-- .../TS29222_CAPIF_Discover_Service_API/requirements.txt | 6 +++--- .../test-requirements.txt | 4 ++-- services/TS29222_CAPIF_Events_API/requirements.txt | 6 +++--- services/TS29222_CAPIF_Events_API/test-requirements.txt | 4 ++-- .../requirements.txt | 6 +++--- .../test-requirements.txt | 4 ++-- services/TS29222_CAPIF_Publish_Service_API/requirements.txt | 6 +++--- .../TS29222_CAPIF_Publish_Service_API/test-requirements.txt | 4 ++-- services/TS29222_CAPIF_Routing_Info_API/requirements.txt | 4 ++-- .../TS29222_CAPIF_Routing_Info_API/test-requirements.txt | 4 ++-- services/TS29222_CAPIF_Security_API/requirements.txt | 6 +++--- services/TS29222_CAPIF_Security_API/test-requirements.txt | 4 ++-- services/helper/requirements.txt | 4 ++-- services/mock_server/requirements.txt | 2 +- services/register/requirements.txt | 4 ++-- 23 files changed, 52 insertions(+), 52 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt index ae88000..dbf0f19 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/requirements.txt @@ -13,12 +13,12 @@ opentelemetry-instrumentation-flask == 0.40b0 opentelemetry-instrumentation-redis == 0.40b0 opentelemetry-instrumentation-pymongo == 0.40b0 opentelemetry-exporter-otlp == 1.19.0 -opentelemetry-exporter-jaeger==1.17.0 +opentelemetry-exporter-jaeger == 1.17.0 fluent == 0.10.0 fluent-logger == 0.10.0 opentelemetry-api == 1.19.0 opentelemetry-sdk == 1.19.0 flask_executor == 1.0.0 Werkzeug == 2.2.3 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/test-requirements.txt b/services/TS29222_CAPIF_API_Invoker_Management_API/test-requirements.txt index 0970f28..202a684 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/test-requirements.txt +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/test-requirements.txt @@ -1,4 +1,4 @@ pytest~=4.6.7 # needed for python 2.7+3.4 pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 -Flask-Testing==0.8.0 +pytest-randomly == 1.2.3 # needed for python 2.7+3.4 +Flask-Testing == 0.8.0 diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt b/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt index aa6fdc5..a5c9cce 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt +++ b/services/TS29222_CAPIF_API_Provider_Management_API/requirements.txt @@ -19,5 +19,5 @@ opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 Werkzeug == 2.2.3 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/test-requirements.txt b/services/TS29222_CAPIF_API_Provider_Management_API/test-requirements.txt index 0970f28..202a684 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/test-requirements.txt +++ b/services/TS29222_CAPIF_API_Provider_Management_API/test-requirements.txt @@ -1,4 +1,4 @@ pytest~=4.6.7 # needed for python 2.7+3.4 pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 -Flask-Testing==0.8.0 +pytest-randomly == 1.2.3 # needed for python 2.7+3.4 +Flask-Testing == 0.8.0 diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt b/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt index 8457c82..2e82931 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/requirements.txt @@ -11,7 +11,7 @@ opentelemetry-instrumentation-flask == 0.38b0 opentelemetry-instrumentation-redis == 0.38b0 opentelemetry-instrumentation-pymongo == 0.38b0 opentelemetry-exporter-otlp == 1.17.0 -opentelemetry-exporter-jaeger==1.17.0 +opentelemetry-exporter-jaeger == 1.17.0 fluent == 0.10.0 fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 @@ -22,5 +22,5 @@ redis == 4.5.4 flask_executor == 1.0.0 Flask-APScheduler == 1.12.4 Flask-Script == 2.0.6 -gunicorn==22.0.0 -packaging==24.0 \ No newline at end of file +gunicorn == 22.0.0 +packaging == 24.0 \ No newline at end of file diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/test-requirements.txt b/services/TS29222_CAPIF_Access_Control_Policy_API/test-requirements.txt index 58f51d6..422ece8 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/test-requirements.txt +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/test-requirements.txt @@ -1,4 +1,4 @@ pytest~=7.1.0 pytest-cov>=2.8.1 pytest-randomly>=1.2.3 -Flask-Testing==0.8.1 +Flask-Testing == 0.8.1 diff --git a/services/TS29222_CAPIF_Auditing_API/requirements.txt b/services/TS29222_CAPIF_Auditing_API/requirements.txt index 4ed2ea2..c2ef0b4 100644 --- a/services/TS29222_CAPIF_Auditing_API/requirements.txt +++ b/services/TS29222_CAPIF_Auditing_API/requirements.txt @@ -11,7 +11,7 @@ opentelemetry-instrumentation-flask == 0.38b0 opentelemetry-instrumentation-redis == 0.38b0 opentelemetry-instrumentation-pymongo == 0.38b0 opentelemetry-exporter-otlp == 1.17.0 -opentelemetry-exporter-jaeger==1.17.0 +opentelemetry-exporter-jaeger == 1.17.0 fluent == 0.10.0 fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 @@ -20,5 +20,5 @@ flask_executor == 1.0.0 cryptography == 42.0.8 Werkzeug == 2.2.3 pyopenssl == 24.1.0 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 diff --git a/services/TS29222_CAPIF_Auditing_API/test-requirements.txt b/services/TS29222_CAPIF_Auditing_API/test-requirements.txt index 0970f28..202a684 100644 --- a/services/TS29222_CAPIF_Auditing_API/test-requirements.txt +++ b/services/TS29222_CAPIF_Auditing_API/test-requirements.txt @@ -1,4 +1,4 @@ pytest~=4.6.7 # needed for python 2.7+3.4 pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 -Flask-Testing==0.8.0 +pytest-randomly == 1.2.3 # needed for python 2.7+3.4 +Flask-Testing == 0.8.0 diff --git a/services/TS29222_CAPIF_Discover_Service_API/requirements.txt b/services/TS29222_CAPIF_Discover_Service_API/requirements.txt index ea568fc..1c57f79 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Discover_Service_API/requirements.txt @@ -13,7 +13,7 @@ opentelemetry-instrumentation-flask == 0.38b0 opentelemetry-instrumentation-redis == 0.38b0 opentelemetry-instrumentation-pymongo == 0.38b0 opentelemetry-exporter-otlp == 1.17.0 -opentelemetry-exporter-jaeger==1.17.0 +opentelemetry-exporter-jaeger == 1.17.0 fluent == 0.10.0 fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 @@ -21,5 +21,5 @@ opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 flask_executor == 1.0.0 Werkzeug == 2.2.3 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 diff --git a/services/TS29222_CAPIF_Discover_Service_API/test-requirements.txt b/services/TS29222_CAPIF_Discover_Service_API/test-requirements.txt index 0970f28..202a684 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/test-requirements.txt +++ b/services/TS29222_CAPIF_Discover_Service_API/test-requirements.txt @@ -1,4 +1,4 @@ pytest~=4.6.7 # needed for python 2.7+3.4 pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 -Flask-Testing==0.8.0 +pytest-randomly == 1.2.3 # needed for python 2.7+3.4 +Flask-Testing == 0.8.0 diff --git a/services/TS29222_CAPIF_Events_API/requirements.txt b/services/TS29222_CAPIF_Events_API/requirements.txt index 63d456f..22b0d35 100644 --- a/services/TS29222_CAPIF_Events_API/requirements.txt +++ b/services/TS29222_CAPIF_Events_API/requirements.txt @@ -9,7 +9,7 @@ opentelemetry-instrumentation-flask == 0.38b0 opentelemetry-instrumentation-redis == 0.38b0 opentelemetry-instrumentation-pymongo == 0.38b0 opentelemetry-exporter-otlp == 1.17.0 -opentelemetry-exporter-jaeger==1.17.0 +opentelemetry-exporter-jaeger == 1.17.0 fluent == 0.10.0 fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 @@ -24,5 +24,5 @@ aiohttp == 3.9.5 async-timeout == 4.0.3 Werkzeug == 2.2.3 pyopenssl == 24.1.0 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 diff --git a/services/TS29222_CAPIF_Events_API/test-requirements.txt b/services/TS29222_CAPIF_Events_API/test-requirements.txt index 0970f28..202a684 100644 --- a/services/TS29222_CAPIF_Events_API/test-requirements.txt +++ b/services/TS29222_CAPIF_Events_API/test-requirements.txt @@ -1,4 +1,4 @@ pytest~=4.6.7 # needed for python 2.7+3.4 pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 -Flask-Testing==0.8.0 +pytest-randomly == 1.2.3 # needed for python 2.7+3.4 +Flask-Testing == 0.8.0 diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt index e01a501..e92ae36 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/requirements.txt @@ -12,7 +12,7 @@ opentelemetry-instrumentation-flask == 0.38b0 opentelemetry-instrumentation-redis == 0.38b0 opentelemetry-instrumentation-pymongo == 0.38b0 opentelemetry-exporter-otlp == 1.17.0 -opentelemetry-exporter-jaeger==1.17.0 +opentelemetry-exporter-jaeger == 1.17.0 fluent == 0.10.0 fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 @@ -21,5 +21,5 @@ flask_executor == 1.0.0 cryptography == 42.0.8 Werkzeug == 2.2.3 pyopenssl == 24.1.0 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/test-requirements.txt b/services/TS29222_CAPIF_Logging_API_Invocation_API/test-requirements.txt index 0970f28..202a684 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/test-requirements.txt +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/test-requirements.txt @@ -1,4 +1,4 @@ pytest~=4.6.7 # needed for python 2.7+3.4 pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 -Flask-Testing==0.8.0 +pytest-randomly == 1.2.3 # needed for python 2.7+3.4 +Flask-Testing == 0.8.0 diff --git a/services/TS29222_CAPIF_Publish_Service_API/requirements.txt b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt index 1026f85..0b1b9e8 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/requirements.txt +++ b/services/TS29222_CAPIF_Publish_Service_API/requirements.txt @@ -10,7 +10,7 @@ opentelemetry-instrumentation-flask == 0.38b0 opentelemetry-instrumentation-redis == 0.38b0 opentelemetry-instrumentation-pymongo == 0.38b0 opentelemetry-exporter-otlp == 1.17.0 -opentelemetry-exporter-jaeger==1.17.0 +opentelemetry-exporter-jaeger == 1.17.0 fluent == 0.10.0 fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 @@ -20,5 +20,5 @@ cryptography == 42.0.8 redis == 4.5.4 flask_executor == 1.0.0 Werkzeug == 2.2.3 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 diff --git a/services/TS29222_CAPIF_Publish_Service_API/test-requirements.txt b/services/TS29222_CAPIF_Publish_Service_API/test-requirements.txt index 0970f28..202a684 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/test-requirements.txt +++ b/services/TS29222_CAPIF_Publish_Service_API/test-requirements.txt @@ -1,4 +1,4 @@ pytest~=4.6.7 # needed for python 2.7+3.4 pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 -Flask-Testing==0.8.0 +pytest-randomly == 1.2.3 # needed for python 2.7+3.4 +Flask-Testing == 0.8.0 diff --git a/services/TS29222_CAPIF_Routing_Info_API/requirements.txt b/services/TS29222_CAPIF_Routing_Info_API/requirements.txt index f046aef..afaac68 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/requirements.txt +++ b/services/TS29222_CAPIF_Routing_Info_API/requirements.txt @@ -4,5 +4,5 @@ python_dateutil >= 2.6.0 setuptools == 68.2.2 Flask == 2.0.3 Werkzeug == 2.2.3 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 diff --git a/services/TS29222_CAPIF_Routing_Info_API/test-requirements.txt b/services/TS29222_CAPIF_Routing_Info_API/test-requirements.txt index 0970f28..202a684 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/test-requirements.txt +++ b/services/TS29222_CAPIF_Routing_Info_API/test-requirements.txt @@ -1,4 +1,4 @@ pytest~=4.6.7 # needed for python 2.7+3.4 pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 -Flask-Testing==0.8.0 +pytest-randomly == 1.2.3 # needed for python 2.7+3.4 +Flask-Testing == 0.8.0 diff --git a/services/TS29222_CAPIF_Security_API/requirements.txt b/services/TS29222_CAPIF_Security_API/requirements.txt index 3763534..b92f321 100644 --- a/services/TS29222_CAPIF_Security_API/requirements.txt +++ b/services/TS29222_CAPIF_Security_API/requirements.txt @@ -14,12 +14,12 @@ opentelemetry-instrumentation-flask == 0.38b0 opentelemetry-instrumentation-redis == 0.38b0 opentelemetry-instrumentation-pymongo == 0.38b0 opentelemetry-exporter-otlp == 1.17.0 -opentelemetry-exporter-jaeger==1.17.0 +opentelemetry-exporter-jaeger == 1.17.0 fluent == 0.10.0 fluent-logger == 0.10.0 opentelemetry-api == 1.17.0 opentelemetry-sdk == 1.17.0 flask_executor == 1.0.0 Werkzeug == 2.2.3 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 diff --git a/services/TS29222_CAPIF_Security_API/test-requirements.txt b/services/TS29222_CAPIF_Security_API/test-requirements.txt index 0970f28..202a684 100644 --- a/services/TS29222_CAPIF_Security_API/test-requirements.txt +++ b/services/TS29222_CAPIF_Security_API/test-requirements.txt @@ -1,4 +1,4 @@ pytest~=4.6.7 # needed for python 2.7+3.4 pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 -Flask-Testing==0.8.0 +pytest-randomly == 1.2.3 # needed for python 2.7+3.4 +Flask-Testing == 0.8.0 diff --git a/services/helper/requirements.txt b/services/helper/requirements.txt index d51eb7e..23c2931 100644 --- a/services/helper/requirements.txt +++ b/services/helper/requirements.txt @@ -6,5 +6,5 @@ flask_jwt_extended == 4.6.0 pyopenssl == 24.1.0 pyyaml == 6.0.1 requests == 2.32.2 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 diff --git a/services/mock_server/requirements.txt b/services/mock_server/requirements.txt index abf2862..0764845 100644 --- a/services/mock_server/requirements.txt +++ b/services/mock_server/requirements.txt @@ -1 +1 @@ -flask==3.0.3 \ No newline at end of file +flask == 3.0.3 \ No newline at end of file diff --git a/services/register/requirements.txt b/services/register/requirements.txt index e95dfe3..bf3e52c 100644 --- a/services/register/requirements.txt +++ b/services/register/requirements.txt @@ -8,5 +8,5 @@ pyyaml == 6.0.1 requests == 2.32.2 bcrypt == 4.0.1 flask_httpauth == 4.8.0 -gunicorn==22.0.0 -packaging==24.0 +gunicorn == 22.0.0 +packaging == 24.0 -- GitLab From a9e4f049012f709c80b755641fa019d3026de97b Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Fri, 21 Jun 2024 11:33:30 +0200 Subject: [PATCH 278/310] Fix eval issue checking monitoring variable at Security Service --- services/TS29222_CAPIF_Security_API/capif_security/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/TS29222_CAPIF_Security_API/capif_security/app.py b/services/TS29222_CAPIF_Security_API/capif_security/app.py index 975488e..0f06b7a 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/app.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/app.py @@ -126,7 +126,8 @@ subscriber = Subscriber() config = Config() configure_logging(app.app) -if eval(os.environ.get("MONITORING").lower().capitalize()): +monitoring_value = os.environ.get("MONITORING", "").lower() +if monitoring_value == "true": configure_monitoring(app.app, config.get_config()) executor = Executor(app.app) -- GitLab From 9820328651016d67192fd4ee44bf3434448245ab Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Fri, 21 Jun 2024 12:46:01 +0300 Subject: [PATCH 279/310] remove unsed import --- .../api_invoker_management/controllers/default_controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py index 9ea3c42..f2b0df6 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/controllers/default_controller.py @@ -8,7 +8,6 @@ from flask import Response, request, current_app from flask_jwt_extended import jwt_required, get_jwt_identity from cryptography import x509 from cryptography.hazmat.backends import default_backend -from ..core.publisher import Publisher from functools import wraps invoker_operations = InvokerManagementOperations() -- GitLab From dc5918c2dc36972a003ee3f80273a14c7c4822e0 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 24 Jun 2024 11:36:09 +0200 Subject: [PATCH 280/310] Change NetApp to Network App --- .../core/apiinvokerenrolmentdetails.py | 4 ++-- .../capif_security/core/servicesecurity.py | 2 +- .../capif_api_invoker_managenet.robot | 24 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py index 5a265b6..995df86 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/core/apiinvokerenrolmentdetails.py @@ -26,7 +26,7 @@ class InvokerManagementOperations(Resource): if old_values is None: current_app.logger.error("Not found api invoker id") - return not_found_error(detail="Please provide an existing Netapp ID", cause= "Not exist NetappID" ) + return not_found_error(detail="Please provide an existing Network App ID", cause= "Not exist Network App ID" ) return old_values @@ -161,7 +161,7 @@ class InvokerManagementOperations(Resource): current_app.logger.debug("Invoker resource removed from database") current_app.logger.debug("Netapp offboarded sucessfuly") - out = "The Netapp matching onboardingId " + onboard_id + " was offboarded." + out = "The Network App matching onboardingId " + onboard_id + " was offboarded." res = make_response(out, status=204) if res.status_code == 204: current_app.logger.info("Invoker Removed") diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py index 9fe7838..36a0895 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py @@ -233,7 +233,7 @@ class SecurityOperations(Resource): current_app.logger.debug( "Removed security context from database") - out = "The security info of Netapp with Netapp ID " + \ + out = "The security info of Network App with Network App ID " + \ api_invoker_id + " were deleted.", 204 return make_response(out, status=204) diff --git a/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot b/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot index 8c40f72..27006b1 100644 --- a/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot +++ b/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot @@ -15,9 +15,9 @@ ${API_INVOKER_NOT_REGISTERED} not-valid *** Test Cases *** -Onboard NetApp +Onboard Network App [Tags] capif_api_invoker_management-1 - # Register Netapp + # Register Network App ${register_user_info}= Register User At Jwt Auth ... username=${INVOKER_USERNAME} role=${INVOKER_ROLE} @@ -42,7 +42,7 @@ Onboard NetApp # Store dummy signed certificate Store In File ${INVOKER_USERNAME}.crt ${resp.json()['onboardingInformation']['apiInvokerCertificate']} -Register NetApp Already Onboarded +Register Network App Already Onboarded [Tags] capif_api_invoker_management-2 # Default Invoker Registration and Onboarding ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding @@ -61,7 +61,7 @@ Register NetApp Already Onboarded ... detail=Invoker already registered ... cause=Identical invoker public key -Update Onboarded NetApp +Update Onboarded Network App [Tags] capif_api_invoker_management-3 ${new_notification_destination}= Set Variable ... http://${CAPIF_CALLBACK_IP}:${CAPIF_CALLBACK_PORT}/netapp_new_callback @@ -83,7 +83,7 @@ Update Onboarded NetApp Check Response Variable Type And Values ${resp} 200 APIInvokerEnrolmentDetails ... notificationDestination=${new_notification_destination} -Update Not Onboarded NetApp +Update Not Onboarded Network App [Tags] capif_api_invoker_management-4 # Default Invoker Registration and Onboarding ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding @@ -99,10 +99,10 @@ Update Not Onboarded NetApp Check Response Variable Type And Values ${resp} 404 ProblemDetails ... status=404 ... title=Not Found - ... detail=Please provide an existing Netapp ID - ... cause=Not exist NetappID + ... detail=Please provide an existing Network App ID + ... cause=Not exist Network App ID -Offboard NetApp +Offboard Network App [Tags] capif_api_invoker_management-5 # Default Invoker Registration and Onboarding ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding @@ -118,7 +118,7 @@ Offboard NetApp # Check Results Should Be Equal As Strings ${resp.status_code} 204 -Offboard Not Previously Onboarded NetApp +Offboard Not Previously Onboarded Network App [Tags] capif_api_invoker_management-6 # Default Invoker Registration and Onboarding ${register_user_info} ${url} ${request_body}= Invoker Default Onboarding @@ -133,10 +133,10 @@ Offboard Not Previously Onboarded NetApp Check Response Variable Type And Values ${resp} 404 ProblemDetails ... status=404 ... title=Not Found - ... detail=Please provide an existing Netapp ID - ... cause=Not exist NetappID + ... detail=Please provide an existing Network App ID + ... cause=Not exist Network App ID -Update Onboarded NetApp Certificate +Update Onboarded Network App Certificate [Tags] capif_api_invoker_management-7 ${new_notification_destination}= Set Variable ... http://${CAPIF_CALLBACK_IP}:${CAPIF_CALLBACK_PORT}/netapp_new_callback -- GitLab From f73a061d37837ef7893fc0a15019ff44d3288260 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 25 Jun 2024 08:32:46 +0200 Subject: [PATCH 281/310] refactoring ocf. Including enabled condition --- .gitignore | 4 - helm/capif/Chart.yaml | 10 + helm/capif/charts/fluentbit/.helmignore | 23 + helm/capif/charts/fluentbit/Chart.yaml | 24 + .../charts/fluentbit/templates/NOTES.txt | 22 + .../charts/fluentbit/templates/_helpers.tpl | 62 + .../fluentbit/templates/configmap.yaml} | 4 +- .../fluentbit/templates/deployment.yaml | 79 + .../capif/charts/fluentbit/templates/hpa.yaml | 34 + .../charts/fluentbit/templates/ingress.yaml | 63 + .../charts/fluentbit/templates/service.yaml | 21 + .../fluentbit/templates/serviceaccount.yaml | 15 + helm/capif/charts/fluentbit/values.yaml | 116 + helm/capif/charts/grafana/.helmignore | 23 + helm/capif/charts/grafana/Chart.yaml | 24 + helm/capif/charts/grafana/README.md | 51 + .../charts/grafana/kubernetes-dashboard.json | 2629 +++++++++++++++++ helm/capif/charts/grafana/loki-logs.json | 281 ++ helm/capif/charts/grafana/templates/NOTES.txt | 22 + .../charts/grafana/templates/_helpers.tpl | 62 + .../grafana/templates/configmap.yaml} | 52 +- .../grafana/templates/deployment.yaml} | 82 +- helm/capif/charts/grafana/templates/hpa.yaml | 30 + .../charts/grafana/templates/ingress.yaml | 61 + helm/capif/charts/grafana/templates/pvc.yaml | 17 + .../charts/grafana/templates/secrets.yaml | 10 + .../charts/grafana/templates/service.yaml | 17 + .../grafana/templates/serviceaccount.yaml | 14 + .../templates/tests/test-connection.yaml | 17 + helm/capif/charts/grafana/values.yaml | 99 + helm/capif/charts/loki/.helmignore | 23 + helm/capif/charts/loki/Chart.yaml | 24 + helm/capif/charts/loki/templates/NOTES.txt | 22 + helm/capif/charts/loki/templates/_helpers.tpl | 62 + .../charts/loki/templates/deployment.yaml | 74 + helm/capif/charts/loki/templates/hpa.yaml | 34 + helm/capif/charts/loki/templates/ingress.yaml | 63 + helm/capif/charts/loki/templates/pvc.yaml | 16 + helm/capif/charts/loki/templates/service.yaml | 17 + .../charts/loki/templates/serviceaccount.yaml | 15 + .../loki/templates/tests/test-connection.yaml | 17 + helm/capif/charts/loki/values.yaml | 115 + .../mock-server/templates/deployment.yaml | 2 + .../charts/mock-server/templates/hpa.yaml | 2 + .../charts/mock-server/templates/ingress.yaml | 2 + .../charts/mock-server/templates/service.yaml | 2 + .../mock-server/templates/serviceaccount.yaml | 2 + .../templates/tests/test-connection.yaml | 2 + helm/capif/charts/mock-server/values.yaml | 2 + .../mongo-express/templates/deployment.yaml | 2 + .../charts/mongo-express/templates/hpa.yaml | 2 + .../mongo-express/templates/ingress.yaml | 6 +- .../mongo-express/templates/service.yaml | 2 + .../templates/serviceaccount.yaml | 2 + .../templates/tests/test-connection.yaml | 2 + helm/capif/charts/mongo-express/values.yaml | 4 +- .../templates/deployment.yaml | 2 + .../mongo-register-express/templates/hpa.yaml | 2 + .../templates/ingress.yaml | 6 +- .../templates/service.yaml | 2 + .../templates/serviceaccount.yaml | 2 + .../templates/tests/test-connection.yaml | 4 +- .../charts/mongo-register-express/values.yaml | 4 +- helm/capif/charts/otelcollector/.helmignore | 23 + helm/capif/charts/otelcollector/Chart.yaml | 24 + .../charts/otelcollector/templates/NOTES.txt | 26 + .../otelcollector/templates/_helpers.tpl | 62 + .../otelcollector/templates/configmap.yaml} | 4 +- .../otelcollector/templates/deployment.yaml | 75 + .../charts/otelcollector/templates/hpa.yaml | 34 + .../otelcollector/templates/ingress.yaml | 63 + .../otelcollector/templates/service.yaml | 19 + .../templates/serviceaccount.yaml | 15 + helm/capif/charts/otelcollector/values.yaml | 119 + helm/capif/charts/renderer/.helmignore | 23 + helm/capif/charts/renderer/Chart.yaml | 24 + .../capif/charts/renderer/templates/NOTES.txt | 22 + .../charts/renderer/templates/_helpers.tpl | 62 + .../charts/renderer/templates/configmap.yaml | 8 + .../charts/renderer/templates/deployment.yaml | 74 + helm/capif/charts/renderer/templates/hpa.yaml | 34 + .../charts/renderer/templates/ingress.yaml | 63 + .../charts/renderer/templates/service.yaml | 17 + .../renderer/templates/serviceaccount.yaml | 15 + .../templates/tests/test-connection.yaml | 17 + helm/capif/charts/renderer/values.yaml | 112 + helm/capif/templates/deployment.yaml | 1 - helm/capif/templates/fluent-bit-service.yaml | 24 - .../capif/templates/fluentbit-deployment.yaml | 59 - .../templates/grafana-ingress-route.yaml | 18 - helm/capif/templates/grafana-ingress.yaml | 34 - helm/capif/templates/grafana-pvc.yaml | 17 - helm/capif/templates/grafana-secrets.yaml | 10 - helm/capif/templates/grafana-service.yaml | 17 - helm/capif/templates/loki-deployment.yaml | 54 - helm/capif/templates/loki-pvc.yaml | 17 - helm/capif/templates/loki-service.yaml | 19 - .../templates/otel-collector-deployment.yaml | 54 - .../templates/otel-collector-service.yaml | 22 - .../templates/prometheus-clusterrole.yaml | 49 - .../capif/templates/prometheus-configmap.yaml | 141 - .../templates/prometheus-deployment.yaml | 68 - .../templates/prometheus-ingress-route.yaml | 20 - helm/capif/templates/prometheus-ingress.yaml | 36 - helm/capif/templates/prometheus-pvc.yaml | 20 - helm/capif/templates/prometheus-service.yaml | 22 - helm/capif/templates/renderer-configmap.yaml | 8 - helm/capif/templates/renderer-deployment.yaml | 44 - helm/capif/templates/renderer-service.yaml | 18 - helm/capif/values.yaml | 202 +- 110 files changed, 5391 insertions(+), 1024 deletions(-) create mode 100644 helm/capif/charts/fluentbit/.helmignore create mode 100644 helm/capif/charts/fluentbit/Chart.yaml create mode 100644 helm/capif/charts/fluentbit/templates/NOTES.txt create mode 100644 helm/capif/charts/fluentbit/templates/_helpers.tpl rename helm/capif/{templates/fluentbit-configmap.yaml => charts/fluentbit/templates/configmap.yaml} (79%) create mode 100644 helm/capif/charts/fluentbit/templates/deployment.yaml create mode 100644 helm/capif/charts/fluentbit/templates/hpa.yaml create mode 100644 helm/capif/charts/fluentbit/templates/ingress.yaml create mode 100644 helm/capif/charts/fluentbit/templates/service.yaml create mode 100644 helm/capif/charts/fluentbit/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/fluentbit/values.yaml create mode 100644 helm/capif/charts/grafana/.helmignore create mode 100644 helm/capif/charts/grafana/Chart.yaml create mode 100644 helm/capif/charts/grafana/README.md create mode 100644 helm/capif/charts/grafana/kubernetes-dashboard.json create mode 100644 helm/capif/charts/grafana/loki-logs.json create mode 100644 helm/capif/charts/grafana/templates/NOTES.txt create mode 100644 helm/capif/charts/grafana/templates/_helpers.tpl rename helm/capif/{templates/grafana-configmap.yaml => charts/grafana/templates/configmap.yaml} (83%) rename helm/capif/{templates/grafana-deployment.yaml => charts/grafana/templates/deployment.yaml} (55%) create mode 100644 helm/capif/charts/grafana/templates/hpa.yaml create mode 100644 helm/capif/charts/grafana/templates/ingress.yaml create mode 100644 helm/capif/charts/grafana/templates/pvc.yaml create mode 100644 helm/capif/charts/grafana/templates/secrets.yaml create mode 100644 helm/capif/charts/grafana/templates/service.yaml create mode 100644 helm/capif/charts/grafana/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/grafana/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/grafana/values.yaml create mode 100644 helm/capif/charts/loki/.helmignore create mode 100644 helm/capif/charts/loki/Chart.yaml create mode 100644 helm/capif/charts/loki/templates/NOTES.txt create mode 100644 helm/capif/charts/loki/templates/_helpers.tpl create mode 100644 helm/capif/charts/loki/templates/deployment.yaml create mode 100644 helm/capif/charts/loki/templates/hpa.yaml create mode 100644 helm/capif/charts/loki/templates/ingress.yaml create mode 100644 helm/capif/charts/loki/templates/pvc.yaml create mode 100644 helm/capif/charts/loki/templates/service.yaml create mode 100644 helm/capif/charts/loki/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/loki/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/loki/values.yaml create mode 100644 helm/capif/charts/otelcollector/.helmignore create mode 100644 helm/capif/charts/otelcollector/Chart.yaml create mode 100644 helm/capif/charts/otelcollector/templates/NOTES.txt create mode 100644 helm/capif/charts/otelcollector/templates/_helpers.tpl rename helm/capif/{templates/otel-collector-configmap.yaml => charts/otelcollector/templates/configmap.yaml} (81%) create mode 100644 helm/capif/charts/otelcollector/templates/deployment.yaml create mode 100644 helm/capif/charts/otelcollector/templates/hpa.yaml create mode 100644 helm/capif/charts/otelcollector/templates/ingress.yaml create mode 100644 helm/capif/charts/otelcollector/templates/service.yaml create mode 100644 helm/capif/charts/otelcollector/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/otelcollector/values.yaml create mode 100644 helm/capif/charts/renderer/.helmignore create mode 100644 helm/capif/charts/renderer/Chart.yaml create mode 100644 helm/capif/charts/renderer/templates/NOTES.txt create mode 100644 helm/capif/charts/renderer/templates/_helpers.tpl create mode 100644 helm/capif/charts/renderer/templates/configmap.yaml create mode 100644 helm/capif/charts/renderer/templates/deployment.yaml create mode 100644 helm/capif/charts/renderer/templates/hpa.yaml create mode 100644 helm/capif/charts/renderer/templates/ingress.yaml create mode 100644 helm/capif/charts/renderer/templates/service.yaml create mode 100644 helm/capif/charts/renderer/templates/serviceaccount.yaml create mode 100644 helm/capif/charts/renderer/templates/tests/test-connection.yaml create mode 100644 helm/capif/charts/renderer/values.yaml delete mode 100644 helm/capif/templates/deployment.yaml delete mode 100644 helm/capif/templates/fluent-bit-service.yaml delete mode 100644 helm/capif/templates/fluentbit-deployment.yaml delete mode 100644 helm/capif/templates/grafana-ingress-route.yaml delete mode 100644 helm/capif/templates/grafana-ingress.yaml delete mode 100644 helm/capif/templates/grafana-pvc.yaml delete mode 100644 helm/capif/templates/grafana-secrets.yaml delete mode 100644 helm/capif/templates/grafana-service.yaml delete mode 100644 helm/capif/templates/loki-deployment.yaml delete mode 100644 helm/capif/templates/loki-pvc.yaml delete mode 100644 helm/capif/templates/loki-service.yaml delete mode 100644 helm/capif/templates/otel-collector-deployment.yaml delete mode 100644 helm/capif/templates/otel-collector-service.yaml delete mode 100644 helm/capif/templates/prometheus-clusterrole.yaml delete mode 100644 helm/capif/templates/prometheus-configmap.yaml delete mode 100644 helm/capif/templates/prometheus-deployment.yaml delete mode 100644 helm/capif/templates/prometheus-ingress-route.yaml delete mode 100644 helm/capif/templates/prometheus-ingress.yaml delete mode 100644 helm/capif/templates/prometheus-pvc.yaml delete mode 100644 helm/capif/templates/prometheus-service.yaml delete mode 100644 helm/capif/templates/renderer-configmap.yaml delete mode 100644 helm/capif/templates/renderer-deployment.yaml delete mode 100644 helm/capif/templates/renderer-service.yaml diff --git a/.gitignore b/.gitignore index ec4110f..c27c6be 100644 --- a/.gitignore +++ b/.gitignore @@ -36,8 +36,4 @@ docs/testing_with_postman/package-lock.json results helm/capif/*.lock -<<<<<<< HEAD -helm/capif/charts -======= helm/capif/charts/tempo* ->>>>>>> staging diff --git a/helm/capif/Chart.yaml b/helm/capif/Chart.yaml index 3cdcb3e..1c49c2c 100644 --- a/helm/capif/Chart.yaml +++ b/helm/capif/Chart.yaml @@ -58,6 +58,16 @@ dependencies: version: "*" - name: redis version: "*" + - name: fluentbit + version: "*" + - name: grafana + version: "*" + - name: loki + version: "*" + - name: otelcollector + version: "*" + - name: renderer + version: "*" - name: "tempo" condition: tempo.enabled repository: "https://grafana.github.io/helm-charts" diff --git a/helm/capif/charts/fluentbit/.helmignore b/helm/capif/charts/fluentbit/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/fluentbit/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/fluentbit/Chart.yaml b/helm/capif/charts/fluentbit/Chart.yaml new file mode 100644 index 0000000..b10c00b --- /dev/null +++ b/helm/capif/charts/fluentbit/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: fluentbit +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/fluentbit/templates/NOTES.txt b/helm/capif/charts/fluentbit/templates/NOTES.txt new file mode 100644 index 0000000..8e6d43e --- /dev/null +++ b/helm/capif/charts/fluentbit/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "fluentbit.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "fluentbit.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "fluentbit.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "fluentbit.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/fluentbit/templates/_helpers.tpl b/helm/capif/charts/fluentbit/templates/_helpers.tpl new file mode 100644 index 0000000..a880f20 --- /dev/null +++ b/helm/capif/charts/fluentbit/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "fluentbit.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "fluentbit.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "fluentbit.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "fluentbit.labels" -}} +helm.sh/chart: {{ include "fluentbit.chart" . }} +{{ include "fluentbit.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "fluentbit.selectorLabels" -}} +app.kubernetes.io/name: {{ include "fluentbit.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "fluentbit.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "fluentbit.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/templates/fluentbit-configmap.yaml b/helm/capif/charts/fluentbit/templates/configmap.yaml similarity index 79% rename from helm/capif/templates/fluentbit-configmap.yaml rename to helm/capif/charts/fluentbit/templates/configmap.yaml index 20467b1..2561e38 100644 --- a/helm/capif/templates/fluentbit-configmap.yaml +++ b/helm/capif/charts/fluentbit/templates/configmap.yaml @@ -1,10 +1,10 @@ -{{- if eq .Values.monitoring.enable "true" }} +{{- if .Values.enabled | default false }} apiVersion: v1 kind: ConfigMap metadata: name: fluent-bit-configmap data: - LOKI_URL: {{ quote .Values.monitoring.fluentBit.env.lokiUrl }} + LOKI_URL: {{ quote .Values.env.lokiUrl }} fluent-bit.conf: | [INPUT] Name forward diff --git a/helm/capif/charts/fluentbit/templates/deployment.yaml b/helm/capif/charts/fluentbit/templates/deployment.yaml new file mode 100644 index 0000000..ce6125c --- /dev/null +++ b/helm/capif/charts/fluentbit/templates/deployment.yaml @@ -0,0 +1,79 @@ +{{- if .Values.enabled | default false }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "fluentbit.fullname" . }} + labels: + {{- include "fluentbit.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "fluentbit.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "fluentbit.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "fluentbit.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: fluent-bit-udp + containerPort: {{ .Values.service.port }} + protocol: UDP + - name: fluent-bit-tcp + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: LOKI_URL + valueFrom: + configMapKeyRef: + name: fluent-bit-configmap + key: LOKI_URL + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/fluentbit/templates/hpa.yaml b/helm/capif/charts/fluentbit/templates/hpa.yaml new file mode 100644 index 0000000..9a6dd95 --- /dev/null +++ b/helm/capif/charts/fluentbit/templates/hpa.yaml @@ -0,0 +1,34 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "fluentbit.fullname" . }} + labels: + {{- include "fluentbit.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "fluentbit.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/fluentbit/templates/ingress.yaml b/helm/capif/charts/fluentbit/templates/ingress.yaml new file mode 100644 index 0000000..601131c --- /dev/null +++ b/helm/capif/charts/fluentbit/templates/ingress.yaml @@ -0,0 +1,63 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "fluentbit.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "fluentbit.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/fluentbit/templates/service.yaml b/helm/capif/charts/fluentbit/templates/service.yaml new file mode 100644 index 0000000..dd9be52 --- /dev/null +++ b/helm/capif/charts/fluentbit/templates/service.yaml @@ -0,0 +1,21 @@ +{{- if .Values.enabled | default false }} +apiVersion: v1 +kind: Service +metadata: + name: fluent-bit + labels: + {{- include "fluentbit.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.port }} + protocol: UDP + name: fluent-bit-udp + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.port }} + protocol: TCP + name: fluent-bit-tcp + selector: + {{- include "fluentbit.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/helm/capif/charts/fluentbit/templates/serviceaccount.yaml b/helm/capif/charts/fluentbit/templates/serviceaccount.yaml new file mode 100644 index 0000000..fbdb575 --- /dev/null +++ b/helm/capif/charts/fluentbit/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "fluentbit.serviceAccountName" . }} + labels: + {{- include "fluentbit.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/fluentbit/values.yaml b/helm/capif/charts/fluentbit/values.yaml new file mode 100644 index 0000000..49aa7e7 --- /dev/null +++ b/helm/capif/charts/fluentbit/values.yaml @@ -0,0 +1,116 @@ +# Default values for fluentbit. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +enabled: false + +replicaCount: 1 + +image: + repository: grafana/fluent-bit-plugin-loki + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + lokiUrl: http://loki:3100/loki/api/v1/push + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 24224 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: + tcpSocket: + port: 24224 + initialDelaySeconds: 20 + periodSeconds: 5 +readinessProbe: + tcpSocket: + port: 24224 + initialDelaySeconds: 20 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: fluent-bit-conf + configMap: + name: fluent-bit-configmap + items: + - key: fluent-bit.conf + path: fluent-bit.conf + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: fluent-bit-conf + mountPath: /fluent-bit/etc/fluent-bit.conf + subPath: fluent-bit.conf + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/grafana/.helmignore b/helm/capif/charts/grafana/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/grafana/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/grafana/Chart.yaml b/helm/capif/charts/grafana/Chart.yaml new file mode 100644 index 0000000..2c7c54e --- /dev/null +++ b/helm/capif/charts/grafana/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: grafana +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/grafana/README.md b/helm/capif/charts/grafana/README.md new file mode 100644 index 0000000..5325ad0 --- /dev/null +++ b/helm/capif/charts/grafana/README.md @@ -0,0 +1,51 @@ +# grafana + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.16.0](https://img.shields.io/badge/AppVersion-1.16.0-informational?style=flat-square) + +A Helm chart for Kubernetes + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| autoscaling.enabled | bool | `false` | | +| autoscaling.maxReplicas | int | `100` | | +| autoscaling.minReplicas | int | `1` | | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | | +| env.gfAuthAnonymousEnable | bool | `true` | | +| env.gfAuthAnonymousOrgRole | string | `"Admin"` | | +| env.gfSecurityAdminPassword | string | `"secure_pass"` | | +| env.gfSecurityAllowEmbedding | bool | `true` | | +| env.prometheusUrl | string | `"http://prometheus.prometheus-system.svc.cluster.local:9090"` | | +| fullnameOverride | string | `""` | | +| image.pullPolicy | string | `"Always"` | | +| image.repository | string | `"grafana/grafana"` | | +| image.tag | string | `"latest"` | | +| imagePullSecrets | list | `[]` | | +| ingress.annotations | object | `{}` | | +| ingress.className | string | `"nginx"` | | +| ingress.enabled | bool | `true` | | +| ingress.environment | string | `"edge"` | | +| ingress.hosts[0].host | string | `"grafana-dt.tactile5g.int"` | | +| ingress.hosts[0].paths[0].path | string | `"/"` | | +| ingress.hosts[0].paths[0].pathType | string | `"Prefix"` | | +| ingress.tls | list | `[]` | | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | | +| persistence.enable | bool | `false` | | +| persistence.storage | string | `"10Gi"` | | +| podAnnotations | object | `{}` | | +| podSecurityContext | object | `{}` | | +| replicaCount | int | `1` | | +| resources | object | `{}` | | +| securityContext | object | `{}` | | +| service.port | int | `80` | | +| service.type | string | `"NodePort"` | | +| serviceAccount.annotations | object | `{}` | | +| serviceAccount.create | bool | `true` | | +| serviceAccount.name | string | `""` | | +| tolerations | list | `[]` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1) diff --git a/helm/capif/charts/grafana/kubernetes-dashboard.json b/helm/capif/charts/grafana/kubernetes-dashboard.json new file mode 100644 index 0000000..ac97f80 --- /dev/null +++ b/helm/capif/charts/grafana/kubernetes-dashboard.json @@ -0,0 +1,2629 @@ +{ + "annotations": { + "list": [ + { + "$$hashKey": "object:103", + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Monitors Kubernetes cluster using Prometheus. Shows overall cluster CPU / Memory / Filesystem usage as well as individual pod, containers, systemd services statistics. Uses cAdvisor metrics only.", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 12740, + "graphTooltip": 0, + "id": 7, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 33, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Network I/O pressure", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 2, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 1 + }, + "height": "200px", + "hiddenSeries": false, + "id": 32, + "legend": { + "alignAsTable": false, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": false, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "10.0.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_network_receive_bytes_total{kubernetes_io_hostname=~\"^$Node$\"}[1m]))", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "Received", + "metric": "network", + "refId": "A", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "- sum (rate (container_network_transmit_bytes_total{kubernetes_io_hostname=~\"^$Node$\"}[1m]))", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "Sent", + "metric": "network", + "refId": "B", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Network I/O pressure", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "logBase": 1, + "show": true + }, + { + "format": "Bps", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 34, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Total usage", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 65 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 4, + "links": [], + "maxDataPoints": 100, + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "10.0.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (container_memory_working_set_bytes{id=\"/\",kubernetes_io_hostname=~\"^$Node$\"}) / sum (machine_memory_bytes{kubernetes_io_hostname=~\"^$Node$\"}) * 100", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "title": "Cluster memory usage", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 65 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 6, + "links": [], + "maxDataPoints": 100, + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "10.0.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_cpu_usage_seconds_total{id=\"/\",kubernetes_io_hostname=~\"^$Node$\"}[1m])) / sum (machine_cpu_cores{kubernetes_io_hostname=~\"^$Node$\"}) * 100", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "title": "Cluster CPU usage (1m avg)", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 65 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 7, + "links": [], + "maxDataPoints": 100, + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "10.0.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (container_fs_usage_bytes{device=~\"^/dev/[sv]d[a-z][1-9]$\",id=\"/\",kubernetes_io_hostname=~\"^$Node$\"}) / sum (container_fs_limit_bytes{device=~\"^/dev/[sv]d[a-z][1-9]$\",id=\"/\",kubernetes_io_hostname=~\"^$Node$\"}) * 100", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 10 + } + ], + "title": "Cluster filesystem usage", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 0, + "y": 13 + }, + "id": 9, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (container_memory_working_set_bytes{id=\"/\",kubernetes_io_hostname=~\"^$Node$\"})", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "title": "Used", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 4, + "y": 13 + }, + "id": 10, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (machine_memory_bytes{kubernetes_io_hostname=~\"^$Node$\"})", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "title": "Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 8, + "y": 13 + }, + "id": 11, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_cpu_usage_seconds_total{id=\"/\",kubernetes_io_hostname=~\"^$Node$\"}[1m]))", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "title": "Used", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 12, + "y": 13 + }, + "id": 12, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (machine_cpu_cores{kubernetes_io_hostname=~\"^$Node$\"})", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "title": "Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 16, + "y": 13 + }, + "id": 13, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (container_fs_usage_bytes{device=~\"^/dev/[sv]d[a-z][1-9]$\",id=\"/\",kubernetes_io_hostname=~\"^$Node$\"})", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "title": "Used", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 13 + }, + "id": 14, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (container_fs_limit_bytes{device=~\"^/dev/[sv]d[a-z][1-9]$\",id=\"/\",kubernetes_io_hostname=~\"^$Node$\"})", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "title": "Total", + "type": "stat" + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 35, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Pods CPU usage", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 3, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 17 + }, + "height": "", + "hiddenSeries": false, + "id": 17, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "10.0.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "editorMode": "code", + "expr": "sum (rate (container_cpu_usage_seconds_total{image!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (pod)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ pod }}", + "metric": "container_cpu", + "range": true, + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Pods CPU usage (1m avg)", + "tooltip": { + "msResolution": true, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:112", + "format": "none", + "label": "cores", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:113", + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 39, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Pods memory usage", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 2, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 25 + }, + "hiddenSeries": false, + "id": 25, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "10.0.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "editorMode": "code", + "expr": "sum (container_memory_working_set_bytes{image!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}) by (pod)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ pod }}", + "metric": "container_memory_usage:sort_desc", + "range": true, + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Pods memory usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:181", + "format": "bytes", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:182", + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 43, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Pods network I/O", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 2, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 33 + }, + "hiddenSeries": false, + "id": 16, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "10.0.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "editorMode": "code", + "expr": "sum (rate (container_network_receive_bytes_total{image!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (pod)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "-> {{ pod }}", + "metric": "network", + "range": true, + "refId": "A", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "- sum (rate (container_network_transmit_bytes_total{image!=\"\",name=~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (pod)", + "hide": true, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "<- {{ pod }}", + "metric": "network", + "refId": "B", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Pods network I/O (1m avg)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 40 + }, + "id": 37, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 3, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 41 + }, + "height": "", + "hiddenSeries": false, + "id": 24, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "10.0.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_cpu_usage_seconds_total{image!=\"\",name=~\"^k8s_.*\",container!=\"POD\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (container, pod)", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "pod: {{ pod }}| {{ container }}", + "metric": "container_cpu", + "refId": "A", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_cpu_usage_seconds_total{image!=\"\",name!~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (kubernetes_io_hostname, name, image)", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "docker: {{ kubernetes_io_hostname }} | {{ image }} ({{ name }})", + "metric": "container_cpu", + "refId": "B", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_cpu_usage_seconds_total{rkt_container_name!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (kubernetes_io_hostname, rkt_container_name)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "rkt: {{ kubernetes_io_hostname }} | {{ rkt_container_name }}", + "metric": "container_cpu", + "refId": "C", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Containers CPU usage (1m avg)", + "tooltip": { + "msResolution": true, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:337", + "format": "none", + "label": "cores", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:338", + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Containers CPU usage", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 41 + }, + "id": 41, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 2, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 42 + }, + "hiddenSeries": false, + "id": 27, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "10.0.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (container_memory_working_set_bytes{image!=\"\",name=~\"^k8s_.*\",container!=\"POD\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}) by (container, pod)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "pod: {{ pod }} | {{ container }}", + "metric": "container_memory_usage:sort_desc", + "refId": "A", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (container_memory_working_set_bytes{image!=\"\",name!~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}) by (kubernetes_io_hostname, name, image)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "docker: {{ kubernetes_io_hostname }} | {{ image }} ({{ name }})", + "metric": "container_memory_usage:sort_desc", + "refId": "B", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (container_memory_working_set_bytes{rkt_container_name!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}) by (kubernetes_io_hostname, rkt_container_name)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "rkt: {{ kubernetes_io_hostname }} | {{ rkt_container_name }}", + "metric": "container_memory_usage:sort_desc", + "refId": "C", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Containers memory usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:406", + "format": "bytes", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:407", + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Containers memory usage", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 42 + }, + "id": 44, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 2, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 43 + }, + "hiddenSeries": false, + "id": 30, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "10.0.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_network_receive_bytes_total{image!=\"\",name=~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (container, pod)", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "-> pod: {{ pod }} | {{ container }}", + "metric": "network", + "refId": "B", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "- sum (rate (container_network_transmit_bytes_total{image!=\"\",name=~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (container, pod)", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "<- pod: {{ pod }} | {{ container }}", + "metric": "network", + "refId": "D", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_network_receive_bytes_total{image!=\"\",name!~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (kubernetes_io_hostname, name, image)", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "-> docker: {{ kubernetes_io_hostname }} | {{ image }} ({{ name }})", + "metric": "network", + "refId": "A", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "- sum (rate (container_network_transmit_bytes_total{image!=\"\",name!~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (kubernetes_io_hostname, name, image)", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "<- docker: {{ kubernetes_io_hostname }} | {{ image }} ({{ name }})", + "metric": "network", + "refId": "C", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_network_transmit_bytes_total{rkt_container_name!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (kubernetes_io_hostname, rkt_container_name)", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "-> rkt: {{ kubernetes_io_hostname }} | {{ rkt_container_name }}", + "metric": "network", + "refId": "E", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "- sum (rate (container_network_transmit_bytes_total{rkt_container_name!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$namespace$\"}[1m])) by (kubernetes_io_hostname, rkt_container_name)", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "<- rkt: {{ kubernetes_io_hostname }} | {{ rkt_container_name }}", + "metric": "network", + "refId": "F", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Containers network I/O (1m avg)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Containers network I/O", + "type": "row" + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 36, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "System services CPU usage", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 3, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 44 + }, + "height": "", + "hiddenSeries": false, + "id": 23, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "10.0.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "editorMode": "code", + "expr": "sum (rate (container_cpu_usage_seconds_total{systemd_service_name!=\"\",kubernetes_io_hostname=~\"^$Node$\"}[1m])) by (systemd_service_name)", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ systemd_service_name }}", + "metric": "container_cpu", + "range": true, + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "System services CPU usage (1m avg)", + "tooltip": { + "msResolution": true, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": "cores", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 51 + }, + "id": 40, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 2, + "editable": true, + "error": false, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 29 + }, + "hiddenSeries": false, + "id": 26, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (container_memory_working_set_bytes{systemd_service_name!=\"\",kubernetes_io_hostname=~\"^$Node$\"}) by (systemd_service_name)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ systemd_service_name }}", + "metric": "container_memory_usage:sort_desc", + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "System services memory usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "System services memory usage", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 52 + }, + "id": 38, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 3, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 14, + "w": 24, + "x": 0, + "y": 46 + }, + "hiddenSeries": false, + "id": 20, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "10.0.2", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_cpu_usage_seconds_total{id!=\"/\",kubernetes_io_hostname=~\"^$Node$\"}[1m])) by (id)", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ id }}", + "metric": "container_cpu", + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "All processes CPU usage (1m avg)", + "tooltip": { + "msResolution": true, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:254", + "format": "none", + "label": "cores", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:255", + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "All processes CPU usage", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 53 + }, + "id": 42, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 2, + "editable": true, + "error": false, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 14, + "w": 24, + "x": 0, + "y": 47 + }, + "hiddenSeries": false, + "id": 28, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (container_memory_working_set_bytes{id!=\"/\",kubernetes_io_hostname=~\"^$Node$\"}) by (id)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ id }}", + "metric": "container_memory_usage:sort_desc", + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "All processes memory usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "All processes memory usage", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 54 + }, + "id": 45, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "decimals": 2, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 14, + "w": 24, + "x": 0, + "y": 48 + }, + "hiddenSeries": false, + "id": 29, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "sum (rate (container_network_receive_bytes_total{id!=\"/\",kubernetes_io_hostname=~\"^$Node$\"}[1m])) by (id)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "-> {{ id }}", + "metric": "network", + "refId": "A", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "expr": "- sum (rate (container_network_transmit_bytes_total{id!=\"/\",kubernetes_io_hostname=~\"^$Node$\"}[1m])) by (id)", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "<- {{ id }}", + "metric": "network", + "refId": "B", + "step": 10 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "All processes network I/O (1m avg)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": false + } + ], + "yaxis": { + "align": false + } + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "All processes network I/O", + "type": "row" + } + ], + "refresh": "10s", + "schemaVersion": 38, + "style": "dark", + "tags": [ + "kubernetes" + ], + "templating": { + "list": [ + { + "allValue": "", + "current": { + "selected": true, + "text": "monitoring", + "value": "monitoring" + }, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "definition": "label_values(namespace)", + "hide": 0, + "includeAll": true, + "multi": false, + "name": "namespace", + "options": [], + "query": "label_values(namespace)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "af6b44aa-0703-4979-825c-c1afba946534" + }, + "definition": "", + "hide": 0, + "includeAll": true, + "multi": false, + "name": "Node", + "options": [], + "query": "label_values(kubernetes_io_hostname)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Kubernetes Monitoring Dashboard", + "uid": "msqzbWjWk", + "version": 2, + "weekStart": "" + } \ No newline at end of file diff --git a/helm/capif/charts/grafana/loki-logs.json b/helm/capif/charts/grafana/loki-logs.json new file mode 100644 index 0000000..e7e4d72 --- /dev/null +++ b/helm/capif/charts/grafana/loki-logs.json @@ -0,0 +1,281 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Simple Loki dashboard", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 13198, + "graphTooltip": 0, + "id": 9, + "links": [], + "liveNow": false, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "fill": 0, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "hideEmpty": true, + "hideZero": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.5.2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "count_over_time({job=\"fluent-bit\"}[1m])", + "legendFormat": "{{ container_name }}", + "queryType": "range", + "refId": "A" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Metric Rate", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "loki", + "uid": "e4f43364-7019-45a7-aa7a-14ce2d4ddb0b" + }, + "gridPos": { + "h": 14, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 2, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "pluginVersion": "7.1.3", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "e4f43364-7019-45a7-aa7a-14ce2d4ddb0b" + }, + "editorMode": "code", + "expr": "{job=~\"fluent-bit\"} |~ \"$string\"", + "legendFormat": "", + "queryType": "range", + "refId": "A" + } + ], + "title": "Loki Search", + "type": "logs" + } + ], + "refresh": "1m", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Loki", + "value": "Loki" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "loki", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "definition": "label_values(container_name)", + "hide": 0, + "includeAll": true, + "label": "app", + "multi": false, + "name": "app", + "options": [], + "query": "label_values(container_name)", + "refresh": 2, + "regex": "(.*)-.*-.*-.*-.*-.*", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "definition": "label_values(container_name)", + "hide": 0, + "includeAll": true, + "label": "job", + "multi": false, + "name": "job", + "options": [], + "query": "label_values(container_name)", + "refresh": 2, + "regex": "$app-(.*)", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "", + "value": "" + }, + "hide": 0, + "label": "string", + "name": "string", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "skipUrlSync": false, + "type": "textbox" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Loki Logs", + "uid": "ffxEJdvGz", + "version": 6, + "weekStart": "" +} \ No newline at end of file diff --git a/helm/capif/charts/grafana/templates/NOTES.txt b/helm/capif/charts/grafana/templates/NOTES.txt new file mode 100644 index 0000000..c7ac87e --- /dev/null +++ b/helm/capif/charts/grafana/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "grafana.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "grafana.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "grafana.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "grafana.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/grafana/templates/_helpers.tpl b/helm/capif/charts/grafana/templates/_helpers.tpl new file mode 100644 index 0000000..993f46b --- /dev/null +++ b/helm/capif/charts/grafana/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "grafana.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "grafana.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "grafana.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "grafana.labels" -}} +helm.sh/chart: {{ include "grafana.chart" . }} +{{ include "grafana.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "grafana.selectorLabels" -}} +app.kubernetes.io/name: {{ include "grafana.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "grafana.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "grafana.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/templates/grafana-configmap.yaml b/helm/capif/charts/grafana/templates/configmap.yaml similarity index 83% rename from helm/capif/templates/grafana-configmap.yaml rename to helm/capif/charts/grafana/templates/configmap.yaml index 654101f..dc70d23 100644 --- a/helm/capif/templates/grafana-configmap.yaml +++ b/helm/capif/charts/grafana/templates/configmap.yaml @@ -1,4 +1,4 @@ -{{- if eq .Values.monitoring.enable "true" }} +{{- if .Values.enabled | default false }} apiVersion: v1 kind: ConfigMap metadata: @@ -7,13 +7,29 @@ data: datasources.yaml: | apiVersion: 1 datasources: + - name: Prometheus + type: prometheus + typeName: Prometheus + typeLogoUrl: public/app/plugins/datasource/prometheus/img/prometheus_logo.svg + access: proxy + url: {{ .Values.env.prometheusUrl }} + uid: af6b44aa-0703-4979-825c-c1afba946534 + user: '' + database: '' + basicAuth: false + isDefault: false + jsonData: + httpMethod: POST + prometheusType: Prometheus + prometheusVersion: 2.40.1 + readOnly: false - name: Loki type: loki uid: e4f43364-7019-45a7-aa7a-14ce2d4ddb0b typeName: Loki typeLogoUrl: public/app/plugins/datasource/loki/img/loki_icon.svg access: proxy - url: {{ .Values.monitoring.grafana.env.lokiUrl }} + url: {{ .Values.env.lokiUrl }} user: '' database: '' basicAuth: false @@ -24,30 +40,14 @@ data: matcherRegex: '"traceID":\s*"([a-fA-F0-9]+)"' name: traceID url: "$${__value.raw}" - readOnly: false - - name: Prometheus - type: prometheus - typeName: Prometheus - typeLogoUrl: public/app/plugins/datasource/prometheus/img/prometheus_logo.svg - access: proxy - url: {{ .Values.monitoring.grafana.env.prometheusUrl }} - uid: af6b44aa-0703-4979-825c-c1afba946534 - user: '' - database: '' - basicAuth: false - isDefault: false - jsonData: - httpMethod: POST - prometheusType: Prometheus - prometheusVersion: 2.40.1 - readOnly: false + readOnly: false - name: Tempo type: tempo typeName: Tempo typeLogoUrl: public/app/plugins/datasource/tempo/img/tempo_logo.svg uid: fee7e008-f836-424a-b701-88cad583c715 access: proxy - url: {{ .Values.monitoring.grafana.env.tempoUrl }} + url: {{ .Values.env.tempoUrl }} user: '' database: '' basicAuth: false @@ -78,16 +78,6 @@ data: --- -apiVersion: v1 -kind: ConfigMap -metadata: - name: docker-monitoring -data: - Docker-monitoring.json: |- -{{ .Files.Get "docker-monitoring.json" | indent 4 }} - ---- - apiVersion: v1 kind: ConfigMap metadata: @@ -105,4 +95,4 @@ metadata: data: loki-logs.json: | {{ .Files.Get "loki-logs.json" | indent 4 }} -{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/grafana-deployment.yaml b/helm/capif/charts/grafana/templates/deployment.yaml similarity index 55% rename from helm/capif/templates/grafana-deployment.yaml rename to helm/capif/charts/grafana/templates/deployment.yaml index 844f32e..d74241f 100644 --- a/helm/capif/templates/grafana-deployment.yaml +++ b/helm/capif/charts/grafana/templates/deployment.yaml @@ -1,54 +1,56 @@ -{{- if eq .Values.monitoring.enable "true" }} +{{- if .Values.enabled | default false }} apiVersion: apps/v1 kind: Deployment metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) + name: {{ include "grafana.fullname" . }} labels: - io.kompose.service: grafana - {{- include "capif.labels" . | nindent 4 }} - name: grafana + {{- include "grafana.labels" . | nindent 4 }} spec: - replicas: 1 + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} strategy: type: Recreate + {{- end }} selector: matchLabels: - io.kompose.service: grafana - {{- include "capif.selectorLabels" . | nindent 6 }} - strategy: - type: Recreate + {{- include "grafana.selectorLabels" . | nindent 6 }} template: metadata: annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - checksum/config: {{ include (print $.Template.BasePath "/grafana-configmap.yaml") . | sha256sum }} + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} labels: - io.kompose.network/monitoring-default: "true" - io.kompose.service: grafana - {{- include "capif.selectorLabels" . | nindent 8 }} + {{- include "grafana.selectorLabels" . | nindent 8 }} spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "grafana.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - env: - name: GF_AUTH_ANONYMOUS_ENABLED - value: {{ quote .Values.monitoring.grafana.env.gfAuthAnonymousEnable }} + value: {{ quote .Values.env.gfAuthAnonymousEnable }} - name: GF_SECURITY_ALLOW_EMBEDDING - value: {{ quote .Values.monitoring.grafana.env.gfSecurityAllowEmbedding }} + value: {{ quote .Values.env.gfSecurityAllowEmbedding }} - name: GF_PATHS_PROVISIONING value: /etc/grafana/provisioning - image: {{ .Values.monitoring.grafana.image.repository }}:{{ .Values.monitoring.grafana.image.tag }} - name: grafana + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: {{ .Chart.Name }} envFrom: - secretRef: name: grafana-secrets ports: - - containerPort: 3000 + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP +# livenessProbe: +# tcpSocket: +# port: {{ .Values.service.port }} resources: - {{- toYaml .Values.monitoring.grafana.resources | nindent 12 }} - securityContext: - runAsUser: 0 + {{- toYaml .Values.resources | nindent 12 }} volumeMounts: - name: grafana-datasources mountPath: /etc/grafana/provisioning/datasources/datasources.yaml @@ -56,16 +58,13 @@ spec: - name: grafana-default mountPath: /etc/grafana/provisioning/dashboards/default.yaml subPath: default.yaml - - name: grafana-docker - mountPath: /var/lib/grafana/dashboards/Docker-monitoring.json - subPath: Docker-monitoring.json - name: kubernetes-dashboard mountPath: /var/lib/grafana/dashboards/kubernetes-dashboard.json subPath: kubernetes-dashboard.json - name: grafana-loki mountPath: /var/lib/grafana/dashboards/Loki-Logs.json subPath: loki-logs.json - {{- if eq .Values.monitoring.grafana.persistence.enable "true" }} + {{- if .Values.persistence.enable | default false }} - name: grafana-claim0 mountPath: /var/lib/grafana {{- end }} @@ -82,12 +81,6 @@ spec: items: - key: "default.yaml" path: "default.yaml" - - name: grafana-docker - configMap: - name: docker-monitoring - items: - - key: "Docker-monitoring.json" - path: "Docker-monitoring.json" - name: kubernetes-dashboard configMap: name: kubernetes-dashboard @@ -100,10 +93,21 @@ spec: items: - key: "loki-logs.json" path: "loki-logs.json" - {{- if eq .Values.monitoring.grafana.persistence.enable "true" }} + {{- if .Values.persistence.enable | default false }} - name: grafana-claim0 persistentVolumeClaim: claimName: grafana-claim0 - {{- end }} - restartPolicy: Always + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} \ No newline at end of file diff --git a/helm/capif/charts/grafana/templates/hpa.yaml b/helm/capif/charts/grafana/templates/hpa.yaml new file mode 100644 index 0000000..f8efd87 --- /dev/null +++ b/helm/capif/charts/grafana/templates/hpa.yaml @@ -0,0 +1,30 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "grafana.fullname" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "grafana.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/grafana/templates/ingress.yaml b/helm/capif/charts/grafana/templates/ingress.yaml new file mode 100644 index 0000000..dfe5c9a --- /dev/null +++ b/helm/capif/charts/grafana/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "grafana.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: grafana + port: + number: {{ $svcPort }} + {{- else }} + serviceName: grafana + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/capif/charts/grafana/templates/pvc.yaml b/helm/capif/charts/grafana/templates/pvc.yaml new file mode 100644 index 0000000..7aa2b72 --- /dev/null +++ b/helm/capif/charts/grafana/templates/pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.persistence.enable | default false }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: grafana-claim0 + name: grafana-claim0 +spec: + storageClassName: {{ .Values.persistence.storageClass }} + accessModes: + - ReadWriteMany + resources: + requests: + storage: {{ .Values.persistence.storage }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/grafana/templates/secrets.yaml b/helm/capif/charts/grafana/templates/secrets.yaml new file mode 100644 index 0000000..0313f54 --- /dev/null +++ b/helm/capif/charts/grafana/templates/secrets.yaml @@ -0,0 +1,10 @@ +{{- if .Values.enabled | default false }} +apiVersion: v1 +kind: Secret +metadata: + name: grafana-secrets +type: Opaque +data: + GF_AUTH_ANONYMOUS_ORG_ROLE: {{ .Values.env.gfAuthAnonymousOrgRole | b64enc | quote }} + GF_SECURITY_ADMIN_PASSWORD: {{ .Values.env.gfSecurityAdminPassword | b64enc | quote }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/grafana/templates/service.yaml b/helm/capif/charts/grafana/templates/service.yaml new file mode 100644 index 0000000..b13c5fc --- /dev/null +++ b/helm/capif/charts/grafana/templates/service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.enabled | default false }} +apiVersion: v1 +kind: Service +metadata: + name: grafana + labels: + {{- include "grafana.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.port }} + protocol: TCP + name: http + selector: + {{- include "grafana.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/grafana/templates/serviceaccount.yaml b/helm/capif/charts/grafana/templates/serviceaccount.yaml new file mode 100644 index 0000000..049f60f --- /dev/null +++ b/helm/capif/charts/grafana/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "grafana.serviceAccountName" . }} + labels: + {{- include "grafana.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/grafana/templates/tests/test-connection.yaml b/helm/capif/charts/grafana/templates/tests/test-connection.yaml new file mode 100644 index 0000000..27d46d2 --- /dev/null +++ b/helm/capif/charts/grafana/templates/tests/test-connection.yaml @@ -0,0 +1,17 @@ +{{- if .Values.enabled | default false }} +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "grafana.fullname" . }}-test-connection" + labels: + {{- include "grafana.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['grafana:{{ .Values.service.port }}'] + restartPolicy: Never +{{- end }} diff --git a/helm/capif/charts/grafana/values.yaml b/helm/capif/charts/grafana/values.yaml new file mode 100644 index 0000000..8391800 --- /dev/null +++ b/helm/capif/charts/grafana/values.yaml @@ -0,0 +1,99 @@ +# Default values for grafana. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +enabled: false + +replicaCount: 1 + +image: + repository: grafana/grafana + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# PENDING: gfAuthAnonymousOrgRole and gfSecurityAdminPassword as aws external-secret +env: + gfAuthAnonymousEnable: true + gfSecurityAllowEmbedding: true + gfAuthAnonymousOrgRole: Admin + gfSecurityAdminPassword: secure_pass + prometheusUrl: http://prometheus.prometheus-system.svc.cluster.local:9090 + lokiUrl: http://loki:3100 + tempoUrl: http://tempo:3100 + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +persistence: + enable: true + storage: 10Gi + storageClass: nfs-01 + +service: + type: ClusterIP + port: 3000 + +ingress: + enabled: true + className: "nginx" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: grafana-dt.tactile5g.int + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/loki/.helmignore b/helm/capif/charts/loki/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/loki/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/loki/Chart.yaml b/helm/capif/charts/loki/Chart.yaml new file mode 100644 index 0000000..1125160 --- /dev/null +++ b/helm/capif/charts/loki/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: loki +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/loki/templates/NOTES.txt b/helm/capif/charts/loki/templates/NOTES.txt new file mode 100644 index 0000000..b448c2f --- /dev/null +++ b/helm/capif/charts/loki/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "loki.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "loki.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "loki.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "loki.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/loki/templates/_helpers.tpl b/helm/capif/charts/loki/templates/_helpers.tpl new file mode 100644 index 0000000..fe4bc4e --- /dev/null +++ b/helm/capif/charts/loki/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "loki.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "loki.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "loki.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "loki.labels" -}} +helm.sh/chart: {{ include "loki.chart" . }} +{{ include "loki.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "loki.selectorLabels" -}} +app.kubernetes.io/name: {{ include "loki.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "loki.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "loki.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/loki/templates/deployment.yaml b/helm/capif/charts/loki/templates/deployment.yaml new file mode 100644 index 0000000..7cd543e --- /dev/null +++ b/helm/capif/charts/loki/templates/deployment.yaml @@ -0,0 +1,74 @@ +{{- if .Values.enabled | default false }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "loki.fullname" . }} + labels: + {{- include "loki.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "loki.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "loki.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "loki.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + args: + - -config.file=/etc/loki/local-config.yaml + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/loki/templates/hpa.yaml b/helm/capif/charts/loki/templates/hpa.yaml new file mode 100644 index 0000000..3ffe63f --- /dev/null +++ b/helm/capif/charts/loki/templates/hpa.yaml @@ -0,0 +1,34 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "loki.fullname" . }} + labels: + {{- include "loki.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "loki.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/loki/templates/ingress.yaml b/helm/capif/charts/loki/templates/ingress.yaml new file mode 100644 index 0000000..0ad6092 --- /dev/null +++ b/helm/capif/charts/loki/templates/ingress.yaml @@ -0,0 +1,63 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "loki.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "loki.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: loki + port: + number: {{ $svcPort }} + {{- else }} + serviceName: loki + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/loki/templates/pvc.yaml b/helm/capif/charts/loki/templates/pvc.yaml new file mode 100644 index 0000000..c6594b2 --- /dev/null +++ b/helm/capif/charts/loki/templates/pvc.yaml @@ -0,0 +1,16 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.persistence.enable | default false }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + name: loki-claim0 +spec: + storageClassName: {{ .Values.persistence.storageClass }} + accessModes: + - ReadWriteMany + resources: + requests: + storage: {{ .Values.persistence.storage }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/loki/templates/service.yaml b/helm/capif/charts/loki/templates/service.yaml new file mode 100644 index 0000000..5eb8004 --- /dev/null +++ b/helm/capif/charts/loki/templates/service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.enabled | default false }} +apiVersion: v1 +kind: Service +metadata: + name: loki + labels: + {{- include "loki.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "loki.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/helm/capif/charts/loki/templates/serviceaccount.yaml b/helm/capif/charts/loki/templates/serviceaccount.yaml new file mode 100644 index 0000000..491cd0e --- /dev/null +++ b/helm/capif/charts/loki/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "loki.serviceAccountName" . }} + labels: + {{- include "loki.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/loki/templates/tests/test-connection.yaml b/helm/capif/charts/loki/templates/tests/test-connection.yaml new file mode 100644 index 0000000..42bf046 --- /dev/null +++ b/helm/capif/charts/loki/templates/tests/test-connection.yaml @@ -0,0 +1,17 @@ +{{- if .Values.enabled | default false }} +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "loki.fullname" . }}-test-connection" + labels: + {{- include "loki.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['loki:{{ .Values.service.port }}'] + restartPolicy: Never +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/loki/values.yaml b/helm/capif/charts/loki/values.yaml new file mode 100644 index 0000000..444311d --- /dev/null +++ b/helm/capif/charts/loki/values.yaml @@ -0,0 +1,115 @@ +# Default values for loki. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +enabled: false + +replicaCount: 1 + +image: + repository: grafana/loki + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "2.8.0" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + runAsUser: 0 + +persistence: + enable: true + storage: 100Mi + storageClass: nfs-01 + +service: + type: ClusterIP + port: 3100 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: + tcpSocket: + port: 3100 + initialDelaySeconds: 20 + periodSeconds: 5 + +readinessProbe: + tcpSocket: + port: 3100 + initialDelaySeconds: 20 + periodSeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: loki-claim0 + persistentVolumeClaim: + claimName: loki-claim0 + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: loki-claim0 + mountPath: /loki/wal + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/mock-server/templates/deployment.yaml b/helm/capif/charts/mock-server/templates/deployment.yaml index 270411b..89261d7 100644 --- a/helm/capif/charts/mock-server/templates/deployment.yaml +++ b/helm/capif/charts/mock-server/templates/deployment.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} apiVersion: apps/v1 kind: Deployment metadata: @@ -66,3 +67,4 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mock-server/templates/hpa.yaml b/helm/capif/charts/mock-server/templates/hpa.yaml index fbdd9bc..d0ae761 100644 --- a/helm/capif/charts/mock-server/templates/hpa.yaml +++ b/helm/capif/charts/mock-server/templates/hpa.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} {{- if .Values.autoscaling.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler @@ -30,3 +31,4 @@ spec: averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- end }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mock-server/templates/ingress.yaml b/helm/capif/charts/mock-server/templates/ingress.yaml index 68406e7..96c13ae 100644 --- a/helm/capif/charts/mock-server/templates/ingress.yaml +++ b/helm/capif/charts/mock-server/templates/ingress.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} {{- if .Values.ingress.enabled -}} {{- $fullName := include "mock-server.fullname" . -}} {{- $svcPort := .Values.service.port -}} @@ -59,3 +60,4 @@ spec: {{- end }} {{- end }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mock-server/templates/service.yaml b/helm/capif/charts/mock-server/templates/service.yaml index f160730..e11c5cf 100644 --- a/helm/capif/charts/mock-server/templates/service.yaml +++ b/helm/capif/charts/mock-server/templates/service.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} apiVersion: v1 kind: Service metadata: @@ -13,3 +14,4 @@ spec: name: http selector: {{- include "mock-server.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mock-server/templates/serviceaccount.yaml b/helm/capif/charts/mock-server/templates/serviceaccount.yaml index 004803d..2538086 100644 --- a/helm/capif/charts/mock-server/templates/serviceaccount.yaml +++ b/helm/capif/charts/mock-server/templates/serviceaccount.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} {{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount @@ -11,3 +12,4 @@ metadata: {{- end }} automountServiceAccountToken: {{ .Values.serviceAccount.automount }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mock-server/templates/tests/test-connection.yaml b/helm/capif/charts/mock-server/templates/tests/test-connection.yaml index 796d72b..7173753 100644 --- a/helm/capif/charts/mock-server/templates/tests/test-connection.yaml +++ b/helm/capif/charts/mock-server/templates/tests/test-connection.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} apiVersion: v1 kind: Pod metadata: @@ -13,3 +14,4 @@ spec: command: ['wget'] args: ['mock-server:{{ .Values.service.port }}'] restartPolicy: Never +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mock-server/values.yaml b/helm/capif/charts/mock-server/values.yaml index f005d9f..cd2dfc0 100644 --- a/helm/capif/charts/mock-server/values.yaml +++ b/helm/capif/charts/mock-server/values.yaml @@ -2,6 +2,8 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. +enabled: false + replicaCount: 1 image: diff --git a/helm/capif/charts/mongo-express/templates/deployment.yaml b/helm/capif/charts/mongo-express/templates/deployment.yaml index 175a045..32bda44 100644 --- a/helm/capif/charts/mongo-express/templates/deployment.yaml +++ b/helm/capif/charts/mongo-express/templates/deployment.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} apiVersion: apps/v1 kind: Deployment metadata: @@ -71,3 +72,4 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-express/templates/hpa.yaml b/helm/capif/charts/mongo-express/templates/hpa.yaml index 2b7ca92..d591daf 100644 --- a/helm/capif/charts/mongo-express/templates/hpa.yaml +++ b/helm/capif/charts/mongo-express/templates/hpa.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} {{- if .Values.autoscaling.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler @@ -30,3 +31,4 @@ spec: averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- end }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-express/templates/ingress.yaml b/helm/capif/charts/mongo-express/templates/ingress.yaml index 9a0f710..f4d860c 100644 --- a/helm/capif/charts/mongo-express/templates/ingress.yaml +++ b/helm/capif/charts/mongo-express/templates/ingress.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} {{- if .Values.ingress.enabled -}} {{- $fullName := include "mongo-express.fullname" . -}} {{- $svcPort := .Values.service.port -}} @@ -49,13 +50,14 @@ spec: backend: {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} service: - name: {{ $fullName }} + name: mongo-express port: number: {{ $svcPort }} {{- else }} - serviceName: {{ $fullName }} + serviceName: mongo-express servicePort: {{ $svcPort }} {{- end }} {{- end }} {{- end }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-express/templates/service.yaml b/helm/capif/charts/mongo-express/templates/service.yaml index 888a03f..c72729e 100644 --- a/helm/capif/charts/mongo-express/templates/service.yaml +++ b/helm/capif/charts/mongo-express/templates/service.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} apiVersion: v1 kind: Service metadata: @@ -13,3 +14,4 @@ spec: name: http selector: {{- include "mongo-express.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-express/templates/serviceaccount.yaml b/helm/capif/charts/mongo-express/templates/serviceaccount.yaml index 4a6a666..4535ab9 100644 --- a/helm/capif/charts/mongo-express/templates/serviceaccount.yaml +++ b/helm/capif/charts/mongo-express/templates/serviceaccount.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} {{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount @@ -11,3 +12,4 @@ metadata: {{- end }} automountServiceAccountToken: {{ .Values.serviceAccount.automount }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-express/templates/tests/test-connection.yaml b/helm/capif/charts/mongo-express/templates/tests/test-connection.yaml index 666e36f..fc31c20 100644 --- a/helm/capif/charts/mongo-express/templates/tests/test-connection.yaml +++ b/helm/capif/charts/mongo-express/templates/tests/test-connection.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} apiVersion: v1 kind: Pod metadata: @@ -13,3 +14,4 @@ spec: command: ['wget'] args: ['mongo-express:{{ .Values.service.port }}'] restartPolicy: Never +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-express/values.yaml b/helm/capif/charts/mongo-express/values.yaml index 447ec98..36f3a8b 100644 --- a/helm/capif/charts/mongo-express/values.yaml +++ b/helm/capif/charts/mongo-express/values.yaml @@ -2,6 +2,8 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. +enabled: false + replicaCount: 1 image: @@ -50,7 +52,7 @@ service: ingress: enabled: false - className: "" + className: "nginx" annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" diff --git a/helm/capif/charts/mongo-register-express/templates/deployment.yaml b/helm/capif/charts/mongo-register-express/templates/deployment.yaml index 3e86c2d..d77c8bc 100644 --- a/helm/capif/charts/mongo-register-express/templates/deployment.yaml +++ b/helm/capif/charts/mongo-register-express/templates/deployment.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} apiVersion: apps/v1 kind: Deployment metadata: @@ -71,3 +72,4 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-register-express/templates/hpa.yaml b/helm/capif/charts/mongo-register-express/templates/hpa.yaml index 7f0a835..3804acd 100644 --- a/helm/capif/charts/mongo-register-express/templates/hpa.yaml +++ b/helm/capif/charts/mongo-register-express/templates/hpa.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} {{- if .Values.autoscaling.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler @@ -30,3 +31,4 @@ spec: averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- end }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-register-express/templates/ingress.yaml b/helm/capif/charts/mongo-register-express/templates/ingress.yaml index 02c99e5..be3d87f 100644 --- a/helm/capif/charts/mongo-register-express/templates/ingress.yaml +++ b/helm/capif/charts/mongo-register-express/templates/ingress.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} {{- if .Values.ingress.enabled -}} {{- $fullName := include "mongo-register-express.fullname" . -}} {{- $svcPort := .Values.service.port -}} @@ -49,13 +50,14 @@ spec: backend: {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} service: - name: {{ $fullName }} + name: mongo-register-express port: number: {{ $svcPort }} {{- else }} - serviceName: {{ $fullName }} + serviceName: mongo-register-express servicePort: {{ $svcPort }} {{- end }} {{- end }} {{- end }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-register-express/templates/service.yaml b/helm/capif/charts/mongo-register-express/templates/service.yaml index eed599c..7598b38 100644 --- a/helm/capif/charts/mongo-register-express/templates/service.yaml +++ b/helm/capif/charts/mongo-register-express/templates/service.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} apiVersion: v1 kind: Service metadata: @@ -13,3 +14,4 @@ spec: name: http selector: {{- include "mongo-register-express.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-register-express/templates/serviceaccount.yaml b/helm/capif/charts/mongo-register-express/templates/serviceaccount.yaml index 21c6862..ade812a 100644 --- a/helm/capif/charts/mongo-register-express/templates/serviceaccount.yaml +++ b/helm/capif/charts/mongo-register-express/templates/serviceaccount.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} {{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount @@ -11,3 +12,4 @@ metadata: {{- end }} automountServiceAccountToken: {{ .Values.serviceAccount.automount }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-register-express/templates/tests/test-connection.yaml b/helm/capif/charts/mongo-register-express/templates/tests/test-connection.yaml index 240abe3..cdafc63 100644 --- a/helm/capif/charts/mongo-register-express/templates/tests/test-connection.yaml +++ b/helm/capif/charts/mongo-register-express/templates/tests/test-connection.yaml @@ -1,3 +1,4 @@ +{{- if .Values.enabled | default false }} apiVersion: v1 kind: Pod metadata: @@ -11,5 +12,6 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "mongo-register-express.fullname" . }}:{{ .Values.service.port }}'] + args: ['mongo-register-express:{{ .Values.service.port }}'] restartPolicy: Never +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/mongo-register-express/values.yaml b/helm/capif/charts/mongo-register-express/values.yaml index d36bf6c..dd225f5 100644 --- a/helm/capif/charts/mongo-register-express/values.yaml +++ b/helm/capif/charts/mongo-register-express/values.yaml @@ -2,6 +2,8 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. +enabled: false + replicaCount: 1 image: @@ -51,7 +53,7 @@ service: ingress: enabled: false - className: "" + className: "nginx" annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" diff --git a/helm/capif/charts/otelcollector/.helmignore b/helm/capif/charts/otelcollector/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/otelcollector/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/otelcollector/Chart.yaml b/helm/capif/charts/otelcollector/Chart.yaml new file mode 100644 index 0000000..77867b3 --- /dev/null +++ b/helm/capif/charts/otelcollector/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: otelcollector +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/otelcollector/templates/NOTES.txt b/helm/capif/charts/otelcollector/templates/NOTES.txt new file mode 100644 index 0000000..a0fe621 --- /dev/null +++ b/helm/capif/charts/otelcollector/templates/NOTES.txt @@ -0,0 +1,26 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else }} +{{- range .Values.service }} + {{- if contains "NodePort" .type }} + export NODE_PORT=$(kubectl get --namespace {{ $.Release.Namespace }} -o jsonpath="{.spec.ports[?(@.name=='{{ .name }}')].nodePort}" services {{ include "otelcollector.fullname" $ }}) + export NODE_IP=$(kubectl get nodes --namespace {{ $.Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + {{- else if contains "LoadBalancer" .type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ $.Release.Namespace }} svc -w {{ include "otelcollector.fullname" $ }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ $.Release.Namespace }} {{ include "otelcollector.fullname" $ }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ (index .ports 0).port }} + {{- else if contains "ClusterIP" .type }} + export POD_NAME=$(kubectl get pods --namespace {{ $.Release.Namespace }} -l "app.kubernetes.io/name={{ include "otelcollector.name" $ }},app.kubernetes.io/instance={{ $.Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ $.Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ $.Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT + {{- end }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/otelcollector/templates/_helpers.tpl b/helm/capif/charts/otelcollector/templates/_helpers.tpl new file mode 100644 index 0000000..0868fea --- /dev/null +++ b/helm/capif/charts/otelcollector/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "otelcollector.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "otelcollector.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "otelcollector.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "otelcollector.labels" -}} +helm.sh/chart: {{ include "otelcollector.chart" . }} +{{ include "otelcollector.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "otelcollector.selectorLabels" -}} +app.kubernetes.io/name: {{ include "otelcollector.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "otelcollector.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "otelcollector.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/otel-collector-configmap.yaml b/helm/capif/charts/otelcollector/templates/configmap.yaml similarity index 81% rename from helm/capif/templates/otel-collector-configmap.yaml rename to helm/capif/charts/otelcollector/templates/configmap.yaml index fed1535..d169e1f 100644 --- a/helm/capif/templates/otel-collector-configmap.yaml +++ b/helm/capif/charts/otelcollector/templates/configmap.yaml @@ -1,4 +1,4 @@ -{{- if eq .Values.monitoring.enable "true" }} +{{- if .Values.enabled | default false }} apiVersion: v1 kind: ConfigMap metadata: @@ -22,7 +22,7 @@ data: loglevel: debug otlp: #timeout: 60s - endpoint: {{ .Values.monitoring.otel.configMap.tempoEndpoint }} + endpoint: {{ .Values.configMap.tempoEndpoint }} tls: insecure: true diff --git a/helm/capif/charts/otelcollector/templates/deployment.yaml b/helm/capif/charts/otelcollector/templates/deployment.yaml new file mode 100644 index 0000000..a39db22 --- /dev/null +++ b/helm/capif/charts/otelcollector/templates/deployment.yaml @@ -0,0 +1,75 @@ +{{- if .Values.enabled | default false }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "otelcollector.fullname" . }} + labels: + {{- include "otelcollector.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "otelcollector.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "otelcollector.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "otelcollector.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + {{- range .Values.service }} + - containerPort: {{ .port }} + name: {{ .name }} + protocol: {{ if eq .name "otel-collector-udp" }}UDP{{ else }}TCP{{ end }} + {{- end }} + args: + - --config + - /etc/otel-collector-config.yaml + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/otelcollector/templates/hpa.yaml b/helm/capif/charts/otelcollector/templates/hpa.yaml new file mode 100644 index 0000000..69863be --- /dev/null +++ b/helm/capif/charts/otelcollector/templates/hpa.yaml @@ -0,0 +1,34 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "otelcollector.fullname" . }} + labels: + {{- include "otelcollector.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "otelcollector.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/otelcollector/templates/ingress.yaml b/helm/capif/charts/otelcollector/templates/ingress.yaml new file mode 100644 index 0000000..5e3fae9 --- /dev/null +++ b/helm/capif/charts/otelcollector/templates/ingress.yaml @@ -0,0 +1,63 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "otelcollector.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "otelcollector.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/otelcollector/templates/service.yaml b/helm/capif/charts/otelcollector/templates/service.yaml new file mode 100644 index 0000000..cb8afee --- /dev/null +++ b/helm/capif/charts/otelcollector/templates/service.yaml @@ -0,0 +1,19 @@ +{{- if .Values.enabled }} +{{- range .Values.service }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .name }} + labels: + {{- include "otelcollector.labels" $ | nindent 4 }} +spec: + type: {{ .type }} + ports: + - port: {{ .port }} + name: {{ .name }} + protocol: {{ if eq .name "otel-collector-udp" }}UDP{{ else }}TCP{{ end }} + selector: + {{- include "otelcollector.selectorLabels" $ | nindent 4 }} +--- +{{- end }} +{{- end }} diff --git a/helm/capif/charts/otelcollector/templates/serviceaccount.yaml b/helm/capif/charts/otelcollector/templates/serviceaccount.yaml new file mode 100644 index 0000000..68267fd --- /dev/null +++ b/helm/capif/charts/otelcollector/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "otelcollector.serviceAccountName" . }} + labels: + {{- include "otelcollector.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/otelcollector/values.yaml b/helm/capif/charts/otelcollector/values.yaml new file mode 100644 index 0000000..cd541af --- /dev/null +++ b/helm/capif/charts/otelcollector/values.yaml @@ -0,0 +1,119 @@ +# Default values for otelcollector. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + + +enabled: false + +replicaCount: 1 + +image: + repository: otel/opentelemetry-collector + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "0.81.0" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +configMap: + tempoEndpoint: tempo:4317 + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + - name: otel-collector + type: ClusterIP + port: 55680 + - name: otel-tcp + type: ClusterIP + port: 4318 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: +# httpGet: +# path: / +# port: http +readinessProbe: +# httpGet: +# path: / +# port: http + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: op-telemetry + configMap: + name: open-telemetry-configmap + items: + - key: "otel-collector-config.yaml" + path: "otel-collector-config.yaml" + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: op-telemetry + mountPath: /etc/otel-collector-config.yaml + subPath: otel-collector-config.yaml + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/charts/renderer/.helmignore b/helm/capif/charts/renderer/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/capif/charts/renderer/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/capif/charts/renderer/Chart.yaml b/helm/capif/charts/renderer/Chart.yaml new file mode 100644 index 0000000..49cab70 --- /dev/null +++ b/helm/capif/charts/renderer/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: renderer +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/capif/charts/renderer/templates/NOTES.txt b/helm/capif/charts/renderer/templates/NOTES.txt new file mode 100644 index 0000000..c3a1259 --- /dev/null +++ b/helm/capif/charts/renderer/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "renderer.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "renderer.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "renderer.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "renderer.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/capif/charts/renderer/templates/_helpers.tpl b/helm/capif/charts/renderer/templates/_helpers.tpl new file mode 100644 index 0000000..6ed72f2 --- /dev/null +++ b/helm/capif/charts/renderer/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "renderer.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "renderer.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "renderer.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "renderer.labels" -}} +helm.sh/chart: {{ include "renderer.chart" . }} +{{ include "renderer.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "renderer.selectorLabels" -}} +app.kubernetes.io/name: {{ include "renderer.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "renderer.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "renderer.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/capif/charts/renderer/templates/configmap.yaml b/helm/capif/charts/renderer/templates/configmap.yaml new file mode 100644 index 0000000..102b859 --- /dev/null +++ b/helm/capif/charts/renderer/templates/configmap.yaml @@ -0,0 +1,8 @@ +{{- if .Values.enabled | default false }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: renderer-configmap +data: + ENABLE_METRICS: {{ quote .Values.env.enableMetrics }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/renderer/templates/deployment.yaml b/helm/capif/charts/renderer/templates/deployment.yaml new file mode 100644 index 0000000..42cab3f --- /dev/null +++ b/helm/capif/charts/renderer/templates/deployment.yaml @@ -0,0 +1,74 @@ +{{- if .Values.enabled | default false }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "renderer.fullname" . }} + labels: + {{- include "renderer.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "renderer.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "renderer.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "renderer.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: ENABLE_METRICS + valueFrom: + configMapKeyRef: + name: renderer-configmap + key: ENABLE_METRICS + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/renderer/templates/hpa.yaml b/helm/capif/charts/renderer/templates/hpa.yaml new file mode 100644 index 0000000..7783b71 --- /dev/null +++ b/helm/capif/charts/renderer/templates/hpa.yaml @@ -0,0 +1,34 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "renderer.fullname" . }} + labels: + {{- include "renderer.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "renderer.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/renderer/templates/ingress.yaml b/helm/capif/charts/renderer/templates/ingress.yaml new file mode 100644 index 0000000..86f62ee --- /dev/null +++ b/helm/capif/charts/renderer/templates/ingress.yaml @@ -0,0 +1,63 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "renderer.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "renderer.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/renderer/templates/service.yaml b/helm/capif/charts/renderer/templates/service.yaml new file mode 100644 index 0000000..47f91af --- /dev/null +++ b/helm/capif/charts/renderer/templates/service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.enabled | default false }} +apiVersion: v1 +kind: Service +metadata: + name: renderer + labels: + {{- include "renderer.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "renderer.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/helm/capif/charts/renderer/templates/serviceaccount.yaml b/helm/capif/charts/renderer/templates/serviceaccount.yaml new file mode 100644 index 0000000..00ff2dc --- /dev/null +++ b/helm/capif/charts/renderer/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- if .Values.enabled | default false }} +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "renderer.serviceAccountName" . }} + labels: + {{- include "renderer.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/capif/charts/renderer/templates/tests/test-connection.yaml b/helm/capif/charts/renderer/templates/tests/test-connection.yaml new file mode 100644 index 0000000..9e7838a --- /dev/null +++ b/helm/capif/charts/renderer/templates/tests/test-connection.yaml @@ -0,0 +1,17 @@ +{{- if .Values.enabled | default false }} +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "renderer.fullname" . }}-test-connection" + labels: + {{- include "renderer.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['renderer:{{ .Values.service.port }}'] + restartPolicy: Never +{{- end }} diff --git a/helm/capif/charts/renderer/values.yaml b/helm/capif/charts/renderer/values.yaml new file mode 100644 index 0000000..7696150 --- /dev/null +++ b/helm/capif/charts/renderer/values.yaml @@ -0,0 +1,112 @@ +# Default values for renderer. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +enabled: false + +replicaCount: 1 + +image: + repository: grafana/grafana-image-renderer + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +env: + enableMetrics: "true" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8081 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/capif/templates/deployment.yaml b/helm/capif/templates/deployment.yaml deleted file mode 100644 index 8b13789..0000000 --- a/helm/capif/templates/deployment.yaml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/helm/capif/templates/fluent-bit-service.yaml b/helm/capif/templates/fluent-bit-service.yaml deleted file mode 100644 index 90653b3..0000000 --- a/helm/capif/templates/fluent-bit-service.yaml +++ /dev/null @@ -1,24 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - creationTimestamp: null - labels: - io.kompose.service: fluent-bit - {{- include "capif.labels" . | nindent 4 }} - name: fluent-bit -spec: - ports: - - name: "24224-tcp" - port: 24224 - targetPort: 24224 - - name: 24224-udp - port: 24224 - protocol: UDP - targetPort: 24224 - selector: - io.kompose.service: fluent-bit -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/fluentbit-deployment.yaml b/helm/capif/templates/fluentbit-deployment.yaml deleted file mode 100644 index 925ec02..0000000 --- a/helm/capif/templates/fluentbit-deployment.yaml +++ /dev/null @@ -1,59 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - labels: - io.kompose.service: fluent-bit - {{- include "capif.labels" . | nindent 4 }} - name: fluent-bit -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: fluent-bit - {{- include "capif.selectorLabels" . | nindent 6 }} - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - checksum/config: {{ include (print $.Template.BasePath "/fluentbit-configmap.yaml") . | sha256sum }} - creationTimestamp: null - labels: - io.kompose.network/monitoring-default: "true" - io.kompose.service: fluent-bit - {{- include "capif.selectorLabels" . | nindent 8 }} - spec: - containers: - - env: - - name: LOKI_URL - valueFrom: - configMapKeyRef: - name: fluent-bit-configmap - key: LOKI_URL - image: {{ .Values.monitoring.fluentBit.image.repository }}:{{ .Values.monitoring.fluentBit.image.tag }} - name: fluent-bit - ports: - - containerPort: 24224 - - containerPort: 24224 - protocol: UDP - resources: - {{- toYaml .Values.monitoring.fluentBit.resources | nindent 12 }} - volumeMounts: - - name: fluent-bit-conf - mountPath: /fluent-bit/etc/fluent-bit.conf - subPath: fluent-bit.conf - restartPolicy: Always - volumes: - - name: fluent-bit-conf - configMap: - name: fluent-bit-configmap - items: - - key: "fluent-bit.conf" - path: "fluent-bit.conf" -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/grafana-ingress-route.yaml b/helm/capif/templates/grafana-ingress-route.yaml deleted file mode 100644 index 2e2648b..0000000 --- a/helm/capif/templates/grafana-ingress-route.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.monitoring.grafana.ingressRoute.enable "true" }} -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: grafana-ingress-route -spec: - entryPoints: [web] - routes: - - kind: Rule - match: Host(`{{ .Values.monitoring.grafana.ingressRoute.host }}`) - services: - - kind: Service - name: grafana - port: {{ .Values.monitoring.grafana.service.port }} - scheme: http -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/grafana-ingress.yaml b/helm/capif/templates/grafana-ingress.yaml deleted file mode 100644 index 7d7d0cb..0000000 --- a/helm/capif/templates/grafana-ingress.yaml +++ /dev/null @@ -1,34 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if .Values.monitoring.grafana.ingress.enabled -}} -{{- $svcPort := .Values.monitoring.grafana.service.port -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: grafana-ingress - labels: - {{- include "capif.labels" . | nindent 4 }} - {{- with .Values.monitoring.grafana.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: -{{- if .Values.monitoring.grafana.ingress.ingressClassName }} - ingressClassName: {{ .Values.monitoring.grafana.ingress.ingressClassName }} -{{- end }} - rules: - {{- range .Values.monitoring.grafana.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - pathType: {{ .pathType }} - backend: - service: - name: grafana - port: - number: {{ $svcPort }} - {{- end }} - {{- end }} -{{- end }} -{{- end }} diff --git a/helm/capif/templates/grafana-pvc.yaml b/helm/capif/templates/grafana-pvc.yaml deleted file mode 100644 index 5a55282..0000000 --- a/helm/capif/templates/grafana-pvc.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.monitoring.grafana.persistence.enable "true" }} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - labels: - io.kompose.service: grafana-claim0 - name: grafana-claim0 -spec: - storageClassName: {{ .Values.monitoring.grafana.persistence.storageClass }} - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.monitoring.grafana.persistence.storage }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/grafana-secrets.yaml b/helm/capif/templates/grafana-secrets.yaml deleted file mode 100644 index a6796d4..0000000 --- a/helm/capif/templates/grafana-secrets.yaml +++ /dev/null @@ -1,10 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: v1 -kind: Secret -metadata: - name: grafana-secrets -type: Opaque -data: - GF_AUTH_ANONYMOUS_ORG_ROLE: {{ .Values.monitoring.grafana.env.gfAuthAnonymousOrgRole | b64enc | quote }} - GF_SECURITY_ADMIN_PASSWORD: {{ .Values.monitoring.grafana.env.gfSecurityAdminPassword | b64enc | quote }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/grafana-service.yaml b/helm/capif/templates/grafana-service.yaml deleted file mode 100644 index c628043..0000000 --- a/helm/capif/templates/grafana-service.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: v1 -kind: Service -metadata: - name: grafana - labels: - {{- include "capif.labels" . | nindent 4 }} -spec: - type: {{ .Values.monitoring.grafana.service.type }} - ports: - - port: {{ .Values.monitoring.grafana.service.port }} - targetPort: {{ .Values.monitoring.grafana.service.port }} - protocol: TCP - name: http-port - selector: - io.kompose.service: grafana -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/loki-deployment.yaml b/helm/capif/templates/loki-deployment.yaml deleted file mode 100644 index cadf37d..0000000 --- a/helm/capif/templates/loki-deployment.yaml +++ /dev/null @@ -1,54 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - labels: - io.kompose.service: loki - {{- include "capif.labels" . | nindent 4 }} - name: loki -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - io.kompose.service: loki - {{- include "capif.selectorLabels" . | nindent 6 }} - strategy: {} - template: - metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - labels: - io.kompose.network/monitoring-default: "true" - io.kompose.service: loki - {{- include "capif.selectorLabels" . | nindent 8 }} - spec: - containers: - - args: - - -config.file=/etc/loki/local-config.yaml - image: {{ .Values.monitoring.loki.image.repository }}:{{ .Values.monitoring.loki.image.tag }} - name: loki - ports: - - containerPort: 3100 - {{- if eq .Values.monitoring.loki.persistence.enable "true" }} - volumeMounts: - - name: loki-claim0 - mountPath: /loki/wal - {{- end }} - resources: - {{- toYaml .Values.monitoring.loki.resources | nindent 12 }} - securityContext: - runAsUser: 0 - {{- if eq .Values.monitoring.loki.persistence.enable "true" }} - volumes: - - name: loki-claim0 - persistentVolumeClaim: - claimName: loki-claim0 - {{- end }} - restartPolicy: Always -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/loki-pvc.yaml b/helm/capif/templates/loki-pvc.yaml deleted file mode 100644 index 0b90bda..0000000 --- a/helm/capif/templates/loki-pvc.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.monitoring.loki.persistence.enable "true" }} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - labels: - io.kompose.service: loki-claim0 - name: loki-claim0 -spec: - storageClassName: {{ .Values.monitoring.loki.persistence.storageClass }} - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.monitoring.loki.persistence.storage }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/loki-service.yaml b/helm/capif/templates/loki-service.yaml deleted file mode 100644 index cf711a9..0000000 --- a/helm/capif/templates/loki-service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - labels: - io.kompose.service: loki - {{- include "capif.labels" . | nindent 4 }} - name: loki -spec: - ports: - - name: "loki-port" - port: 3100 - targetPort: 3100 - selector: - io.kompose.service: loki -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/otel-collector-deployment.yaml b/helm/capif/templates/otel-collector-deployment.yaml deleted file mode 100644 index 8c83eca..0000000 --- a/helm/capif/templates/otel-collector-deployment.yaml +++ /dev/null @@ -1,54 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - labels: - io.kompose.service: otel-collector - {{- include "capif.labels" . | nindent 4 }} - name: otel-collector -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: otel-collector - {{- include "capif.selectorLabels" . | nindent 6 }} - strategy: - type: Recreate - template: - metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - checksum/config: {{ include (print $.Template.BasePath "/otel-collector-configmap.yaml") . | sha256sum }} - labels: - io.kompose.network/monitoring-default: "true" - io.kompose.service: otel-collector - {{- include "capif.selectorLabels" . | nindent 8 }} - spec: - containers: - - args: - - --config - - /etc/otel-collector-config.yaml - image: {{ .Values.monitoring.otel.image.repository }}:{{ .Values.monitoring.otel.image.tag }} - name: otel-collector - ports: - - containerPort: 55680 - - containerPort: 4317 - resources: - {{- toYaml .Values.monitoring.otel.resources | nindent 12 }} - volumeMounts: - - name: op-telemetry - mountPath: /etc/otel-collector-config.yaml - subPath: otel-collector-config.yaml - restartPolicy: Always - volumes: - - name: op-telemetry - configMap: - name: open-telemetry-configmap - items: - - key: "otel-collector-config.yaml" - path: "otel-collector-config.yaml" -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/otel-collector-service.yaml b/helm/capif/templates/otel-collector-service.yaml deleted file mode 100644 index 761b8ce..0000000 --- a/helm/capif/templates/otel-collector-service.yaml +++ /dev/null @@ -1,22 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - labels: - io.kompose.service: otel-collector - {{- include "capif.labels" . | nindent 4 }} - name: otel-collector -spec: - ports: - - name: "grpc-port" - port: 55680 - targetPort: 55680 - - name: "http-port" - port: 4318 - targetPort: 4318 - selector: - io.kompose.service: otel-collector -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/prometheus-clusterrole.yaml b/helm/capif/templates/prometheus-clusterrole.yaml deleted file mode 100644 index 3470ffd..0000000 --- a/helm/capif/templates/prometheus-clusterrole.yaml +++ /dev/null @@ -1,49 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.monitoring.prometheus.enable "true" }} -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: prometheus - labels: - app: prometheus -rules: -- apiGroups: [""] - resources: - - nodes - - nodes/proxy - - services - - endpoints - - pods - verbs: ["get", "list", "watch"] -- apiGroups: - - extensions - resources: - - ingresses - verbs: ["get", "list", "watch"] -- nonResourceURLs: ["/metrics"] - verbs: ["get"] ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: prometheus - namespace: {{ .Release.Namespace }} - labels: - app: prometheus ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: prometheus - labels: - app: prometheus -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: prometheus -subjects: -- kind: ServiceAccount - name: prometheus - namespace: {{ .Release.Namespace }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/prometheus-configmap.yaml b/helm/capif/templates/prometheus-configmap.yaml deleted file mode 100644 index d2ab952..0000000 --- a/helm/capif/templates/prometheus-configmap.yaml +++ /dev/null @@ -1,141 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.monitoring.prometheus.enable "true" }} -apiVersion: v1 -kind: ConfigMap -metadata: - labels: - app: prometheus - name: prometheus-config -data: - prometheus.rules: |- - groups: - - name: devopscube alert - rules: - - alert: High Pod Memory - expr: sum(container_memory_usage_bytes) > 1 - for: 1m - labels: - severity: slack - annotations: - summary: High Memory Usage - prometheus.yml: |- - global: - scrape_interval: 30s - scrape_timeout: 10s - scrape_configs: - #------------- configuration to collect pods metrics kubelet ------------------- - - job_name: 'kubernetes-cadvisor' - scheme: https - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - kubernetes_sd_configs: - - role: node - relabel_configs: - - action: labelmap - regex: __meta_kubernetes_node_label_(.+) - - target_label: __address__ - replacement: kubernetes.default.svc:443 - - source_labels: [__meta_kubernetes_node_name] - regex: (.+) - target_label: __metrics_path__ - replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor - #------------- configuration to collect pods metrics ------------------- - - job_name: 'kubernetes-pods' - honor_labels: true - kubernetes_sd_configs: - - role: pod - relabel_configs: - # select only those pods that has "prometheus.io/scrape: true" annotation - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] - action: keep - regex: true - # set metrics_path (default is /metrics) to the metrics path specified in "prometheus.io/path: " annotation. - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] - action: replace - target_label: __metrics_path__ - regex: (.+) - # set the scrapping port to the port specified in "prometheus.io/port: " annotation and set address accordingly. - - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] - action: replace - regex: ([^:]+)(?::\d+)?;(\d+) - replacement: $1:$2 - target_label: __address__ - - action: labelmap - regex: __meta_kubernetes_pod_label_(.+) - - source_labels: [__meta_kubernetes_namespace] - action: replace - target_label: kubernetes_namespace - - source_labels: [__meta_kubernetes_pod_name] - action: replace - target_label: kubernetes_pod_name - - #-------------- configuration to collect metrics from service endpoints ----------------------- - - job_name: 'kubernetes-service-endpoints' - honor_labels: true - kubernetes_sd_configs: - - role: endpoints - relabel_configs: - # select only those endpoints whose service has "prometheus.io/scrape: true" annotation - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] - action: keep - regex: true - # set the metrics_path to the path specified in "prometheus.io/path: " annotation. - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] - action: replace - target_label: __metrics_path__ - regex: (.+) - # set the scrapping port to the port specified in "prometheus.io/port: " annotation and set address accordingly. - - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port] - action: replace - target_label: __address__ - regex: ([^:]+)(?::\d+)?;(\d+) - replacement: $1:$2 - - action: labelmap - regex: __meta_kubernetes_service_label_(.+) - - source_labels: [__meta_kubernetes_namespace] - action: replace - target_label: kubernetes_namespace - - source_labels: [__meta_kubernetes_service_name] - action: replace - target_label: kubernetes_name - - #---------------- configuration to collect metrics from kubernetes apiserver ------------------------- - - job_name: 'kubernetes-apiservers' - honor_labels: true - kubernetes_sd_configs: - - role: endpoints - # kubernetes apiserver serve metrics on a TLS secure endpoints. so, we have to use "https" scheme - scheme: https - # we have to provide certificate to establish tls secure connection - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - # bearer_token_file is required for authorizating prometheus server to kubernetes apiserver - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - - relabel_configs: - - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] - action: keep - regex: default;kubernetes;https - - #--------------- configuration to collect metrics from nodes ----------------------- - - job_name: 'kubernetes-nodes' - honor_labels: true - scheme: https - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - - kubernetes_sd_configs: - - role: node - relabel_configs: - - action: labelmap - regex: __meta_kubernetes_node_label_(.+) - - target_label: __address__ - replacement: kubernetes.default.svc:443 - - source_labels: [__meta_kubernetes_node_name] - regex: (.+) - target_label: __metrics_path__ - replacement: /api/v1/nodes/${1}/proxy/metrics -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/prometheus-deployment.yaml b/helm/capif/templates/prometheus-deployment.yaml deleted file mode 100644 index d70cf09..0000000 --- a/helm/capif/templates/prometheus-deployment.yaml +++ /dev/null @@ -1,68 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.monitoring.prometheus.enable "true" }} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: prometheus - labels: - app: prometheus - {{- include "capif.labels" . | nindent 4 }} -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - app: prometheus - {{- include "capif.selectorLabels" . | nindent 6 }} - template: - metadata: - annotations: - checksum/config: {{ include (print $.Template.BasePath "/prometheus-configmap.yaml") . | sha256sum }} - labels: - app: prometheus - {{- include "capif.selectorLabels" . | nindent 8 }} - spec: - serviceAccountName: prometheus - containers: - - name: prometheus - image: {{ .Values.monitoring.prometheus.image.repository }}:{{ .Values.monitoring.prometheus.image.tag }} - args: - - "--config.file=/etc/prometheus/prometheus.yml" - - "--storage.tsdb.path=/prometheus/" - - "--storage.tsdb.retention.time={{.Values.monitoring.prometheus.retentionTime }}" - ports: - - containerPort: 9090 - resources: - {{- toYaml .Values.monitoring.prometheus.resources | nindent 12 }} - securityContext: - runAsUser: 0 - livenessProbe: - tcpSocket: - port: 9090 - initialDelaySeconds: 20 - volumeMounts: - - name: prometheus-config - mountPath: /etc/prometheus/ - {{- if eq .Values.monitoring.prometheus.persistence.enable "true" }} - - name: prometheus-storage-volume - mountPath: /prometheus/ - {{ else }} - - name: prometheus-storage - mountPath: /prometheus/ - {{- end }} - volumes: - - name: prometheus-config - configMap: - defaultMode: 420 - name: prometheus-config - {{- if eq .Values.monitoring.prometheus.persistence.enable "true" }} - - name: prometheus-storage-volume - persistentVolumeClaim: - claimName: prometheus-pvc - {{ else }} - - name: prometheus-storage - emptyDir: {} - {{- end }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/prometheus-ingress-route.yaml b/helm/capif/templates/prometheus-ingress-route.yaml deleted file mode 100644 index b7a0d2b..0000000 --- a/helm/capif/templates/prometheus-ingress-route.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.monitoring.prometheus.enable "true" }} -{{- if eq .Values.monitoring.prometheus.ingressRoute.enable "true" }} -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: prometheus-ingress-route -spec: - entryPoints: [web] - routes: - - kind: Rule - match: Host(`{{ .Values.monitoring.prometheus.ingressRoute.host }}`) - services: - - kind: Service - name: prometheus - port: {{ .Values.monitoring.prometheus.service.port }} - scheme: http -{{- end }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/prometheus-ingress.yaml b/helm/capif/templates/prometheus-ingress.yaml deleted file mode 100644 index d082973..0000000 --- a/helm/capif/templates/prometheus-ingress.yaml +++ /dev/null @@ -1,36 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.monitoring.prometheus.enable "true" }} -{{- if .Values.monitoring.prometheus.ingress.enabled -}} -{{- $svcPort := .Values.monitoring.prometheus.service.port -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: prometheus-ingress - labels: - {{- include "capif.labels" . | nindent 4 }} - {{- with .Values.monitoring.prometheus.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: -{{- if .Values.monitoring.prometheus.ingress.ingressClassName }} - ingressClassName: {{ .Values.monitoring.prometheus.ingress.ingressClassName }} -{{- end }} - rules: - {{- range .Values.monitoring.prometheus.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - pathType: {{ .pathType }} - backend: - service: - name: prometheus - port: - number: {{ $svcPort }} - {{- end }} - {{- end }} -{{- end }} -{{- end }} -{{- end }} diff --git a/helm/capif/templates/prometheus-pvc.yaml b/helm/capif/templates/prometheus-pvc.yaml deleted file mode 100644 index 8f763c6..0000000 --- a/helm/capif/templates/prometheus-pvc.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.monitoring.prometheus.enable "true" }} -{{- if eq .Values.monitoring.prometheus.persistence.enable "true" }} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: prometheus-pvc - labels: - app: prometheus - {{- include "capif.labels" . | nindent 4 }} -spec: - storageClassName: {{ .Values.monitoring.prometheus.persistence.storageClass }} - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.monitoring.prometheus.persistence.storage }} -{{- end }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/prometheus-service.yaml b/helm/capif/templates/prometheus-service.yaml deleted file mode 100644 index 778dbd5..0000000 --- a/helm/capif/templates/prometheus-service.yaml +++ /dev/null @@ -1,22 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -{{- if eq .Values.monitoring.prometheus.enable "true" }} -apiVersion: v1 -kind: Service -metadata: - annotations: - prometheus.io/path: /metrics - prometheus.io/port: {{ quote .Values.monitoring.prometheus.service.port }} - prometheus.io/scrape: "true" - name: prometheus - labels: - {{- include "capif.labels" . | nindent 4 }} -spec: - type: {{ .Values.monitoring.prometheus.service.type }} - ports: - - port: {{ .Values.monitoring.prometheus.service.port }} - protocol: TCP - targetPort: {{ .Values.monitoring.prometheus.service.port }} - selector: - app: prometheus -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/renderer-configmap.yaml b/helm/capif/templates/renderer-configmap.yaml deleted file mode 100644 index 0159fcb..0000000 --- a/helm/capif/templates/renderer-configmap.yaml +++ /dev/null @@ -1,8 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: renderer-configmap -data: - ENABLE_METRICS: {{ quote .Values.monitoring.renderer.env.enableMetrics }} -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/renderer-deployment.yaml b/helm/capif/templates/renderer-deployment.yaml deleted file mode 100644 index 83a7ee2..0000000 --- a/helm/capif/templates/renderer-deployment.yaml +++ /dev/null @@ -1,44 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - labels: - io.kompose.service: renderer - {{- include "capif.labels" . | nindent 4 }} - name: renderer -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: renderer - {{- include "capif.selectorLabels" . | nindent 6 }} - strategy: {} - template: - metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - checksum/config: {{ include (print $.Template.BasePath "/renderer-configmap.yaml") . | sha256sum }} - labels: - io.kompose.network/monitoring-default: "true" - io.kompose.service: renderer - {{- include "capif.selectorLabels" . | nindent 8 }} - spec: - containers: - - env: - - name: ENABLE_METRICS - valueFrom: - configMapKeyRef: - name: renderer-configmap - key: ENABLE_METRICS - image: {{ .Values.monitoring.renderer.image.repository }}:{{ .Values.monitoring.renderer.image.tag }} - name: grafana-image-renderer - ports: - - containerPort: 8081 - resources: - {{- toYaml .Values.monitoring.renderer.resources | nindent 12 }} - restartPolicy: Always -{{- end }} \ No newline at end of file diff --git a/helm/capif/templates/renderer-service.yaml b/helm/capif/templates/renderer-service.yaml deleted file mode 100644 index 471a51d..0000000 --- a/helm/capif/templates/renderer-service.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if eq .Values.monitoring.enable "true" }} -apiVersion: v1 -kind: Service -metadata: - annotations: - kompose.cmd: kompose -f docker-compose.yml convert - kompose.version: 1.28.0 (c4137012e) - labels: - io.kompose.service: renderer - name: renderer -spec: - ports: - - name: "rendere-port" - port: 8081 - targetPort: 8081 - selector: - io.kompose.service: renderer -{{- end }} \ No newline at end of file diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index a2c7347..e70b634 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -1,8 +1,9 @@ -monitoring: - enable: "true" +# -- To enable monitoring in ocf. +# -- enabled the next services: +# -- tempo, fluentbit, grafana, loki, otelcollector and renderer. +# -- prometheus service must be previously installed in kubernetes # -- With tempo.enabled: false. It won't be deployed -# -- If monitoring.enable: "true". Also enable tempo.enabled: true tempo: enabled: true tempo: @@ -13,167 +14,34 @@ tempo: enabled: true size: 3Gi -monitoring: - # -- If monitoring enabled. enable: true, enable: "" = not enabled - enable: "true" - fluentBit: - image: - # -- The docker image repository to use - repository: "grafana/fluent-bit-plugin-loki" - # -- The docker image tag to use - # @default Chart version - tag: "latest" - env: - lokiUrl: http://loki:3100/loki/api/v1/push - resources: {} - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - loki: - image: - # -- The docker image repository to use - repository: "grafana/loki" - # -- The docker image tag to use - # @default Chart version - tag: "2.8.0" - resources: {} - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - # -- If grafana.persistence enabled. enable: true, enable: "" = not enabled - persistence: - enable: "true" - storage: 100Mi - storageClass: nfs-01 - otel: - image: - # -- The docker image repository to use - repository: "otel/opentelemetry-collector" - # -- The docker image tag to use - # @default Chart version - tag: "0.81.0" - configMap: - tempoEndpoint: monitoring-capif-tempo:4317 - resources: {} - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - renderer: - image: - # -- The docker image repository to use - repository: "grafana/grafana-image-renderer" - # -- The docker image tag to use - # @default Chart version - tag: "latest" - env: - enableMetrics: "true" - resources: {} - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - prometheus: - # -- With prometheus.enabled: "". It won't be deployed. prometheus.enable: "true" - # -- It will deploy prometheus - enable: "" - image: - # -- The docker image repository to use - repository: "prom/prometheus" - # -- The docker image tag to use - # @default Chart version - tag: "latest" - retentionTime: 3d - resources: {} - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - persistence: - enable: "true" - storage: 8Gi - storageClass: nfs-01 - service: - type: ClusterIP - port: 9090 - ingress: - enabled: true - ingressClassName: nginx - annotations: - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: prometheus.test.int - paths: - - path: / - pathType: Prefix - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - # -- If ingressRoute enable=true, use monitoring.prometheus.ingress.enabled="" - ingressRoute: - enable: "" - host: prometheus.test.int - grafana: - image: - # -- The docker image repository to use - repository: "grafana/grafana" - # -- The docker image tag to use - # @default Chart version - tag: "latest" - env: - gfAuthAnonymousEnable: true - gfSecurityAllowEmbedding: true - gfAuthAnonymousOrgRole: Admin - gfSecurityAdminPassword: secure_pass - lokiUrl: http://loki:3100 - prometheusUrl: http://prometheus.mon.svc.cluster.local:9090 - tempoUrl: http://monitoring-capif-tempo:3100 - resources: {} - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - # -- If grafana.persistence enabled. enable: true, enable: "" = not enabled - persistence: - enable: "true" - storage: 100Mi - storageClass: nfs-01 - service: - type: ClusterIP - port: 3000 - # -- If ingress enabled=true, use monitoring.grafana.ingressRoute.enable="" - ingress: - enabled: true - ingressClassName: nginx - annotations: - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: grafana.test.int - paths: - - path: / - pathType: Prefix - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - # -- If ingressRoute enable=true, use monitoring.grafana.ingress.enabled="" - ingressRoute: - enable: "" - host: grafana.5gnacar.int +# -- With fluentbit.enabled: false. It won't be deployed +fluentbit: + enabled: true + +# -- With grafana.enabled: false. It won't be deployed +grafana: + enabled: true + +# -- With loki.enabled: false. It won't be deployed +loki: + enabled: true + +# -- With otelcollector.enabled: false. It won't be deployed +otelcollector: + enabled: true + +# -- With renderer.enabled: false. It won't be deployed +renderer: + enabled: true + +# -- With mongo-express.enabled: false. It won't be deployed +mongo-express: + enabled: true + +# -- With mongo-register-express.enabled: false. It won't be deployed +mongo-register-express: + enabled: true + +# -- With mock-server.enabled: false. It won't be deployed +mock-server: + enabled: true \ No newline at end of file -- GitLab From 6cd68177f1e291f74bcc7642e6b424c6d5733b07 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 25 Jun 2024 08:41:54 +0200 Subject: [PATCH 282/310] enabled false values.yaaml --- helm/capif/values.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index e70b634..b90d7b2 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -16,32 +16,32 @@ tempo: # -- With fluentbit.enabled: false. It won't be deployed fluentbit: - enabled: true + enabled: false # -- With grafana.enabled: false. It won't be deployed grafana: - enabled: true + enabled: false # -- With loki.enabled: false. It won't be deployed loki: - enabled: true + enabled: false # -- With otelcollector.enabled: false. It won't be deployed otelcollector: - enabled: true + enabled: false # -- With renderer.enabled: false. It won't be deployed renderer: - enabled: true + enabled: false # -- With mongo-express.enabled: false. It won't be deployed mongo-express: - enabled: true + enabled: false # -- With mongo-register-express.enabled: false. It won't be deployed mongo-register-express: - enabled: true + enabled: false # -- With mock-server.enabled: false. It won't be deployed mock-server: - enabled: true \ No newline at end of file + enabled: false \ No newline at end of file -- GitLab From 004e22d1873335399f39566791e424a74512d551 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 25 Jun 2024 09:10:34 +0200 Subject: [PATCH 283/310] enabled --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index b90d7b2..9ecc11e 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -44,4 +44,4 @@ mongo-register-express: # -- With mock-server.enabled: false. It won't be deployed mock-server: - enabled: false \ No newline at end of file + enabled: false -- GitLab From ab607e260361cf9afcd608f05c347040ef4efe21 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 25 Jun 2024 09:30:09 +0200 Subject: [PATCH 284/310] typing deleted --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index 9ecc11e..b90d7b2 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -44,4 +44,4 @@ mongo-register-express: # -- With mock-server.enabled: false. It won't be deployed mock-server: - enabled: false + enabled: false \ No newline at end of file -- GitLab From da2a614e06efb951c3002ee17f74203c17613448 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 25 Jun 2024 09:34:56 +0200 Subject: [PATCH 285/310] deleting typing --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index b90d7b2..9ecc11e 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -44,4 +44,4 @@ mongo-register-express: # -- With mock-server.enabled: false. It won't be deployed mock-server: - enabled: false \ No newline at end of file + enabled: false -- GitLab From 0818c59a934cf3c64ad729a1cdf68f19ac038d97 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Tue, 25 Jun 2024 09:40:12 +0200 Subject: [PATCH 286/310] deleting typing --- helm/capif/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index 9ecc11e..b90d7b2 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -44,4 +44,4 @@ mongo-register-express: # -- With mock-server.enabled: false. It won't be deployed mock-server: - enabled: false + enabled: false \ No newline at end of file -- GitLab From 89bafac2cd944c0b843dbc5e796a1f3e3c1afcae Mon Sep 17 00:00:00 2001 From: Stavros-Anastasios Charismiadis Date: Tue, 25 Jun 2024 14:04:01 +0300 Subject: [PATCH 287/310] Fix duplication in Routing Info Dockerfile --- services/TS29222_CAPIF_Routing_Info_API/Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/TS29222_CAPIF_Routing_Info_API/Dockerfile b/services/TS29222_CAPIF_Routing_Info_API/Dockerfile index 1ea2ba9..470877e 100644 --- a/services/TS29222_CAPIF_Routing_Info_API/Dockerfile +++ b/services/TS29222_CAPIF_Routing_Info_API/Dockerfile @@ -11,6 +11,4 @@ COPY . /usr/src/app EXPOSE 8080 -EXPOSE 8080 - CMD ["sh", "prepare_routing_info.sh"] \ No newline at end of file -- GitLab From 737cb243588adeca2b2638797d687a8435fed64e Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Tue, 25 Jun 2024 14:06:21 +0200 Subject: [PATCH 288/310] fix logs --- services/register/register_service/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/register/register_service/app.py b/services/register/register_service/app.py index 4dc2ded..1e38eda 100644 --- a/services/register/register_service/app.py +++ b/services/register/register_service/app.py @@ -7,7 +7,7 @@ import requests import json from config import Config from db.db import MongoDatabse - +import logging app = Flask(__name__) @@ -77,6 +77,7 @@ key_data = json.loads(response.text)["data"]["data"]["key"] # Create an Admin in the Admin Collection client = MongoDatabse() if not client.get_col_by_name(client.capif_admins).find_one({"admin_name": config["register"]["admin_users"]["admin_user"], "admin_pass": config["register"]["admin_users"]["admin_pass"]}): + print(f'Inserting Initial Admin admin_name: {config["register"]["admin_users"]["admin_user"]}, admin_pass: {config["register"]["admin_users"]["admin_pass"]}') client.get_col_by_name(client.capif_admins).insert_one({"admin_name": config["register"]["admin_users"]["admin_user"], "admin_pass": config["register"]["admin_users"]["admin_pass"]}) @@ -84,4 +85,6 @@ app.config['JWT_ALGORITHM'] = 'RS256' app.config['JWT_PRIVATE_KEY'] = key_data app.config['REGISTRE_SECRET_KEY'] = config["register"]["register_uuid"] +app.logger.setLevel(logging.DEBUG) + app.register_blueprint(register_routes) \ No newline at end of file -- GitLab From 66714ee527bb561b4965573d887466f44dfe05b8 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Wed, 26 Jun 2024 14:03:22 +0200 Subject: [PATCH 289/310] ocf.develop and ocf.validation --- helm/capif/values.yaml | 2 +- helm/vault-job/vault-job.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/helm/capif/values.yaml b/helm/capif/values.yaml index b90d7b2..9ecc11e 100644 --- a/helm/capif/values.yaml +++ b/helm/capif/values.yaml @@ -44,4 +44,4 @@ mongo-register-express: # -- With mock-server.enabled: false. It won't be deployed mock-server: - enabled: false \ No newline at end of file + enabled: false diff --git a/helm/vault-job/vault-job.yaml b/helm/vault-job/vault-job.yaml index e30a394..84b247f 100644 --- a/helm/vault-job/vault-job.yaml +++ b/helm/vault-job/vault-job.yaml @@ -25,10 +25,10 @@ data: # to execute the next commands in vault # otherwise, if use the vault as dev's mode. Just # type the token's dev. - export VAULT_TOKEN="hvs.mn50Q8kpMuxsPUsCNlwQekCd" - export DOMAIN1=*.pre-prod.int - export DOMAIN2=*.staging.int - export DOMAIN3=*.developer.int + export VAULT_TOKEN="" + export DOMAIN1=*.ocf.pre-production + export DOMAIN2=*.ocf.validation + export DOMAIN3=*.ocf.develop # local domains # export DOMAIN4=*.pre-prod.svc.cluster.local @@ -175,7 +175,7 @@ data: openssl x509 -pubkey -noout -in server_certificate.crt.pem > server_certificate_pub.pem - #vault kv put secret/ca ca=@root_ca.crt.pem root_2023_ca.crt + #vault kv put secret/ca ca=@root_helm.pem root_2023_ca.crt #cat root_2023_ca.crt root_2023_ca.crt > ca.crt -- GitLab From 9125665293fefe917255a5f9611c6912052fb1da Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Wed, 26 Jun 2024 15:12:29 +0200 Subject: [PATCH 290/310] minor fix, problem deatils not serialized correctly --- .../capif_security/core/validate_user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/validate_user.py b/services/TS29222_CAPIF_Security_API/capif_security/core/validate_user.py index 45ec518..04d893e 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/validate_user.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/validate_user.py @@ -3,7 +3,7 @@ import json from ..models.problem_details import ProblemDetails from ..encoder import JSONEncoder from .resources import Resource -from .responses import internal_server_error +from .responses import internal_server_error,serialize_clean_camel_case class ControlAccess(Resource): @@ -18,6 +18,7 @@ class ControlAccess(Resource): if cert_entry is not None: if cert_entry["cert_signature"] != cert_signature: prob = ProblemDetails(title="Unauthorized", detail="User not authorized", cause="You are not the owner of this resource") + prob = serialize_clean_camel_case(prob) return Response(json.dumps(prob, cls=JSONEncoder), status=401, mimetype="application/json") except Exception as e: -- GitLab From 944ad92046a3ba31c48e29c9e065f8c08bb6f69d Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Thu, 27 Jun 2024 10:01:54 +0200 Subject: [PATCH 291/310] Code Refactor on register --- .../register/register_service/core/register_operations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index dac2654..a33df69 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -1,4 +1,4 @@ -from flask import Flask, jsonify, request, Response, current_app +from flask import jsonify from flask_jwt_extended import create_access_token from db.db import MongoDatabse from datetime import datetime @@ -26,7 +26,7 @@ class RegisterOperations: user_info["uuid"] = user_uuid user_info["onboarding_date"]=datetime.now() - obj = mycol.insert_one(user_info) + mycol.insert_one(user_info) return jsonify(message="User registered successfully", uuid=user_uuid), 201 -- GitLab From 5bd05c33b505770e9cabb0fb20448008143bf113 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Thu, 27 Jun 2024 10:09:05 +0200 Subject: [PATCH 292/310] Minor fix on register --- .../register/register_service/core/register_operations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/register/register_service/core/register_operations.py b/services/register/register_service/core/register_operations.py index 96cc468..42627fd 100644 --- a/services/register/register_service/core/register_operations.py +++ b/services/register/register_service/core/register_operations.py @@ -1,4 +1,4 @@ -from flask import jsonify +from flask import jsonify, current_app from flask_jwt_extended import create_access_token from db.db import MongoDatabse from datetime import datetime @@ -43,8 +43,8 @@ class RegisterOperations: exist_user = mycol.find_one({"username": username}) if exist_user is None: - current_app.logger.debug(f"Not exister user with this credentials : {username}") - return jsonify("Not exister user with this credentials"), 400 + current_app.logger.debug(f"No user exists with these credentials: {username}") + return jsonify("No user exists with these credentials"), 400 access_token = create_access_token(identity=(username + " " + exist_user["uuid"])) current_app.logger.debug(f"Access token generated for user {username} : {access_token}") -- GitLab From 1fbe673cb9be2b7ebe6d97da2a57abbbf5d0b6df Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Thu, 27 Jun 2024 10:11:19 +0200 Subject: [PATCH 293/310] Remove commented code --- .../capif_acl/controllers/default_controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/default_controller.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/default_controller.py index eb047f0..52174b1 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/default_controller.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/controllers/default_controller.py @@ -43,5 +43,4 @@ def access_control_policy_list_service_api_id_get(service_api_id, aef_id, api_in :rtype: Union[AccessControlPolicyList, Tuple[AccessControlPolicyList, int], Tuple[AccessControlPolicyList, int, Dict[str, str]] """ current_app.logger.debug("Obtaining service ACLs") - #current_app.logger.debug(f"AEF-id: {service_api_id}") return accessControlPolicyApi().get_acl(service_api_id, aef_id, api_invoker_id, supported_features) -- GitLab From 08a0dab6698f4596acb2c3e06e9247b215dd6d98 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Thu, 27 Jun 2024 11:54:38 +0200 Subject: [PATCH 294/310] Add configurable timeout to requests at robot tests --- tests/resources/common.resource | 2 ++ tests/resources/common/basicRequests.robot | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/resources/common.resource b/tests/resources/common.resource index d4733b8..b2f19ae 100644 --- a/tests/resources/common.resource +++ b/tests/resources/common.resource @@ -32,6 +32,8 @@ ${CAPIF_CALLBACK_PORT} 8086 ${REGISTER_ADMIN_USER} admin ${REGISTER_ADMIN_PASSWORD} password123 +${REQUESTS_TIMEOUT} 120s + ${MOCK_SERVER_URL} diff --git a/tests/resources/common/basicRequests.robot b/tests/resources/common/basicRequests.robot index 433a13c..6d4f456 100644 --- a/tests/resources/common/basicRequests.robot +++ b/tests/resources/common/basicRequests.robot @@ -84,7 +84,7 @@ Create Register Admin Session ## NEW REQUESTS TO REGISTER Post Request Admin Register - [Timeout] 60s + [Timeout] ${REQUESTS_TIMEOUT} [Arguments] ... ${endpoint} ... ${json}=${NONE} @@ -114,7 +114,7 @@ Post Request Admin Register RETURN ${resp} Get Request Admin Register - [Timeout] 60s + [Timeout] ${REQUESTS_TIMEOUT} [Arguments] ... ${endpoint} ... ${server}=${NONE} @@ -142,7 +142,7 @@ Get Request Admin Register # NEW REQUESTS END Post Request Capif - [Timeout] 60s + [Timeout] ${REQUESTS_TIMEOUT} [Arguments] ... ${endpoint} ... ${json}=${NONE} @@ -172,7 +172,7 @@ Post Request Capif RETURN ${resp} Get Request Capif - [Timeout] 60s + [Timeout] ${REQUESTS_TIMEOUT} [Arguments] ... ${endpoint} ... ${server}=${NONE} @@ -198,7 +198,7 @@ Get Request Capif RETURN ${resp} Get CA Vault - [Timeout] 60s + [Timeout] ${REQUESTS_TIMEOUT} [Arguments] ... ${endpoint} ... ${server}=${NONE} @@ -225,7 +225,7 @@ Get CA Vault RETURN ${response} Put Request Capif - [Timeout] 60s + [Timeout] ${REQUESTS_TIMEOUT} [Arguments] ... ${endpoint} ... ${json}=${NONE} @@ -254,7 +254,7 @@ Put Request Capif RETURN ${resp} Patch Request Capif - [Timeout] 60s + [Timeout] ${REQUESTS_TIMEOUT} [Arguments] ... ${endpoint} ... ${json}=${NONE} @@ -283,7 +283,7 @@ Patch Request Capif RETURN ${resp} Delete Request Capif - [Timeout] 60s + [Timeout] ${REQUESTS_TIMEOUT} [Arguments] ... ${endpoint} ... ${server}=${NONE} -- GitLab From e9d8f4a7d05e8ccd8afb2d3498453e10aed98762 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Fri, 28 Jun 2024 10:37:10 +0200 Subject: [PATCH 295/310] Add LOG LEVEL to services --- .../api_invoker_management/app.py | 10 +- .../api_provider_management/app.py | 10 +- .../capif_acl/app.py | 10 +- .../TS29222_CAPIF_Auditing_API/logs/app.py | 10 +- .../service_apis/app.py | 9 +- .../capif_events/app.py | 10 +- .../api_invocation_logs/app.py | 9 +- .../published_apis/app.py | 9 +- .../capif_security/__main__.py | 149 ------------------ .../capif_security/app.py | 9 +- services/docker-compose-capif.yml | 11 ++ services/docker-compose-register.yml | 1 + services/helper/helper_service/app.py | 7 +- services/mock_server/mock_server.py | 10 +- services/register/register_service/app.py | 6 +- services/run.sh | 12 +- 16 files changed, 97 insertions(+), 185 deletions(-) delete mode 100644 services/TS29222_CAPIF_Security_API/capif_security/__main__.py diff --git a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py index b459410..9d55bc5 100644 --- a/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py +++ b/services/TS29222_CAPIF_API_Invoker_Management_API/api_invoker_management/app.py @@ -22,6 +22,10 @@ from opentelemetry.instrumentation.redis import RedisInstrumentor NAME = "Invoker-Service" +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) + def configure_monitoring(app, config): resource = Resource(attributes={"service.name": NAME}) @@ -86,10 +90,10 @@ def configure_logging(app): loggers = [app.logger, ] handlers = [] console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(numeric_level) console_handler.setFormatter(verbose_formatter()) file_handler = RotatingFileHandler(filename="invoker_logs.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(numeric_level) file_handler.setFormatter(verbose_formatter()) handlers.append(console_handler) handlers.append(file_handler) @@ -99,7 +103,7 @@ def configure_logging(app): for handler in handlers: l.addHandler(handler) l.propagate = False - l.setLevel(logging.DEBUG) + l.setLevel(numeric_level) diff --git a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py index 94ca3ed..2896830 100644 --- a/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py +++ b/services/TS29222_CAPIF_API_Provider_Management_API/api_provider_management/app.py @@ -23,6 +23,10 @@ from opentelemetry.instrumentation.redis import RedisInstrumentor NAME = "Provider-Service" +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) + def configure_monitoring(app, config): resource = Resource(attributes={"service.name": NAME}) @@ -85,10 +89,10 @@ def configure_logging(app): loggers = [app.logger, ] handlers = [] console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(numeric_level) console_handler.setFormatter(verbose_formatter()) file_handler = RotatingFileHandler(filename="provider_logs.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(numeric_level) file_handler.setFormatter(verbose_formatter()) handlers.append(console_handler) handlers.append(file_handler) @@ -97,7 +101,7 @@ def configure_logging(app): for handler in handlers: l.addHandler(handler) l.propagate = False - l.setLevel(logging.DEBUG) + l.setLevel(numeric_level) def verbose_formatter(): return logging.Formatter( diff --git a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py index a473355..59a0f90 100644 --- a/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py +++ b/services/TS29222_CAPIF_Access_Control_Policy_API/capif_acl/app.py @@ -26,6 +26,10 @@ from opentelemetry.instrumentation.redis import RedisInstrumentor NAME = "Acl-Service" +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) + def configure_monitoring(app, config): resource = Resource(attributes={"service.name": NAME}) @@ -89,10 +93,10 @@ def configure_logging(app): loggers = [app.logger, ] handlers = [] console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(numeric_level) console_handler.setFormatter(verbose_formatter()) file_handler = RotatingFileHandler(filename="acl_logs.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(numeric_level) file_handler.setFormatter(verbose_formatter()) handlers.append(console_handler) handlers.append(file_handler) @@ -101,7 +105,7 @@ def configure_logging(app): for handler in handlers: l.addHandler(handler) l.propagate = False - l.setLevel(logging.DEBUG) + l.setLevel(numeric_level) def verbose_formatter(): return logging.Formatter( diff --git a/services/TS29222_CAPIF_Auditing_API/logs/app.py b/services/TS29222_CAPIF_Auditing_API/logs/app.py index a0bd01c..f68880c 100644 --- a/services/TS29222_CAPIF_Auditing_API/logs/app.py +++ b/services/TS29222_CAPIF_Auditing_API/logs/app.py @@ -18,6 +18,10 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor NAME = "Logs-Service" +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) + def configure_monitoring(app, config): resource = Resource(attributes={"service.name": NAME}) @@ -79,10 +83,10 @@ def configure_logging(app): loggers = [app.logger, ] handlers = [] console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(numeric_level) console_handler.setFormatter(verbose_formatter()) file_handler = RotatingFileHandler(filename="service_logs.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(numeric_level) file_handler.setFormatter(verbose_formatter()) handlers.append(console_handler) handlers.append(file_handler) @@ -91,7 +95,7 @@ def configure_logging(app): for handler in handlers: l.addHandler(handler) l.propagate = False - l.setLevel(logging.DEBUG) + l.setLevel(numeric_level) def verbose_formatter(): return logging.Formatter( diff --git a/services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py b/services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py index 2e37d0e..beec708 100644 --- a/services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py +++ b/services/TS29222_CAPIF_Discover_Service_API/service_apis/app.py @@ -22,6 +22,9 @@ from opentelemetry.instrumentation.redis import RedisInstrumentor NAME = "Discover-Service" +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) def configure_monitoring(app, config): @@ -86,10 +89,10 @@ def configure_logging(app): loggers = [app.logger, ] handlers = [] console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(numeric_level) console_handler.setFormatter(verbose_formatter()) file_handler = RotatingFileHandler(filename="discover_logs.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(numeric_level) file_handler.setFormatter(verbose_formatter()) handlers.append(console_handler) handlers.append(file_handler) @@ -98,7 +101,7 @@ def configure_logging(app): for handler in handlers: l.addHandler(handler) l.propagate = False - l.setLevel(logging.DEBUG) + l.setLevel(numeric_level) def verbose_formatter(): return logging.Formatter( diff --git a/services/TS29222_CAPIF_Events_API/capif_events/app.py b/services/TS29222_CAPIF_Events_API/capif_events/app.py index eb43ca1..d1d3de3 100644 --- a/services/TS29222_CAPIF_Events_API/capif_events/app.py +++ b/services/TS29222_CAPIF_Events_API/capif_events/app.py @@ -34,6 +34,10 @@ from opentelemetry.instrumentation.redis import RedisInstrumentor NAME = "Events-Service" +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) + def configure_monitoring(app, config): resource = Resource(attributes={"service.name": NAME}) @@ -97,10 +101,10 @@ def configure_logging(app): loggers = [app.logger, ] handlers = [] console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(numeric_level) console_handler.setFormatter(verbose_formatter()) file_handler = RotatingFileHandler(filename="events_logs.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(numeric_level) file_handler.setFormatter(verbose_formatter()) handlers.append(console_handler) handlers.append(file_handler) @@ -109,7 +113,7 @@ def configure_logging(app): for handler in handlers: l.addHandler(handler) l.propagate = False - l.setLevel(logging.DEBUG) + l.setLevel(numeric_level) diff --git a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py index 5eb97b3..bf76d0f 100644 --- a/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py +++ b/services/TS29222_CAPIF_Logging_API_Invocation_API/api_invocation_logs/app.py @@ -18,6 +18,9 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor NAME = "Logging-Service" +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) def configure_monitoring(app, config): @@ -80,10 +83,10 @@ def configure_logging(app): loggers = [app.logger, ] handlers = [] console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(numeric_level) console_handler.setFormatter(verbose_formatter()) file_handler = RotatingFileHandler(filename="logging_logs.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(numeric_level) file_handler.setFormatter(verbose_formatter()) handlers.append(console_handler) handlers.append(file_handler) @@ -93,7 +96,7 @@ def configure_logging(app): for handler in handlers: l.addHandler(handler) l.propagate = False - l.setLevel(logging.DEBUG) + l.setLevel(numeric_level) def verbose_formatter(): diff --git a/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py b/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py index 3c0c473..dedd2b5 100644 --- a/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py +++ b/services/TS29222_CAPIF_Publish_Service_API/published_apis/app.py @@ -25,6 +25,9 @@ from opentelemetry.instrumentation.redis import RedisInstrumentor NAME = "Publish-Service" +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) def configure_monitoring(app, config): @@ -89,10 +92,10 @@ def configure_logging(app): loggers = [app.logger, ] handlers = [] console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(numeric_level) console_handler.setFormatter(verbose_formatter()) file_handler = RotatingFileHandler(filename="publish_logs.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(numeric_level) file_handler.setFormatter(verbose_formatter()) handlers.append(console_handler) handlers.append(file_handler) @@ -102,7 +105,7 @@ def configure_logging(app): for handler in handlers: l.addHandler(handler) l.propagate = False - l.setLevel(logging.DEBUG) + l.setLevel(numeric_level) def verbose_formatter(): diff --git a/services/TS29222_CAPIF_Security_API/capif_security/__main__.py b/services/TS29222_CAPIF_Security_API/capif_security/__main__.py deleted file mode 100644 index 26eec19..0000000 --- a/services/TS29222_CAPIF_Security_API/capif_security/__main__.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python3 - -import connexion -import logging -from capif_security import encoder -from flask_jwt_extended import JWTManager -from .config import Config -from .core.consumer_messager import Subscriber -from threading import Thread -from flask_executor import Executor -from logging.handlers import RotatingFileHandler -import sys -import os -from fluent import sender -from flask_executor import Executor -from opentelemetry.instrumentation.flask import FlaskInstrumentor -from opentelemetry import trace -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.instrumentation.redis import RedisInstrumentor - - - -NAME = "Security-Service" - -def configure_monitoring(app, config): - - resource = Resource(attributes={"service.name": NAME}) - - fluent_bit_host = config['monitoring']['fluent_bit_host'] - fluent_bit_port = config['monitoring']['fluent_bit_port'] - fluent_bit_sender = sender.FluentSender('Security-Service', host=fluent_bit_host, port=fluent_bit_port) - propagator = TraceContextTextMapPropagator() - - tracer_provider = TracerProvider(resource=resource) - trace.set_tracer_provider(tracer_provider) - exporter = OTLPSpanExporter(endpoint=f"http://{config['monitoring']['opentelemetry_url']}:{config['monitoring']['opentelemetry_port']}", insecure=True) - span_processor = BatchSpanProcessor( - exporter, - max_queue_size=config['monitoring']['opentelemetry_max_queue_size'], - schedule_delay_millis=config['monitoring']['opentelemetry_schedule_delay_millis'], - max_export_batch_size=config['monitoring']['opentelemetry_max_export_batch_size'], - export_timeout_millis=config['monitoring']['opentelemetry_export_timeout_millis'], - ) - - trace.get_tracer_provider().add_span_processor(span_processor) - - FlaskInstrumentor().instrument_app(app) - - RedisInstrumentor().instrument() - - class FluentBitHandler(logging.Handler): - - def __init__(self): - logging.Handler.__init__(self) - - def emit(self, record): - log_entry = self.format(record) - log_data = { - 'message': log_entry, - 'level': record.levelname, - 'timestamp': record.created, - 'logger': record.name, - 'function': record.funcName, - 'line': record.lineno, - 'container_name': os.environ.get('CONTAINER_NAME', ''), - } - - # # Obtener el trace ID actual - current_context = trace.get_current_span().get_span_context() - - trace_id = current_context.trace_id - traceparent_id = current_context.span_id - log_data['traceID'] = hex(trace_id)[2:] - if traceparent_id != None: - log_data['traceparent'] = hex(traceparent_id)[2:] - fluent_bit_sender.emit('Security-Service', log_data) - - loggers = [app.logger, ] - for l in loggers: - l.addHandler(FluentBitHandler()) - - -def configure_logging(app): - del app.logger.handlers[:] - loggers = [app.logger, ] - handlers = [] - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) - console_handler.setFormatter(verbose_formatter()) - file_handler = RotatingFileHandler(filename="security_logs.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(verbose_formatter()) - handlers.append(console_handler) - handlers.append(file_handler) - - for l in loggers: - for handler in handlers: - l.addHandler(handler) - l.propagate = False - l.setLevel(logging.DEBUG) - - -def verbose_formatter(): - return logging.Formatter( - '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "logger": "%(name)s", "function": "%(funcName)s", "line": %(lineno)d, "message": %(message)s}', - datefmt='%d/%m/%Y %H:%M:%S' - ) - -def main(): - - with open("/usr/src/app/capif_security/server.key", "rb") as key_file: - key_data = key_file.read() - - app = connexion.App(__name__, specification_dir='./openapi/') - app.app.json_encoder = encoder.JSONEncoder - - - app.app.config['JWT_ALGORITHM'] = 'RS256' - app.app.config['JWT_PRIVATE_KEY'] = key_data - app.add_api('openapi.yaml', - arguments={'title': 'CAPIF_Security_API'}, - pythonic_params=True) - - JWTManager(app.app) - subscriber = Subscriber() - - config = Config() - configure_logging(app.app) - - monitoring_value = os.environ.get("MONITORING", "").lower() - if monitoring_value == "true": - configure_monitoring(app.app, config.get_config()) - - executor = Executor(app.app) - - @app.app.before_first_request - def up_listener(): - executor.submit(subscriber.listen) - - - app.run(port=8080, debug=True) - -if __name__ == '__main__': - main() - diff --git a/services/TS29222_CAPIF_Security_API/capif_security/app.py b/services/TS29222_CAPIF_Security_API/capif_security/app.py index 0f06b7a..4345700 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/app.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/app.py @@ -21,6 +21,9 @@ from opentelemetry.instrumentation.redis import RedisInstrumentor NAME = "Security-Service" +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) def configure_monitoring(app, config): @@ -85,10 +88,10 @@ def configure_logging(app): loggers = [app.logger, ] handlers = [] console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(numeric_level) console_handler.setFormatter(verbose_formatter()) file_handler = RotatingFileHandler(filename="security_logs.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(numeric_level) file_handler.setFormatter(verbose_formatter()) handlers.append(console_handler) handlers.append(file_handler) @@ -97,7 +100,7 @@ def configure_logging(app): for handler in handlers: l.addHandler(handler) l.propagate = False - l.setLevel(logging.DEBUG) + l.setLevel(numeric_level) def verbose_formatter(): diff --git a/services/docker-compose-capif.yml b/services/docker-compose-capif.yml index 779df21..44ed625 100644 --- a/services/docker-compose-capif.yml +++ b/services/docker-compose-capif.yml @@ -31,6 +31,7 @@ services: - VAULT_HOSTNAME=vault - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 + - LOG_LEVEL=${LOG_LEVEL} depends_on: - nginx @@ -47,6 +48,7 @@ services: environment: - CONTAINER_NAME=access-control-policy - MONITORING=${MONITORING} + - LOG_LEVEL=${LOG_LEVEL} restart: unless-stopped image: public.ecr.aws/o2v4a8t6/opencapif/access-control-policy:3.1.3 depends_on: @@ -71,6 +73,7 @@ services: - VAULT_HOSTNAME=vault - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 + - LOG_LEVEL=${LOG_LEVEL} restart: unless-stopped image: public.ecr.aws/o2v4a8t6/opencapif/api-invoker-management-api:3.1.3 depends_on: @@ -95,6 +98,7 @@ services: - VAULT_HOSTNAME=vault - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 + - LOG_LEVEL=${LOG_LEVEL} depends_on: - redis - nginx @@ -115,6 +119,7 @@ services: environment: - CONTAINER_NAME=api-auditing - MONITORING=${MONITORING} + - LOG_LEVEL=${LOG_LEVEL} depends_on: - mongo @@ -134,6 +139,7 @@ services: environment: - CONTAINER_NAME=services-apis - MONITORING=${MONITORING} + - LOG_LEVEL=${LOG_LEVEL} depends_on: - mongo @@ -147,6 +153,7 @@ services: environment: - CONTAINER_NAME=api-events - MONITORING=${MONITORING} + - LOG_LEVEL=${LOG_LEVEL} extra_hosts: - host.docker.internal:host-gateway - fluent-bit:host-gateway @@ -172,6 +179,7 @@ services: - CAPIF_HOSTNAME=${CAPIF_HOSTNAME} - CONTAINER_NAME=api-invocation-logs - MONITORING=${MONITORING} + - LOG_LEVEL=${LOG_LEVEL} depends_on: - mongo @@ -190,6 +198,7 @@ services: environment: - CONTAINER_NAME=api-publish-apis - MONITORING=${MONITORING} + - LOG_LEVEL=${LOG_LEVEL} depends_on: - redis - mongo @@ -215,6 +224,7 @@ services: - VAULT_HOSTNAME=vault - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 + - LOG_LEVEL=${LOG_LEVEL} extra_hosts: - host.docker.internal:host-gateway - fluent-bit:host-gateway @@ -262,6 +272,7 @@ services: - VAULT_HOSTNAME=vault - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 + - LOG_LEVEL=${LOG_LEVEL} hostname: ${CAPIF_HOSTNAME} volumes: - ./nginx/certs:/etc/nginx/certs diff --git a/services/docker-compose-register.yml b/services/docker-compose-register.yml index b0e5571..3505057 100644 --- a/services/docker-compose-register.yml +++ b/services/docker-compose-register.yml @@ -11,6 +11,7 @@ services: - VAULT_HOSTNAME=vault - VAULT_ACCESS_TOKEN=dev-only-token - VAULT_PORT=8200 + - LOG_LEVEL=${LOG_LEVEL} extra_hosts: - host.docker.internal:host-gateway - vault:host-gateway diff --git a/services/helper/helper_service/app.py b/services/helper/helper_service/app.py index c0af3df..e7a65ec 100644 --- a/services/helper/helper_service/app.py +++ b/services/helper/helper_service/app.py @@ -5,10 +5,15 @@ from OpenSSL.crypto import PKey, TYPE_RSA, X509Req, dump_certificate_request, FI from config import Config import json import requests +import os app = Flask(__name__) config = Config().get_config() +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) + # Create a superadmin CSR and keys key = PKey() key.generate_key(TYPE_RSA, 2048) @@ -61,5 +66,5 @@ cert_file.write(bytes(ca_root, 'utf-8')) cert_file.close() app.register_blueprint(helper_routes) -app.logger.setLevel(logging.DEBUG) +app.logger.setLevel(numeric_level) diff --git a/services/mock_server/mock_server.py b/services/mock_server/mock_server.py index e45fae0..41ba650 100644 --- a/services/mock_server/mock_server.py +++ b/services/mock_server/mock_server.py @@ -5,6 +5,10 @@ import os app = Flask(__name__) +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) + # Lista para almacenar las solicitudes recibidas requests_received = [] @@ -20,10 +24,10 @@ def configure_logging(app): loggers = [app.logger, ] handlers = [] console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(numeric_level) console_handler.setFormatter(verbose_formatter()) file_handler = RotatingFileHandler(filename="mock_server.log", maxBytes=1024 * 1024 * 100, backupCount=20) - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(numeric_level) file_handler.setFormatter(verbose_formatter()) handlers.append(console_handler) handlers.append(file_handler) @@ -33,7 +37,7 @@ def configure_logging(app): for handler in handlers: l.addHandler(handler) l.propagate = False - l.setLevel(logging.DEBUG) + l.setLevel(numeric_level) @app.route('/testing', methods=['POST', 'GET']) def index(): diff --git a/services/register/register_service/app.py b/services/register/register_service/app.py index 1e38eda..093aa9f 100644 --- a/services/register/register_service/app.py +++ b/services/register/register_service/app.py @@ -16,6 +16,10 @@ jwt_manager = JWTManager(app) config = Config().get_config() +# Setting log level +log_level = os.getenv('LOG_LEVEL', 'INFO').upper() +numeric_level = getattr(logging, log_level, logging.INFO) + # Create a superadmin CSR and keys key = PKey() key.generate_key(TYPE_RSA, 2048) @@ -85,6 +89,6 @@ app.config['JWT_ALGORITHM'] = 'RS256' app.config['JWT_PRIVATE_KEY'] = key_data app.config['REGISTRE_SECRET_KEY'] = config["register"]["register_uuid"] -app.logger.setLevel(logging.DEBUG) +app.logger.setLevel(numeric_level) app.register_blueprint(register_routes) \ No newline at end of file diff --git a/services/run.sh b/services/run.sh index 3053019..91cbb5c 100755 --- a/services/run.sh +++ b/services/run.sh @@ -5,6 +5,7 @@ help() { echo " -c : Setup different hostname for capif" echo " -s : Run Mock server" echo " -m : Clean monitoring service" + echo " -l : Set Log Level (default DEBUG)" echo " -h : show this help" exit 1 } @@ -12,6 +13,7 @@ help() { HOSTNAME=capifcore MONITORING_STATE=false DEPLOY=all +LOG_LEVEL=DEBUG # Needed to avoid write permissions on bind volumes with prometheus and grafana DUID=$(id -u) @@ -33,7 +35,7 @@ else fi # Read params -while getopts ":c:msh" opt; do +while getopts ":c:l:msh" opt; do case $opt in c) HOSTNAME="$OPTARG" @@ -46,7 +48,9 @@ while getopts ":c:msh" opt; do ;; h) help - ;; + ;; + l) + LOG_LEVEL="$OPTARG" \?) echo "Not valid option: -$OPTARG" >&2 help @@ -86,7 +90,7 @@ else exit $status fi -CAPIF_HOSTNAME=$HOSTNAME MONITORING=$MONITORING_STATE docker compose -f "docker-compose-capif.yml" up --detach --build +CAPIF_HOSTNAME=$HOSTNAME MONITORING=$MONITORING_STATE LOG_LEVEL=$LOG_LEVEL docker compose -f "docker-compose-capif.yml" up --detach --build status=$? if [ $status -eq 0 ]; then @@ -98,7 +102,7 @@ fi CAPIF_PRIV_KEY_BASE_64=$(echo "$(cat nginx/certs/server.key)") -CAPIF_PRIV_KEY=$CAPIF_PRIV_KEY_BASE_64 docker compose -f "docker-compose-register.yml" up --detach --build +CAPIF_PRIV_KEY=$CAPIF_PRIV_KEY_BASE_64 LOG_LEVEL=$LOG_LEVEL docker compose -f "docker-compose-register.yml" up --detach --build status=$? if [ $status -eq 0 ]; then -- GitLab From b3c7c8f4a90a4d52e2c7082c82179cef179cf825 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Fri, 28 Jun 2024 10:38:15 +0200 Subject: [PATCH 296/310] fix run.sh --- services/run.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/services/run.sh b/services/run.sh index 91cbb5c..682d22e 100755 --- a/services/run.sh +++ b/services/run.sh @@ -51,6 +51,7 @@ while getopts ":c:l:msh" opt; do ;; l) LOG_LEVEL="$OPTARG" + ;; \?) echo "Not valid option: -$OPTARG" >&2 help -- GitLab From 4665ea16345276e621e7c2215a7022409cdc5feb Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Fri, 28 Jun 2024 10:41:16 +0200 Subject: [PATCH 297/310] add os import to register --- services/register/register_service/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/services/register/register_service/app.py b/services/register/register_service/app.py index 093aa9f..53d4b05 100644 --- a/services/register/register_service/app.py +++ b/services/register/register_service/app.py @@ -8,6 +8,7 @@ import json from config import Config from db.db import MongoDatabse import logging +import os app = Flask(__name__) -- GitLab From 9f00a1517d4e506d3c2896b19df004f6ed03251c Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Fri, 28 Jun 2024 11:40:22 +0200 Subject: [PATCH 298/310] Change loggin driver at docker compose register --- services/docker-compose-register.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/docker-compose-register.yml b/services/docker-compose-register.yml index 3505057..53ed101 100644 --- a/services/docker-compose-register.yml +++ b/services/docker-compose-register.yml @@ -22,6 +22,8 @@ services: mongo_register: image: mongo:6.0.2 + logging: + driver: 'none' restart: unless-stopped environment: MONGO_INITDB_ROOT_USERNAME: root -- GitLab From e895c328a1651838ec08ff9970e988afde7678dd Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Fri, 28 Jun 2024 11:48:43 +0200 Subject: [PATCH 299/310] Improve run.sh help with enum values for log level --- services/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/run.sh b/services/run.sh index 682d22e..cf500a9 100755 --- a/services/run.sh +++ b/services/run.sh @@ -5,7 +5,7 @@ help() { echo " -c : Setup different hostname for capif" echo " -s : Run Mock server" echo " -m : Clean monitoring service" - echo " -l : Set Log Level (default DEBUG)" + echo " -l : Set Log Level (default DEBUG). Select one of: [CRITICAL, FATAL, ERROR, WARNING, WARN, INFO, DEBUG, NOTSET]" echo " -h : show this help" exit 1 } -- GitLab From 8a666892fc0f672a3d6bac78aa4152bcbb62d150 Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Fri, 28 Jun 2024 13:45:04 +0200 Subject: [PATCH 300/310] Improve tests with teardown by test --- services/clean_capif_docker_services.sh | 2 +- services/nginx/nginx.conf | 2 +- .../capif_api_access_control_policy.robot | 1 + .../CAPIF Api Auditing Service/capif_auditing_api.robot | 1 + .../CAPIF Api Discover Service/capif_api_service_discover.robot | 2 +- tests/features/CAPIF Api Events/capif_events_api.robot | 1 + .../capif_api_invoker_managenet.robot | 1 + .../features/CAPIF Api Logging Service/capif_logging_api.robot | 1 + .../capif_api_provider_management.robot | 1 + .../CAPIF Api Publish Service/capif_api_publish_service.robot | 1 + tests/features/CAPIF Security Api/capif_security_api.robot | 1 + 11 files changed, 11 insertions(+), 3 deletions(-) diff --git a/services/clean_capif_docker_services.sh b/services/clean_capif_docker_services.sh index 617ffed..bd6ef11 100755 --- a/services/clean_capif_docker_services.sh +++ b/services/clean_capif_docker_services.sh @@ -68,7 +68,7 @@ echo "${FILES[@]}" for FILE in "${FILES[@]}"; do echo "Executing 'docker compose down' for file $FILE" - CAPIF_PRIV_KEY=$CAPIF_PRIV_KEY_BASE_64 DUID=$DUID DGID=$DGID MONITORING=$MONITORING_STATE docker compose -f "$FILE" down --rmi all + CAPIF_PRIV_KEY=$CAPIF_PRIV_KEY_BASE_64 DUID=$DUID DGID=$DGID MONITORING=$MONITORING_STATE LOG_LEVEL=$LOG_LEVEL docker compose -f "$FILE" down --rmi all status=$? if [ $status -eq 0 ]; then echo "*** Removed Service from $FILE ***" diff --git a/services/nginx/nginx.conf b/services/nginx/nginx.conf index ec70d57..82d4dc5 100644 --- a/services/nginx/nginx.conf +++ b/services/nginx/nginx.conf @@ -1,6 +1,6 @@ worker_processes auto; -error_log /var/log/nginx/error.log debug; +error_log /var/log/nginx/error.log error; pid /tmp/nginx.pid; events { diff --git a/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot b/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot index ec0f082..cd49784 100644 --- a/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot +++ b/tests/features/CAPIF Api Access Control Policy/capif_api_access_control_policy.robot @@ -7,6 +7,7 @@ Resource ../../resources/common.resource Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment +Test Teardown Reset Testing Environment *** Variables *** diff --git a/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot b/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot index 0676168..d850a57 100644 --- a/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot +++ b/tests/features/CAPIF Api Auditing Service/capif_auditing_api.robot @@ -7,6 +7,7 @@ Resource ../../resources/common.resource Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment +Test Teardown Reset Testing Environment *** Variables *** diff --git a/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot b/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot index ff82ec3..e780526 100644 --- a/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot +++ b/tests/features/CAPIF Api Discover Service/capif_api_service_discover.robot @@ -6,7 +6,7 @@ Library /opt/robot-tests/tests/libraries/bodyRequests.py Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment -# Test Setup Initialize Test And Register role=invoker +Test Teardown Reset Testing Environment *** Variables *** diff --git a/tests/features/CAPIF Api Events/capif_events_api.robot b/tests/features/CAPIF Api Events/capif_events_api.robot index d5e02d2..fdec6a9 100644 --- a/tests/features/CAPIF Api Events/capif_events_api.robot +++ b/tests/features/CAPIF Api Events/capif_events_api.robot @@ -8,6 +8,7 @@ Resource ../../resources/common.resource Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment +Test Teardown Reset Testing Environment *** Variables *** diff --git a/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot b/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot index 27006b1..6ab8ba5 100644 --- a/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot +++ b/tests/features/CAPIF Api Invoker Management/capif_api_invoker_managenet.robot @@ -8,6 +8,7 @@ Library Collections Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment +Test Teardown Reset Testing Environment *** Variables *** diff --git a/tests/features/CAPIF Api Logging Service/capif_logging_api.robot b/tests/features/CAPIF Api Logging Service/capif_logging_api.robot index a7a0253..4db54b2 100644 --- a/tests/features/CAPIF Api Logging Service/capif_logging_api.robot +++ b/tests/features/CAPIF Api Logging Service/capif_logging_api.robot @@ -7,6 +7,7 @@ Resource ../../resources/common.resource Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment +Test Teardown Reset Testing Environment *** Variables *** diff --git a/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot b/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot index a6f9674..31aa55e 100644 --- a/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot +++ b/tests/features/CAPIF Api Provider Management/capif_api_provider_management.robot @@ -7,6 +7,7 @@ Library Collections Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment +Test Teardown Reset Testing Environment *** Variables *** diff --git a/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot b/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot index 56484d2..f3556a4 100644 --- a/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot +++ b/tests/features/CAPIF Api Publish Service/capif_api_publish_service.robot @@ -6,6 +6,7 @@ Library /opt/robot-tests/tests/libraries/bodyRequests.py Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment +Test Teardown Reset Testing Environment *** Variables *** diff --git a/tests/features/CAPIF Security Api/capif_security_api.robot b/tests/features/CAPIF Security Api/capif_security_api.robot index 6342aa9..f644839 100644 --- a/tests/features/CAPIF Security Api/capif_security_api.robot +++ b/tests/features/CAPIF Security Api/capif_security_api.robot @@ -7,6 +7,7 @@ Resource ../../resources/common.resource Suite Teardown Reset Testing Environment Test Setup Reset Testing Environment +Test Teardown Reset Testing Environment *** Variables *** -- GitLab From 81ba53787a14db112de7ec31d0ef2016795018da Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 28 Jun 2024 16:56:10 +0200 Subject: [PATCH 301/310] mongodb log level --- helm/capif/charts/mongo/templates/deployment.yaml | 8 ++++++++ helm/capif/charts/mongo/values.yaml | 1 + helm/vault-job/vault-job.yaml | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/helm/capif/charts/mongo/templates/deployment.yaml b/helm/capif/charts/mongo/templates/deployment.yaml index 80cedad..0066ead 100644 --- a/helm/capif/charts/mongo/templates/deployment.yaml +++ b/helm/capif/charts/mongo/templates/deployment.yaml @@ -40,11 +40,19 @@ spec: - name: http containerPort: {{ .Values.service.port }} protocol: TCP + args: + - mongod + - "--setParameter" + - "diagnosticDataCollectionEnabled=true" + - "--setParameter" + - "logComponentVerbosity={default: {verbosity: ${MONGODB_LOG_LEVEL}}}" env: - name: MONGO_INITDB_ROOT_PASSWORD value: {{ quote .Values.env.mongoInitdbRootPassword }} - name: MONGO_INITDB_ROOT_USERNAME value: {{ quote .Values.env.mongoInitdbRootUsername }} + - name: MONGODB_LOG_LEVEL + value: {{ .Values.env.mongoLeveLog }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/mongo/values.yaml b/helm/capif/charts/mongo/values.yaml index 53b8cf5..a226dba 100644 --- a/helm/capif/charts/mongo/values.yaml +++ b/helm/capif/charts/mongo/values.yaml @@ -17,6 +17,7 @@ fullnameOverride: "" env: mongoInitdbRootPassword: example mongoInitdbRootUsername: root + mongoLeveLog: 0 serviceAccount: # Specifies whether a service account should be created diff --git a/helm/vault-job/vault-job.yaml b/helm/vault-job/vault-job.yaml index 84b247f..a68f2f9 100644 --- a/helm/vault-job/vault-job.yaml +++ b/helm/vault-job/vault-job.yaml @@ -76,7 +76,7 @@ data: vault write pki_int/intermediate/set-signed certificate=@capif_intermediate.cert.pem #Crear rol en Vault - vault write pki_int/roles/my-ca use_csr_common_name=true require_cn=false allowed_domains="*" allow_any_name=true allow_bare_domains=true allow_glob_domains=true allow_subdomains=true max_ttl=4300h ttl=4300h + vault write pki_int/roles/my-ca use_csr_common_name=false require_cn=false allowed_domains="*" allow_any_name=true allow_bare_domains=true allow_glob_domains=true allow_subdomains=true max_ttl=4300h ttl=4300h # Emitir un certificado firmado por la CA intermedia # vault write -format=json pki_int/issue/my-ca \ -- GitLab From 4bd20e571e0501cc165a38815d939051cdc283ca Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 28 Jun 2024 17:00:27 +0200 Subject: [PATCH 302/310] mongo log level --- helm/capif/charts/mongo/values.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/capif/charts/mongo/values.yaml b/helm/capif/charts/mongo/values.yaml index a226dba..81281b6 100644 --- a/helm/capif/charts/mongo/values.yaml +++ b/helm/capif/charts/mongo/values.yaml @@ -17,6 +17,7 @@ fullnameOverride: "" env: mongoInitdbRootPassword: example mongoInitdbRootUsername: root + # log level host 0-5. 0 min log level. 5 the max log level mongoLeveLog: 0 serviceAccount: -- GitLab From dc1d09408be707987f3fa9b97b4f34e7ab3acc87 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Fri, 28 Jun 2024 17:12:24 +0200 Subject: [PATCH 303/310] fix nginx log level critical --- services/nginx/nginx.conf | 2 +- services/nginx/nginx_prepare.sh | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/services/nginx/nginx.conf b/services/nginx/nginx.conf index 82d4dc5..f51e177 100644 --- a/services/nginx/nginx.conf +++ b/services/nginx/nginx.conf @@ -1,6 +1,6 @@ worker_processes auto; -error_log /var/log/nginx/error.log error; +error_log /var/log/nginx/error.log ${LOG_LEVEL}; pid /tmp/nginx.pid; events { diff --git a/services/nginx/nginx_prepare.sh b/services/nginx/nginx_prepare.sh index 081b2c9..52c11ab 100644 --- a/services/nginx/nginx_prepare.sh +++ b/services/nginx/nginx_prepare.sh @@ -34,5 +34,20 @@ curl -k -retry 30 \ --header "X-Vault-Token: $VAULT_TOKEN" \ --request GET "$VAULT_ADDR/v1/secret/data/server_cert/private" 2>/dev/null | jq -r '.data.data.key' -j > $CERTS_FOLDER/server.key +LOG_LEVEL=$(echo "${LOG_LEVEL}" | tr '[:upper:]' '[:lower:]')  ✔  base  +case "$LOG_LEVEL" in + critical) + LOG_LEVEL="crit" + ;; + fatal) + LOG_LEVEL="error" + ;; + notset) + LOG_LEVEL="info" + ;; +esac + +envsubst '$LOG_LEVEL' < /etc/nginx/nginx.conf > /etc/nginx/nginx.conf.tmp +mv /etc/nginx/nginx.conf.tmp /etc/nginx/nginx.conf nginx \ No newline at end of file -- GitLab From 51f71e97e658420e49f1609a92144eff5b9bc483 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 28 Jun 2024 17:13:44 +0200 Subject: [PATCH 304/310] CD_ENV_NAME --- helm/capif/charts/mongo/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/charts/mongo/values.yaml b/helm/capif/charts/mongo/values.yaml index 81281b6..4a14c6c 100644 --- a/helm/capif/charts/mongo/values.yaml +++ b/helm/capif/charts/mongo/values.yaml @@ -115,4 +115,4 @@ nodeSelector: {} tolerations: [] -affinity: {} +affinity: {} \ No newline at end of file -- GitLab From d5ffca5ed857653e83d9a3868ef340fb1fe9db24 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 28 Jun 2024 17:20:15 +0200 Subject: [PATCH 305/310] mongoLeveLog --- helm/capif/charts/mongo/templates/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/capif/charts/mongo/templates/deployment.yaml b/helm/capif/charts/mongo/templates/deployment.yaml index 0066ead..6c8d480 100644 --- a/helm/capif/charts/mongo/templates/deployment.yaml +++ b/helm/capif/charts/mongo/templates/deployment.yaml @@ -52,7 +52,7 @@ spec: - name: MONGO_INITDB_ROOT_USERNAME value: {{ quote .Values.env.mongoInitdbRootUsername }} - name: MONGODB_LOG_LEVEL - value: {{ .Values.env.mongoLeveLog }} + value: {{ .Values.env.mongoLeveLog | quote }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: -- GitLab From 2655e28d5042332f6db24ffe676721a06a923836 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 28 Jun 2024 17:29:52 +0200 Subject: [PATCH 306/310] mongoLeveLog --- helm/capif/charts/mongo/templates/deployment.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/helm/capif/charts/mongo/templates/deployment.yaml b/helm/capif/charts/mongo/templates/deployment.yaml index 6c8d480..efe4e8e 100644 --- a/helm/capif/charts/mongo/templates/deployment.yaml +++ b/helm/capif/charts/mongo/templates/deployment.yaml @@ -45,14 +45,12 @@ spec: - "--setParameter" - "diagnosticDataCollectionEnabled=true" - "--setParameter" - - "logComponentVerbosity={default: {verbosity: ${MONGODB_LOG_LEVEL}}}" + - "logComponentVerbosity={default: {verbosity: {{ .Values.env.mongoLeveLog }} }}" env: - name: MONGO_INITDB_ROOT_PASSWORD value: {{ quote .Values.env.mongoInitdbRootPassword }} - name: MONGO_INITDB_ROOT_USERNAME value: {{ quote .Values.env.mongoInitdbRootUsername }} - - name: MONGODB_LOG_LEVEL - value: {{ .Values.env.mongoLeveLog | quote }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: -- GitLab From 993260975be4b542c1899a730d6aef478c6837bb Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Fri, 28 Jun 2024 18:07:25 +0200 Subject: [PATCH 307/310] logLevel ocf core --- helm/capif/charts/mock-server/templates/deployment.yaml | 3 +++ helm/capif/charts/mock-server/values.yaml | 3 +++ helm/capif/charts/mongo/templates/deployment.yaml | 6 ------ helm/capif/charts/mongo/values.yaml | 2 -- .../ocf-access-control-policy/templates/deployment.yaml | 2 ++ helm/capif/charts/ocf-access-control-policy/values.yaml | 1 + .../ocf-api-invocation-logs/templates/deployment.yaml | 4 +++- helm/capif/charts/ocf-api-invocation-logs/values.yaml | 1 + .../ocf-api-invoker-management/templates/deployment.yaml | 4 +++- helm/capif/charts/ocf-api-invoker-management/values.yaml | 1 + .../ocf-api-provider-management/templates/deployment.yaml | 4 +++- helm/capif/charts/ocf-api-provider-management/values.yaml | 1 + .../charts/ocf-auditing-api-logs/templates/deployment.yaml | 2 ++ helm/capif/charts/ocf-auditing-api-logs/values.yaml | 1 + .../ocf-discover-service-api/templates/deployment.yaml | 2 ++ helm/capif/charts/ocf-discover-service-api/values.yaml | 1 + helm/capif/charts/ocf-events/templates/deployment.yaml | 2 ++ helm/capif/charts/ocf-events/values.yaml | 1 + helm/capif/charts/ocf-helper/templates/deployment.yaml | 2 ++ helm/capif/charts/ocf-helper/values.yaml | 3 ++- .../ocf-publish-service-api/templates/deployment.yaml | 2 ++ helm/capif/charts/ocf-publish-service-api/values.yaml | 1 + helm/capif/charts/ocf-register/templates/deployment.yaml | 2 ++ helm/capif/charts/ocf-register/values.yaml | 3 ++- .../capif/charts/ocf-routing-info/templates/deployment.yaml | 2 ++ helm/capif/charts/ocf-routing-info/values.yaml | 1 + helm/capif/charts/ocf-security/templates/deployment.yaml | 2 ++ helm/capif/charts/ocf-security/values.yaml | 1 + 28 files changed, 47 insertions(+), 13 deletions(-) diff --git a/helm/capif/charts/mock-server/templates/deployment.yaml b/helm/capif/charts/mock-server/templates/deployment.yaml index 89261d7..ea29ba9 100644 --- a/helm/capif/charts/mock-server/templates/deployment.yaml +++ b/helm/capif/charts/mock-server/templates/deployment.yaml @@ -41,6 +41,9 @@ spec: - name: http containerPort: {{ .Values.service.port }} protocol: TCP + env: + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/mock-server/values.yaml b/helm/capif/charts/mock-server/values.yaml index cd2dfc0..a34433a 100644 --- a/helm/capif/charts/mock-server/values.yaml +++ b/helm/capif/charts/mock-server/values.yaml @@ -16,6 +16,9 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +env: + logLevel: "INFO" + serviceAccount: # Specifies whether a service account should be created create: true diff --git a/helm/capif/charts/mongo/templates/deployment.yaml b/helm/capif/charts/mongo/templates/deployment.yaml index efe4e8e..80cedad 100644 --- a/helm/capif/charts/mongo/templates/deployment.yaml +++ b/helm/capif/charts/mongo/templates/deployment.yaml @@ -40,12 +40,6 @@ spec: - name: http containerPort: {{ .Values.service.port }} protocol: TCP - args: - - mongod - - "--setParameter" - - "diagnosticDataCollectionEnabled=true" - - "--setParameter" - - "logComponentVerbosity={default: {verbosity: {{ .Values.env.mongoLeveLog }} }}" env: - name: MONGO_INITDB_ROOT_PASSWORD value: {{ quote .Values.env.mongoInitdbRootPassword }} diff --git a/helm/capif/charts/mongo/values.yaml b/helm/capif/charts/mongo/values.yaml index 4a14c6c..38e4b9b 100644 --- a/helm/capif/charts/mongo/values.yaml +++ b/helm/capif/charts/mongo/values.yaml @@ -17,8 +17,6 @@ fullnameOverride: "" env: mongoInitdbRootPassword: example mongoInitdbRootUsername: root - # log level host 0-5. 0 min log level. 5 the max log level - mongoLeveLog: 0 serviceAccount: # Specifies whether a service account should be created diff --git a/helm/capif/charts/ocf-access-control-policy/templates/deployment.yaml b/helm/capif/charts/ocf-access-control-policy/templates/deployment.yaml index 3a8000f..987f209 100644 --- a/helm/capif/charts/ocf-access-control-policy/templates/deployment.yaml +++ b/helm/capif/charts/ocf-access-control-policy/templates/deployment.yaml @@ -43,6 +43,8 @@ spec: value: {{ quote .Values.env.capifHostname }} - name: MONITORING value: {{ quote .Values.env.monitoring }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-access-control-policy/values.yaml b/helm/capif/charts/ocf-access-control-policy/values.yaml index 2912e09..9184d26 100644 --- a/helm/capif/charts/ocf-access-control-policy/values.yaml +++ b/helm/capif/charts/ocf-access-control-policy/values.yaml @@ -17,6 +17,7 @@ fullnameOverride: "" env: capifHostname: my-capif.apps.ocp-epg.hi.inet monitoring: "true" + logLevel: "INFO" serviceAccount: # Specifies whether a service account should be created diff --git a/helm/capif/charts/ocf-api-invocation-logs/templates/deployment.yaml b/helm/capif/charts/ocf-api-invocation-logs/templates/deployment.yaml index fc3ce11..b0262fe 100644 --- a/helm/capif/charts/ocf-api-invocation-logs/templates/deployment.yaml +++ b/helm/capif/charts/ocf-api-invocation-logs/templates/deployment.yaml @@ -49,7 +49,9 @@ spec: - name: VAULT_PORT value: {{ quote .Values.env.vaultPort }} - name: VAULT_ACCESS_TOKEN - value: {{ quote .Values.env.vaultAccessToken }} + value: {{ quote .Values.env.vaultAccessToken }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-api-invocation-logs/values.yaml b/helm/capif/charts/ocf-api-invocation-logs/values.yaml index 4c4431b..dc63d4b 100644 --- a/helm/capif/charts/ocf-api-invocation-logs/values.yaml +++ b/helm/capif/charts/ocf-api-invocation-logs/values.yaml @@ -22,6 +22,7 @@ env: vaultAccessToken: dev-only-token mongoInitdbRootUsername: root mongoInitdbRootPassword: example + logLevel: "INFO" serviceAccount: # Specifies whether a service account should be created diff --git a/helm/capif/charts/ocf-api-invoker-management/templates/deployment.yaml b/helm/capif/charts/ocf-api-invoker-management/templates/deployment.yaml index c4fd0c9..5b210cb 100644 --- a/helm/capif/charts/ocf-api-invoker-management/templates/deployment.yaml +++ b/helm/capif/charts/ocf-api-invoker-management/templates/deployment.yaml @@ -47,7 +47,9 @@ spec: - name: VAULT_PORT value: {{ quote .Values.env.vaultPort }} - name: VAULT_ACCESS_TOKEN - value: {{ quote .Values.env.vaultAccessToken }} + value: {{ quote .Values.env.vaultAccessToken }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-api-invoker-management/values.yaml b/helm/capif/charts/ocf-api-invoker-management/values.yaml index a296a41..e832c7d 100644 --- a/helm/capif/charts/ocf-api-invoker-management/values.yaml +++ b/helm/capif/charts/ocf-api-invoker-management/values.yaml @@ -21,6 +21,7 @@ env: vaultAccessToken: dev-only-token mongoInitdbRootUsername: root mongoInitdbRootPassword: example + logLevel: "INFO" mongoRegister: mongoInitdbRootUsername: root mongoInitdbRootPassword: example diff --git a/helm/capif/charts/ocf-api-provider-management/templates/deployment.yaml b/helm/capif/charts/ocf-api-provider-management/templates/deployment.yaml index c5ff215..7f95b9d 100644 --- a/helm/capif/charts/ocf-api-provider-management/templates/deployment.yaml +++ b/helm/capif/charts/ocf-api-provider-management/templates/deployment.yaml @@ -47,7 +47,9 @@ spec: - name: VAULT_PORT value: {{ quote .Values.env.vaultPort }} - name: VAULT_ACCESS_TOKEN - value: {{ quote .Values.env.vaultAccessToken }} + value: {{ quote .Values.env.vaultAccessToken }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-api-provider-management/values.yaml b/helm/capif/charts/ocf-api-provider-management/values.yaml index 019b214..547bb05 100644 --- a/helm/capif/charts/ocf-api-provider-management/values.yaml +++ b/helm/capif/charts/ocf-api-provider-management/values.yaml @@ -21,6 +21,7 @@ env: vaultAccessToken: dev-only-token mongoInitdbRootUsername: root mongoInitdbRootPassword: example + logLevel: "INFO" mongoRegister: mongoInitdbRootUsername: root mongoInitdbRootPassword: example diff --git a/helm/capif/charts/ocf-auditing-api-logs/templates/deployment.yaml b/helm/capif/charts/ocf-auditing-api-logs/templates/deployment.yaml index 62cbf03..8248606 100644 --- a/helm/capif/charts/ocf-auditing-api-logs/templates/deployment.yaml +++ b/helm/capif/charts/ocf-auditing-api-logs/templates/deployment.yaml @@ -42,6 +42,8 @@ spec: env: - name: MONITORING value: {{ quote .Values.env.monitoring }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-auditing-api-logs/values.yaml b/helm/capif/charts/ocf-auditing-api-logs/values.yaml index 41e3d1f..859ba12 100644 --- a/helm/capif/charts/ocf-auditing-api-logs/values.yaml +++ b/helm/capif/charts/ocf-auditing-api-logs/values.yaml @@ -18,6 +18,7 @@ env: monitoring: "true" mongoInitdbRootUsername: root mongoInitdbRootPassword: example + logLevel: "INFO" serviceAccount: # Specifies whether a service account should be created diff --git a/helm/capif/charts/ocf-discover-service-api/templates/deployment.yaml b/helm/capif/charts/ocf-discover-service-api/templates/deployment.yaml index 438b986..0c6bfb0 100644 --- a/helm/capif/charts/ocf-discover-service-api/templates/deployment.yaml +++ b/helm/capif/charts/ocf-discover-service-api/templates/deployment.yaml @@ -42,6 +42,8 @@ spec: env: - name: MONITORING value: {{ quote .Values.env.monitoring }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-discover-service-api/values.yaml b/helm/capif/charts/ocf-discover-service-api/values.yaml index b69232d..6aa8e61 100644 --- a/helm/capif/charts/ocf-discover-service-api/values.yaml +++ b/helm/capif/charts/ocf-discover-service-api/values.yaml @@ -18,6 +18,7 @@ env: monitoring: "true" mongoInitdbRootUsername: root mongoInitdbRootPassword: example + logLevel: "INFO" serviceAccount: # Specifies whether a service account should be created diff --git a/helm/capif/charts/ocf-events/templates/deployment.yaml b/helm/capif/charts/ocf-events/templates/deployment.yaml index f94cc7b..50b58cc 100644 --- a/helm/capif/charts/ocf-events/templates/deployment.yaml +++ b/helm/capif/charts/ocf-events/templates/deployment.yaml @@ -42,6 +42,8 @@ spec: env: - name: MONITORING value: {{ quote .Values.env.monitoring }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-events/values.yaml b/helm/capif/charts/ocf-events/values.yaml index c600141..b3ca6b0 100644 --- a/helm/capif/charts/ocf-events/values.yaml +++ b/helm/capif/charts/ocf-events/values.yaml @@ -18,6 +18,7 @@ env: monitoring: "true" mongoInitdbRootUsername: root mongoInitdbRootPassword: example + logLevel: "INFO" serviceAccount: # Specifies whether a service account should be created diff --git a/helm/capif/charts/ocf-helper/templates/deployment.yaml b/helm/capif/charts/ocf-helper/templates/deployment.yaml index 7c55930..16f43a0 100644 --- a/helm/capif/charts/ocf-helper/templates/deployment.yaml +++ b/helm/capif/charts/ocf-helper/templates/deployment.yaml @@ -58,6 +58,8 @@ spec: value: {{ quote .Values.env.vaultPort }} - name: VAULT_ACCESS_TOKEN value: {{ quote .Values.env.vaultAccessToken }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} {{- with .Values.volumes }} volumes: {{- toYaml . | nindent 8 }} diff --git a/helm/capif/charts/ocf-helper/values.yaml b/helm/capif/charts/ocf-helper/values.yaml index 36e0989..f9e35bd 100644 --- a/helm/capif/charts/ocf-helper/values.yaml +++ b/helm/capif/charts/ocf-helper/values.yaml @@ -22,7 +22,8 @@ env: mongoPort: 27017 capifHostname: capif mongoInitdbRootUsername: root - mongoInitdbRootPassword: example + mongoInitdbRootPassword: example + logLevel: "INFO" serviceAccount: # Specifies whether a service account should be created diff --git a/helm/capif/charts/ocf-publish-service-api/templates/deployment.yaml b/helm/capif/charts/ocf-publish-service-api/templates/deployment.yaml index 49d9b2c..ceced65 100644 --- a/helm/capif/charts/ocf-publish-service-api/templates/deployment.yaml +++ b/helm/capif/charts/ocf-publish-service-api/templates/deployment.yaml @@ -42,6 +42,8 @@ spec: env: - name: MONITORING value: {{ quote .Values.env.monitoring }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-publish-service-api/values.yaml b/helm/capif/charts/ocf-publish-service-api/values.yaml index 4ab3c9c..ac32a98 100644 --- a/helm/capif/charts/ocf-publish-service-api/values.yaml +++ b/helm/capif/charts/ocf-publish-service-api/values.yaml @@ -18,6 +18,7 @@ env: monitoring: "true" mongoInitdbRootUsername: root mongoInitdbRootPassword: example + logLevel: "INFO" serviceAccount: # Specifies whether a service account should be created diff --git a/helm/capif/charts/ocf-register/templates/deployment.yaml b/helm/capif/charts/ocf-register/templates/deployment.yaml index 5437dfc..0009f19 100644 --- a/helm/capif/charts/ocf-register/templates/deployment.yaml +++ b/helm/capif/charts/ocf-register/templates/deployment.yaml @@ -46,6 +46,8 @@ spec: value: {{ quote .Values.env.vaultPort }} - name: VAULT_ACCESS_TOKEN value: {{ quote .Values.env.vaultAccessToken }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-register/values.yaml b/helm/capif/charts/ocf-register/values.yaml index 5605ef8..ca8cfc3 100644 --- a/helm/capif/charts/ocf-register/values.yaml +++ b/helm/capif/charts/ocf-register/values.yaml @@ -21,7 +21,8 @@ env: vaultPort: 8200 vaultAccessToken: dev-only-token capifHostname: capif-test.example.int - + logLevel: "INFO" + serviceAccount: # Specifies whether a service account should be created create: true diff --git a/helm/capif/charts/ocf-routing-info/templates/deployment.yaml b/helm/capif/charts/ocf-routing-info/templates/deployment.yaml index 2e1abf1..214f38f 100644 --- a/helm/capif/charts/ocf-routing-info/templates/deployment.yaml +++ b/helm/capif/charts/ocf-routing-info/templates/deployment.yaml @@ -41,6 +41,8 @@ spec: env: - name: MONITORING value: {{ quote .Values.env.monitoring }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-routing-info/values.yaml b/helm/capif/charts/ocf-routing-info/values.yaml index 8ba779c..d6c6a3d 100644 --- a/helm/capif/charts/ocf-routing-info/values.yaml +++ b/helm/capif/charts/ocf-routing-info/values.yaml @@ -16,6 +16,7 @@ fullnameOverride: "" env: monitoring: "true" + logLevel: "INFO" serviceAccount: # Specifies whether a service account should be created diff --git a/helm/capif/charts/ocf-security/templates/deployment.yaml b/helm/capif/charts/ocf-security/templates/deployment.yaml index 44bd7fa..48ab764 100644 --- a/helm/capif/charts/ocf-security/templates/deployment.yaml +++ b/helm/capif/charts/ocf-security/templates/deployment.yaml @@ -50,6 +50,8 @@ spec: value: {{ quote .Values.env.vaultPort }} - name: VAULT_ACCESS_TOKEN value: {{ quote .Values.env.vaultAccessToken }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/ocf-security/values.yaml b/helm/capif/charts/ocf-security/values.yaml index 37b57c7..2be4288 100644 --- a/helm/capif/charts/ocf-security/values.yaml +++ b/helm/capif/charts/ocf-security/values.yaml @@ -22,6 +22,7 @@ env: vaultAccessToken: dev-only-token mongoInitdbRootUsername: root mongoInitdbRootPassword: example + logLevel: "INFO" serviceAccount: # Specifies whether a service account should be created -- GitLab From d52562fc332f3408fa70bd18eaa0e7e93189773c Mon Sep 17 00:00:00 2001 From: Jorge Moratinos Salcines Date: Mon, 1 Jul 2024 08:37:17 +0200 Subject: [PATCH 308/310] Added Interrupt Exception --- services/run_capif_tests.sh | 3 ++- tests/libraries/interrupt_listener.py | 24 ++++++++++++++++++++++++ tests/resources/common.resource | 1 + 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/libraries/interrupt_listener.py diff --git a/services/run_capif_tests.sh b/services/run_capif_tests.sh index 7762fcd..f0a3f07 100755 --- a/services/run_capif_tests.sh +++ b/services/run_capif_tests.sh @@ -1,11 +1,12 @@ #!/bin/bash +timestamp=$(date +"%Y%m%d_%H%M%S") DOCKER_ROBOT_IMAGE=dockerhub.hi.inet/5ghacking/5gnow-robot-test-image DOCKER_ROBOT_IMAGE_VERSION=4.0 cd .. REPOSITORY_BASE_FOLDER=${PWD} TEST_FOLDER=$REPOSITORY_BASE_FOLDER/tests -RESULT_FOLDER=$REPOSITORY_BASE_FOLDER/results +RESULT_FOLDER=$REPOSITORY_BASE_FOLDER/results/$timestamp ROBOT_DOCKER_FILE_FOLDER=$REPOSITORY_BASE_FOLDER/tools/robot # nginx Hostname and http port (80 by default) to reach for tests diff --git a/tests/libraries/interrupt_listener.py b/tests/libraries/interrupt_listener.py new file mode 100644 index 0000000..ec2f5d4 --- /dev/null +++ b/tests/libraries/interrupt_listener.py @@ -0,0 +1,24 @@ +import signal +from robot.libraries.BuiltIn import BuiltIn + +class InterruptListener: + ROBOT_LISTENER_API_VERSION = 3 + + def __init__(self): + signal.signal(signal.SIGINT, self._handle_interrupt) + self.builtin = BuiltIn() + + def _handle_interrupt(self, signum, frame): + print("Execution interrupted! Running cleanup keyword...") + try: + self.builtin.run_keyword('Reset Testing Environment') + except Exception as e: + print(f"Error during cleanup: {e}") + finally: + exit(0) + + def start_suite(self, name, attrs): + print(f"Starting suite: {name}") + + def end_suite(self, name, attrs): + print(f"Ending suite: {name}") \ No newline at end of file diff --git a/tests/resources/common.resource b/tests/resources/common.resource index b2f19ae..fcd9a97 100644 --- a/tests/resources/common.resource +++ b/tests/resources/common.resource @@ -1,5 +1,6 @@ *** Settings *** Library /opt/robot-tests/tests/libraries/helpers.py +Library /opt/robot-tests/tests/libraries/interrupt_listener.py Library Process Library Collections Variables /opt/robot-tests/tests/libraries/environment.py -- GitLab From 4b303f23e88feb73f2f97b19201f2abe4df12534 Mon Sep 17 00:00:00 2001 From: Pelayo Torres Date: Mon, 1 Jul 2024 08:50:53 +0200 Subject: [PATCH 309/310] fix base --- services/nginx/nginx_prepare.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/nginx/nginx_prepare.sh b/services/nginx/nginx_prepare.sh index 52c11ab..75fc9fd 100644 --- a/services/nginx/nginx_prepare.sh +++ b/services/nginx/nginx_prepare.sh @@ -34,7 +34,7 @@ curl -k -retry 30 \ --header "X-Vault-Token: $VAULT_TOKEN" \ --request GET "$VAULT_ADDR/v1/secret/data/server_cert/private" 2>/dev/null | jq -r '.data.data.key' -j > $CERTS_FOLDER/server.key -LOG_LEVEL=$(echo "${LOG_LEVEL}" | tr '[:upper:]' '[:lower:]')  ✔  base  +LOG_LEVEL=$(echo "${LOG_LEVEL}" | tr '[:upper:]' '[:lower:]') case "$LOG_LEVEL" in critical) -- GitLab From 21b74a57ffaa28ccb7364412951290f1fe700753 Mon Sep 17 00:00:00 2001 From: andresanaya21 Date: Mon, 1 Jul 2024 08:51:36 +0200 Subject: [PATCH 310/310] nginx.env.logLevel --- helm/capif/charts/nginx/templates/deployment.yaml | 2 ++ helm/capif/charts/nginx/values.yaml | 1 + 2 files changed, 3 insertions(+) diff --git a/helm/capif/charts/nginx/templates/deployment.yaml b/helm/capif/charts/nginx/templates/deployment.yaml index a5cd26c..221ebfd 100644 --- a/helm/capif/charts/nginx/templates/deployment.yaml +++ b/helm/capif/charts/nginx/templates/deployment.yaml @@ -48,6 +48,8 @@ spec: value: {{ quote .Values.env.vaultPort }} - name: VAULT_ACCESS_TOKEN value: {{ quote .Values.env.vaultAccessToken }} + - name: LOG_LEVEL + value: {{ quote .Values.env.logLevel }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: diff --git a/helm/capif/charts/nginx/values.yaml b/helm/capif/charts/nginx/values.yaml index e3ba001..db0541e 100644 --- a/helm/capif/charts/nginx/values.yaml +++ b/helm/capif/charts/nginx/values.yaml @@ -19,6 +19,7 @@ env: vaultHostname: vault-internal.mon.svc.cluster.local vaultPort: 8200 vaultAccessToken: dev-only-token + logLevel: "info" serviceAccount: # Specifies whether a service account should be created -- GitLab