Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

✨ do not allow start service when credits bellow zero #4905

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,8 @@ 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) {
console.log(err);
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
if ("status" in err && (err.status === 409 || err.status === 402)) {
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
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,10 @@ qx.Class.define("osparc.desktop.StudyEditor", {
this.__requestStartPipeline(studyId, partialPipeline, true);
}
}, this);
}
else if (e.getTarget().getStatus() == "402") {
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -22,6 +22,7 @@
)
from simcore_service_webserver.db.plugin import get_database_engine
from simcore_service_webserver.users.exceptions import UserDefaultWalletNotFoundError
from simcore_service_webserver.wallets.errors import WalletNotEnoughCreditsError
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved

from .._constants import RQ_PRODUCT_KEY
from .._meta import API_VTAG as VTAG
Expand Down Expand Up @@ -69,83 +70,99 @@ 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)
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 +216,10 @@ 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, reason="Test", 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 @@ -44,6 +44,7 @@
from servicelib.json_serialization import json_dumps
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
from simcore_postgres_database.models.users import UserRole
from simcore_service_webserver.wallets.errors import WalletNotEnoughCreditsError
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved

from .._meta import API_VTAG as VTAG
from ..catalog import client as catalog_client
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 @@ -24,6 +24,7 @@
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 simcore_service_webserver.wallets.errors import WalletNotEnoughCreditsError
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved

from .._meta import API_VTAG as VTAG
from ..director_v2.exceptions import DirectorServiceError
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 All @@ -57,6 +57,7 @@
ProjectNodesNodeNotFound,
)
from simcore_postgres_database.webserver_models import ProjectType
from simcore_service_webserver.wallets.errors import WalletNotEnoughCreditsError
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved

from ..application_settings import get_settings
from ..catalog import client as catalog_client
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