diff --git a/src/spaceone/cost_analysis/interface/grpc/cost.py b/src/spaceone/cost_analysis/interface/grpc/cost.py index da33fb15..469fb24b 100644 --- a/src/spaceone/cost_analysis/interface/grpc/cost.py +++ b/src/spaceone/cost_analysis/interface/grpc/cost.py @@ -1,49 +1,46 @@ from spaceone.api.cost_analysis.v1 import cost_pb2, cost_pb2_grpc from spaceone.core.pygrpc import BaseAPI +from spaceone.cost_analysis.service import CostService -class Cost(BaseAPI, cost_pb2_grpc.CostServicer): +class Cost(BaseAPI, cost_pb2_grpc.CostServicer): pb2 = cost_pb2 pb2_grpc = cost_pb2_grpc def create(self, request, context): params, metadata = self.parse_request(request, context) - with self.locator.get_service('CostService', metadata) as cost_service: - return self.locator.get_info('CostInfo', cost_service.create(params)) + with self.locator.get_service("CostService", metadata) as cost_service: + return self.locator.get_info("CostInfo", cost_service.create(params)) def delete(self, request, context): params, metadata = self.parse_request(request, context) - with self.locator.get_service('CostService', metadata) as cost_service: + with self.locator.get_service("CostService", metadata) as cost_service: cost_service.delete(params) - return self.locator.get_info('EmptyInfo') + return self.locator.get_info("EmptyInfo") def get(self, request, context): params, metadata = self.parse_request(request, context) - - with self.locator.get_service('CostService', metadata) as cost_service: - return self.locator.get_info('CostInfo', cost_service.get(params)) + cost_svc = CostService(metadata) + response: dict = cost_svc.get(params) + return self.dict_to_message(response) def list(self, request, context): params, metadata = self.parse_request(request, context) - - with self.locator.get_service('CostService', metadata) as cost_service: - cost_vos, total_count = cost_service.list(params) - return self.locator.get_info('CostsInfo', - cost_vos, - total_count, - minimal=self.get_minimal(params)) + cost_svc = CostService(metadata) + response: dict = cost_svc.list(params) + return self.dict_to_message(response) def analyze(self, request, context): params, metadata = self.parse_request(request, context) - with self.locator.get_service('CostService', metadata) as cost_service: - return self.locator.get_info('StatisticsInfo', cost_service.analyze(params)) + with self.locator.get_service("CostService", metadata) as cost_service: + return self.locator.get_info("StatisticsInfo", cost_service.analyze(params)) def stat(self, request, context): params, metadata = self.parse_request(request, context) - with self.locator.get_service('CostService', metadata) as cost_service: - return self.locator.get_info('StatisticsInfo', cost_service.stat(params)) + with self.locator.get_service("CostService", metadata) as cost_service: + return self.locator.get_info("StatisticsInfo", cost_service.stat(params)) diff --git a/src/spaceone/cost_analysis/interface/grpc/data_source.py b/src/spaceone/cost_analysis/interface/grpc/data_source.py index 8a9fb6de..2aa053c6 100644 --- a/src/spaceone/cost_analysis/interface/grpc/data_source.py +++ b/src/spaceone/cost_analysis/interface/grpc/data_source.py @@ -3,6 +3,8 @@ from spaceone.api.cost_analysis.v1 import data_source_pb2, data_source_pb2_grpc from spaceone.core.pygrpc import BaseAPI +from spaceone.cost_analysis.service import DataSourceService + _LOGGER = logging.getLogger(__name__) @@ -30,6 +32,12 @@ def update(self, request, context): "DataSourceInfo", data_source_service.update(params) ) + def update_permissions(self, request, context): + params, metadata = self.parse_request(request, context) + data_source_svc = DataSourceService(metadata) + response: dict = data_source_svc.update_permissions(params) + return self.dict_to_message(response) + def update_secret_data(self, request, context): params, metadata = self.parse_request(request, context) diff --git a/src/spaceone/cost_analysis/manager/cost_manager.py b/src/spaceone/cost_analysis/manager/cost_manager.py index e7d9b769..3fc25ca5 100644 --- a/src/spaceone/cost_analysis/manager/cost_manager.py +++ b/src/spaceone/cost_analysis/manager/cost_manager.py @@ -119,6 +119,7 @@ def filter_costs(self, **conditions): return self.cost_model.filter(**conditions) def list_costs(self, query: dict, domain_id: str, data_source_id: str): + query = self.change_filter_v_workspace_id(query, domain_id, data_source_id) query = self._change_filter_project_group_id(query, domain_id) return self.cost_model.query(**query) @@ -204,6 +205,7 @@ def analyze_yearly_costs_with_cache( def analyze_costs_by_granularity( self, query: dict, domain_id: str, data_source_id: str ): + self._check_group_by(query) self._check_date_range(query) granularity = query["granularity"] @@ -497,15 +499,6 @@ def change_filter_v_workspace_id( query["filter"] = change_filter return query - @staticmethod - def _get_data_source_id_from_filter(query: dict) -> str: - for condition in query.get("filter", []): - key = condition.get("k", condition.get("key")) - value = condition.get("v", condition.get("value")) - - if key == "data_source_id": - return value - def _change_response_workspace_group_by( self, response: dict, query: dict, domain_id: str, data_source_id: str ) -> dict: @@ -550,3 +543,26 @@ def _get_workspace_id_from_v_workspace_id( workspace_id = ds_account_vos[0].workspace_id return workspace_id + + @staticmethod + def _get_data_source_id_from_filter(query: dict) -> str: + for condition in query.get("filter", []): + key = condition.get("k", condition.get("key")) + value = condition.get("v", condition.get("value")) + + if key == "data_source_id": + return value + + @staticmethod + def _check_group_by(query: dict) -> None: + group_by = query.get("group_by", []) + for group_by_field in group_by: + if group_by_field.split(".")[0] == "data": + raise ERROR_INVALID_PARAMETER( + key=group_by_field, reason=f"Data field is not allowed to group by." + ) + elif group_by_field in ["cost", "usage_quantity"]: + raise ERROR_INVALID_PARAMETER( + key=group_by_field, + reason=f"{group_by_field} are not allowed to group by.", + ) diff --git a/src/spaceone/cost_analysis/manager/data_source_manager.py b/src/spaceone/cost_analysis/manager/data_source_manager.py index f3d9bc60..b66d6c2c 100644 --- a/src/spaceone/cost_analysis/manager/data_source_manager.py +++ b/src/spaceone/cost_analysis/manager/data_source_manager.py @@ -27,7 +27,9 @@ def _rollback(data_source_vo): return data_source_vo - def update_data_source_by_vo(self, params, data_source_vo: DataSource): + def update_data_source_by_vo( + self, params, data_source_vo: DataSource + ) -> DataSource: def _rollback(old_data): _LOGGER.info( f"[update_data_source_by_vo._rollback] Revert Data : " diff --git a/src/spaceone/cost_analysis/model/cost/__init__.py b/src/spaceone/cost_analysis/model/cost/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/spaceone/cost_analysis/model/cost/response.py b/src/spaceone/cost_analysis/model/cost/response.py new file mode 100644 index 00000000..1e361a6c --- /dev/null +++ b/src/spaceone/cost_analysis/model/cost/response.py @@ -0,0 +1,34 @@ +from typing import Union, List +from pydantic import BaseModel + +__all__ = ["CostResponse", "CostsResponse"] + + +class CostResponse(BaseModel): + cost_id: Union[str, None] = None + cost: Union[float, None] = None + usage_quantity: Union[float, None] = None + usage_unit: Union[str, None] = None + provider: Union[str, None] = None + region_code: Union[str, None] = None + region_key: Union[str, None] = None + product: Union[str, None] = None + usage_type: Union[str, None] = None + resource: Union[str, None] = None + tags: Union[dict, None] = None + additional_info: Union[dict, None] = None + data: Union[dict, None] = None + account_id: Union[str, None] = None + service_account_id: Union[str, None] = None + project_id: Union[str, None] = None + data_source_id: Union[str, None] = None + workspace_id: Union[str, None] = None + domain_id: Union[str, None] = None + billed_year: Union[str, None] = None + billed_month: Union[str, None] = None + billed_date: Union[str, None] = None + + +class CostsResponse(BaseModel): + results: List[CostResponse] + total_count: int diff --git a/src/spaceone/cost_analysis/model/cost_report/database.py b/src/spaceone/cost_analysis/model/cost_report/database.py index 02464859..c49b2179 100644 --- a/src/spaceone/cost_analysis/model/cost_report/database.py +++ b/src/spaceone/cost_analysis/model/cost_report/database.py @@ -5,9 +5,7 @@ class CostReport(MongoModel): cost_report_id = StringField(max_length=40, generate_id="cost-report", unique=True) cost = DictField(default={}) - status = StringField( - max_length=20, choices=("IN_PROGRESS", "SUCCESS"), default="IN_PROGRESS" - ) + status = StringField(max_length=20, choices=("IN_PROGRESS", "SUCCESS")) report_number = StringField(max_length=255) currency = StringField(choices=["KRW", "USD", "JPY"], default="KRW") currency_date = StringField(max_length=20) @@ -39,8 +37,5 @@ class CostReport(MongoModel): "-created_at", "-report_number", ], - "indexes": [ - "cost_report_config_id", - "domain_id", - ], + "indexes": ["cost_report_config_id", "status", "domain_id", "workspace_id"], } diff --git a/src/spaceone/cost_analysis/model/cost_report/request.py b/src/spaceone/cost_analysis/model/cost_report/request.py index 3f627f19..0b5f3838 100644 --- a/src/spaceone/cost_analysis/model/cost_report/request.py +++ b/src/spaceone/cost_analysis/model/cost_report/request.py @@ -33,6 +33,7 @@ class CostReportGetRequest(BaseModel): class CostReportSearchQueryRequest(BaseModel): query: Union[dict, None] = None cost_report_id: Union[str, None] = None + cost_report_config_id: Union[str, None] = None status: Union[Status, None] = None issue_date: Union[str, None] = None workspace_name: Union[str, None] = None diff --git a/src/spaceone/cost_analysis/model/cost_report_config/database.py b/src/spaceone/cost_analysis/model/cost_report_config/database.py index ea48443e..8689e226 100644 --- a/src/spaceone/cost_analysis/model/cost_report_config/database.py +++ b/src/spaceone/cost_analysis/model/cost_report_config/database.py @@ -40,11 +40,7 @@ class CostReportConfig(MongoModel): "created_at", ], "ordering": ["-created_at"], - "indexes": [ - "state", - "domain_id", - "cost_report_config_id", - ], + "indexes": ["state", "domain_id"], } @queryset_manager diff --git a/src/spaceone/cost_analysis/model/cost_report_data/request.py b/src/spaceone/cost_analysis/model/cost_report_data/request.py index 1d6df0f8..d0931178 100644 --- a/src/spaceone/cost_analysis/model/cost_report_data/request.py +++ b/src/spaceone/cost_analysis/model/cost_report_data/request.py @@ -7,9 +7,6 @@ "CostReportDataStatQueryRequest", ] -State = Literal["ENABLED", "DISABLED", "PENDING"] -AuthType = Literal["LOCAL", "EXTERNAL"] - class CostReportDataSearchQueryRequest(BaseModel): query: Union[dict, None] = None diff --git a/src/spaceone/cost_analysis/model/cost_report_data/response.py b/src/spaceone/cost_analysis/model/cost_report_data/response.py index 5e983029..08ad0bc4 100644 --- a/src/spaceone/cost_analysis/model/cost_report_data/response.py +++ b/src/spaceone/cost_analysis/model/cost_report_data/response.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import Union, List from pydantic import BaseModel + from spaceone.core import utils __all__ = ["CostReportDataResponse", "CostReportsDataResponse"] diff --git a/src/spaceone/cost_analysis/model/data_source/__init__.py b/src/spaceone/cost_analysis/model/data_source/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/spaceone/cost_analysis/model/data_source/database.py b/src/spaceone/cost_analysis/model/data_source/database.py new file mode 100644 index 00000000..e69de29b diff --git a/src/spaceone/cost_analysis/model/data_source/request.py b/src/spaceone/cost_analysis/model/data_source/request.py new file mode 100644 index 00000000..968f34dd --- /dev/null +++ b/src/spaceone/cost_analysis/model/data_source/request.py @@ -0,0 +1,16 @@ +from typing import Union, Literal +from pydantic import BaseModel + +__all__ = [ + "DataSourceUpdatePermissionsRequest", +] +State = Literal["ENABLED", "DISABLED"] +DataSourceType = Literal["LOCAL", "EXTERNAL"] +SecretType = Literal["MANUAL", "USE_SERVICE_ACCOUNT_SECRET"] +ResourceGroup = Literal["DOMAIN", "WORKSPACE"] + + +class DataSourceUpdatePermissionsRequest(BaseModel): + data_source_id: str + permissions: dict + domain_id: str diff --git a/src/spaceone/cost_analysis/model/data_source/response.py b/src/spaceone/cost_analysis/model/data_source/response.py new file mode 100644 index 00000000..f365119c --- /dev/null +++ b/src/spaceone/cost_analysis/model/data_source/response.py @@ -0,0 +1,50 @@ +from datetime import datetime +from typing import Union, Literal +from pydantic import BaseModel + +from spaceone.core import utils + +from spaceone.cost_analysis.model.data_source.request import ( + State, + DataSourceType, + SecretType, + ResourceGroup, +) + +__all__ = [ + "DataSourceResponse", +] + + +class DataSourceResponse(BaseModel): + data_source_id: Union[str, None] = None + name: Union[str, None] = None + state: Union[State, None] = None + data_source_type: Union[DataSourceType, None] = None + permissions: Union[dict, None] = None + provider: Union[str, None] = None + secret_type: Union[SecretType, None] = None + secret_filter: Union[dict, None] = None + plugin_info: Union[dict, None] = None + template: Union[dict, None] = None + tags: Union[dict, None] = None + cost_tag_keys: Union[list, None] = None + cost_additional_info_keys: Union[list, None] = None + cost_data_keys: Union[list, None] = None + data_source_account_count: Union[int, None] = None + connected_workspace_count: Union[int, None] = None + resource_group: Union[ResourceGroup, None] = None + workspace_id: Union[str, None] = None + domain_id: Union[str, None] = None + created_at: Union[datetime, None] = None + updated_at: Union[datetime, None] = None + last_synchronized_at: Union[datetime, None] = None + + def dict(self, *args, **kwargs): + data = super().dict(*args, **kwargs) + data["created_at"] = utils.datetime_to_iso8601(data["created_at"]) + data["updated_at"] = utils.datetime_to_iso8601(data.get("updated_at")) + data["last_synchronized_at"] = utils.datetime_to_iso8601( + data.get("last_synchronized_at") + ) + return data diff --git a/src/spaceone/cost_analysis/model/data_source_model.py b/src/spaceone/cost_analysis/model/data_source_model.py index 0f548b7a..f5aff4a2 100644 --- a/src/spaceone/cost_analysis/model/data_source_model.py +++ b/src/spaceone/cost_analysis/model/data_source_model.py @@ -43,6 +43,7 @@ class DataSource(MongoModel): choices=("MANUAL", "USE_SERVICE_ACCOUNT_SECRET"), ) secret_filter = EmbeddedDocumentField(SecretFilter, default=None, null=True) + permissions = DictField(default=None, null=True) provider = StringField(max_length=40, default=None, null=True) plugin_info = EmbeddedDocumentField(PluginInfo, default=None, null=True) template = DictField(default={}) @@ -58,16 +59,19 @@ class DataSource(MongoModel): workspace_id = StringField(max_length=40) domain_id = StringField(max_length=40) created_at = DateTimeField(auto_now_add=True) + updated_at = DateTimeField(auto_now=True) last_synchronized_at = DateTimeField(default=None, null=True) meta = { "updatable_fields": [ "name", "state", + "permissions", "plugin_info", "secret_filter", "template", "tags", + "updated_at", "last_synchronized_at", "cost_tag_keys", "cost_additional_info_keys", diff --git a/src/spaceone/cost_analysis/service/cost_report_serivce.py b/src/spaceone/cost_analysis/service/cost_report_serivce.py index e2399203..1fdc65d0 100644 --- a/src/spaceone/cost_analysis/service/cost_report_serivce.py +++ b/src/spaceone/cost_analysis/service/cost_report_serivce.py @@ -141,10 +141,11 @@ def get(self, params: CostReportGetRequest) -> Union[CostReportResponse, dict]: ) @append_query_filter( [ - "cost_report_id", + "cost_report_config_id", "status", - "workspace_id", "domain_id", + "workspace_id", + "cost_report_id", ] ) @append_keyword_filter( @@ -174,6 +175,15 @@ def list( ] return CostReportsResponse(results=cost_reports_info, total_count=total_count) + @transaction( + permission="cost-analysis:CostReport.read", + role_types=["DOMAIN_ADMIN", "WORKSPACE_OWNER"], + ) + @append_query_filter(["cost_config_report_id", "domain_id"]) + @convert_model + def analyze(self): + pass + @transaction( permission="cost-analysis:CostReport.read", role_types=["DOMAIN_ADMIN", "WORKSPACE_OWNER"], diff --git a/src/spaceone/cost_analysis/service/cost_service.py b/src/spaceone/cost_analysis/service/cost_service.py index 879bcb5c..17b63acd 100644 --- a/src/spaceone/cost_analysis/service/cost_service.py +++ b/src/spaceone/cost_analysis/service/cost_service.py @@ -1,10 +1,20 @@ import logging +from typing import Union from spaceone.core.service import * from spaceone.core import utils +from spaceone.cost_analysis.model.cost.response import CostsResponse + +from spaceone.cost_analysis.manager.data_source_account_manager import ( + DataSourceAccountManager, +) + from spaceone.cost_analysis.error import * +from spaceone.cost_analysis.manager import DataSourceManager from spaceone.cost_analysis.manager.cost_manager import CostManager from spaceone.cost_analysis.manager.identity_manager import IdentityManager +from spaceone.cost_analysis.model import DataSource +from spaceone.cost_analysis.model.cost.response import CostResponse from spaceone.cost_analysis.model.cost_model import Cost _LOGGER = logging.getLogger(__name__) @@ -20,6 +30,7 @@ class CostService(BaseService): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.cost_mgr: CostManager = self.locator.get_manager("CostManager") + self.data_source_mgr = DataSourceManager() @transaction(permission="cost-analysis:Cost.write", role_types=["WORKSPACE_OWNER"]) @check_required( @@ -56,6 +67,8 @@ def create(self, params): identity_mgr: IdentityManager = self.locator.get_manager("IdentityManager") identity_mgr.get_project(params["project_id"], params["domain_id"]) + # todo : only local type datasource can create + cost_vo: Cost = self.cost_mgr.create_cost(params) self.cost_mgr.remove_stat_cache(params["domain_id"], params["data_source_id"]) @@ -98,7 +111,8 @@ def delete(self, params): role_types=["DOMAIN_ADMIN", "WORKSPACE_OWNER", "WORKSPACE_MEMBER"], ) @check_required(["cost_id", "domain_id"]) - def get(self, params): + @convert_model + def get(self, params: dict) -> Union[CostResponse, dict]: """Get cost Args: @@ -118,7 +132,35 @@ def get(self, params): workspace_id = params.get("workspace_id") domain_id = params["domain_id"] - return self.cost_mgr.get_cost(cost_id, domain_id, workspace_id, user_projects) + if workspace_id: + cost_vo: Cost = self.cost_mgr.get_cost(cost_id, domain_id, user_projects) + + v_workspace_ids = self._get_v_workspace_ids_related_with_workspace_id( + domain_id, workspace_id + ) + if ( + cost_vo.workspace_id not in v_workspace_ids + and cost_vo.workspace_id != workspace_id + ): + raise ERROR_PERMISSION_DENIED() + else: + cost_vo: Cost = self.cost_mgr.get_cost( + cost_id, domain_id, workspace_id, user_projects + ) + + cost_info = cost_vo.to_dict() + + # Check fields permissions + if self.transaction.get_meta("authorization.role_type") != "DOMAIN_ADMIN": + data_source_id = cost_vo.data_source_id + data_source_vo = self.data_source_mgr.get_data_source( + data_source_id, domain_id + ) + cost_info = self._remove_deny_fields_with_data_source_vo( + cost_info, data_source_vo + ) + + return CostResponse(**cost_info) @transaction( permission="cost-analysis:Cost.read", @@ -146,7 +188,8 @@ def get(self, params): ) @append_keyword_filter(["cost_id"]) @set_query_page_limit(1000) - def list(self, params): + @convert_model + def list(self, params: dict) -> Union[CostsResponse, dict]: """List costs Args: @@ -174,7 +217,28 @@ def list(self, params): query = params.get("query", {}) domain_id = params["domain_id"] data_source_id = params["data_source_id"] - return self.cost_mgr.list_costs(query, domain_id, data_source_id) + + cost_vos, total_count = self.cost_mgr.list_costs( + query, domain_id, data_source_id + ) + + # Check data fields permissions + if self.transaction.get_meta("authorization.role_type") != "DOMAIN_ADMIN": + data_source_vo = self.data_source_mgr.get_data_source( + data_source_id, domain_id + ) + cost_reports_info = [] + for cost_vo in cost_vos: + cost_info = cost_vo.to_dict() + + cost_info = self._remove_deny_fields_with_data_source_vo( + cost_info, data_source_vo + ) + cost_reports_info.append(cost_info) + else: + cost_reports_info = [cost_vo.to_dict() for cost_vo in cost_vos] + + return CostsResponse(results=cost_reports_info, total_count=total_count) @transaction( permission="cost-analysis:Cost.read", @@ -215,6 +279,12 @@ def analyze(self, params): data_source_id = params["data_source_id"] query = params.get("query", {}) + if self.transaction.get_meta("authorization.role_type") != "DOMAIN_ADMIN": + data_source_vo = self.data_source_mgr.get_data_source( + data_source_id, domain_id + ) + self._check_fields_with_data_source_permissions(query, data_source_vo) + return self.cost_mgr.analyze_costs_by_granularity( query, domain_id, data_source_id ) @@ -335,3 +405,44 @@ def _page_results(response, page): response["results"] = results return response + + @staticmethod + def _get_v_workspace_ids_related_with_workspace_id( + domain_id: str, workspace_id: str + ) -> list: + v_workspace_ids = [] + data_source_account_mgr = DataSourceAccountManager() + data_source_account_vos = data_source_account_mgr.filter_data_source_accounts( + domain_id=domain_id, + workspace_id=workspace_id, + ) + + v_workspace_ids.extend(data_source_account_vos.values_list("v_workspace_id")) + return v_workspace_ids + + @staticmethod + def _remove_deny_fields_with_data_source_vo( + cost_info: dict, data_source_vo: DataSource + ): + permissions = data_source_vo.permissions or {} + if permissions: + deny = permissions.get("deny", []) + for deny_field in deny: + if utils.get_dict_value(cost_info, deny_field): + utils.change_dict_value(cost_info, deny_field, None) + return cost_info + + @staticmethod + def _check_fields_with_data_source_permissions( + query: dict, data_source_vo: DataSource + ): + permissions = data_source_vo.permissions or {} + deny = permissions.get("deny", []) + + fields = query.get("fields", {}) + + for field_key in fields.keys(): + field_info = fields[field_key] + if _field_info_key := field_info.get("key"): + if _field_info_key in deny: + raise ERROR_PERMISSION_DENIED() diff --git a/src/spaceone/cost_analysis/service/data_source_service.py b/src/spaceone/cost_analysis/service/data_source_service.py index 7e65b529..a125323b 100644 --- a/src/spaceone/cost_analysis/service/data_source_service.py +++ b/src/spaceone/cost_analysis/service/data_source_service.py @@ -5,6 +5,10 @@ from mongoengine import QuerySet from spaceone.core.service import * from spaceone.cost_analysis.error import * +from spaceone.cost_analysis.model.data_source.request import ( + DataSourceUpdatePermissionsRequest, +) +from spaceone.cost_analysis.model.data_source.response import DataSourceResponse from spaceone.cost_analysis.service.job_service import JobService from spaceone.cost_analysis.manager.repository_manager import RepositoryManager from spaceone.cost_analysis.manager.secret_manager import SecretManager @@ -219,6 +223,38 @@ def update(self, params): return self.data_source_mgr.update_data_source_by_vo(params, data_source_vo) + @transaction( + permission="cost-analysis:DataSource.write", role_types=["DOMAIN_ADMIN"] + ) + @convert_model + def update_permissions( + self, params: DataSourceUpdatePermissionsRequest + ) -> Union[DataSourceResponse, dict]: + """Update data source permissions + + Args: + params (dict): { + 'data_source_id': 'str', # required + 'permissions': 'dict', # required + 'domain_id': 'str' # injected from auth + } + + Returns: + data_source_vo (object) + """ + + data_source_id = params.data_source_id + domain_id = params.domain_id + + data_source_vo: DataSource = self.data_source_mgr.get_data_source( + data_source_id, domain_id + ) + + data_source_vo = self.data_source_mgr.update_data_source_by_vo( + params.dict(exclude_unset=True), data_source_vo + ) + return DataSourceResponse(**data_source_vo.to_dict()) + @transaction( permission="cost-analysis:DataSource.write", role_types=["DOMAIN_ADMIN", "WORKSPACE_OWNER"],