From ddcc4d934e37b330e151c64a071b2c72938765ea Mon Sep 17 00:00:00 2001 From: Dmytro Smyk Date: Wed, 4 Dec 2024 19:55:30 +0200 Subject: [PATCH 1/5] END-486: new cmds for org/user balance (#3137) ENG-486: new commands for org/user balance --- CHANGELOG.D/3137.feature | 12 + CLI.md | 82 ++++--- apolo-cli/docs/admin.md | 97 +++++--- apolo-cli/src/apolo_cli/admin.py | 226 +++++++++--------- apolo-cli/src/apolo_cli/formatters/admin.py | 22 +- apolo-cli/src/apolo_cli/formatters/config.py | 21 +- apolo-cli/tests/e2e/test_e2e_admin.py | 19 ++ ...atter.test_list_users_with_user_info_0.ref | 18 +- ...atter.test_list_users_with_user_info_0.ref | 9 + .../unit/formatters/test_admin_formatters.py | 71 ++++++ apolo-cli/tests/unit/test_admin.py | 151 +++++++----- apolo-cli/tests/unit/test_alias.py | 4 +- 12 files changed, 470 insertions(+), 262 deletions(-) create mode 100644 CHANGELOG.D/3137.feature create mode 100644 apolo-cli/tests/unit/formatters/ascii/TestOrgUserFormatter.test_list_users_with_user_info_0.ref diff --git a/CHANGELOG.D/3137.feature b/CHANGELOG.D/3137.feature new file mode 100644 index 000000000..129ea05c6 --- /dev/null +++ b/CHANGELOG.D/3137.feature @@ -0,0 +1,12 @@ +Balance is no longer stored on a cluster level, and was moved to an organization level, e.g., +to an org itself, and to an org users, instead of a cluster / cluster users. + +New commands: + - `apolo admin set-org-defaults` - allows to set an organization default values, such as a default user credits + +Existing commands changes: + - `apolo admin add-cluster-user` cmd is no longer accepting a `credits` argument. + - `apolo admin set-user-credits` cmd is now expecting an org name instead of a cluster name. + - `apolo admin add-user-credits` cmd is now expecting an org name instead of a cluster name. + - `apolo admin set-org-cluster-credits` was removed in a favor of an `apolo admin set-org-credits`. + - `apolo admin add-org-cluster-credits` was removed in a favor of an `apolo admin add-org-credits`. diff --git a/CLI.md b/CLI.md index 8c0ad8c64..54d9a51c6 100644 --- a/CLI.md +++ b/CLI.md @@ -14,7 +14,7 @@ * [apolo admin add-cluster-user](#apolo-admin-add-cluster-user) * [apolo admin add-org](#apolo-admin-add-org) * [apolo admin add-org-cluster](#apolo-admin-add-org-cluster) - * [apolo admin add-org-cluster-credits](#apolo-admin-add-org-cluster-credits) + * [apolo admin add-org-credits](#apolo-admin-add-org-credits) * [apolo admin add-org-user](#apolo-admin-add-org-user) * [apolo admin add-project](#apolo-admin-add-project) * [apolo admin add-project-user](#apolo-admin-add-project-user) @@ -38,9 +38,10 @@ * [apolo admin remove-project](#apolo-admin-remove-project) * [apolo admin remove-project-user](#apolo-admin-remove-project-user) * [apolo admin remove-resource-preset](#apolo-admin-remove-resource-preset) - * [apolo admin set-org-cluster-credits](#apolo-admin-set-org-cluster-credits) * [apolo admin set-org-cluster-defaults](#apolo-admin-set-org-cluster-defaults) * [apolo admin set-org-cluster-quota](#apolo-admin-set-org-cluster-quota) + * [apolo admin set-org-credits](#apolo-admin-set-org-credits) + * [apolo admin set-org-defaults](#apolo-admin-set-org-defaults) * [apolo admin set-user-credits](#apolo-admin-set-user-credits) * [apolo admin set-user-quota](#apolo-admin-set-user-quota) * [apolo admin show-cluster-options](#apolo-admin-show-cluster-options) @@ -453,15 +454,15 @@ Name | Description| |Usage|Description| |---|---| | _[apolo admin add-cluster](#apolo-admin-add-cluster)_| Create a new cluster | -| _[apolo admin add\-cluster-user](#apolo-admin-add-cluster-user)_| Add user access to specified cluster | +| _[apolo admin add\-cluster-user](#apolo-admin-add-cluster-user)_| Add user access to a specified cluster | | _[apolo admin add-org](#apolo-admin-add-org)_| Create a new org | | _[apolo admin add\-org-cluster](#apolo-admin-add-org-cluster)_| Add org access to specified cluster | -| _[apolo admin add\-org-cluster-credits](#apolo-admin-add-org-cluster-credits)_| Add given values to org cluster balance | +| _[apolo admin add\-org-credits](#apolo-admin-add-org-credits)_| Add given values to org balance | | _[apolo admin add\-org-user](#apolo-admin-add-org-user)_| Add user access to specified org | | _[apolo admin add-project](#apolo-admin-add-project)_| Add new project to specified cluster | | _[apolo admin add\-project-user](#apolo-admin-add-project-user)_| Add user access to specified project | | _[apolo admin add\-resource-preset](#apolo-admin-add-resource-preset)_| Add new resource preset | -| _[apolo admin add\-user-credits](#apolo-admin-add-user-credits)_| Add given values to user quota | +| _[apolo admin add\-user-credits](#apolo-admin-add-user-credits)_| Add given values to user credits | | _[apolo admin generate\-cluster-config](#apolo-admin-generate-cluster-config)_| Create a cluster configuration file | | _[apolo admin get\-cluster-orgs](#apolo-admin-get-cluster-orgs)_| Print the list of all orgs in the cluster | | _[apolo admin get\-cluster-users](#apolo-admin-get-cluster-users)_| List users in specified cluster | @@ -480,9 +481,10 @@ Name | Description| | _[apolo admin remove-project](#apolo-admin-remove-project)_| Drop a project | | _[apolo admin remove\-project-user](#apolo-admin-remove-project-user)_| Remove user access from the project | | _[apolo admin remove\-resource-preset](#apolo-admin-remove-resource-preset)_| Remove resource preset | -| _[apolo admin set\-org-cluster-credits](#apolo-admin-set-org-cluster-credits)_| Set org cluster credits to given value | | _[apolo admin set\-org-cluster-defaults](#apolo-admin-set-org-cluster-defaults)_| Set org cluster defaults to given value | | _[apolo admin set\-org-cluster-quota](#apolo-admin-set-org-cluster-quota)_| Set org cluster quota to given values | +| _[apolo admin set\-org-credits](#apolo-admin-set-org-credits)_| Set org credits to given value | +| _[apolo admin set\-org-defaults](#apolo-admin-set-org-defaults)_| Set org defaults to a given value | | _[apolo admin set\-user-credits](#apolo-admin-set-user-credits)_| Set user credits to given value | | _[apolo admin set\-user-quota](#apolo-admin-set-user-quota)_| Set user quota to given values | | _[apolo admin show\-cluster-options](#apolo-admin-show-cluster-options)_| Show available cluster options | @@ -521,7 +523,7 @@ Name | Description| ### apolo admin add-cluster-user -Add user access to specified cluster.

The command supports one of 3 user roles: admin, manager or user. +Add user access to a specified cluster.

The command supports one of three user roles: admin, manager or user. **Usage:** @@ -534,7 +536,6 @@ apolo admin add-cluster-user [OPTIONS] CLUSTER_NAME USER_NAME [ROLE] Name | Description| |----|------------| |_--help_|Show this message and exit.| -|_\-c, --credits AMOUNT_|Credits amount to set \(`unlimited' stands for no limit)| |_\-j, --jobs AMOUNT_|Maximum running jobs quota \(`unlimited' stands for no limit)| |_--org ORG_|org name for org-cluster users| @@ -585,14 +586,14 @@ Name | Description| -### apolo admin add-org-cluster-credits +### apolo admin add-org-credits -Add given values to org cluster balance +Add given values to org balance **Usage:** ```bash -apolo admin add-org-cluster-credits [OPTIONS] CLUSTER_NAME ORG_NAME +apolo admin add-org-credits [OPTIONS] ORG ``` **Options:** @@ -607,7 +608,7 @@ Name | Description| ### apolo admin add-org-user -Add user access to specified org.

The command supports one of 3 user roles: admin, manager or user. +Add user access to specified org.

The command supports one of three user roles: admin, manager or user. **Usage:** @@ -620,6 +621,7 @@ apolo admin add-org-user [OPTIONS] ORG_NAME USER_NAME [ROLE] Name | Description| |----|------------| |_--help_|Show this message and exit.| +|_\-c, --credits AMOUNT_|Credits amount to set \(`unlimited' stands for no limit)| @@ -702,12 +704,12 @@ Name | Description| ### apolo admin add-user-credits -Add given values to user quota +Add given values to user credits **Usage:** ```bash -apolo admin add-user-credits [OPTIONS] CLUSTER_NAME USER_NAME +apolo admin add-user-credits [OPTIONS] ORG USER_NAME ``` **Options:** @@ -716,7 +718,6 @@ Name | Description| |----|------------| |_--help_|Show this message and exit.| |_\-c, --credits AMOUNT_|Credits amount to add \[required]| -|_--org ORG_|org name for org-cluster users| @@ -1077,14 +1078,14 @@ Name | Description| -### apolo admin set-org-cluster-credits +### apolo admin set-org-cluster-defaults -Set org cluster credits to given value +Set org cluster defaults to given value **Usage:** ```bash -apolo admin set-org-cluster-credits [OPTIONS] CLUSTER_NAME ORG_NAME +apolo admin set-org-cluster-defaults [OPTIONS] CLUSTER_NAME ORG_NAME ``` **Options:** @@ -1092,19 +1093,21 @@ apolo admin set-org-cluster-credits [OPTIONS] CLUSTER_NAME ORG_NAME Name | Description| |----|------------| |_--help_|Show this message and exit.| -|_\-c, --credits AMOUNT_|Credits amount to set \(`unlimited' stands for no limit) \[required]| +|_\--default-credits AMOUNT_|Default credits amount to set \(`unlimited' stands for no limit) \[default: unlimited]| +|_\--default-jobs AMOUNT_|Default maximum running jobs quota \(`unlimited' stands for no limit) \[default: unlimited]| +|_\--default-role \[ROLE]_|Default role for new users added to org cluster \[default: user]| -### apolo admin set-org-cluster-defaults +### apolo admin set-org-cluster-quota -Set org cluster defaults to given value +Set org cluster quota to given values **Usage:** ```bash -apolo admin set-org-cluster-defaults [OPTIONS] CLUSTER_NAME ORG_NAME +apolo admin set-org-cluster-quota [OPTIONS] CLUSTER_NAME ORG_NAME ``` **Options:** @@ -1112,21 +1115,19 @@ apolo admin set-org-cluster-defaults [OPTIONS] CLUSTER_NAME ORG_NAME Name | Description| |----|------------| |_--help_|Show this message and exit.| -|_\--default-credits AMOUNT_|Default credits amount to set \(`unlimited' stands for no limit) \[default: unlimited]| -|_\--default-jobs AMOUNT_|Default maximum running jobs quota \(`unlimited' stands for no limit) \[default: unlimited]| -|_\--default-role \[ROLE]_|Default role for new users added to org cluster \[default: user]| +|_\-j, --jobs AMOUNT_|Maximum running jobs quota \(`unlimited' stands for no limit) \[required]| -### apolo admin set-org-cluster-quota +### apolo admin set-org-credits -Set org cluster quota to given values +Set org credits to given value **Usage:** ```bash -apolo admin set-org-cluster-quota [OPTIONS] CLUSTER_NAME ORG_NAME +apolo admin set-org-credits [OPTIONS] ORG ``` **Options:** @@ -1134,7 +1135,27 @@ apolo admin set-org-cluster-quota [OPTIONS] CLUSTER_NAME ORG_NAME Name | Description| |----|------------| |_--help_|Show this message and exit.| -|_\-j, --jobs AMOUNT_|Maximum running jobs quota \(`unlimited' stands for no limit) \[required]| +|_\-c, --credits AMOUNT_|Credits amount to set \(`unlimited' stands for no limit) \[required]| + + + + +### apolo admin set-org-defaults + +Set org defaults to a given value + +**Usage:** + +```bash +apolo admin set-org-defaults [OPTIONS] ORG_NAME +``` + +**Options:** + +Name | Description| +|----|------------| +|_--help_|Show this message and exit.| +|_\--user-default-credits AMOUNT_|Default credits amount to set for org users \(`unlimited' stands for no limit) \[default: unlimited]| @@ -1146,7 +1167,7 @@ Set user credits to given value **Usage:** ```bash -apolo admin set-user-credits [OPTIONS] CLUSTER_NAME USER_NAME +apolo admin set-user-credits [OPTIONS] ORG USER_NAME ``` **Options:** @@ -1155,7 +1176,6 @@ Name | Description| |----|------------| |_--help_|Show this message and exit.| |_\-c, --credits AMOUNT_|Credits amount to set \(`unlimited' stands for no limit) \[required]| -|_--org ORG_|org name for org-cluster users| diff --git a/apolo-cli/docs/admin.md b/apolo-cli/docs/admin.md index 1894ddca8..705c4ccb4 100644 --- a/apolo-cli/docs/admin.md +++ b/apolo-cli/docs/admin.md @@ -14,15 +14,15 @@ Cluster administration commands. | Usage | Description | | :--- | :--- | | [_add-cluster_](admin.md#add-cluster) | Create a new cluster | -| [_add-cluster-user_](admin.md#add-cluster-user) | Add user access to specified cluster | +| [_add-cluster-user_](admin.md#add-cluster-user) | Add user access to a specified cluster | | [_add-org_](admin.md#add-org) | Create a new org | | [_add-org-cluster_](admin.md#add-org-cluster) | Add org access to specified cluster | -| [_add-org-cluster-credits_](admin.md#add-org-cluster-credits) | Add given values to org cluster balance | +| [_add-org-credits_](admin.md#add-org-credits) | Add given values to org balance | | [_add-org-user_](admin.md#add-org-user) | Add user access to specified org | | [_add-project_](admin.md#add-project) | Add new project to specified cluster | | [_add-project-user_](admin.md#add-project-user) | Add user access to specified project | | [_add-resource-preset_](admin.md#add-resource-preset) | Add new resource preset | -| [_add-user-credits_](admin.md#add-user-credits) | Add given values to user quota | +| [_add-user-credits_](admin.md#add-user-credits) | Add given values to user credits | | [_generate-cluster-config_](admin.md#generate-cluster-config) | Create a cluster configuration file | | [_get-cluster-orgs_](admin.md#get-cluster-orgs) | Print the list of all orgs in the cluster | | [_get-cluster-users_](admin.md#get-cluster-users) | List users in specified cluster | @@ -41,9 +41,10 @@ Cluster administration commands. | [_remove-project_](admin.md#remove-project) | Drop a project | | [_remove-project-user_](admin.md#remove-project-user) | Remove user access from the project | | [_remove-resource-preset_](admin.md#remove-resource-preset) | Remove resource preset | -| [_set-org-cluster-credits_](admin.md#set-org-cluster-credits) | Set org cluster credits to given value | | [_set-org-cluster-defaults_](admin.md#set-org-cluster-defaults) | Set org cluster defaults to given value | | [_set-org-cluster-quota_](admin.md#set-org-cluster-quota) | Set org cluster quota to given values | +| [_set-org-credits_](admin.md#set-org-credits) | Set org credits to given value | +| [_set-org-defaults_](admin.md#set-org-defaults) | Set org defaults to a given value | | [_set-user-credits_](admin.md#set-user-credits) | Set user credits to given value | | [_set-user-quota_](admin.md#set-user-quota) | Set user quota to given values | | [_show-cluster-options_](admin.md#show-cluster-options) | Show available cluster options | @@ -86,7 +87,7 @@ provided config. ### add-cluster-user -Add user access to specified cluster +Add user access to a specified cluster #### Usage @@ -95,17 +96,16 @@ Add user access to specified cluster apolo admin add-cluster-user [OPTIONS] CLUSTER_NAME USER_NAME [ROLE] ``` -Add user access to specified cluster. +Add user access to a specified cluster. -The command supports one of 3 user -roles: admin, manager or user. +The command supports one of three +user roles: admin, manager or user. #### Options | Name | Description | | :--- | :--- | | _--help_ | Show this message and exit. | -| _-c, --credits AMOUNT_ | Credits amount to set \(`unlimited' stands for no limit\) | | _-j, --jobs AMOUNT_ | Maximum running jobs quota \(`unlimited' stands for no limit\) | | _--org ORG_ | org name for org-cluster users | @@ -159,18 +159,18 @@ Add org access to specified cluster. -### add-org-cluster-credits +### add-org-credits -Add given values to org cluster balance +Add given values to org balance #### Usage ```bash -apolo admin add-org-cluster-credits [OPTIONS] CLUSTER_NAME ORG_NAME +apolo admin add-org-credits [OPTIONS] ORG ``` -Add given values to org cluster balance +Add given values to org balance #### Options @@ -194,14 +194,15 @@ apolo admin add-org-user [OPTIONS] ORG_NAME USER_NAME [ROLE] Add user access to specified org. -The command supports one of 3 user roles: -admin, manager or user. +The command supports one of three user +roles: admin, manager or user. #### Options | Name | Description | | :--- | :--- | | _--help_ | Show this message and exit. | +| _-c, --credits AMOUNT_ | Credits amount to set \(`unlimited' stands for no limit\) | @@ -291,16 +292,16 @@ Add new resource preset ### add-user-credits -Add given values to user quota +Add given values to user credits #### Usage ```bash -apolo admin add-user-credits [OPTIONS] CLUSTER_NAME USER_NAME +apolo admin add-user-credits [OPTIONS] ORG USER_NAME ``` -Add given values to user quota +Add given values to user credits #### Options @@ -308,7 +309,6 @@ Add given values to user quota | :--- | :--- | | _--help_ | Show this message and exit. | | _-c, --credits AMOUNT_ | Credits amount to add _\[required\]_ | -| _--org ORG_ | org name for org-cluster users | @@ -711,71 +711,93 @@ Remove resource preset -### set-org-cluster-credits +### set-org-cluster-defaults -Set org cluster credits to given value +Set org cluster defaults to given value #### Usage ```bash -apolo admin set-org-cluster-credits [OPTIONS] CLUSTER_NAME ORG_NAME +apolo admin set-org-cluster-defaults [OPTIONS] CLUSTER_NAME ORG_NAME ``` -Set org cluster credits to given value +Set org cluster defaults to given value #### Options | Name | Description | | :--- | :--- | | _--help_ | Show this message and exit. | -| _-c, --credits AMOUNT_ | Credits amount to set \(`unlimited' stands for no limit\) _\[required\]_ | +| _--default-credits AMOUNT_ | Default credits amount to set \(`unlimited' stands for no limit\) _\[default: unlimited\]_ | +| _--default-jobs AMOUNT_ | Default maximum running jobs quota \(`unlimited' stands for no limit\) _\[default: unlimited\]_ | +| _--default-role \[ROLE\]_ | Default role for new users added to org cluster _\[default: user\]_ | -### set-org-cluster-defaults +### set-org-cluster-quota -Set org cluster defaults to given value +Set org cluster quota to given values #### Usage ```bash -apolo admin set-org-cluster-defaults [OPTIONS] CLUSTER_NAME ORG_NAME +apolo admin set-org-cluster-quota [OPTIONS] CLUSTER_NAME ORG_NAME ``` -Set org cluster defaults to given value +Set org cluster quota to given values #### Options | Name | Description | | :--- | :--- | | _--help_ | Show this message and exit. | -| _--default-credits AMOUNT_ | Default credits amount to set \(`unlimited' stands for no limit\) _\[default: unlimited\]_ | -| _--default-jobs AMOUNT_ | Default maximum running jobs quota \(`unlimited' stands for no limit\) _\[default: unlimited\]_ | -| _--default-role \[ROLE\]_ | Default role for new users added to org cluster _\[default: user\]_ | +| _-j, --jobs AMOUNT_ | Maximum running jobs quota \(`unlimited' stands for no limit\) _\[required\]_ | -### set-org-cluster-quota +### set-org-credits -Set org cluster quota to given values +Set org credits to given value #### Usage ```bash -apolo admin set-org-cluster-quota [OPTIONS] CLUSTER_NAME ORG_NAME +apolo admin set-org-credits [OPTIONS] ORG ``` -Set org cluster quota to given values +Set org credits to given value #### Options | Name | Description | | :--- | :--- | | _--help_ | Show this message and exit. | -| _-j, --jobs AMOUNT_ | Maximum running jobs quota \(`unlimited' stands for no limit\) _\[required\]_ | +| _-c, --credits AMOUNT_ | Credits amount to set \(`unlimited' stands for no limit\) _\[required\]_ | + + + +### set-org-defaults + +Set org defaults to a given value + + +#### Usage + +```bash +apolo admin set-org-defaults [OPTIONS] ORG_NAME +``` + +Set org defaults to a given value + +#### Options + +| Name | Description | +| :--- | :--- | +| _--help_ | Show this message and exit. | +| _--user-default-credits AMOUNT_ | Default credits amount to set for org users \(`unlimited' stands for no limit\) _\[default: unlimited\]_ | @@ -787,7 +809,7 @@ Set user credits to given value #### Usage ```bash -apolo admin set-user-credits [OPTIONS] CLUSTER_NAME USER_NAME +apolo admin set-user-credits [OPTIONS] ORG USER_NAME ``` Set user credits to given value @@ -798,7 +820,6 @@ Set user credits to given value | :--- | :--- | | _--help_ | Show this message and exit. | | _-c, --credits AMOUNT_ | Credits amount to set \(`unlimited' stands for no limit\) _\[required\]_ | -| _--org ORG_ | org name for org-cluster users | diff --git a/apolo-cli/src/apolo_cli/admin.py b/apolo-cli/src/apolo_cli/admin.py index e2a64ab35..7f45c6657 100644 --- a/apolo-cli/src/apolo_cli/admin.py +++ b/apolo-cli/src/apolo_cli/admin.py @@ -43,6 +43,7 @@ ClusterUserWithInfoFormatter, OrgClusterFormatter, OrgClustersFormatter, + OrgFormatter, OrgsFormatter, OrgUserFormatter, ProjectFormatter, @@ -58,10 +59,16 @@ UNLIMITED = "unlimited" -def _get_org(root: Root, org: str | None) -> str | None: - if org == "NO_ORG": - return None - return org or root.client.config.org_name +def _get_org_or_none(root: Root, org: str | None) -> str | None: + org_name = org or root.client.config.org_name + return None if org_name == "NO_ORG" else org_name + + +def _get_org(root: Root, org: str | None) -> str: + org_name = _get_org_or_none(root, org) + if not org_name: + raise ValueError("Org name is required") + return org_name @group() @@ -593,7 +600,7 @@ async def get_cluster_users( users = await root.client._admin.list_cluster_users( # type: ignore cluster_name=cluster_name, with_user_info=details, - org_name=_get_org(root, org), + org_name=_get_org_or_none(root, org), ) users = sorted(users, key=lambda user: (user.user_name, user.org_name or "")) with root.pager(): @@ -620,15 +627,6 @@ async def get_cluster_users( type=str, help="org name for org-cluster users", ) -@option( - "-c", - "--credits", - metavar="AMOUNT", - type=str, - default=None, - show_default=True, - help="Credits amount to set (`unlimited' stands for no limit)", -) @option( "-j", "--jobs", @@ -643,21 +641,16 @@ async def add_cluster_user( cluster_name: str, user_name: str, role: str | None, - credits: str | None, jobs: str | None, org: str | None, ) -> None: """ - Add user access to specified cluster. + Add user access to a specified cluster. - The command supports one of 3 user roles: admin, manager or user. + The command supports one of three user roles: admin, manager or user. """ # Use cluster defaults credits/quota for "user-like" roles. # Unlimited for other roles. - if credits is None and role in (None, "user", "member"): - balance = None - else: - balance = _Balance(credits=_parse_credits_value(credits or UNLIMITED)) if jobs is None and role in (None, "user", "member"): quota = None else: @@ -666,8 +659,7 @@ async def add_cluster_user( cluster_name, user_name, _ClusterUserRoleType(role), - org_name=_get_org(root, org), - balance=balance, + org_name=_get_org_or_none(root, org), quota=quota, ) assert user.role @@ -684,9 +676,7 @@ async def add_cluster_user( markup=True, ) quota_fmt = AdminQuotaFormatter() - balance_fmt = BalanceFormatter() root.print(quota_fmt(user.quota)) - root.print(balance_fmt(user.balance)) @command() @@ -713,7 +703,7 @@ async def update_cluster_user( org: str | None, ) -> None: cluster_user = await root.client._admin.get_cluster_user( - cluster_name, user_name, org_name=_get_org(root, org) + cluster_name, user_name, org_name=_get_org_or_none(root, org) ) cluster_user = replace(cluster_user, role=_ClusterUserRoleType(role)) await root.client._admin.update_cluster_user(cluster_user) @@ -778,7 +768,7 @@ async def remove_cluster_user( Remove user access from the cluster. """ await root.client._admin.delete_cluster_user( - cluster_name, user_name, org_name=_get_org(root, org) + cluster_name, user_name, org_name=_get_org_or_none(root, org) ) if not root.quiet: root.print( @@ -815,7 +805,7 @@ async def get_user_quota( user_with_quota = await root.client._admin.get_cluster_user( cluster_name=cluster_name, user_name=user_name, - org_name=_get_org(root, org), + org_name=_get_org_or_none(root, org), ) quota_fmt = AdminQuotaFormatter() balance_fmt = BalanceFormatter() @@ -865,7 +855,7 @@ async def set_user_quota( cluster_name=cluster_name, user_name=user_name, quota=_Quota(total_running_jobs=_parse_jobs_value(jobs)), - org_name=_get_org(root, org), + org_name=_get_org_or_none(root, org), ) fmt = AdminQuotaFormatter() root.print( @@ -882,7 +872,7 @@ async def set_user_quota( @command() -@argument("cluster_name", required=True, type=str) +@argument("org", required=True, type=str) @argument("user_name", required=True, type=str) @option( "-c", @@ -892,46 +882,33 @@ async def set_user_quota( required=True, help="Credits amount to set (`unlimited' stands for no limit)", ) -@option( - "--org", - metavar="ORG", - default=None, - type=str, - help="org name for org-cluster users", -) async def set_user_credits( root: Root, - cluster_name: str, + org: str, user_name: str, credits: str, - org: str | None, ) -> None: """ Set user credits to given value """ + org_name = _get_org(root, org) credits_decimal = _parse_credits_value(credits) - user_with_quota = await root.client._admin.update_cluster_user_balance( - cluster_name=cluster_name, + user_with_quota = await root.client._admin.update_org_user_balance( user_name=user_name, credits=credits_decimal, - org_name=_get_org(root, org), + org_name=org_name, ) fmt = BalanceFormatter() root.print( f"New credits for [u]{rich_escape(user_with_quota.user_name)}[/u] " - + ( - f"as member of org [bold]{rich_escape(org)}[/bold] " - if org is not None - else "" - ) - + f"on cluster [u]{rich_escape(cluster_name)}[/u]:", + + f"as member of org [bold]{rich_escape(org)}[/bold]:", markup=True, ) root.print(fmt(user_with_quota.balance)) @command() -@argument("cluster_name", required=True, type=str) +@argument("org", required=True, type=str) @argument("user_name", required=True, type=str) @option( "-c", @@ -941,39 +918,19 @@ async def set_user_credits( required=True, help="Credits amount to add", ) -@option( - "--org", - metavar="ORG", - default=None, - type=str, - help="org name for org-cluster users", -) -async def add_user_credits( - root: Root, - cluster_name: str, - user_name: str, - credits: str, - org: str | None, -) -> None: +async def add_user_credits(root: Root, org: str, user_name: str, credits: str) -> None: """ - Add given values to user quota + Add given values to user credits """ + org_name = _get_org(root, org) additional_credits = _parse_finite_decimal(credits) - user_with_quota = await root.client._admin.update_cluster_user_balance_by_delta( - cluster_name, - user_name, - delta=additional_credits, - org_name=_get_org(root, org), + user_with_quota = await root.client._admin.update_org_user_balance_by_delta( + org_name, user_name, delta=additional_credits ) fmt = BalanceFormatter() root.print( f"New credits for [u]{rich_escape(user_with_quota.user_name)}[/u] " - + ( - f"as member of org [bold]{rich_escape(org)}[/bold] " - if org is not None - else "" - ) - + f"on cluster [u]{rich_escape(cluster_name)}[/u]:", + + f"as member of org [bold]{rich_escape(org)}[/bold]:", markup=True, ) root.print(fmt(user_with_quota.balance)) @@ -1354,6 +1311,7 @@ async def add_org( await root.client._admin.create_org( org_name, skip_auto_add_to_clusters=skip_default_tenants ) + await root.client.config.fetch() @command() @@ -1402,21 +1360,37 @@ async def get_org_users(root: Root, org_name: str) -> None: metavar="[ROLE]", type=click.Choice([str(role) for role in list(_OrgUserRoleType)]), ) +@option( + "-c", + "--credits", + metavar="AMOUNT", + type=str, + default=None, + show_default=True, + help="Credits amount to set (`unlimited' stands for no limit)", +) async def add_org_user( root: Root, org_name: str, user_name: str, role: str, + credits: str | None, ) -> None: """ Add user access to specified org. - The command supports one of 3 user roles: admin, manager or user. + The command supports one of three user roles: admin, manager or user. """ + if credits is None and role == "user": + balance = None + else: + balance = _Balance(credits=_parse_credits_value(credits or UNLIMITED)) + user = await root.client._admin.create_org_user( - org_name, - user_name, - _OrgUserRoleType(role), + org_name=org_name, + user_name=user_name, + role=_OrgUserRoleType(role), + balance=balance, ) if not root.quiet: root.print( @@ -1425,6 +1399,8 @@ async def add_org_user( f"[bold]{rich_escape(user.role)}[/bold]", markup=True, ) + balance_fmt = BalanceFormatter() + root.print(balance_fmt(user.balance)) @command() @@ -1768,8 +1744,7 @@ async def set_org_cluster_quota( @command() -@argument("cluster_name", required=True, type=str) -@argument("org_name", required=True, type=str) +@argument("org", required=True, type=str) @option( "-c", "--credits", @@ -1778,33 +1753,30 @@ async def set_org_cluster_quota( required=True, help="Credits amount to set (`unlimited' stands for no limit)", ) -async def set_org_cluster_credits( +async def set_org_credits( root: Root, - cluster_name: str, - org_name: str, + org: str, credits: str, ) -> None: """ - Set org cluster credits to given value + Set org credits to given value """ + org_name = _get_org(root, org) credits_decimal = _parse_credits_value(credits) - org = await root.client._admin.update_org_cluster_balance( - cluster_name=cluster_name, + updated_org = await root.client._admin.update_org_balance( org_name=org_name, credits=credits_decimal, ) fmt = BalanceFormatter() root.print( - f"New credits for org [u]{rich_escape(org_name)}[/u] " - + f"on cluster [u]{rich_escape(cluster_name)}[/u]:", + f"New credits for org [u]{rich_escape(org_name)}[/u] ", markup=True, ) - root.print(fmt(org.balance)) + root.print(fmt(updated_org.balance)) @command() -@argument("cluster_name", required=True, type=str) -@argument("org_name", required=True, type=str) +@argument("org", required=True, type=str) @option( "-c", "--credits", @@ -1812,29 +1784,60 @@ async def set_org_cluster_credits( type=str, help="Credits amount to add", ) -async def add_org_cluster_credits( +async def add_org_credits( root: Root, - cluster_name: str, - org_name: str, + org: str, credits: str, ) -> None: """ - Add given values to org cluster balance + Add given values to org balance """ + org_name = _get_org(root, org) additional_credits = _parse_finite_decimal(credits) assert additional_credits - org = await root.client._admin.update_org_cluster_balance_by_delta( - cluster_name, + updated_org = await root.client._admin.update_org_balance_by_delta( org_name, delta=additional_credits, ) fmt = BalanceFormatter() root.print( - f"New credits for org [u]{rich_escape(org_name)}[/u] " - + f"on cluster [u]{rich_escape(cluster_name)}[/u]:", + f"New credits for org [u]{rich_escape(org_name)}[/u] ", markup=True, ) - root.print(fmt(org.balance)) + root.print(fmt(updated_org.balance)) + + +@command() +@argument("org_name", required=True, type=str) +@option( + "--user-default-credits", + metavar="AMOUNT", + type=str, + default=UNLIMITED, + show_default=True, + help="Default credits amount to set for org users " + "(`unlimited' stands for no limit)", +) +async def set_org_defaults( + root: Root, + org_name: str, + user_default_credits: str, +) -> None: + """ + Set org defaults to a given value + """ + org = await root.client._admin.update_org_defaults( + org_name=org_name, + user_default_credits=_parse_credits_value(user_default_credits), + ) + if not root.quiet: + root.print( + f"Org [bold]{rich_escape(org_name)}[/bold] successfully updated. " + f"New info:", + markup=True, + ) + fmt = OrgFormatter() + root.print(fmt(org)) # Projects @@ -1856,7 +1859,7 @@ async def get_projects(root: Root, cluster_name: str, org: str | None = None) -> fmt = ProjectsFormatter() with root.status(f"Fetching the list of projects of cluster [b]{cluster_name}[/b]"): org_clusters = await root.client._admin.list_projects( - cluster_name=cluster_name, org_name=_get_org(root, org) + cluster_name=cluster_name, org_name=_get_org_or_none(root, org) ) with root.pager(): root.print(fmt(org_clusters)) @@ -1902,7 +1905,7 @@ async def add_project( project = await root.client._admin.create_project( name=name, cluster_name=cluster_name, - org_name=_get_org(root, org), + org_name=_get_org_or_none(root, org), default_role=_ProjectUserRoleType(default_role), is_default=default, ) @@ -1966,7 +1969,7 @@ async def update_project( project = _Project( name=name, cluster_name=cluster_name, - org_name=_get_org(root, org), + org_name=_get_org_or_none(root, org), default_role=_ProjectUserRoleType(default_role), is_default=default, ) @@ -2014,7 +2017,7 @@ async def remove_project( await root.client._admin.delete_project( project_name=name, cluster_name=cluster_name, - org_name=_get_org(root, org), + org_name=_get_org_or_none(root, org), ) @@ -2041,7 +2044,7 @@ async def get_project_users( users = await root.client._admin.list_project_users( project_name=project_name, cluster_name=cluster_name, - org_name=_get_org(root, org), + org_name=_get_org_or_none(root, org), with_user_info=True, ) with root.pager(): @@ -2082,7 +2085,7 @@ async def add_project_user( user = await root.client._admin.create_project_user( project_name=project_name, cluster_name=cluster_name, - org_name=_get_org(root, org), + org_name=_get_org_or_none(root, org), user_name=user_name, role=_ProjectUserRoleType(role) if role else None, ) @@ -2128,7 +2131,7 @@ async def update_project_user( user = _ProjectUser( project_name=project_name, cluster_name=cluster_name, - org_name=_get_org(root, org), + org_name=_get_org_or_none(root, org), user_name=user_name, role=_ProjectUserRoleType(role), ) @@ -2167,7 +2170,7 @@ async def remove_project_user( await root.client._admin.delete_project_user( project_name=project_name, cluster_name=cluster_name, - org_name=_get_org(root, org), + org_name=_get_org_or_none(root, org), user_name=user_name, ) if not root.quiet: @@ -2217,8 +2220,9 @@ async def remove_project_user( admin.add_command(set_org_cluster_defaults) admin.add_command(get_org_cluster_quota) admin.add_command(set_org_cluster_quota) -admin.add_command(set_org_cluster_credits) -admin.add_command(add_org_cluster_credits) +admin.add_command(set_org_credits) +admin.add_command(add_org_credits) +admin.add_command(set_org_defaults) admin.add_command(get_projects) admin.add_command(add_project) diff --git a/apolo-cli/src/apolo_cli/formatters/admin.py b/apolo-cli/src/apolo_cli/formatters/admin.py index c9657f4fe..778f534b5 100644 --- a/apolo-cli/src/apolo_cli/formatters/admin.py +++ b/apolo-cli/src/apolo_cli/formatters/admin.py @@ -45,8 +45,6 @@ def __call__( table.add_column("Email") table.add_column("Full name") table.add_column("Registered") - table.add_column("Credits", max_width=10, overflow="fold") - table.add_column("Spent credits", max_width=14, overflow="fold") table.add_column("Max jobs", max_width=10, overflow="fold") rows = [] @@ -59,8 +57,6 @@ def __call__( user.user_info.email, user.user_info.full_name, format_datetime_iso(user.user_info.created_at), - format_quota_details(user.balance.credits), - format_quota_details(user.balance.spent_credits), format_quota_details(user.quota.total_running_jobs), ) ) @@ -95,6 +91,8 @@ def __call__(self, org_users: Iterable[_OrgUserWithInfo]) -> RenderableType: table.add_column("Email") table.add_column("Full name") table.add_column("Registered") + table.add_column("Credits", max_width=10, overflow="fold") + table.add_column("Spent credits", max_width=14, overflow="fold") rows = [] for user in org_users: @@ -105,6 +103,8 @@ def __call__(self, org_users: Iterable[_OrgUserWithInfo]) -> RenderableType: user.user_info.email, user.user_info.full_name, format_datetime_iso(user.user_info.created_at), + format_quota_details(user.balance.credits), + format_quota_details(user.balance.spent_credits), ) ) rows.sort(key=operator.itemgetter(0)) @@ -346,6 +346,20 @@ def _has_idle(node_pools: Iterable[_NodePool]) -> bool: return False +class OrgFormatter: + def __call__(self, org: _Org) -> RenderableType: + table = Table(box=None, show_header=False, show_edge=False) + table.add_column() + table.add_column(style="bold") + table.add_row("Name", org.name) + table.add_row("Credits", format_quota_details(org.balance.credits)) + table.add_row("Spent credits", format_quota_details(org.balance.spent_credits)) + table.add_row( + "User default credits", format_quota_details(org.user_default_credits) + ) + return table + + class OrgsFormatter: def __call__(self, orgs: Iterable[_Org]) -> RenderableType: table = Table(box=box.SIMPLE_HEAVY) diff --git a/apolo-cli/src/apolo_cli/formatters/config.py b/apolo-cli/src/apolo_cli/formatters/config.py index 0f97a7a95..67592ae1b 100644 --- a/apolo-cli/src/apolo_cli/formatters/config.py +++ b/apolo-cli/src/apolo_cli/formatters/config.py @@ -69,12 +69,29 @@ def __call__(self, balance: _Balance) -> RenderableType: credits_details = format_quota_details(balance.credits) spent_credits_details = format_quota_details(balance.spent_credits) return RichGroup( - Text.assemble(Text("Credits", style="bold"), f": ", credits_details), Text.assemble( - Text("Credits spent", style="bold"), f": ", spent_credits_details + Text("Credits", style="bold"), + f": ", + Text( + credits_details, style=self.calculate_balance_color(balance.credits) + ), + ), + Text.assemble( + Text("Credits spent", style="bold"), + f": ", + Text( + spent_credits_details, + style=self.calculate_balance_color(balance.spent_credits), + ), ), ) + @staticmethod + def calculate_balance_color(credits: Optional[Decimal] = None) -> str: + if not credits: # handles both `0` and `None` cases + return "none" + return "green" if credits >= 0 else "red" + class ClustersFormatter: def __call__( diff --git a/apolo-cli/tests/e2e/test_e2e_admin.py b/apolo-cli/tests/e2e/test_e2e_admin.py index 9c13c2a6c..cd55b01df 100644 --- a/apolo-cli/tests/e2e/test_e2e_admin.py +++ b/apolo-cli/tests/e2e/test_e2e_admin.py @@ -375,6 +375,25 @@ def test_remove_org_user_does_not_exist(helper: Helper, tmp_test_org: str) -> No assert f"User 'some-clearly-invalid-username' not found" in cm.value.stderr +@pytest.mark.e2e +def test_org_level_defaults( + helper: Helper, tmp_test_org: str, test_user_names: List[str] +) -> None: + helper.run_cli( + [ + "admin", + "set-org-defaults", + "--user-default-credits", + "21", + tmp_test_org, + ] + ) + username = test_user_names[0] + helper.run_cli(["admin", "add-org-user", tmp_test_org, username, "user"]) + captured = helper.run_cli(["admin", "get-org-users", tmp_test_org]) + assert "Credits: 21" in captured.out + + @pytest.mark.e2e @pytest.mark.skip def test_list_org_clusters( diff --git a/apolo-cli/tests/unit/formatters/ascii/TestClusterUserFormatter.test_list_users_with_user_info_0.ref b/apolo-cli/tests/unit/formatters/ascii/TestClusterUserFormatter.test_list_users_with_user_info_0.ref index 9b37356d6..d58db062d 100644 --- a/apolo-cli/tests/unit/formatters/ascii/TestClusterUserFormatter.test_list_users_with_user_info_0.ref +++ b/apolo-cli/tests/unit/formatters/ascii/TestClusterUserFormatter.test_list_users_with_user_info_0.ref @@ -1,9 +1,9 @@ -╷ ╷ ╷ ╷ ╷ ╷ ╷ ╷ - Name │ Org │ Role │ Email │ Full name │ Registered │ Credits │ Spent credits │ Max jobs -╺━━━━━━━━┿━━━━━━━━━━┿━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━┿━━━━━━━━━━━━━━━┿━━━━━━━━━━━╸ - alex │ │ user │ alex@domain.name │ │ │ 100.00 │ 20.00 │ 2 - alex │ some-org │ user │ alex@domain.name │ │ │ 100.00 │ 20.00 │ 2 - andrew │ │ manager │ andrew@domain.name │ andrew │ 2017-03-04T12:28:59.759433+00:00 │ 100.00 │ 0.00 │ unlimited - denis │ │ admin │ denis@domain.name │ denis admin │ 2017-03-04T12:28:59.759433+00:00 │ unlimited │ 0.00 │ unlimited - ivan │ │ user │ ivan@domain.name │ user │ 2017-03-04T12:28:59.759433+00:00 │ unlimited │ 0.00 │ 1 - ╵ ╵ ╵ ╵ ╵ ╵ ╵ ╵ +╷ ╷ ╷ ╷ ╷ ╷ + Name │ Org │ Role │ Email │ Full name │ Registered │ Max jobs +╺━━━━━━━━┿━━━━━━━━━━┿━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━╸ + alex │ │ user │ alex@domain.name │ │ │ 2 + alex │ some-org │ user │ alex@domain.name │ │ │ 2 + andrew │ │ manager │ andrew@domain.name │ andrew │ 2017-03-04T12:28:59.759433+00:00 │ unlimited + denis │ │ admin │ denis@domain.name │ denis admin │ 2017-03-04T12:28:59.759433+00:00 │ unlimited + ivan │ │ user │ ivan@domain.name │ user │ 2017-03-04T12:28:59.759433+00:00 │ 1 + ╵ ╵ ╵ ╵ ╵ ╵ diff --git a/apolo-cli/tests/unit/formatters/ascii/TestOrgUserFormatter.test_list_users_with_user_info_0.ref b/apolo-cli/tests/unit/formatters/ascii/TestOrgUserFormatter.test_list_users_with_user_info_0.ref new file mode 100644 index 000000000..e9082bd03 --- /dev/null +++ b/apolo-cli/tests/unit/formatters/ascii/TestOrgUserFormatter.test_list_users_with_user_info_0.ref @@ -0,0 +1,9 @@ +╷ ╷ ╷ ╷ ╷ ╷ + Name │ Role │ Email │ Full name │ Registered │ Credits │ Spent credits +╺━━━━━━━━┿━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━┿━━━━━━━━━━━━━━━╸ + alex │ user │ alex@domain.name │ │ │ 100.00 │ 20.00 + alex │ user │ alex@domain.name │ │ │ 100.00 │ 20.00 + andrew │ manager │ andrew@domain.name │ andrew │ 2017-03-04T12:28:59.759433+00:00 │ 100.00 │ 0.00 + denis │ admin │ denis@domain.name │ denis admin │ 2017-03-04T12:28:59.759433+00:00 │ unlimited │ 0.00 + ivan │ user │ ivan@domain.name │ user │ 2017-03-04T12:28:59.759433+00:00 │ unlimited │ 0.00 + ╵ ╵ ╵ ╵ ╵ ╵ diff --git a/apolo-cli/tests/unit/formatters/test_admin_formatters.py b/apolo-cli/tests/unit/formatters/test_admin_formatters.py index 246526c5d..1a7df0164 100644 --- a/apolo-cli/tests/unit/formatters/test_admin_formatters.py +++ b/apolo-cli/tests/unit/formatters/test_admin_formatters.py @@ -29,6 +29,8 @@ _NodePoolOptions, _OnPremCloudProvider, _OrgCluster, + _OrgUserRoleType, + _OrgUserWithInfo, _Quota, _StorageInstance, _UserInfo, @@ -40,6 +42,7 @@ ClusterUserFormatter, ClusterUserWithInfoFormatter, OrgClusterFormatter, + OrgUserFormatter, ) RichCmp = Callable[[RenderableType], None] @@ -471,3 +474,71 @@ def test_formatter_azure(self, rich_cmp: RichCmp) -> None: ], ) rich_cmp(formatter(options)) + + +class TestOrgUserFormatter: + def test_list_users_with_user_info(self, rich_cmp: RichCmp) -> None: + formatter = OrgUserFormatter() + users = [ + _OrgUserWithInfo( + user_name="denis", + org_name="org", + role=_OrgUserRoleType("admin"), + balance=_Balance(), + user_info=_UserInfo( + first_name="denis", + last_name="admin", + email="denis@domain.name", + created_at=isoparse("2017-03-04T12:28:59.759433+00:00"), + ), + ), + _OrgUserWithInfo( + user_name="andrew", + org_name="org", + role=_OrgUserRoleType("manager"), + balance=_Balance(credits=Decimal(100)), + user_info=_UserInfo( + first_name="andrew", + last_name=None, + email="andrew@domain.name", + created_at=isoparse("2017-03-04T12:28:59.759433+00:00"), + ), + ), + _OrgUserWithInfo( + user_name="ivan", + org_name="org", + role=_OrgUserRoleType("user"), + balance=_Balance(), + user_info=_UserInfo( + first_name=None, + last_name="user", + email="ivan@domain.name", + created_at=isoparse("2017-03-04T12:28:59.759433+00:00"), + ), + ), + _OrgUserWithInfo( + user_name="alex", + org_name="org", + role=_OrgUserRoleType("user"), + balance=_Balance(credits=Decimal(100), spent_credits=Decimal(20)), + user_info=_UserInfo( + first_name=None, + last_name=None, + email="alex@domain.name", + created_at=None, + ), + ), + _OrgUserWithInfo( + user_name="alex", + org_name="org", + role=_OrgUserRoleType("user"), + balance=_Balance(credits=Decimal(100), spent_credits=Decimal(20)), + user_info=_UserInfo( + first_name=None, + last_name=None, + email="alex@domain.name", + created_at=None, + ), + ), + ] + rich_cmp(formatter(users)) diff --git a/apolo-cli/tests/unit/test_admin.py b/apolo-cli/tests/unit/test_admin.py index c50c08d5b..7902f66e2 100644 --- a/apolo-cli/tests/unit/test_admin.py +++ b/apolo-cli/tests/unit/test_admin.py @@ -17,6 +17,8 @@ _EFSPerformanceMode, _EFSThroughputMode, _NodePoolOptions, + _OrgUserRoleType, + _OrgUserWithInfo, _Quota, _ResourcePreset, _TPUPreset, @@ -37,7 +39,6 @@ async def create_cluster_user( cluster_name: str, user_name: str, role: _ClusterUserRoleType, - balance: _Balance, quota: _Quota, org_name: Optional[str] = None, ) -> _ClusterUserWithInfo: @@ -48,7 +49,7 @@ async def create_cluster_user( org_name=org_name, role=_ClusterUserRoleType.MANAGER, quota=quota, - balance=balance, + balance=_Balance(), user_info=_UserInfo( email="some@email.com", created_at=None, @@ -61,13 +62,39 @@ async def create_cluster_user( yield +@contextmanager +def mock_create_org_user() -> Iterator[None]: + with mock.patch.object(_Admin, "create_org_user") as mocked: + + async def create_org_user( + org_name: str, + user_name: str, + role: _OrgUserRoleType, + balance: _Balance, + ) -> _OrgUserWithInfo: + return _OrgUserWithInfo( + user_name=user_name, + org_name=org_name, + role=role, + balance=balance, + user_info=_UserInfo( + email="some@email.com", + created_at=None, + first_name=None, + last_name=None, + ), + ) + + mocked.side_effect = create_org_user + yield + + def test_add_cluster_user_print_result(run_cli: _RunCli) -> None: with mock_create_cluster_user(): capture = run_cli(["admin", "add-cluster-user", "default", "ivan", "admin"]) assert not capture.err assert "Added ivan to cluster default as manager" in capture.out assert "Jobs: unlimited" in capture.out - assert "Credits: unlimited" in capture.out assert capture.code == 0 # Same with quiet mode @@ -80,40 +107,6 @@ def test_add_cluster_user_print_result(run_cli: _RunCli) -> None: assert capture.code == 0 -def test_add_cluster_user_with_credits(run_cli: _RunCli) -> None: - for value in ("1234.5", "0", "-1234.5", "unlimited"): - with mock_create_cluster_user(): - capture = run_cli( - [ - "admin", - "add-cluster-user", - "default", - "ivan", - "admin", - "--credits", - value, - ] - ) - assert not capture.err - assert capture.code == 0 - - for value in ("spam", "inf", "nan", "infinity", "Infinity"): - with mock_create_cluster_user(): - capture = run_cli( - [ - "admin", - "add-cluster-user", - "default", - "ivan", - "admin", - "--credits", - value, - ] - ) - assert f"{value} is not valid decimal number" in capture.err, capture - assert capture.code == 2 - - def test_add_cluster_user_with_jobs(run_cli: _RunCli) -> None: for value in ("100", "0", "unlimited"): with mock_create_cluster_user(): @@ -208,21 +201,18 @@ async def update_cluster_user( def test_set_user_credits(run_cli: _RunCli) -> None: - with mock.patch.object(_Admin, "update_cluster_user_balance") as mocked: + with mock.patch.object(_Admin, "update_org_user_balance") as mocked: - async def update_cluster_user_balance( - cluster_name: str, + async def update_org_user_balance( + org_name: str, user_name: str, credits: Optional[Decimal], - org_name: Optional[str] = None, - ) -> _ClusterUserWithInfo: - return _ClusterUserWithInfo( - cluster_name=cluster_name, + ) -> _OrgUserWithInfo: + return _OrgUserWithInfo( + org_name=org_name, user_name=user_name, - role=_ClusterUserRoleType.USER, - quota=_Quota(), + role=_OrgUserRoleType.USER, balance=_Balance(credits=credits), - org_name=org_name, user_info=_UserInfo(email=f"{user_name}@example.org"), ) @@ -232,48 +222,45 @@ async def update_cluster_user_balance( ("-1234.5", "-1234.50"), ("unlimited", "unlimited"), ): - mocked.side_effect = update_cluster_user_balance + mocked.side_effect = update_org_user_balance capture = run_cli( ["admin", "set-user-credits", "default", "ivan", "--credits", value] ) assert not capture.err assert capture.out == ( - f"New credits for ivan on cluster default:\n" + f"New credits for ivan as member of org default:\n" f"Credits: {outvalue}\n" f"Credits spent: 0.00" ) assert capture.code == 0 for value in ("spam", "inf", "nan", "infinity", "Infinity"): - mocked.side_effect = update_cluster_user_balance + mocked.side_effect = update_org_user_balance capture = run_cli( ["admin", "set-user-credits", "default", "ivan", "--credits", value] ) assert f"{value} is not valid decimal number" in capture.err assert capture.code == 2 - mocked.side_effect = update_cluster_user_balance + mocked.side_effect = update_org_user_balance capture = run_cli(["admin", "set-user-credits", "default", "ivan"]) assert "Missing option '-c' / '--credits'." in capture.err assert capture.code == 2 def test_add_user_credits(run_cli: _RunCli) -> None: - with mock.patch.object(_Admin, "update_cluster_user_balance_by_delta") as mocked: + with mock.patch.object(_Admin, "update_org_user_balance_by_delta") as mocked: - async def update_cluster_user_balance_by_delta( - cluster_name: str, + async def update_org_user_balance_by_delta( + org_name: str, user_name: str, delta: Decimal, - org_name: Optional[str] = None, - ) -> _ClusterUserWithInfo: - return _ClusterUserWithInfo( - cluster_name=cluster_name, + ) -> _OrgUserWithInfo: + return _OrgUserWithInfo( + org_name=org_name, user_name=user_name, - role=_ClusterUserRoleType.USER, - quota=_Quota(), + role=_OrgUserRoleType.USER, balance=_Balance(credits=100 + delta), - org_name=org_name, user_info=_UserInfo(email=f"{user_name}@example.org"), ) @@ -282,27 +269,27 @@ async def update_cluster_user_balance_by_delta( ("0", "100.00"), ("-1234.5", "-1134.50"), ): - mocked.side_effect = update_cluster_user_balance_by_delta + mocked.side_effect = update_org_user_balance_by_delta capture = run_cli( ["admin", "add-user-credits", "default", "ivan", "--credits", value] ) assert not capture.err assert capture.out == ( - f"New credits for ivan on cluster default:\n" + f"New credits for ivan as member of org default:\n" f"Credits: {outvalue}\n" f"Credits spent: 0.00" ) assert capture.code == 0 for value in ("spam", "unlimited", "inf", "nan", "infinity", "Infinity"): - mocked.side_effect = update_cluster_user_balance_by_delta + mocked.side_effect = update_org_user_balance_by_delta capture = run_cli( ["admin", "add-user-credits", "default", "ivan", "--credits", value] ) assert f"{value} is not valid decimal number" in capture.err assert capture.code == 2 - mocked.side_effect = update_cluster_user_balance_by_delta + mocked.side_effect = update_org_user_balance_by_delta capture = run_cli(["admin", "add-user-credits", "default", "ivan"]) assert "Missing option '-c' / '--credits'." in capture.err assert capture.code == 2 @@ -700,3 +687,37 @@ async def update_node_pool( ) assert not capture.err assert not capture.out + + +def test_add_org_user_with_credits(run_cli: _RunCli) -> None: + for value in ("1234.5", "0", "-1234.5", "unlimited"): + with mock_create_org_user(): + capture = run_cli( + [ + "admin", + "add-org-user", + "default", + "ivan", + "admin", + "--credits", + value, + ] + ) + assert not capture.err + assert capture.code == 0 + + for value in ("spam", "inf", "nan", "infinity", "Infinity"): + with mock_create_cluster_user(): + capture = run_cli( + [ + "admin", + "add-org-user", + "default", + "ivan", + "admin", + "--credits", + value, + ] + ) + assert f"{value} is not valid decimal number" in capture.err, capture + assert capture.code == 2 diff --git a/apolo-cli/tests/unit/test_alias.py b/apolo-cli/tests/unit/test_alias.py index 1c5aaf8df..21773dd58 100644 --- a/apolo-cli/tests/unit/test_alias.py +++ b/apolo-cli/tests/unit/test_alias.py @@ -57,7 +57,7 @@ def test_internal_alias_help(self, run_cli: _RunCli, nmrc_path: Path) -> None: f"""\ Usage: {prog_name} lsl [OPTIONS] - Alias for "pytest storage ls -l" + Alias for "{prog_name} storage ls -l" Options: --help Show this message and exit. @@ -88,7 +88,7 @@ def test_internal_alias_help_custom_msg( f"""\ Usage: {prog_name} lsl [OPTIONS] - Alias for "pytest storage ls -l" + Alias for "{prog_name} storage ls -l" Custom ls with long output. From 39e84186c1c3e55bbb0fa3ad4546576af1697793 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 5 Dec 2024 10:07:16 +0100 Subject: [PATCH 2/5] Release 24.12.0 --- .github/workflows/ci.yaml | 29 ++++++++++++++++++----------- CHANGELOG.D/3137.feature | 12 ------------ CHANGELOG.md | 18 ++++++++++++++++++ VERSION.txt | 2 +- apolo-cli/setup.cfg | 5 +++-- apolo-cli/src/apolo_cli/__init__.py | 2 +- apolo-sdk/setup.cfg | 5 +++-- apolo-sdk/src/apolo_sdk/__init__.py | 2 +- 8 files changed, 45 insertions(+), 30 deletions(-) delete mode 100644 CHANGELOG.D/3137.feature diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fd7d233fe..db179cf2a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,7 +65,7 @@ jobs: needs: lint strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] continue-on-error: [false] cmd: [sdk, cli] # temporarily disable windows tests @@ -77,6 +77,9 @@ jobs: - python-version: '3.11' os: macos continue-on-error: false + - python-version: '3.12' + os: macos + continue-on-error: false # os: [ubuntu, macos, windows] # exclude: # - python-version: '3.10' @@ -87,15 +90,15 @@ jobs: # os: macos # - python-version: '3.11' # os: windows - include: - - python-version: '3.13' - os: ubuntu - cmd: sdk - continue-on-error: true - - python-version: '3.13' - os: ubuntu - cmd: cli - continue-on-error: true + # include: + # - python-version: '3.14' + # os: ubuntu + # cmd: sdk + # continue-on-error: true + # - python-version: '3.14' + # os: ubuntu + # cmd: cli + # continue-on-error: true fail-fast: false runs-on: ${{ matrix.os }}-latest timeout-minutes: 15 @@ -141,7 +144,7 @@ jobs: (github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]') strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] os: [ubuntu, macos, windows] exclude: - python-version: '3.10' @@ -152,6 +155,10 @@ jobs: os: macos - python-version: '3.11' os: windows + - python-version: '3.12' + os: macos + - python-version: '3.12' + os: windows fail-fast: false runs-on: ${{ matrix.os }}-latest timeout-minutes: 90 diff --git a/CHANGELOG.D/3137.feature b/CHANGELOG.D/3137.feature deleted file mode 100644 index 129ea05c6..000000000 --- a/CHANGELOG.D/3137.feature +++ /dev/null @@ -1,12 +0,0 @@ -Balance is no longer stored on a cluster level, and was moved to an organization level, e.g., -to an org itself, and to an org users, instead of a cluster / cluster users. - -New commands: - - `apolo admin set-org-defaults` - allows to set an organization default values, such as a default user credits - -Existing commands changes: - - `apolo admin add-cluster-user` cmd is no longer accepting a `credits` argument. - - `apolo admin set-user-credits` cmd is now expecting an org name instead of a cluster name. - - `apolo admin add-user-credits` cmd is now expecting an org name instead of a cluster name. - - `apolo admin set-org-cluster-credits` was removed in a favor of an `apolo admin set-org-credits`. - - `apolo admin add-org-cluster-credits` was removed in a favor of an `apolo admin add-org-credits`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a9e92d8e..c31b57d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ [comment]: # (towncrier release notes start) +# Apolo SDK/CLI 24.12.0 (2024-12-05) + +### Features + +- Balance is no longer stored on a cluster level, and was moved to an organization level, e.g., + to an org itself, and to an org users, instead of a cluster / cluster users. + + New commands: + - `apolo admin set-org-defaults` - allows to set an organization default values, such as a default user credits + + Existing commands changes: + - `apolo admin add-cluster-user` cmd is no longer accepting a `credits` argument. + - `apolo admin set-user-credits` cmd is now expecting an org name instead of a cluster name. + - `apolo admin add-user-credits` cmd is now expecting an org name instead of a cluster name. + - `apolo admin set-org-cluster-credits` was removed in a favor of an `apolo admin set-org-credits`. + - `apolo admin add-org-cluster-credits` was removed in a favor of an `apolo admin add-org-credits`. ([#3137](https://github.com/neuro-inc/neuro-cli/issues/3137)) + + # Apolo SDK/CLI 24.11.4 (2024-11-22) No significant changes. diff --git a/VERSION.txt b/VERSION.txt index aca0252f2..13d831a08 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1,4 +1,4 @@ # Global version number, # Update the file and run 'make fmt' to apply it everywhere -24.11.4 +24.12.0 diff --git a/apolo-cli/setup.cfg b/apolo-cli/setup.cfg index af75d25ab..35ee1c614 100644 --- a/apolo-cli/setup.cfg +++ b/apolo-cli/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = apolo-cli -version = 24.11.4 +version = 24.12.0 description = Apolo Platform client url = https://github.com/neuro-inc/platform-client-python long_description = file: README.md @@ -14,6 +14,7 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Operating System :: OS Independent Development Status :: 4 - Beta Environment :: Console @@ -35,7 +36,7 @@ include_package_data = True install_requires = python-jose>=3.0.0 python-dateutil>=2.7.0 - apolo-sdk>=24.11.4 + apolo-sdk>=24.12.0 click>=8.0 humanize>=3.3 # certifi has no version requirement diff --git a/apolo-cli/src/apolo_cli/__init__.py b/apolo-cli/src/apolo_cli/__init__.py index fb9116c95..a6b149417 100644 --- a/apolo-cli/src/apolo_cli/__init__.py +++ b/apolo-cli/src/apolo_cli/__init__.py @@ -1 +1 @@ -__version__ = "24.11.4" +__version__ = "24.12.0" diff --git a/apolo-sdk/setup.cfg b/apolo-sdk/setup.cfg index 7bcb42294..03a4de75a 100644 --- a/apolo-sdk/setup.cfg +++ b/apolo-sdk/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = apolo-sdk -version = 24.11.4 +version = 24.12.0 description = Apolo SDK url = https://github.com/neuro-inc/platform-client-python long_description = file: README.md @@ -14,6 +14,7 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Operating System :: OS Independent Development Status :: 4 - Beta Environment :: Console @@ -48,7 +49,7 @@ install_requires = # https://github.com/python/importlib_metadata/issues/410#issuecomment-1304258228 importlib_metadata>=4.11.4; python_version<"3.11" packaging>=20.4 - neuro-admin-client>=23.5.0 + neuro-admin-client>=24.11.2 neuro-config-client>=24.11.0 [options.packages.find] diff --git a/apolo-sdk/src/apolo_sdk/__init__.py b/apolo-sdk/src/apolo_sdk/__init__.py index 2bb82c978..c81fa6c84 100644 --- a/apolo-sdk/src/apolo_sdk/__init__.py +++ b/apolo-sdk/src/apolo_sdk/__init__.py @@ -147,7 +147,7 @@ from ._users import Action, Permission, Quota, Share, Users from ._utils import _ContextManager, find_project_root -__version__ = "24.11.4" +__version__ = "24.12.0" __all__ = ( From b22168412324f3ac586457d1664dc7c0bed3d856 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 5 Dec 2024 11:24:07 +0100 Subject: [PATCH 3/5] Get rid of resource warnings --- apolo-cli/tests/unit/test_stats.py | 3 ++- apolo-sdk/setup.cfg | 1 + apolo-sdk/src/apolo_sdk/_config.py | 9 ++++----- setup.cfg | 1 + 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apolo-cli/tests/unit/test_stats.py b/apolo-cli/tests/unit/test_stats.py index 3f1d9c90c..602fafc5b 100644 --- a/apolo-cli/tests/unit/test_stats.py +++ b/apolo-cli/tests/unit/test_stats.py @@ -22,7 +22,8 @@ def db() -> sqlite3.Connection: db = sqlite3.connect(":memory:") db.row_factory = sqlite3.Row - return db + yield db + db.close() def check_tables(db: sqlite3.Connection) -> None: diff --git a/apolo-sdk/setup.cfg b/apolo-sdk/setup.cfg index 03a4de75a..378ac0245 100644 --- a/apolo-sdk/setup.cfg +++ b/apolo-sdk/setup.cfg @@ -102,6 +102,7 @@ filterwarnings=error ; Remove the following when aiohttp is fixed (probably in 4.0.0 release) ignore::ResourceWarning:asyncio ignore::ResourceWarning:contextlib + ignore::ResourceWarning: ignore:The loop argument is deprecated*:DeprecationWarning:asyncio ignore:.*pkg_resources\.declare_namespace.*:DeprecationWarning:pkg_resources [mypy] diff --git a/apolo-sdk/src/apolo_sdk/_config.py b/apolo-sdk/src/apolo_sdk/_config.py index d04928fa8..1ace60578 100644 --- a/apolo-sdk/src/apolo_sdk/_config.py +++ b/apolo-sdk/src/apolo_sdk/_config.py @@ -474,10 +474,10 @@ def _open_db_rw( os.chmod(config_file, 0o600) conn.row_factory = sqlite3.Row - with conn as db: - db.execute("PRAGMA journal_mode=WAL") - yield db + conn.execute("PRAGMA journal_mode=WAL") + yield conn except sqlite3.DatabaseError as exc: + conn.close() if not suppress_errors: raise msg = "Cannot send the usage statistics: %s" @@ -528,8 +528,7 @@ def _open_db_ro( if not skip_schema_check: _check_db(conn) conn.row_factory = sqlite3.Row - with conn as db: - yield db + yield conn finally: conn.close() diff --git a/setup.cfg b/setup.cfg index f3d91c924..d86ed5231 100644 --- a/setup.cfg +++ b/setup.cfg @@ -100,6 +100,7 @@ filterwarnings=error ignore:(rm_rf) error removing.+:UserWarning:pytest ; Remove the following when aiohttp is fixed (probably in 4.0.0 release) ignore::ResourceWarning:asyncio + ignore::ResourceWarning: ignore:The loop argument is deprecated*:DeprecationWarning:asyncio ; deprecations introduced by jose and its dependencies: ignore:int_from_bytes is deprecated:cryptography.utils.CryptographyDeprecationWarning:jose From bc46ec183fb15d2c9f62678e7e8bbd4aa2f07b09 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 5 Dec 2024 11:31:29 +0100 Subject: [PATCH 4/5] Fix linter --- apolo-cli/tests/unit/test_stats.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apolo-cli/tests/unit/test_stats.py b/apolo-cli/tests/unit/test_stats.py index 602fafc5b..bfca3769b 100644 --- a/apolo-cli/tests/unit/test_stats.py +++ b/apolo-cli/tests/unit/test_stats.py @@ -1,6 +1,7 @@ import os import sqlite3 import urllib +from collections.abc import Iterator from unittest import mock import pytest @@ -19,7 +20,7 @@ @pytest.fixture -def db() -> sqlite3.Connection: +def db() -> Iterator[sqlite3.Connection]: db = sqlite3.connect(":memory:") db.row_factory = sqlite3.Row yield db From 2c4568fd9a30fee3bcd684ef905e61b902d7b082 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:19:01 +0100 Subject: [PATCH 5/5] Bump yarl from 1.17.2 to 1.18.3 (#3140) Bumps [yarl](https://github.com/aio-libs/yarl) from 1.17.2 to 1.18.3. - [Release notes](https://github.com/aio-libs/yarl/releases) - [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/yarl/compare/v1.17.2...v1.18.3) --- updated-dependencies: - dependency-name: yarl dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrew Svetlov --- requirements/base.txt | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 2d3635380..9e6f27b19 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -20,4 +20,4 @@ pyyaml==6.0.2 rich==13.9.4 toml==0.10.2 wcwidth==0.2.13 -yarl==1.17.2 +yarl==1.18.3 diff --git a/setup.cfg b/setup.cfg index d86ed5231..9a4f9c8b6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,7 @@ python_requires = >=3.8.0 include_package_data = True install_requires = aiohttp>=3.8.1 - yarl>=1.7.0,<1.18.0 + yarl>=1.7.0,<1.19.0 pyyaml>=5.0 python-jose>=3.0.0 python-dateutil>=2.7.0