Skip to content

Commit

Permalink
✨ do not allow start service when credits bellow zero (ITISFoundation…
Browse files Browse the repository at this point in the history
  • Loading branch information
matusdrobuliak66 authored Oct 24, 2023
1 parent 2e5d96c commit 85a4f94
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 74 deletions.
4 changes: 4 additions & 0 deletions packages/models-library/src/models_library/wallets.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
from decimal import Decimal
from enum import auto
from typing import Any, ClassVar, TypeAlias

Expand All @@ -24,6 +25,9 @@ class Config:
}


ZERO_CREDITS = Decimal(0)


### DB


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,7 @@ qx.Class.define("osparc.data.model.Node", {
osparc.data.Resources.fetch("studies", "startNode", params)
.then(() => this.startDynamicService())
.catch(err => {
if ("status" in err && err.status === 409) {
if ("status" in err && (err.status === 409 || err.status === 402)) {
osparc.FlashMessenger.getInstance().logAs(err.message, "WARNING");
} else {
console.error(err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ qx.Class.define("osparc.desktop.StudyEditor", {
this.getStudyLogger().error(null, "Error submitting pipeline");
this.getStudy().setPipelineRunning(false);
}, this);
req.addListener("fail", e => {
req.addListener("fail", async e => {
if (e.getTarget().getStatus() == "403") {
this.getStudyLogger().error(null, "Pipeline is already running");
} else if (e.getTarget().getStatus() == "422") {
Expand All @@ -357,6 +357,9 @@ qx.Class.define("osparc.desktop.StudyEditor", {
this.__requestStartPipeline(studyId, partialPipeline, true);
}
}, this);
} else if (e.getTarget().getStatus() == "402") {
const msg = await e.getTarget().getResponse().error.errors[0].message;
osparc.FlashMessenger.getInstance().logAs(msg, "WARNING");
} else {
this.getStudyLogger().error(null, "Failed submitting pipeline");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from models_library.clusters import ClusterID
from models_library.projects import ProjectID
from models_library.users import UserID
from models_library.wallets import WalletID, WalletInfo
from models_library.wallets import ZERO_CREDITS, WalletID, WalletInfo
from pydantic import BaseModel, Field, ValidationError, parse_obj_as
from pydantic.types import NonNegativeInt
from servicelib.aiohttp.rest_responses import create_error_response, get_http_error
Expand All @@ -20,20 +20,21 @@
from simcore_postgres_database.utils_groups_extra_properties import (
GroupExtraPropertiesRepo,
)
from simcore_service_webserver.db.plugin import get_database_engine
from simcore_service_webserver.users.exceptions import UserDefaultWalletNotFoundError

from .._constants import RQ_PRODUCT_KEY
from .._meta import API_VTAG as VTAG
from ..application_settings import get_settings
from ..db.plugin import get_database_engine
from ..login.decorators import login_required
from ..products import api as products_api
from ..projects import api as projects_api
from ..security.decorators import permission_required
from ..users import preferences_api as user_preferences_api
from ..users.exceptions import UserDefaultWalletNotFoundError
from ..utils_aiohttp import envelope_json_response
from ..version_control.models import CommitID
from ..wallets import api as wallets_api
from ..wallets.errors import WalletNotEnoughCreditsError
from ._abc import get_project_run_policy
from ._core_computations import ComputationsApi
from .exceptions import DirectorServiceError
Expand Down Expand Up @@ -69,83 +70,100 @@ class _ComputationStarted(BaseModel):
@permission_required("services.pipeline.*")
@permission_required("project.read")
async def start_computation(request: web.Request) -> web.Response:
req_ctx = RequestContext.parse_obj(request)
computations = ComputationsApi(request.app)
# pylint: disable=too-many-statements
try:
req_ctx = RequestContext.parse_obj(request)
computations = ComputationsApi(request.app)

run_policy = get_project_run_policy(request.app)
assert run_policy # nosec
run_policy = get_project_run_policy(request.app)
assert run_policy # nosec

project_id = ProjectID(request.match_info["project_id"])
project_id = ProjectID(request.match_info["project_id"])

subgraph: set[str] = set()
force_restart: bool = False # NOTE: deprecate this entry
cluster_id: NonNegativeInt = 0
subgraph: set[str] = set()
force_restart: bool = False # NOTE: deprecate this entry
cluster_id: NonNegativeInt = 0

if request.can_read_body:
body = await request.json()
assert parse_obj_as(_ComputationStart, body) is not None # nosec
if request.can_read_body:
body = await request.json()
assert parse_obj_as(_ComputationStart, body) is not None # nosec

subgraph = body.get("subgraph", [])
force_restart = bool(body.get("force_restart", force_restart))
cluster_id = body.get("cluster_id")
subgraph = body.get("subgraph", [])
force_restart = bool(body.get("force_restart", force_restart))
cluster_id = body.get("cluster_id")

simcore_user_agent = request.headers.get(
X_SIMCORE_USER_AGENT, UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
)
async with get_database_engine(request.app).acquire() as conn:
group_properties = (
await GroupExtraPropertiesRepo.get_aggregated_properties_for_user(
conn, user_id=req_ctx.user_id, product_name=req_ctx.product_name
)
simcore_user_agent = request.headers.get(
X_SIMCORE_USER_AGENT, UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
)
async with get_database_engine(request.app).acquire() as conn:
group_properties = (
await GroupExtraPropertiesRepo.get_aggregated_properties_for_user(
conn, user_id=req_ctx.user_id, product_name=req_ctx.product_name
)
)

# Get wallet information
wallet_info = None
product = products_api.get_current_product(request)
app_settings = get_settings(request.app)
if product.is_payment_enabled and app_settings.WEBSERVER_CREDIT_COMPUTATION_ENABLED:
project_wallet = await projects_api.get_project_wallet(
request.app, project_id=project_id
)
if project_wallet is None:
user_default_wallet_preference = await user_preferences_api.get_user_preference(
request.app,
user_id=req_ctx.user_id,
product_name=req_ctx.product_name,
preference_class=user_preferences_api.PreferredWalletIdFrontendUserPreference,
# Get wallet information
wallet_info = None
product = products_api.get_current_product(request)
app_settings = get_settings(request.app)
if (
product.is_payment_enabled
and app_settings.WEBSERVER_CREDIT_COMPUTATION_ENABLED
):
project_wallet = await projects_api.get_project_wallet(
request.app, project_id=project_id
)
if user_default_wallet_preference is None:
raise UserDefaultWalletNotFoundError(uid=req_ctx.user_id)
project_wallet_id = parse_obj_as(
WalletID, user_default_wallet_preference.value
if project_wallet is None:
user_default_wallet_preference = await user_preferences_api.get_user_preference(
request.app,
user_id=req_ctx.user_id,
product_name=req_ctx.product_name,
preference_class=user_preferences_api.PreferredWalletIdFrontendUserPreference,
)
if user_default_wallet_preference is None:
raise UserDefaultWalletNotFoundError(uid=req_ctx.user_id)
project_wallet_id = parse_obj_as(
WalletID, user_default_wallet_preference.value
)
await projects_api.connect_wallet_to_project(
request.app,
product_name=req_ctx.product_name,
project_id=project_id,
user_id=req_ctx.user_id,
wallet_id=project_wallet_id,
)
else:
project_wallet_id = project_wallet.wallet_id

# Check whether user has access to the wallet
wallet = (
await wallets_api.get_wallet_with_available_credits_by_user_and_wallet(
request.app,
req_ctx.user_id,
project_wallet_id,
req_ctx.product_name,
)
)
await projects_api.connect_wallet_to_project(
request.app,
product_name=req_ctx.product_name,
project_id=project_id,
user_id=req_ctx.user_id,
wallet_id=project_wallet_id,
if wallet.available_credits <= ZERO_CREDITS:
raise WalletNotEnoughCreditsError(
reason=f"Wallet {wallet.wallet_id} credit balance {wallet.available_credits}"
)
wallet_info = WalletInfo(
wallet_id=project_wallet_id, wallet_name=wallet.name
)
else:
project_wallet_id = project_wallet.wallet_id

# Check whether user has access to the wallet
wallet = await wallets_api.get_wallet_by_user(
request.app, req_ctx.user_id, project_wallet_id, req_ctx.product_name
)
wallet_info = WalletInfo(wallet_id=project_wallet_id, wallet_name=wallet.name)

options = {
"start_pipeline": True,
"subgraph": list(subgraph), # sets are not natively json serializable
"force_restart": force_restart,
"cluster_id": None if group_properties.use_on_demand_clusters else cluster_id,
"simcore_user_agent": simcore_user_agent,
"use_on_demand_clusters": group_properties.use_on_demand_clusters,
"wallet_info": wallet_info,
}
options = {
"start_pipeline": True,
"subgraph": list(subgraph), # sets are not natively json serializable
"force_restart": force_restart,
"cluster_id": None
if group_properties.use_on_demand_clusters
else cluster_id,
"simcore_user_agent": simcore_user_agent,
"use_on_demand_clusters": group_properties.use_on_demand_clusters,
"wallet_info": wallet_info,
}

try:
running_project_ids: list[ProjectID]
project_vc_commits: list[CommitID]

Expand Down Expand Up @@ -199,6 +217,8 @@ async def start_computation(request: web.Request) -> web.Response:
)
except UserDefaultWalletNotFoundError as exc:
return create_error_response(exc, http_error_cls=web.HTTPNotFound)
except WalletNotEnoughCreditsError as exc:
return create_error_response(exc, http_error_cls=web.HTTPPaymentRequired)


@routes.post(f"/{VTAG}/computations/{{project_id}}:stop", name="stop_computation")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from ..users.api import get_user_role
from ..users.exceptions import UserDefaultWalletNotFoundError
from ..utils_aiohttp import envelope_json_response
from ..wallets.errors import WalletNotEnoughCreditsError
from . import projects_api
from ._common_models import ProjectPathParams, RequestContext
from ._nodes_api import NodeScreenshot, get_node_screenshots
Expand Down Expand Up @@ -83,6 +84,8 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
DefaultPricingUnitNotFoundError,
) as exc:
raise web.HTTPNotFound(reason=f"{exc}") from exc
except (WalletNotEnoughCreditsError) as exc:
raise web.HTTPPaymentRequired(reason=f"{exc}") from exc

return wrapper

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
from simcore_postgres_database.models.users import UserRole
from simcore_postgres_database.webserver_models import ProjectType
from simcore_service_webserver.users.exceptions import UserDefaultWalletNotFoundError
from simcore_service_webserver.utils_aiohttp import envelope_json_response

from .._meta import API_VTAG as VTAG
from ..director_v2.exceptions import DirectorServiceError
Expand All @@ -32,6 +30,9 @@
from ..products.api import Product, get_current_product
from ..security.decorators import permission_required
from ..users import api
from ..users.exceptions import UserDefaultWalletNotFoundError
from ..utils_aiohttp import envelope_json_response
from ..wallets.errors import WalletNotEnoughCreditsError
from . import projects_api
from ._common_models import ProjectPathParams, RequestContext
from .exceptions import (
Expand Down Expand Up @@ -64,6 +65,9 @@ async def _wrapper(request: web.Request) -> web.StreamResponse:
except ProjectTooManyProjectOpenedError as exc:
raise web.HTTPConflict(reason=f"{exc}") from exc

except WalletNotEnoughCreditsError as exc:
raise web.HTTPPaymentRequired(reason=f"{exc}") from exc

return _wrapper


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from models_library.services_resources import ServiceResourcesDict
from models_library.users import UserID
from models_library.utils.fastapi_encoders import jsonable_encoder
from models_library.wallets import WalletID, WalletInfo
from models_library.wallets import ZERO_CREDITS, WalletID, WalletInfo
from pydantic import parse_obj_as
from servicelib.aiohttp.application_keys import APP_FIRE_AND_FORGET_TASKS_KEY
from servicelib.common_headers import (
Expand Down Expand Up @@ -86,6 +86,7 @@
get_user_preference,
)
from ..wallets import api as wallets_api
from ..wallets.errors import WalletNotEnoughCreditsError
from . import _crud_api_delete, _nodes_api
from ._nodes_utils import set_reservation_same_as_limit, validate_new_service_resources
from ._wallets_api import connect_wallet_to_project, get_project_wallet
Expand Down Expand Up @@ -337,9 +338,15 @@ async def _start_dynamic_service(
else:
project_wallet_id = project_wallet.wallet_id
# Check whether user has access to the wallet
wallet = await wallets_api.get_wallet_by_user(
request.app, user_id, project_wallet_id, product_name
wallet = (
await wallets_api.get_wallet_with_available_credits_by_user_and_wallet(
request.app, user_id, project_wallet_id, product_name
)
)
if wallet.available_credits <= ZERO_CREDITS:
raise WalletNotEnoughCreditsError(
reason=f"Wallet {wallet.wallet_id} credit balance {wallet.available_credits}"
)
wallet_info = WalletInfo(
wallet_id=project_wallet_id, wallet_name=wallet.name
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from ._api import (
get_wallet_by_user,
get_wallet_with_available_credits_by_user_and_wallet,
get_wallet_with_permissions_by_user,
list_wallets_for_user,
)
Expand All @@ -8,6 +9,7 @@
__all__: tuple[str, ...] = (
"get_wallet_by_user",
"get_wallet_with_permissions_by_user",
"get_wallet_with_available_credits_by_user_and_wallet",
"list_wallets_for_user",
"list_wallet_groups_with_read_access_by_wallet",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class WalletAccessForbiddenError(WalletsValueError):
msg_template = "Wallet access forbidden. {reason}"


class WalletNotEnoughCreditsError(WalletsValueError):
msg_template = "Wallet does not have enough credits. {reason}"


# Wallet groups


Expand Down

0 comments on commit 85a4f94

Please sign in to comment.