diff --git a/docs/CLI/apps.md b/docs/CLI/apps.md index e8ea802..4177384 100644 --- a/docs/CLI/apps.md +++ b/docs/CLI/apps.md @@ -15,6 +15,7 @@ Available Commands: logs Get logs of the PrimeHub Application by id start Start the PrimeHub Application stop Stop the PrimeHub Application + update Update an application Options: -h, --help Show the help @@ -135,10 +136,59 @@ primehub apps stop + +### update + +Update an application + + +``` +primehub apps update +``` + +* id: The id of PrimeHub application + + +* *(optional)* file: The file path of PrimeHub application configuration + + + ## Examples +### Fields for creating or updating + +| field | required | type | description | +| --- | --- | --- | --- | +| templateId | required | string | The id of a PhAppTemplate *only used with creating*| +| id | required* | string | The id of a PhApp *only used with creating* | +| displayName | required | string | | +| instanceType | required | string | | +| scope | required | string | one of `[public, primehub, group]` | +| env | optional | EnvVar[] | a list of EnvVar | + +#### EnvVar + +EnvVar is a dict with `name` and `value` with string values: + +```json +{ + "name": "my_var", + "value": "1" +} +``` + +### Auto-filling Fields + +Auto-filling will happen when the inputs omitted fields + +| field | value | comment | +| --- | --- | --- | +| id | {templateId}-{random-hex} | Generate a valid PhApp id from the templateId | + +### Creating + The `create` action helps you to install a new PrimeHub application. It shows an example that can be used to create a `code-server` application: diff --git a/docs/notebook/apps.ipynb b/docs/notebook/apps.ipynb index 64a783c..2c942f2 100644 --- a/docs/notebook/apps.ipynb +++ b/docs/notebook/apps.ipynb @@ -67,7 +67,38 @@ "id": "97735451", "metadata": {}, "source": [ - "## Examples" + "## Examples\n", + "\n", + "\n", + "### Fields for creating or updating\n", + "\n", + "| field | required | type | description |\n", + "| --- | --- | --- | --- |\n", + "| templateId | required | string | The id of a PhAppTemplate *only used with creating*|\n", + "| id | required* | string | The id of a PhApp *only used with creating* |\n", + "| displayName | required | string | |\n", + "| instanceType | required | string | |\n", + "| scope | required | string | one of `[public, primehub, group]` |\n", + "| env | optional | EnvVar[] | a list of EnvVar |\n", + "\n", + "#### EnvVar\n", + "\n", + "EnvVar is a dict with `name` and `value` with string values:\n", + "\n", + "```json\n", + "{\n", + " \"name\": \"my_var\",\n", + " \"value\": \"1\"\n", + "}\n", + "```\n", + "\n", + "### Auto-filling Fields\n", + "\n", + "Auto-filling will happen when the inputs omitted fields\n", + "\n", + "| field | value | comment |\n", + "| --- | --- | --- |\n", + "| id | {templateId}-{random-hex} | Generate a valid PhApp id from the templateId |" ] }, { diff --git a/primehub/apps.py b/primehub/apps.py index 817fb90..5a399f9 100644 --- a/primehub/apps.py +++ b/primehub/apps.py @@ -3,8 +3,10 @@ from primehub import Helpful, cmd, Module, primehub_load_config from primehub.utils import resource_not_found, PrimeHubException +from primehub.utils.core import auto_gen_id from primehub.utils.display import display_tree_like_format from primehub.utils.optionals import toggle_flag, file_flag +from primehub.utils.validator import ValidationSpec _query_ph_applications = query = """ query PhApplicationsConnection( @@ -143,37 +145,107 @@ def create(self, config: dict): raise PrimeHubException('config is required') config['groupName'] = self.group_name - # verify required fields in the config - if 'instanceType' not in config: - invalid_config('instanceType is required') - if 'templateId' not in config: - invalid_config('templateId is required') - if 'displayName' not in config: - invalid_config('displayName is required') - if 'scope' not in config: - invalid_config('scope is required') - else: - if config['scope'] not in scope_list: - invalid_config('scope is invalid') - - if 'env' in config: - if not isinstance(config['env'], list): - invalid_config('env should be a list of the name and value pair') - for x in config['env']: - if not isinstance(x, dict): - invalid_config('entry in the "env" should be a name and value pair') - valid = 'name' in x and 'value' in x - if not valid: - invalid_config('entry in the "env" should be a name and value pair') - if not isinstance(x['value'], str): - invalid_config(f'value in an entry must be the string type => {x}') + self.apply_auto_filling(config) + self.validate_creation(config) - self._verify_dependency(config) results = self.request({'data': config}, query) if 'data' in results: return results['data']['createPhApplication'] return results + @cmd(name='update', description='Update an application', optionals=[('file', file_flag)]) + def _update_cmd(self, id: str, **kwargs): + """ + Update a PrimeHub application + + :type id: str + :param id: The id of PrimeHub application + + :type file: str + :param file: The file path of PrimeHub application configuration + + :rtype dict + :return The information of the PrimeHub application + """ + + config = primehub_load_config(filename=kwargs.get('file', None)) + if not config: + invalid_config('PrimeHub application configuration is required.') + return self.update(id, config) + + def update(self, id: str, config: dict): + """ + Update a PrimeHub application + + :type id: str + :param id: The id of PrimeHub application + + :type config: dict + :param config: The file path of PrimeHub application configuration + + :rtype dict + :return The information of the PrimeHub application + """ + + self.validate_updating(config) + query = """ + mutation UpdatePhApplication( + $where: PhApplicationWhereUniqueInput! + $data: PhApplicationUpdateInput! + ) { + updatePhApplication(where: $where, data: $data) { + id + } + } + """ + + # config without instanceType will make the API crash + if 'instanceType' not in config: + current = self.get(id) + if not current: + return None + config['instanceType'] = current['instanceType'] + + results = self.request({'data': config, 'where': {'id': id}}, query) + if 'data' in results: + return results['data']['updatePhApplication'] + return results + + def apply_auto_filling(self, config): + if 'id' not in config: + config['id'] = auto_gen_id(config['templateId']) + if 'env' not in config: + template = self.primehub.apptemplates.get(config['templateId']) + if isinstance(template['defaultEnvs'], list): + config['env'] = [{'name': x['name'], 'value': x['defaultValue']} for x in template['defaultEnvs']] + + def validate_creation(self, config): + spec = ValidationSpec(""" + input PhApplicationCreateInput { + templateId: String! + id: ID! + displayName: String! + groupName: String! + env: EnvList + instanceType: String! + scope: PhAppScope! + } + """) + spec.validate(config) + self._verify_dependency(config) + + def validate_updating(self, config): + spec = ValidationSpec(""" + input PhApplicationUpdateInput { + env: EnvList + instanceType: String + scope: PhAppScope + displayName: String + } + """) + spec.validate(config) + self._verify_dependency(config) + def _verify_dependency(self, config): if 'instanceType' in config: self.primehub.instancetypes.get(config['instanceType']) diff --git a/primehub/deployments.py b/primehub/deployments.py index 3513f05..6a0e86c 100644 --- a/primehub/deployments.py +++ b/primehub/deployments.py @@ -6,7 +6,7 @@ from primehub.utils import resource_not_found, PrimeHubException from primehub.utils.optionals import toggle_flag, file_flag from primehub.utils.permission import ask_for_permission -from primehub.utils.auto_fill import auto_gen_id +from primehub.utils.core import auto_gen_id def _error_handler(response): diff --git a/primehub/extras/templates/examples/apps.md b/primehub/extras/templates/examples/apps.md index 45b9faa..82be33e 100644 --- a/primehub/extras/templates/examples/apps.md +++ b/primehub/extras/templates/examples/apps.md @@ -1,3 +1,35 @@ +### Fields for creating or updating + +| field | required | type | description | +| --- | --- | --- | --- | +| templateId | required | string | The id of a PhAppTemplate *only used with creating*| +| id | required* | string | The id of a PhApp *only used with creating* | +| displayName | required | string | | +| instanceType | required | string | | +| scope | required | string | one of `[public, primehub, group]` | +| env | optional | EnvVar[] | a list of EnvVar | + +#### EnvVar + +EnvVar is a dict with `name` and `value` with string values: + +```json +{ + "name": "my_var", + "value": "1" +} +``` + +### Auto-filling Fields + +Auto-filling will happen when the inputs omitted fields + +| field | value | comment | +| --- | --- | --- | +| id | {templateId}-{random-hex} | Generate a valid PhApp id from the templateId | + +### Creating + The `create` action helps you to install a new PrimeHub application. It shows an example that can be used to create a `code-server` application: @@ -73,4 +105,4 @@ stop: False status: Ready message: Deployment is ready pods: [{'logEndpoint': 'http://primehub-python-sdk.primehub.io/api/logs/pods/app-code-server-26fcc-765bf579c5-srcft'}] -``` \ No newline at end of file +``` diff --git a/primehub/utils/auto_fill.py b/primehub/utils/auto_fill.py deleted file mode 100644 index f8e6648..0000000 --- a/primehub/utils/auto_fill.py +++ /dev/null @@ -1,8 +0,0 @@ -import re -import random - - -def auto_gen_id(name: str): - normalized_name = re.sub(r'[\W_]', '-', name).lower() - random_string = str(float.hex(random.random()))[4:9] - return f'{normalized_name}-{random_string}' diff --git a/primehub/utils/core.py b/primehub/utils/core.py index 31d77c7..69014ca 100644 --- a/primehub/utils/core.py +++ b/primehub/utils/core.py @@ -1,3 +1,5 @@ +import random +import re from collections import UserDict @@ -20,3 +22,9 @@ def __contains__(self, item): if has_item: return True return super(CommandContainer, self).__contains__(f':{item}') + + +def auto_gen_id(name: str): + normalized_name = re.sub(r'[\W_]', '-', name).lower() + random_string = str(float.hex(random.random()))[4:9] + return f'{normalized_name}-{random_string}' diff --git a/primehub/utils/validator.py b/primehub/utils/validator.py index 8bb53c8..8bb27f8 100644 --- a/primehub/utils/validator.py +++ b/primehub/utils/validator.py @@ -1,3 +1,4 @@ +import json import re from typing import Union, Any, Optional, Dict, List @@ -169,6 +170,44 @@ class OpJSON(OpBase): def __init__(self): super().__init__([dict]) + class OpPhAppScope(OpBase): + def __init__(self): + super().__init__([str]) + self.scope_list = ['public', 'primehub', 'group'] + + def validate(self, value): + return value in self.scope_list + + def error_message(self, field: str): + return f'The value of the {field} should be one of [{", ".join(self.scope_list)}]' + + class OpEnvList(OpBase): + def __init__(self): + super().__init__([list]) + + def validate(self, value): + if not isinstance(value, list): + return False + for entry in value: + if not isinstance(entry, dict): + return False + if sorted(entry.keys()) != sorted(['name', 'value']): + return False + if not isinstance(entry['value'], str): + return False + return True + + def error_message(self, field: str): + example = [ + dict(name="name", value="my-name"), + dict(name="int_value", value="1"), + dict(name="float_value", value="1.5"), + dict(name="bool_value", value="true"), + ] + return f'The value of the {field} should be an EnvList. ' \ + f'It is a list of {{name, value}} and all values MUST be a string.\n' \ + f'For example: {json.dumps(example)}' + def __init__(self, type_def: str): self.type_def = type_def self.type_name = type_def.replace('!', '')