From aa6beb07658be5a5a2ad78097abdbee63dbf005a Mon Sep 17 00:00:00 2001 From: Nathan Freeman Date: Thu, 12 Sep 2024 18:12:54 -0500 Subject: [PATCH 1/9] Add client and operation level configurations, Add workflow to run unit tests on push --- .github/workflows/main.yml | 21 +++++++++ tapipy/configuration.py | 57 ++++++++++++++++++++++++ tapipy/tapis.py | 13 +++++- tapipy/util.py | 40 ++++++++++------- tests/TestConfig.py | 30 +++++++++++++ tests/TestRetriableDecorator.py | 77 +++++++++++++++++---------------- tests/TestUtils.py | 2 +- tests/mocks.py | 10 ++++- tests/run.sh | 1 + 9 files changed, 192 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 tapipy/configuration.py create mode 100644 tests/TestConfig.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..09dbc69 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,21 @@ +name: Workflows CI + +on: + push: + branches: [ dev, staging, test, release-** ] + pull_request: + branches: [ dev, staging, test, release-** ] + +jobs: + Unit_Tests: + name: Unit_Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup_Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Run_Unit_Tests + run: ./tests/run.sh \ No newline at end of file diff --git a/tapipy/configuration.py b/tapipy/configuration.py new file mode 100644 index 0000000..d373469 --- /dev/null +++ b/tapipy/configuration.py @@ -0,0 +1,57 @@ +from typing import List, Union, Literal + +from tapipy import errors + + +class Config: + def __init__( + self, + retries:int = 0, + retry_delay_sec:int = 0, + retry_on_exceptions: List[Exception] = [errors.InternalServerError], + retry_backoff_algo: str = "constant", + retry_backoff_exponent: int = 2 + ): + # Validate retries + if type(retries) != int or retries < 0: + raise ValueError("Configuration Error: 'retries' must be an integer >= 0") + + # Set the number of permitted retries + self.retries = retries + + # Validate retry_delay_sec + if type(retry_delay_sec) not in [int, float]: + raise TypeError("Configuration Error: 'retry_delay_sec' must be an integer or a float") + + if retry_delay_sec < 0: + raise ValueError("Configuration Error: 'retry_delay_sec' must be >= 0") + + # Set the retry delay (in seconds) + self.retry_delay_sec = retry_delay_sec + + # Validate retry_on_exception. Must be a list of exception + if type(retry_on_exceptions) != list: + raise TypeError("Configuration Error: 'retry_on_execptions' must be a list of 'Execption' object") + + self.retry_on_exceptions = [] + i = 0 + # Validate the type of each item and append to the list of exceptions + for e in retry_on_exceptions: + if not issubclass(e, Exception): + raise TypeError(f"Configuration Error: 'retry_on_execptions' must be a list of class that inherit from 'Execption'. Item at index {i} is of type '{type(e)}'") + self.retry_on_exceptions.append(e) + i += 1 + + # Validate the backoff algorithm. Must be in the list of valid values + backoff_algos = ["constant", "exponential"] + if retry_backoff_algo not in backoff_algos: + raise ValueError(f"Configuration Error: 'retry_backoff_algo' must be one of the following values: {backoff_algos}") + + # Set the backoff algorithm to use for failed retries + self.retry_backoff_algo = retry_backoff_algo + + # Validate retry_backoff_exponent + if type(retry_backoff_exponent) != int: + raise ValueError("Configuration Error: 'retry_backoff_exponent' must be an integer") + + self.retry_backoff_exponent = retry_backoff_exponent \ No newline at end of file diff --git a/tapipy/tapis.py b/tapipy/tapis.py index 0718046..d545d24 100644 --- a/tapipy/tapis.py +++ b/tapipy/tapis.py @@ -13,8 +13,9 @@ import time import concurrent.futures import copy -from typing import Dict, NewType, Mapping, Optional +from typing import Dict, NewType, Mapping, Optional, Union from atomicwrites import atomic_write +from tapipy.configuration import Config from tapipy.util import dereference_spec, retriable import tapipy.errors @@ -362,7 +363,6 @@ def get_tenant_config(self, tenant_id): except KeyError: raise errors.BaseTapyException("Tenant not found.") - class Tapis(object): """ A client for the Tapis API. @@ -386,6 +386,7 @@ def __init__(self, tenants: Tenants = None, debug_prints: bool = True, resource_dicts: dict = {}, + config: Union[Config, dict] = Config(), # Provide a default configuration object plugins = [], # list[str] <-- this type doesn't compile in python < 3.9 **kwargs ): @@ -506,6 +507,14 @@ def __init__(self, if t.base_url == base_url: self.tenant_id = t.tenant_id + # Set the configuration object + if type(config) not in [Config, dict]: + raise TypeError("Tapis Client Config must be an instance of 'Config' or a dictionary") + + self.config = config + if type(config) == dict: + self.config = Config(**config) + for p in plugins: # call each plugin's on_tapis_client_instantiation() function with all args passed in. importlib.import_module(p).on_tapis_client_instantiation(self, diff --git a/tapipy/util.py b/tapipy/util.py index c067e6d..d00a916 100644 --- a/tapipy/util.py +++ b/tapipy/util.py @@ -1,6 +1,7 @@ import time from tapipy import errors +from tapipy.configuration import Config class AttrDict(dict): @@ -85,22 +86,24 @@ def dereference_spec(spec, new_spec=None): # Return the new dictionary containing the dereferenced schema return new_spec -def exponential_time(time_sec, _retry_exponent=2): +def exponential_time(time_sec, retry_backoff_exponent=2): # If time_sec is 1, just double the time if time_sec == 1: return 2 - return time_sec**_retry_exponent + return time_sec**retry_backoff_exponent def constant_time(time_sec): return time_sec # Wait the specified period of time then return the recalculated wait time # according to the backoff alogrithm provided -def backoff(time_sec, algo="constant", **kwargs): +def backoff(time_sec, algo="constant", exponent=2): backoff_recalculation_fn = constant_time + kwargs = {} if algo == "constant" or algo == None: pass elif algo == "exponential": backoff_recalculation_fn = exponential_time + kwargs["retry_backoff_exponent"] = exponent else: raise errors.TapyClientConfigurationError @@ -114,30 +117,35 @@ def backoff(time_sec, algo="constant", **kwargs): # NOTE Only to be used on the __call__ function of an Operation instance def retriable(op__call__): def wrapper( - self, + self, # Operation object *args, - _retries=0, - _retry_delay_sec=0, - _retry_on_exceptions=[errors.InternalServerError], - _retry_backoff_algo="constant", + _config: Config=None, **kwargs ): - if type(_retries) != int or _retries < 0 : - raise ValueError("Value provided to _retry_delay_sec must be an integer and must be >= 0") + config = self.tapis_client.config + retries = config.retries if _config == None else _config.retries + retry_delay_sec = config.retry_delay_sec if _config == None else _config.retry_delay_sec + retry_on_exceptions = config.retry_on_exceptions if _config == None else _config.retry_on_exceptions + retry_backoff_algo = config.retry_backoff_algo if _config == None else _config.retry_backoff_algo + retry_backoff_exponent = config.retry_backoff_exponent if _config == None else _config.retry_backoff_exponent + + if type(retries) != int or retries < 0 : + raise ValueError("Value provided to retry_delay_sec must be an integer and must be >= 0") exception = None - while _retries >= 0: + while retries >= 0: try: return op__call__(self, *args, **kwargs) except Exception as e: exception = e - if type(e) in _retry_on_exceptions: - _retries -= 1 + if type(e) in retry_on_exceptions: + retries -= 1 # Wait the delay time, then recalculate delay time based on # backoff algorithm - _retry_delay_sec = backoff( - _retry_delay_sec, - algo=_retry_backoff_algo + retry_delay_sec = backoff( + retry_delay_sec, + algo=retry_backoff_algo, + exponent=retry_backoff_exponent ) continue diff --git a/tests/TestConfig.py b/tests/TestConfig.py new file mode 100644 index 0000000..96f156f --- /dev/null +++ b/tests/TestConfig.py @@ -0,0 +1,30 @@ +import unittest + +from tapipy.configuration import Config + + +class TestConfig(unittest.TestCase): + def testNoErrors(self): + try: + Config() + Config( + retries=5, + retry_on_exceptions=[ValueError], + retry_backoff_algo="exponential", + retry_backoff_exponent=2, + retry_delay_sec=5 + ) + except Exception as e: + self.fail(str(e)) + + + def testMisconfigured(self): + self.assertRaises(ValueError, Config, retries=1.5) # Only integers + self.assertRaises(ValueError, Config, retries=-1) # Must be >= 0 + self.assertRaises(TypeError, Config, retry_on_exceptions={}) # Must be list + self.assertRaises(TypeError, Config, retry_on_exceptions=["NotException"]) # Items must be a subclass of Exception + self.assertRaises(TypeError, Config, retry_delay_sec="notNumber") # Must be number + self.assertRaises(ValueError, Config, retry_delay_sec=-1) # Must >= 0 + + # retry_backoff_algo: str = "constant", + # retry_backoff_exponent: int = 2 \ No newline at end of file diff --git a/tests/TestRetriableDecorator.py b/tests/TestRetriableDecorator.py index 8cbc442..165bab9 100644 --- a/tests/TestRetriableDecorator.py +++ b/tests/TestRetriableDecorator.py @@ -1,30 +1,26 @@ import unittest -from .mocks import RetriableMock +from .mocks import OperationMock from tapipy.errors import InternalServerError +from tapipy.configuration import Config class TestRetriableDecorator(unittest.TestCase): def testRetriableCallsOnce(self): # NOTE Retriable decorator can be found on the __call__ method of the - # RetriableMock - mock = RetriableMock() + # OperationMock + mock = OperationMock() mock() assert mock.times_called == 1 assert mock.times_retried == 0 - - def testRetriableMisconfigured(self): - mock = RetriableMock(result=1337) - assert mock(_retries=0) == 1337 - self.assertRaises(ValueError, mock, _retries=1.5) # Only integers - self.assertRaises(ValueError, mock, _retries=-1) # Must be >= 0 def testRetriableRaisesSpecifedExceptionWhichOverridesDefault(self): - _ = RetriableMock( + _ = OperationMock( raises=InternalServerError, - succeeds_on_nth_retry=None # Fails forever + succeeds_on_nth_retry=None, # Fails forever + config=Config(retries=3, retry_on_exceptions=[TypeError]) ) - mock2 = RetriableMock( + mock2 = OperationMock( raises=TypeError, succeeds_on_nth_retry=None # Fails forever ) @@ -32,69 +28,74 @@ def testRetriableRaisesSpecifedExceptionWhichOverridesDefault(self): self.assertRaises( TypeError, mock2, - _retries=3, - _retry_on_exceptions=[TypeError] ) def testRetriableRaisesExceptionDifferentThanSpecifiedRetryOn(self): - mock = RetriableMock( + mock = OperationMock( raises=TypeError, - succeeds_on_nth_retry=None # Fails forever + succeeds_on_nth_retry=None, # Fails forever + config=Config(retries=3, retry_on_exceptions=[ValueError]) ) self.assertRaises( TypeError, mock, - _retries=3, - _retry_on_exceptions=[ValueError] ) assert mock.times_called == 1 assert mock.times_retried == 0 - def testRetriableRetriesUnitilRetryLimitReached(self): - mock = RetriableMock( + def testRetriableRetriesUntilRetryLimitReached(self): + mock = OperationMock( raises=InternalServerError, - succeeds_on_nth_retry=None # Fails forever + succeeds_on_nth_retry=None, # Fails forever + config=Config(retries=3, retry_on_exceptions=[InternalServerError]) ) self.assertRaises( InternalServerError, mock, - _retries=3, - _retry_on_exceptions=[InternalServerError] ) assert mock.times_called == 4 assert mock.times_retried == 3 def testRetriableSuccedsBeforeReachingRetryLimit(self): - mock = RetriableMock( + mock = OperationMock( raises=InternalServerError, succeeds_on_nth_retry=2, - result=1337 - ) - result = mock( - _retries=3, - _retry_on_exceptions=[InternalServerError] + result=1337, + config=Config(retries=3, retry_on_exceptions=[InternalServerError]) ) + result = mock() assert result == 1337 assert mock.times_called == 3 assert mock.times_retried == 2 def testRetriableSucceedsOnFirstRetry(self): - mock = RetriableMock( + mock = OperationMock( raises=InternalServerError, succeeds_on_nth_retry=1, + result=1337, + config=Config(retries=1, retry_on_exceptions=[InternalServerError]) + ) + result = mock() + assert result == 1337 + assert mock.times_called == 2 + assert mock.times_retried == 1 + + def testSucceedsWithClientConfigOverrideViaCallMethod(self): + mock = OperationMock( + raises=ValueError, + succeeds_on_nth_retry=1, result=1337 ) result = mock( - _retries=1, - _retry_on_exceptions=[InternalServerError] + _config=Config(retries=1, retry_on_exceptions=[ValueError]) ) assert result == 1337 assert mock.times_called == 2 assert mock.times_retried == 1 def testRetriableFirstArgIsSelfPlusArgsKwargsZeroRetries(self): - mock = RetriableMock(result=1337) + mock = OperationMock(result=1337) args = ("arg1", "arg2") kwargs = {"kwarg1":1, "kwarg2":2} result = mock(*args, **kwargs) @@ -103,23 +104,23 @@ def testRetriableFirstArgIsSelfPlusArgsKwargsZeroRetries(self): assert mock.times_retried == 0 # Testing args kwargs - assert isinstance(mock.args[0], RetriableMock) # Self is first arg + assert isinstance(mock.args[0], OperationMock) # Self is first arg assert "arg1" in mock.args and mock.args[1] == "arg1" assert "arg2" in mock.args and mock.args[2] == "arg2" assert "kwarg1" in mock.kwargs and mock.kwargs.get("kwarg1") == 1 assert "kwarg2" in mock.kwargs and mock.kwargs.get("kwarg2") == 2 def testRetriableFirstArgIsSelfPlusArgsKwargsAndNRetries(self): - mock = RetriableMock( + mock = OperationMock( raises=InternalServerError, succeeds_on_nth_retry=2, - result=1337 + result=1337, + config=Config(retries=2), ) args = ("arg1", "arg2") kwargs = {"kwarg1":1, "kwarg2":2} result = mock( *args, - _retries=2, **kwargs ) assert result == 1337 @@ -127,7 +128,7 @@ def testRetriableFirstArgIsSelfPlusArgsKwargsAndNRetries(self): assert mock.times_retried == 2 # Testing args kwargs - assert isinstance(mock.args[0], RetriableMock) + assert isinstance(mock.args[0], OperationMock) assert "arg1" in mock.args and mock.args[1] == "arg1" assert "arg2" in mock.args and mock.args[2] == "arg2" assert "kwarg1" in mock.kwargs and mock.kwargs.get("kwarg1") == 1 diff --git a/tests/TestUtils.py b/tests/TestUtils.py index 6d4f806..ab914ff 100644 --- a/tests/TestUtils.py +++ b/tests/TestUtils.py @@ -15,7 +15,7 @@ def testExponentialTime(self): assert exponential_time(0) == 0 assert exponential_time(1) == 2 assert exponential_time(2) == 4 - assert exponential_time(2, _retry_exponent=3) == 8 + assert exponential_time(2, retry_backoff_exponent=3) == 8 def testBackoff(self): assert backoff(0, algo="constant") == 0 diff --git a/tests/mocks.py b/tests/mocks.py index f7726a0..1a04d92 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -1,8 +1,14 @@ from tapipy.util import retriable +from tapipy.configuration import Config -class RetriableMock: - def __init__(self, raises=None, succeeds_on_nth_retry=None, result=None): +class TapisClientMock: + def __init__(self, config: Config=None): + self.config = Config() if config == None else config + +class OperationMock: + def __init__(self, raises=None, succeeds_on_nth_retry=None, result=None, config: Config=None): + self.tapis_client = TapisClientMock(config=config) self._raises = raises self._succeeds_on_nth_retry=succeeds_on_nth_retry self._result = result diff --git a/tests/run.sh b/tests/run.sh index 8e6aa08..0b5289b 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1,5 +1,6 @@ cd $(dirname $0) cd ../ +python3 -m unittest -v tests.TestConfig python3 -m unittest -v tests.TestUtils python3 -m unittest -v tests.TestRetriableDecorator \ No newline at end of file From 39187de3c85de2d68aeea8e2452822ec688057d3 Mon Sep 17 00:00:00 2001 From: Nathan Freeman Date: Thu, 12 Sep 2024 18:15:45 -0500 Subject: [PATCH 2/9] Update workflow to exit if any command fails --- tests/run.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/run.sh b/tests/run.sh index 0b5289b..a304544 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1,3 +1,8 @@ +#!/bin/bash + +# Exit if any command throws an error +set -e + cd $(dirname $0) cd ../ From 30303e6884cba291249716ab634a37b79a89438a Mon Sep 17 00:00:00 2001 From: Nathan Freeman Date: Thu, 12 Sep 2024 18:16:44 -0500 Subject: [PATCH 3/9] Update gh actions to trigger on push to main --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 09dbc69..89cfe10 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,9 +2,9 @@ name: Workflows CI on: push: - branches: [ dev, staging, test, release-** ] + branches: [ dev, staging, test, main, release-** ] pull_request: - branches: [ dev, staging, test, release-** ] + branches: [ dev, staging, test, main, release-** ] jobs: Unit_Tests: From 8835a1904ba547d35903aa31dd017df0c0ec7f55 Mon Sep 17 00:00:00 2001 From: Nathan Freeman Date: Thu, 12 Sep 2024 18:21:33 -0500 Subject: [PATCH 4/9] Update unnitest mock names to be more clear --- tests/TestRetriableDecorator.py | 84 ++++++++++++++++----------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/tests/TestRetriableDecorator.py b/tests/TestRetriableDecorator.py index 165bab9..5492d30 100644 --- a/tests/TestRetriableDecorator.py +++ b/tests/TestRetriableDecorator.py @@ -9,10 +9,10 @@ class TestRetriableDecorator(unittest.TestCase): def testRetriableCallsOnce(self): # NOTE Retriable decorator can be found on the __call__ method of the # OperationMock - mock = OperationMock() - mock() - assert mock.times_called == 1 - assert mock.times_retried == 0 + mock_op = OperationMock() + mock_op() + assert mock_op.times_called == 1 + assert mock_op.times_retried == 0 def testRetriableRaisesSpecifedExceptionWhichOverridesDefault(self): _ = OperationMock( @@ -31,7 +31,7 @@ def testRetriableRaisesSpecifedExceptionWhichOverridesDefault(self): ) def testRetriableRaisesExceptionDifferentThanSpecifiedRetryOn(self): - mock = OperationMock( + mock_op = OperationMock( raises=TypeError, succeeds_on_nth_retry=None, # Fails forever config=Config(retries=3, retry_on_exceptions=[ValueError]) @@ -39,79 +39,79 @@ def testRetriableRaisesExceptionDifferentThanSpecifiedRetryOn(self): self.assertRaises( TypeError, - mock, + mock_op, ) - assert mock.times_called == 1 - assert mock.times_retried == 0 + assert mock_op.times_called == 1 + assert mock_op.times_retried == 0 def testRetriableRetriesUntilRetryLimitReached(self): - mock = OperationMock( + mock_op = OperationMock( raises=InternalServerError, succeeds_on_nth_retry=None, # Fails forever config=Config(retries=3, retry_on_exceptions=[InternalServerError]) ) self.assertRaises( InternalServerError, - mock, + mock_op, ) - assert mock.times_called == 4 - assert mock.times_retried == 3 + assert mock_op.times_called == 4 + assert mock_op.times_retried == 3 def testRetriableSuccedsBeforeReachingRetryLimit(self): - mock = OperationMock( + mock_op = OperationMock( raises=InternalServerError, succeeds_on_nth_retry=2, result=1337, config=Config(retries=3, retry_on_exceptions=[InternalServerError]) ) - result = mock() + result = mock_op() assert result == 1337 - assert mock.times_called == 3 - assert mock.times_retried == 2 + assert mock_op.times_called == 3 + assert mock_op.times_retried == 2 def testRetriableSucceedsOnFirstRetry(self): - mock = OperationMock( + mock_op = OperationMock( raises=InternalServerError, succeeds_on_nth_retry=1, result=1337, config=Config(retries=1, retry_on_exceptions=[InternalServerError]) ) - result = mock() + result = mock_op() assert result == 1337 - assert mock.times_called == 2 - assert mock.times_retried == 1 + assert mock_op.times_called == 2 + assert mock_op.times_retried == 1 def testSucceedsWithClientConfigOverrideViaCallMethod(self): - mock = OperationMock( + mock_op = OperationMock( raises=ValueError, succeeds_on_nth_retry=1, result=1337 ) - result = mock( + result = mock_op( _config=Config(retries=1, retry_on_exceptions=[ValueError]) ) assert result == 1337 - assert mock.times_called == 2 - assert mock.times_retried == 1 + assert mock_op.times_called == 2 + assert mock_op.times_retried == 1 def testRetriableFirstArgIsSelfPlusArgsKwargsZeroRetries(self): - mock = OperationMock(result=1337) + mock_op = OperationMock(result=1337) args = ("arg1", "arg2") kwargs = {"kwarg1":1, "kwarg2":2} - result = mock(*args, **kwargs) + result = mock_op(*args, **kwargs) assert result == 1337 - assert mock.times_called == 1 - assert mock.times_retried == 0 + assert mock_op.times_called == 1 + assert mock_op.times_retried == 0 # Testing args kwargs - assert isinstance(mock.args[0], OperationMock) # Self is first arg - assert "arg1" in mock.args and mock.args[1] == "arg1" - assert "arg2" in mock.args and mock.args[2] == "arg2" - assert "kwarg1" in mock.kwargs and mock.kwargs.get("kwarg1") == 1 - assert "kwarg2" in mock.kwargs and mock.kwargs.get("kwarg2") == 2 + assert isinstance(mock_op.args[0], OperationMock) # Self is first arg + assert "arg1" in mock_op.args and mock_op.args[1] == "arg1" + assert "arg2" in mock_op.args and mock_op.args[2] == "arg2" + assert "kwarg1" in mock_op.kwargs and mock_op.kwargs.get("kwarg1") == 1 + assert "kwarg2" in mock_op.kwargs and mock_op.kwargs.get("kwarg2") == 2 def testRetriableFirstArgIsSelfPlusArgsKwargsAndNRetries(self): - mock = OperationMock( + mock_op = OperationMock( raises=InternalServerError, succeeds_on_nth_retry=2, result=1337, @@ -119,19 +119,19 @@ def testRetriableFirstArgIsSelfPlusArgsKwargsAndNRetries(self): ) args = ("arg1", "arg2") kwargs = {"kwarg1":1, "kwarg2":2} - result = mock( + result = mock_op( *args, **kwargs ) assert result == 1337 - assert mock.times_called == 3 - assert mock.times_retried == 2 + assert mock_op.times_called == 3 + assert mock_op.times_retried == 2 # Testing args kwargs - assert isinstance(mock.args[0], OperationMock) - assert "arg1" in mock.args and mock.args[1] == "arg1" - assert "arg2" in mock.args and mock.args[2] == "arg2" - assert "kwarg1" in mock.kwargs and mock.kwargs.get("kwarg1") == 1 - assert "kwarg2" in mock.kwargs and mock.kwargs.get("kwarg2") == 2 + assert isinstance(mock_op.args[0], OperationMock) + assert "arg1" in mock_op.args and mock_op.args[1] == "arg1" + assert "arg2" in mock_op.args and mock_op.args[2] == "arg2" + assert "kwarg1" in mock_op.kwargs and mock_op.kwargs.get("kwarg1") == 1 + assert "kwarg2" in mock_op.kwargs and mock_op.kwargs.get("kwarg2") == 2 From 3f047660b4c0262f8164763be9301ad0981b1771 Mon Sep 17 00:00:00 2001 From: "Christian R. Garcia" Date: Sat, 14 Sep 2024 00:20:23 -0700 Subject: [PATCH 5/9] 1.7.0 updates --- CHANGELOG.md | 12 + Dockerfile-jupyter | 10 +- Dockerfile-tests | 2 +- tapipy/__init__.py | 2 +- tapipy/resources/openapi_v3-apps.yml | 192 +- tapipy/resources/openapi_v3-globus-proxy.yml | 51 +- tapipy/resources/openapi_v3-jobs.yml | 81 +- tapipy/resources/openapi_v3-notifications.yml | 45 +- tapipy/resources/openapi_v3-pods.yml | 3892 +++++++++++------ tapipy/resources/openapi_v3-systems.yml | 226 +- tapipy/resources/resource_etags.json | 2 +- ...in-tapipy-resources-openapi_v3-pods.pickle | Bin 60738 -> 106898 bytes ...od-tapipy-resources-openapi_v3-apps.pickle | Bin 77690 -> 82025 bytes ...y-resources-openapi_v3-globus-proxy.pickle | Bin 19736 -> 20201 bytes ...od-tapipy-resources-openapi_v3-jobs.pickle | Bin 58674 -> 62672 bytes ...-resources-openapi_v3-notifications.pickle | Bin 41335 -> 42543 bytes ...tapipy-resources-openapi_v3-systems.pickle | Bin 87940 -> 91408 bytes ...pipy-resources-openapi_v3-workflows.pickle | Bin 253213 -> 338448 bytes tests/tapipy-tests.py | 4 +- 19 files changed, 3165 insertions(+), 1354 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c231ef3..f23e74d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ # Change Log All notable changes to this project will be documented in this file. + +## 1.7.0 - 2024-09-13 +### Added +- Poetry lock update + +### Changed +- Updating specs for systems, pods, jobs, apps, notifications, globus-proxy. + +### Removed +- No change. + + ## 1.6.3 - 2024-03-13 ### Added - Poetry lock update diff --git a/Dockerfile-jupyter b/Dockerfile-jupyter index 00fde26..57d3d19 100644 --- a/Dockerfile-jupyter +++ b/Dockerfile-jupyter @@ -6,11 +6,11 @@ # optionally mount a directory for persistent data: # docker run --rm -it -p 8888:8888 -v $(pwd)/notebook-data:/home/jovyan/data tapis/jupyter -from python:3.8 -add requirements-jupyter.txt /requirements.txt -run pip install --upgrade pip -run pip install --upgrade setuptools -run pip install -r /requirements.txt +FROM python:3.8 +ADD requirements-jupyter.txt /requirements.txt +RUN pip install --upgrade pip +RUN pip install --upgrade setuptools +RUN pip install -r /requirements.txt ENTRYPOINT ["jupyter-notebook", "--ip", "0.0.0.0", "--allow-root"] ENV JUPYTER_ENABLE_LAB yes WORKDIR "/home" diff --git a/Dockerfile-tests b/Dockerfile-tests index 78849b4..9ec8f3a 100644 --- a/Dockerfile-tests +++ b/Dockerfile-tests @@ -7,7 +7,7 @@ # docker run -it --rm -v $(pwd)/test/resource_examples:/home/tapis/resource_examples tapis/tapipy-tests bash # docker run -it --rm --network tenants-api_tenants tapis/tapipy-tests bash -from python:3.7 +FROM python:3.7 # Upgrade pip RUN python -m pip install --upgrade pip diff --git a/tapipy/__init__.py b/tapipy/__init__.py index 4574cc8..0e1a38d 100644 --- a/tapipy/__init__.py +++ b/tapipy/__init__.py @@ -1 +1 @@ -__version__ = '1.6.3' +__version__ = '1.7.0' diff --git a/tapipy/resources/openapi_v3-apps.yml b/tapipy/resources/openapi_v3-apps.yml index e8e10c6..13ecbdb 100644 --- a/tapipy/resources/openapi_v3-apps.yml +++ b/tapipy/resources/openapi_v3-apps.yml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: Tapis Applications API description: The Tapis Applications API provides for management of Tapis applications including permissions. - version: '1.6.0' + version: '1.6.2' termsOfService: "https://tapis-project.org" contact: name: "Applications API - CICSupport" @@ -21,8 +21,6 @@ servers: - url: 'https://dev.develop.tapis.io/' description: Development environment variables: {} -security: - - TapisJWT: [] tags: - name: General description: General service health and readiness @@ -90,7 +88,9 @@ paths: tags: - Applications description: | - Retrieve list of applications. Use *listType*, *search* and *select* query parameters to limit results. Query + Retrieve list of applications. + + Use *listType*, *search* and *select* query parameters to limit results. Query parameter *listType* allows for filtering results based on authorization. Options for *listType* are - *OWNED* Include only items owned by requester (Default) @@ -100,6 +100,8 @@ paths: - *MINE* Include items owned or shared directly with requester. Exclude publicly shared items. - *ALL* Include all items requester is authorized to view. Includes check for READ or MODIFY permission. operationId: getApps + security: + - TapisJWT: [] parameters: - name: search in: query @@ -183,11 +185,15 @@ paths: - tenant - uuid + - isPublic + - sharedWithUsers - sharedAppCtx - deleted - created - updated operationId: createAppVersion + security: + - TapisJWT: [] requestBody: required: true description: A JSON object specifying information for the app to be created. @@ -243,6 +249,8 @@ paths: - *MINE* Include items owned or shared directly with requester. Exclude publicly shared items. - *ALL* Include all items requester is authorized to view. Includes check for READ or MODIFY permission. operationId: searchAppsQueryParameters + security: + - TapisJWT: [] parameters: - name: listType in: query @@ -316,6 +324,8 @@ paths: - *MINE* Include items owned or shared directly with requester. Exclude publicly shared items. - *ALL* Include all items requester is authorized to view. Includes check for READ or MODIFY permission. operationId: searchAppsRequestBody + security: + - TapisJWT: [] parameters: - name: listType in: query @@ -390,6 +400,8 @@ paths: application is available to the user because it has been shared with the user. The value of *sharedAppCtx* will be the grantor, the Tapis user who shared the application. operationId: getAppLatestVersion + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -455,6 +467,8 @@ paths: Certain services may use the query parameter *impersonationId* to be used in place of the requesting Tapis user. Tapis will use this user Id when performing authorization. operationId: getApp + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -531,10 +545,14 @@ paths: - id - owner - enabled + - versionEnabled - locked + - deleted - Note that the attributes owner, enabled and locked may be modified using other endpoints. + Note that the attributes owner, enabled, versionEnabled, locked and deleted may be modified using other endpoints. operationId: patchApp + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -597,21 +615,26 @@ paths: - tenant - uuid + - isPublic + - sharedWithUsers - sharedAppCtx - deleted - created - updated - In addition for a PUT operation the following non-updatable attributes are allowed but ignored + In addition, for a PUT operation, the following non-updatable attributes are allowed but ignored - id - version - owner - enabled + - versionEnabled - locked - Note that the attributes owner, enabled and locked may be modified using other endpoints. + Note that the attributes owner, enabled, versionEnabled, locked and deleted may be modified using other endpoints. operationId: putApp + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -666,14 +689,23 @@ paths: tags: - Applications description: | - Check if an application is currently enabled, i.e. available for use. + Check if an application is currently enabled, i.e. available for use. If the query parameter *version* + is specified then both the top level attribute *enabled* and the version specific attribute *versionEnabled* + are checked. Both must be set to *true* for the application to be considered *enabled*. operationId: isEnabled + security: + - TapisJWT: [] parameters: - name: appId in: path required: true schema: $ref: '#/components/schemas/IdString' + - name: version + in: query + description: Include specified application version in the check. + schema: + $ref: '#/components/schemas/AppVersionString' responses: '200': description: Check successful. @@ -705,7 +737,10 @@ paths: - Applications description: | Mark an application available for use. Applies to all versions. + Requester must be owner of the app or a tenant administrator. operationId: enableApp + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -743,13 +778,108 @@ paths: - Applications description: | Mark an application unavailable for use. Applies to all versions. + Requester must be owner of the app or a tenant administrator. operationId: disableApp + security: + - TapisJWT: [] + parameters: + - name: appId + in: path + required: true + schema: + $ref: '#/components/schemas/IdString' + responses: + '200': + description: Application disabled. + content: + application/json: + schema: + $ref: '#/components/schemas/RespChangeCount' + '403': + description: Permission denied. + content: + application/json: + schema: + $ref: '#/components/schemas/RespBasic' + '404': + description: Application not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RespBasic' + '500': + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RespBasic' + '/v3/apps/{appId}/{appVersion}/enable': + post: + tags: + - Applications + description: | + Mark a specific version of an application available for use. + Requester must be owner of the app or a tenant administrator. + operationId: enableAppVersion + security: + - TapisJWT: [] + parameters: + - name: appId + in: path + required: true + schema: + $ref: '#/components/schemas/IdString' + - name: appVersion + in: path + required: true + schema: + $ref: '#/components/schemas/AppVersionString' + responses: + '200': + description: Application version enabled. + content: + application/json: + schema: + $ref: '#/components/schemas/RespChangeCount' + '403': + description: Permission denied. + content: + application/json: + schema: + $ref: '#/components/schemas/RespBasic' + '404': + description: Application not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RespBasic' + '500': + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RespBasic' + '/v3/apps/{appId}/{appVersion}/disable': + post: + tags: + - Applications + description: | + Mark a specific version of an application unavailable for use. + Requester must be owner of the app or a tenant administrator. + operationId: disableAppVersion + security: + - TapisJWT: [] parameters: - name: appId in: path required: true schema: $ref: '#/components/schemas/IdString' + - name: appVersion + in: path + required: true + schema: + $ref: '#/components/schemas/AppVersionString' responses: '200': description: Application disabled. @@ -783,6 +913,8 @@ paths: Lock a version of an application to prevent updates via PUT or PATCH. Requester must be owner of the app or a tenant administrator. operationId: lockApp + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -827,6 +959,8 @@ paths: Unlock a version of an application to allow updates via PUT and PATCH. Requester must be owner of the app or a tenant administrator. operationId: unlockApp + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -870,6 +1004,8 @@ paths: description: | Mark an application as deleted. Application will not appear in queries unless explicitly requested. operationId: deleteApp + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -908,6 +1044,8 @@ paths: description: | Mark an application as not deleted. Application will appear in queries. operationId: undeleteApp + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -946,6 +1084,8 @@ paths: description: | Change owner of an application. Applies to all versions. operationId: changeAppOwner + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -989,6 +1129,8 @@ paths: description: | Retrieve history of changes for a given appId. operationId: getHistory + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -1029,6 +1171,8 @@ paths: description: | Retrieve all application related permissions for a given application and user. operationId: getUserPerms + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -1077,6 +1221,8 @@ paths: description: | Create permissions in the Security Kernel for a user. Requester must be owner. Permissions are READ, MODIFY, EXECUTE. operationId: grantUserPerms + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -1128,6 +1274,8 @@ paths: description: | Remove permissions from the Security Kernel for a user. Requester must be owner. Permissions are READ, MODIFY, EXECUTE. operationId: revokeUserPerms + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -1173,6 +1321,8 @@ paths: description: | Remove user permission from the Security Kernel. Requester must be owner. Permissions are READ, MODIFY, EXECUTE. operationId: revokeUserPerm + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -1225,6 +1375,8 @@ paths: Retrieve all sharing information for an app. This includes all users with whom the app has been shared and whether or not the app has been made publicly available. operationId: getShareInfo + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -1270,6 +1422,8 @@ paths: Create or update sharing information for an app. The app will be shared with the list of users provided in the request body. Requester must be owner of the app. operationId: shareApp + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -1317,6 +1471,8 @@ paths: description: | Share an app with all users in the tenant. Requester must be owner of the app. operationId: shareAppPublic + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -1352,6 +1508,8 @@ paths: Create or update sharing information for an app. The app will be unshared with the list of users provided in the request body. Requester must be owner of the app. operationId: unShareApp + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -1399,6 +1557,8 @@ paths: description: | Remove public sharing for an app. Requester must be owner of the app. operationId: unShareAppPublic + security: + - TapisJWT: [] parameters: - name: appId in: path @@ -1456,6 +1616,9 @@ components: enabled: type: boolean default: true + versionEnabled: + type: boolean + default: true locked: type: boolean default: false @@ -1531,6 +1694,10 @@ components: $ref: '#/components/schemas/DirString' execSystemOutputDir: $ref: '#/components/schemas/DirString' + dtnSystemInputDir: + $ref: '#/components/schemas/DirString' + dtnSystemOutputDir: + $ref: '#/components/schemas/DirString' execSystemLogicalQueue: $ref: '#/components/schemas/QueueNameString' archiveSystemId: @@ -1636,6 +1803,9 @@ components: $ref: '#/components/schemas/ArgDescriptionString' inputMode: $ref: '#/components/schemas/FileInputModeEnum' + envKey: + type: string + minLength: 1 autoMountLocal: type: boolean default: true @@ -1660,6 +1830,9 @@ components: $ref: '#/components/schemas/ArgDescriptionString' inputMode: $ref: '#/components/schemas/FileInputModeEnum' + envKey: + type: string + minLength: 1 notes: type: object sourceUrls: @@ -1832,6 +2005,9 @@ components: enabled: type: boolean default: true + versionEnabled: + type: boolean + default: true locked: type: boolean default: false diff --git a/tapipy/resources/openapi_v3-globus-proxy.yml b/tapipy/resources/openapi_v3-globus-proxy.yml index a2053ae..71f3267 100644 --- a/tapipy/resources/openapi_v3-globus-proxy.yml +++ b/tapipy/resources/openapi_v3-globus-proxy.yml @@ -56,7 +56,7 @@ paths: description: Server error. # --- Paths for Auth ----------------------------------------------------- - '/v3/globus-proxy/auth/url/{client_id}': + '/v3/globus-proxy/auth/url/{client_id}/{endpoint_id}': get: tags: - Auth @@ -72,6 +72,12 @@ paths: description: Globus client associated with the request. schema: $ref: '#/components/schemas/ClientIdString' + - name: endpoint_id + in: path + required: true + description: Globus endpoint associated with the request. + schema: + $ref: '#/components/schemas/EndpointIdString' responses: '200': description: Success. @@ -129,6 +135,12 @@ paths: application/json: schema: $ref: '#/components/schemas/RespAuthTokens' + '407': + description: Consent required. + content: + application/json: + schema: + $ref: '#/components/schemas/RespBasic' '500': description: Server error. content: @@ -503,6 +515,12 @@ paths: application/json: schema: $ref: '#/components/schemas/RespEndpointArray' + '409': + description: Directory already exists. + content: + application/json: + schema: + $ref: '#/components/schemas/RespBasic' '500': description: Server error. content: @@ -727,6 +745,11 @@ components: # } # } # ============================================================================================ + GlobusConsentInfo: + type: object + properties: + scopes: + type: string # --- GlobusFileInfo ------------------------------------------------------------- GlobusFileInfo: type: object @@ -747,6 +770,7 @@ components: type: string size: type: integer + format: int64 description: size in bytes # --- GlobusTransferItem ------------------------------------------------------------- GlobusTransferItem: @@ -823,7 +847,7 @@ components: type: string example: "ddb59aef-6d04-11e5-ba46-22000b92c6ec" status: - $ref: '#/components/schemas/GlobusTaskStatusEnum' + type: string symlinks: type: integer sync_level: @@ -832,7 +856,7 @@ components: type: string example: "36ccfc18-8493-11ec-9008-3132d806a822" type: - $ref: '#/components/schemas/GlobusTaskTypeEnum' + type: string verify_checksum: type: boolean # TRANSFER TASK @@ -964,6 +988,10 @@ components: type: string example: "1784148a-8ae0-44b7-80b5-b5999e92de3a" minLength: 1 + EndpointIdString: + type: string + example: "1784148a-8ae0-44b7-80b5-b5999e92de3a" + minLength: 1 AuthCodeString: type: string example: "T0aymuUlUyLaOvvR58xxDCzycq5Cd3" @@ -1074,6 +1102,19 @@ components: $ref: '#/components/schemas/AuthTokens' metadata: type: object + RespConsentRequired: + type: object + properties: + status: + type: string + message: + type: string + version: + type: string + result: + $ref: '#/components/schemas/GlobusConsentInfo' + metadata: + type: object RespEndpointArray: type: object properties: @@ -1176,6 +1217,8 @@ components: enum: - TRANSFER - DELETE + - NULL + - UNKNOWN GlobusTaskStatusEnum: type: string enum: @@ -1183,6 +1226,8 @@ components: - INACTIVE - SUCCEEDED - FAILED + - NULL + - UNKNOWN GlobusTaskCancelEnum: type: string enum: diff --git a/tapipy/resources/openapi_v3-jobs.yml b/tapipy/resources/openapi_v3-jobs.yml index 6d072a9..5a1feda 100644 --- a/tapipy/resources/openapi_v3-jobs.yml +++ b/tapipy/resources/openapi_v3-jobs.yml @@ -1637,6 +1637,34 @@ components: - FINISHED - CANCELLED - FAILED + condition: + type: string + enum: + - CANCELLED_BY_USER + - JOB_ARCHIVING_FAILED + - JOB_DATABASE_ERROR + - JOB_EXECUTION_MONITORING_ERROR + - JOB_EXECUTION_MONITORING_TIMEOUT + - JOB_FILES_SERVICE_ERROR + - JOB_INTERNAL_ERROR + - JOB_INVALID_DEFINITION + - JOB_LAUNCH_FAILURE + - JOB_QUEUE_MONITORING_ERROR + - JOB_RECOVERY_FAILURE + - JOB_RECOVERY_TIMEOUT + - JOB_REMOTE_ACCESS_ERROR + - JOB_REMOTE_OUTCOME_ERROR + - JOB_UNABLE_TO_STAGE_INPUTS + - JOB_UNABLE_TO_STAGE_JOB + - JOB_TRANSFER_FAILED_OR_CANCELLED + - JOB_TRANSFER_MONITORING_TIMEOUT + - NORMAL_COMPLETION + - SCHEDULER_CANCELLED + - SCHEDULER_DEADLINE + - SCHEDULER_OUT_OF_MEMORY + - SCHEDULER_STOPPED + - SCHEDULER_TIMEOUT + - SCHEDULER_TERMINATED lastMessage: type: string created: @@ -1671,9 +1699,9 @@ components: type: string dtnSystemId: type: string - dtnMountSourcePath: + dtnSystemInputDir: type: string - dtnMountPoint: + dtnSystemOutputDir: type: string nodeCount: type: integer @@ -1741,6 +1769,14 @@ components: type: string stageAppCorrelationId: type: string + dtnInputTransactionId: + type: string + dtnInputCorrelationId: + type: string + dtnOutputTransactionId: + type: string + dtnOutputCorrelationId: + type: string tapisQueue: type: string visible: @@ -1775,6 +1811,9 @@ components: - SAC_EXEC_SYSTEM_OUTPUT_DIR - SAC_ARCHIVE_SYSTEM_ID - SAC_ARCHIVE_SYSTEM_DIR + - SAC_DTN_SYSTEM_ID + - SAC_DTN_SYSTEM_INPUT_DIR + - SAC_DTN_SYSTEM_OUTPUT_DIR notes: type: string mpi: @@ -1859,6 +1898,34 @@ components: - FINISHED - CANCELLED - FAILED + condition: + type: string + enum: + - CANCELLED_BY_USER + - JOB_ARCHIVING_FAILED + - JOB_DATABASE_ERROR + - JOB_EXECUTION_MONITORING_ERROR + - JOB_EXECUTION_MONITORING_TIMEOUT + - JOB_FILES_SERVICE_ERROR + - JOB_INTERNAL_ERROR + - JOB_INVALID_DEFINITION + - JOB_LAUNCH_FAILURE + - JOB_QUEUE_MONITORING_ERROR + - JOB_RECOVERY_FAILURE + - JOB_RECOVERY_TIMEOUT + - JOB_REMOTE_ACCESS_ERROR + - JOB_REMOTE_OUTCOME_ERROR + - JOB_UNABLE_TO_STAGE_INPUTS + - JOB_UNABLE_TO_STAGE_JOB + - JOB_TRANSFER_FAILED_OR_CANCELLED + - JOB_TRANSFER_MONITORING_TIMEOUT + - NORMAL_COMPLETION + - SCHEDULER_CANCELLED + - SCHEDULER_DEADLINE + - SCHEDULER_OUT_OF_MEMORY + - SCHEDULER_STOPPED + - SCHEDULER_TIMEOUT + - SCHEDULER_TERMINATED remoteStarted: type: string format: date-time @@ -2068,6 +2135,8 @@ components: properties: status: type: string + condition: + type: string RespGetJobStatus: type: object properties: @@ -2118,6 +2187,8 @@ components: type: string description: type: string + envKey: + type: string autoMountLocal: type: boolean sourceUrl: @@ -2133,6 +2204,8 @@ components: type: string description: type: string + envKey: + type: string sourceUrls: type: array items: @@ -2235,6 +2308,10 @@ components: type: string archiveSystemDir: type: string + dtnSystemInputDir: + type: string + dtnSystemOutputDir: + type: string nodeCount: type: integer format: int32 diff --git a/tapipy/resources/openapi_v3-notifications.yml b/tapipy/resources/openapi_v3-notifications.yml index a23be9c..a1c96a7 100644 --- a/tapipy/resources/openapi_v3-notifications.yml +++ b/tapipy/resources/openapi_v3-notifications.yml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: Tapis Notifications API description: The Tapis Notifications API provides for management of subscriptions and event publication - version: '1.6.0' + version: '1.6.1' termsOfService: "https://tapis-project.org" contact: name: "Notifications API - CICSupport" @@ -395,7 +395,7 @@ paths: - name: Short descriptive name. - description: Optional more verbose description. - enabled: Indicates if subscription is currently active. - - typeFilter: Filter to use when matching events. Format is *..*. Each field may be a specific type or the wildcard character *. + - typeFilter: Filter to use when matching events. Format is *service.category.detail*. Each field may be a specific type or the wildcard character *. - subjectFilter: Filter to use when matching events. Filter for subject. May be wildcard character *. - deliveryTargets: List of delivery targets to be used when delivering a matching event. - ttlMinutes: Time to live in minutes. 0 indicates no expiration. @@ -578,7 +578,7 @@ paths: - name: Short descriptive name. - description: Optional more verbose description. - enabled: Indicates if subscription is currently active. - - typeFilter: Filter to use when matching events. Format is *..*. Each field may be a specific type or the wildcard character *. + - typeFilter: Filter to use when matching events. Format is *service.category.detail*. Each field may be a specific type or the wildcard character *. - subjectFilter: Filter to use when matching events. Filter for subject. May be wildcard character *. - deliveryTargets: List of delivery targets to be used when delivering a matching event. - ttlMinutes: Time to live in minutes. 0 indicates no expiration. @@ -918,14 +918,21 @@ paths: description: | Publish an event to be distributed to all subscribers. Only services may publish events. - The attributes *source*, *type* and *timestamp* are required. + The attributes *source*, *type* and *timestamp* are required. Note that certain attributes, marked in + *italics* in the list below, are allowed but ignored. These attributes are maintained by Tapis. They are + present when the event is part of a delivered notification. + Event attributes: - source: Context in which event happened: Examples: *Jobs*, *Systems*. - type: Type of event. Used for routing notifications. A series of 3 fields separated by the dot character. Pattern is *service.category.detail*. Examples: *jobs.new_status.complete*, *systems.system.create*, *files.object.delete* - subject: Subject of event in the context of the service. Examples: job Id, system Id, file path, role name, etc. - - seriesId: Optional Id that may be used to group events from the same source in a series, thereby preserving event order during notification delivery. - timestamp: When the event happened. - data: Optional additional information associated with the event. Data specific to the service associated with the event. + - deleteSubscriptionsMatchingSubject: Boolean indicating that all subscriptions whose *subjectFilter* matches the *subject* of the event should be deleted once all notifications are delivered. + - seriesId: Optional Id that may be used to group events from the same tenant, source and subject. In a series, event order is preserved when sending out notifications. + - *tenant*: Tapis tenant associated with the event. + - *uuid*: Tapis generated unique identifier. + - *user*: Tapis user associated with the event. Note that events are not persisted by the front end api service. When received they are simply sent to a message broker. The back end dispatch service will persist events temporarily in order to support recovery. @@ -941,6 +948,14 @@ paths: For details on the schema for a Notification object, please see the request body specification included under the endpoint for *recordTestNotification*, at path */v3/notifications/test/callback/{name}* + + Note that certain attributes in the request body (such as tenant) are allowed but ignored. These attributes + are maintained by Tapis. They are present when the event is part of a delivered notification. + The attributes that are allowed but ignored are + + - tenant + - uuid + - user operationId: postEvent parameters: - name: tenant @@ -990,21 +1005,22 @@ paths: Only services may perform this operation. The subscription will have the following properties: - - owner: - - name: + - owner: *api_user* + - name: *subscription_uuid* - typeFilter: notifications.test.* - - subjectFilter: + - subjectFilter: *subscription_uuid* - deliveryTarget: - deliveryMethod: WEBHOOK - - deliveryAddress: /v3/notifications/test/callback/ + - deliveryAddress: *tenant_base_url*/v3/notifications/test/callback/*subscription_uuid* - ttlMinutes: 60 The default TTL of 1 hour may be overridden using the query parameter *subscriptionTTL*. The initial event will have the following properties: - - source: /v3/notifications/test + - source: *tenant_base_url*/v3/notifications/test - type: notifications.test.begin - - subject: + - subject: *subscription_uuid* + - seriesId: *subscription_uuid* The initial event will be published to the main queue and the subscription will be returned to the caller. The sequence of test events may be continued by publishing events that match the test subscription. @@ -1256,6 +1272,13 @@ components: type: string deleteSubscriptionsMatchingSubject: type: boolean + default: false + tenant: + type: string + user: + type: string + uuid: + type: string # --- Notification ------------------------------------------------------------ Notification: type: object diff --git a/tapipy/resources/openapi_v3-pods.yml b/tapipy/resources/openapi_v3-pods.yml index da6d18a..ce06400 100644 --- a/tapipy/resources/openapi_v3-pods.yml +++ b/tapipy/resources/openapi_v3-pods.yml @@ -1,259 +1,258 @@ -openapi: 3.0.2 +openapi: 3.1.0 info: title: Tapis Pods Service - description: > - - The Pods Service is a web service and distributed computing platform - providing pods-as-a-service (PaaS). The service - - implements a message broker and processor model that requests pods, - alongside a health module to poll for pod - - data, including logs, status, and health. The primary use of this service is - to have quick to deploy long-lived - - services based on Docker images that are exposed via HTTP or TCP endpoints - listed by the API. - - - **The Pods service provides functionality for two types of pod solutions:** - * **Templated Pods** for run-as-is popular images. Neo4J is one example, the template manages TCP ports, user creation, and permissions. - * **Custom Pods** for arbitrary docker images with less functionality. In this case we will expose port 5000 and do nothing else. + description: "\nThe Pods Service is a web service and distributed computing platform\ + \ providing pods-as-a-service (PaaS). The service \nimplements a message broker\ + \ and processor model that requests pods, alongside a health module to poll for\ + \ pod\ndata, including logs, status, and health. The primary use of this service\ + \ is to have quick to deploy long-lived\nservices based on Docker images that\ + \ are exposed via HTTP or TCP endpoints listed by the API.\n\n**The Pods service\ + \ provides functionality for two types of pod solutions:**\n * **Templated Pods**\ + \ for run-as-is popular images. Neo4J is one example, the template manages TCP\ + \ ports, user creation, and permissions.\n * **Custom Pods** for arbitrary docker\ + \ images with less functionality. In this case we will expose port 5000 and do\ + \ nothing else.\n\n The live-docs act as the most up-to-date API reference. Visit\ + \ the [documentation for more information](https://tapis.readthedocs.io/en/latest/technical/pods.html).\n" contact: name: CIC Support + url: https://tapis-project.org email: cicsupport@tacc.utexas.edu license: name: BSD 3.0 - url: 'https://github.com/tapis-project/pods_service' - version: '0.30' + url: https://github.com/tapis-project/pods_service + version: 1.6.0 paths: - /pods/snapshots: + /traefik-config: get: tags: - - Snapshots - summary: get_snapshots - description: >- - Get all snapshots in your respective tenant and site that you have READ - or higher access to. - - - Returns a list of snapshots. - operationId: get_snapshots + - Misc + summary: traefik_config + description: "Supplies traefik-config to service. Returns json traefik-config\ + \ object for\ntraefik to use with the http provider. Dynamic configs don't\ + \ work well in \nKubernetes." + operationId: traefik_config responses: '200': description: Successful Response content: application/json: - schema: - $ref: '#/components/schemas/SnapshotsResponse' - post: + schema: {} + /healthcheck: + get: tags: - - Snapshots - summary: create_snapshot - description: |- - Create a snapshot with inputted information. - - Notes: - - Author will be given ADMIN level permissions to the snapshot. + - Misc + summary: healthcheck + description: 'Health check for service. Returns healthy when api is running. - Returns new snapshot object. - operationId: create_snapshot - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/NewSnapshot' - required: true + Should add database health check, should add kubernetes health check' + operationId: healthcheck responses: '200': description: Successful Response content: application/json: - schema: - $ref: '#/components/schemas/SnapshotResponse' + schema: {} + /error-handler/{status}: + get: + tags: + - Misc + summary: error_handler + description: Handles all error codes from Traefik. + operationId: error_handler + parameters: + - required: true + schema: + title: Status + name: status + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/snapshots/{snapshot_id}': + /pods/auth: get: tags: - - Snapshots - summary: get_snapshot - description: |- - Get a snapshot. - - Returns retrieved snapshot object. - operationId: get_snapshot + - Misc + summary: OAuth2 endpoint to act as middleware between pods and user traffic, + checking for authorization on a per url basis. + description: "Write to session\n\nTraefik continues to user pod if 200, otherwise\ + \ goes to result.\nProcess a callback from a Tapis authorization server:\n\ + \ 1) Get the authorization code from the query parameters.\n 2) Exchange\ + \ the code for a token\n 3) Add the user and token to the sessionhttps\n\ + \ 4) Redirect to the /data endpoint." + operationId: auth parameters: - - required: true - schema: - title: Snapshot Id - name: snapshot_id - in: path + - required: false + schema: + type: string + title: Username + name: username + in: query responses: '200': description: Successful Response content: application/json: - schema: - $ref: '#/components/schemas/SnapshotResponse' + schema: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - put: + /pods/auth/callback: + get: tags: - - Snapshots - summary: update_snapshot - description: >- - Update a snapshot. - - - Note: - - - Fields that change snapshot source or sink are not modifiable. Please - recreate your snapshot in that case. - - - Returns updated snapshot object. - operationId: update_snapshot - parameters: - - required: true - schema: - title: Snapshot Id - name: snapshot_id - in: path - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateSnapshot' - required: true + - Misc + summary: callback. + operationId: auth responses: '200': description: Successful Response content: application/json: - schema: - $ref: '#/components/schemas/SnapshotResponse' - '422': - description: Validation Error + schema: {} + /pods/templates: + get: + tags: + - Templates + summary: list_templates + description: 'Get all templates allowed globally + in respective tenant + for + specific user. + + Returns a list of templates.' + operationId: list_templates + responses: + '200': + description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/HTTPValidationError' - delete: + $ref: '#/components/schemas/TemplatesResponse' + post: tags: - - Snapshots - summary: delete_snapshot - description: |- - Delete a snapshot. + - Templates + summary: add_template + description: 'Add a template with inputted information. - Returns "". - operationId: delete_snapshot - parameters: - - required: true - schema: - title: Snapshot Id - name: snapshot_id - in: path + + Returns new template object.' + operationId: add_template + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Template' + required: true responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/DeleteSnapshotResponse' + $ref: '#/components/schemas/TemplateResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/snapshots/{snapshot_id}/list': + /pods/templates/tags: get: tags: - - Snapshots - summary: list_snapshot_files - description: List files in snapshot. - operationId: list_snapshot_files + - Templates + summary: list_templates_and_tags + description: 'Get all templates and their tags for the user. + + Returns a dictionary with templates and their tags.' + operationId: list_templates_and_tags parameters: - - required: true - schema: - title: Snapshot Id - name: snapshot_id - in: path + - description: Returns tag pod_definition with tag when full=true + required: false + schema: + type: boolean + title: Full + description: Returns tag pod_definition with tag when full=true + default: true + name: full + in: query responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/FilesListResponse' + type: object + title: Response List Templates And Tags '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/snapshots/{snapshot_id}/permissions': + /pods/templates/{template_id}: get: tags: - - Permissions - summary: get_snapshot_permissions - description: |- - Get a snapshots permissions. + - Templates + summary: get_template + description: 'Get a templates. - Note: - - There are 3 levels of permissions, READ, USER, and ADMIN. - - Permissions are granted/revoked to individual TACC usernames. - Returns all volue permissions. - operationId: get_snapshot_permissions + Returns retrieved templates object.' + operationId: get_template parameters: - - required: true - schema: - title: Snapshot Id - name: snapshot_id - in: path + - required: true + schema: + title: Template Id + name: template_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/SnapshotPermissionsResponse' + $ref: '#/components/schemas/TemplateResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - post: + put: tags: - - Permissions - summary: set_snapshot_permission - description: |- - Set a permission for a snapshot. + - Templates + summary: update_template + description: 'Update a template. - Returns updated snapshot permissions. - operationId: set_snapshot_permission + + Note: + + - Fields that change template id cannot be modified. Please recreate your + template in that case. + + + Returns updated template object.' + operationId: update_template parameters: - - required: true - schema: - title: Snapshot Id - name: snapshot_id - in: path + - required: true + schema: + title: Template Id + name: template_id + in: path requestBody: content: application/json: schema: - $ref: '#/components/schemas/SetPermission' + $ref: '#/components/schemas/UpdateTemplate' required: true responses: '200': @@ -261,89 +260,97 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SnapshotPermissionsResponse' + $ref: '#/components/schemas/TemplateResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/snapshots/{snapshot_id}/permissions/{user}': delete: tags: - - Permissions - summary: delete_snapshot_permission - description: |- - Delete a permission from a snapshot. + - Templates + summary: delete_template + description: 'Delete a template. - Returns updated snapshot permissions. - operationId: delete_snapshot_permission + + Returns "".' + operationId: delete_template parameters: - - required: true - schema: - title: Snapshot Id - name: snapshot_id - in: path - - required: true - schema: - title: User - name: user - in: path + - required: true + schema: + title: Template Id + name: template_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/SnapshotPermissionsResponse' + $ref: '#/components/schemas/TemplateDeleteResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /pods/volumes: + /pods/templates/{template_id}/tags: get: tags: - - Volumes - summary: get_volumes - description: >- - Get all volumes in your respective tenant and site that you have READ or - higher access to. + - Templates + summary: list_template_tags + description: 'List tag entries the template has - Returns a list of volumes. - operationId: get_volumes + Returns the ledger of template tags' + operationId: list_template_tags + parameters: + - required: true + schema: + type: string + title: Template Id + name: template_id + in: path + - description: Return pod_definition in tag when full=true + required: false + schema: + type: boolean + title: Full + description: Return pod_definition in tag when full=true + default: true + name: full + in: query responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/VolumesResponse' + $ref: '#/components/schemas/TemplateTagsResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' post: tags: - - Volumes - summary: create_volume - description: >- - Create a volume with inputted information. - - - Notes: - - - Author will be given ADMIN level permissions to the volume. - - - status_requested defaults to "ON". So volume will immediately begin - creation. - - - Returns new volume object. - operationId: create_volume + - Templates + summary: add_template_tag + operationId: add_template_tag + parameters: + - required: true + schema: + type: string + title: Template Id + name: template_id + in: path requestBody: content: application/json: schema: - $ref: '#/components/schemas/NewVolume' + $ref: '#/components/schemas/NewTemplateTag' required: true responses: '200': @@ -351,317 +358,294 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/VolumeResponse' + $ref: '#/components/schemas/TemplateTagResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/volumes/{volume_id}': + /pods/templates/{template_id}/tags/{tag_id}: get: tags: - - Volumes - summary: get_volume - description: |- - Get a volume. + - Templates + summary: get_template_tag + description: 'Get a specific tag entry the template has - Returns retrieved volume object. - operationId: get_volume + + Returns the tag entry' + operationId: get_template_tag parameters: - - required: true - schema: - title: Volume Id - name: volume_id - in: path + - required: true + schema: + type: string + title: Template Id + name: template_id + in: path + - required: true + schema: + type: string + title: Tag Id + name: tag_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/VolumeResponse' + $ref: '#/components/schemas/TemplateTagsResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - put: + /pods/templates/{template_id}/permissions: + get: tags: - - Volumes - summary: update_volume - description: >- - Update a volume. + - Templates + summary: get_template_permissions + description: 'Get a templates permissions. Note: - - Fields that change volume source or sink are not modifiable. Please - recreate your volume in that case. + - There are 3 levels of permissions, READ, USER, and ADMIN. + + - Permissions are granted/revoked to individual TACC usernames. + + - Permissions can be set for TENANT or SITE keys for tenant-level or site-level + sharing. - Returns updated volume object. - operationId: update_volume + Returns all template permissions.' + operationId: get_template_permissions parameters: - - required: true - schema: - title: Volume Id - name: volume_id - in: path - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateVolume' - required: true + - required: true + schema: + title: Template Id + name: template_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/VolumeResponse' + $ref: '#/components/schemas/TemplatePermissionsResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - delete: + post: tags: - - Volumes - summary: delete_volume - description: |- - Delete a volume. + - Templates + summary: set_template_permission + description: 'Set a permission for a template. - Returns "". - operationId: delete_volume + + Returns updated template permissions.' + operationId: set_template_permission parameters: - - required: true - schema: - title: Volume Id - name: volume_id - in: path + - required: true + schema: + title: Template Id + name: template_id + in: path + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SetPermission' + required: true responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/DeleteVolumeResponse' + $ref: '#/components/schemas/TemplatePermissionsResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/volumes/{volume_id}/list': - get: + /pods/templates/{template_id}/permissions/{user}: + delete: tags: - - Volumes - summary: list_volume_files - description: List files in volume. - operationId: list_volume_files + - Templates + summary: delete_template_permission + description: 'Delete a permission from a template. + + + Returns updated template permissions.' + operationId: delete_template_permission parameters: - - required: true - schema: - title: Volume Id - name: volume_id - in: path + - required: true + schema: + title: Template Id + name: template_id + in: path + - required: true + schema: + title: User + name: user + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/FilesListResponse' + $ref: '#/components/schemas/TemplatePermissionsResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/volumes/{volume_id}/upload/{path}': - post: + /pods/images: + get: tags: - - Volumes - summary: upload_to_volume - description: Upload to volume. - operationId: upload_to_volume - parameters: - - required: true - schema: - title: Volume Id - name: volume_id - in: path - - required: true - schema: - title: Path - name: path - in: path - requestBody: - content: - multipart/form-data: - schema: - $ref: '#/components/schemas/Body_upload_to_volume' - required: true + - Images + summary: get_images + description: 'Get all images allowed globally + in respective tenant. + + Returns a list of images.' + operationId: get_images responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/FilesUploadResponse' - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - '/pods/volumes/{volume_id}/permissions': - get: + $ref: '#/components/schemas/ImagesResponse' + post: tags: - - Permissions - summary: get_volume_permissions - description: |- - Get a volumes permissions. + - Images + summary: add_image + description: 'Add a image with inputted information. - Note: - - There are 3 levels of permissions, READ, USER, and ADMIN. - - Permissions are granted/revoked to individual TACC usernames. - Returns all volue permissions. - operationId: get_volume_permissions - parameters: - - required: true - schema: - title: Volume Id - name: volume_id - in: path + Returns new image object.' + operationId: add_image + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewImage' + required: true responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/VolumePermissionsResponse' + $ref: '#/components/schemas/ImageResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - post: + /pods/images/{image_id}: + get: tags: - - Permissions - summary: set_volume_permission - description: |- - Set a permission for a volume. + - Images + summary: get_image + description: 'Get an image. - Returns updated volume permissions. - operationId: set_volume_permission + + Returns retrieved image object.' + operationId: get_image parameters: - - required: true - schema: - title: Volume Id - name: volume_id - in: path - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/SetPermission' - required: true + - required: true + schema: + title: Image Id + name: image_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/VolumePermissionsResponse' + $ref: '#/components/schemas/ImageResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/volumes/{volume_id}/permissions/{user}': delete: tags: - - Permissions - summary: delete_volume_permission - description: |- - Delete a permission from a volume. + - Images + summary: delete_image + description: 'Delete an image. - Returns updated volume permissions. - operationId: delete_volume_permission + + Returns "".' + operationId: delete_image parameters: - - required: true - schema: - title: Volume Id - name: volume_id - in: path - - required: true - schema: - title: User - name: user - in: path + - required: true + schema: + title: Image Id + name: image_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/VolumePermissionsResponse' + $ref: '#/components/schemas/ImageDeleteResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /pods: + /pods/snapshots: get: tags: - - Pods - summary: get_pods - description: >- - Get all pods in your respective tenant and site that you have READ or - higher access to. + - Snapshots + summary: list_snapshots + description: 'Get all snapshots in your respective tenant and site that you + have READ or higher access to. - Returns a list of pods. - operationId: get_pods + Returns a list of snapshots.' + operationId: list_snapshots responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/PodsResponse' + $ref: '#/components/schemas/SnapshotsResponse' post: tags: - - Pods - summary: create_pod - description: >- - Create a pod with inputted information. + - Snapshots + summary: create_snapshot + description: 'Create a snapshot with inputted information. Notes: - - Author will be given ADMIN level permissions to the pod. - - - status_requested defaults to "ON". So pod will immediately begin - creation. + - Author will be given ADMIN level permissions to the snapshot. - Returns new pod object. - operationId: create_pod + Returns new snapshot object.' + operationId: create_snapshot requestBody: content: application/json: schema: - $ref: '#/components/schemas/NewPod' + $ref: '#/components/schemas/NewSnapshot' required: true responses: '200': @@ -669,36 +653,36 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PodResponse' + $ref: '#/components/schemas/SnapshotResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/{pod_id}': + /pods/snapshots/{snapshot_id}: get: tags: - - Pods - summary: get_pod - description: |- - Get a pod. + - Snapshots + summary: get_snapshot + description: 'Get a snapshot. - Returns retrieved pod object. - operationId: get_pod + + Returns retrieved snapshot object.' + operationId: get_snapshot parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path + - required: true + schema: + title: Snapshot Id + name: snapshot_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/PodResponse' + $ref: '#/components/schemas/SnapshotResponse' '422': description: Validation Error content: @@ -707,31 +691,30 @@ paths: $ref: '#/components/schemas/HTTPValidationError' put: tags: - - Pods - summary: update_pod - description: >- - Update a pod. + - Snapshots + summary: update_snapshot + description: 'Update a snapshot. Note: - - Pod will not be restarted, you must restart the pod for any - pod-related changes to proliferate. + - Fields that change snapshot source or sink are not modifiable. Please recreate + your snapshot in that case. - Returns updated pod object. - operationId: update_pod + Returns updated snapshot object.' + operationId: update_snapshot parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path + - required: true + schema: + title: Snapshot Id + name: snapshot_id + in: path requestBody: content: application/json: schema: - $ref: '#/components/schemas/UpdatePod' + $ref: '#/components/schemas/UpdateSnapshot' required: true responses: '200': @@ -739,7 +722,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PodResponse' + $ref: '#/components/schemas/SnapshotResponse' '422': description: Validation Error content: @@ -748,133 +731,143 @@ paths: $ref: '#/components/schemas/HTTPValidationError' delete: tags: - - Pods - summary: delete_pod - description: |- - Delete a pod. + - Snapshots + summary: delete_snapshot + description: 'Delete a snapshot. - Returns "". - operationId: delete_pod + + Returns "".' + operationId: delete_snapshot parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path + - required: true + schema: + title: Snapshot Id + name: snapshot_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/DeletePodResponse' + $ref: '#/components/schemas/DeleteSnapshotResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/{pod_id}/credentials': + /pods/snapshots/{snapshot_id}/list: get: tags: - - Credentials - summary: get_pod_credentials - description: >- - Get the credentials created for a pod. - - - Note: - - - These credentials are used in the case of templated pods, but for - custom pods they're not. - - - Returns user accessible credentials. - operationId: get_pod_credentials + - Snapshots + summary: list_snapshot_files + description: List files in snapshot. + operationId: list_snapshot_files parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path + - required: true + schema: + title: Snapshot Id + name: snapshot_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/PodCredentialsResponse' + $ref: '#/components/schemas/FilesListResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/{pod_id}/logs': + /pods/snapshots/{snapshot_id}/contents/{path}: get: tags: - - Logs - summary: get_pod_logs - description: >- - Get a pods logs. + - Snapshots + summary: get_snapshot_contents + description: 'Get file or directory contents as a stream of data from a Tapis + Snapshot. - Note: - - - These are only retrieved while pod is running. - - - If a pod is restarted or turned off and then on, the logs will be - reset. - - - Returns pod logs. - operationId: get_pod_logs + Use the **zip** query parameter to request directories as a zip archive. This + is not allowed if path would result in all files in the snapshot being included. + Please download individual directories, files or objects.' + operationId: get_snapshot_contents parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path + - description: Unique identifier for the snapshot. + required: true + schema: + type: string + title: Snapshot Id + description: Unique identifier for the snapshot. + name: snapshot_id + in: path + - description: Path relative to the snapshot's root directory. Cannot be empty + or /. + required: true + schema: + type: string + title: Path + description: Path relative to the snapshot's root directory. Cannot be empty + or /. + name: path + in: path + - description: If true, directory contents are compressed using ZIP format. + required: false + schema: + type: boolean + title: Zip + description: If true, directory contents are compressed using ZIP format. + default: false + name: zip + in: query responses: '200': - description: Successful Response + description: A streamed response of the file contents. content: application/json: - schema: - $ref: '#/components/schemas/PodLogsResponse' + schema: {} + application/octet-stream: {} + application/zip: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/{pod_id}/permissions': + /pods/snapshots/{snapshot_id}/permissions: get: tags: - - Permissions - summary: get_pod_permissions - description: |- - Get a pods permissions. + - Permissions + summary: get_snapshot_permissions + description: 'Get a snapshots permissions. + Note: + - There are 3 levels of permissions, READ, USER, and ADMIN. + - Permissions are granted/revoked to individual TACC usernames. - Returns all pod permissions. - operationId: get_pod_permissions + + Returns all volue permissions.' + operationId: get_snapshot_permissions parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path + - required: true + schema: + title: Snapshot Id + name: snapshot_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/PodPermissionsResponse' + $ref: '#/components/schemas/SnapshotPermissionsResponse' '422': description: Validation Error content: @@ -883,19 +876,19 @@ paths: $ref: '#/components/schemas/HTTPValidationError' post: tags: - - Permissions - summary: set_pod_permission - description: |- - Set a permission for a pod. + - Permissions + summary: set_snapshot_permission + description: 'Set a permission for a snapshot. - Returns updated pod permissions. - operationId: set_pod_permission + + Returns updated snapshot permissions.' + operationId: set_snapshot_permission parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path + - required: true + schema: + title: Snapshot Id + name: snapshot_id + in: path requestBody: content: application/json: @@ -908,1403 +901,2776 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PodPermissionsResponse' + $ref: '#/components/schemas/SnapshotPermissionsResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/{pod_id}/permissions/{user}': + /pods/snapshots/{snapshot_id}/permissions/{user}: delete: tags: - - Permissions - summary: delete_pod_permission - description: |- - Delete a permission from a pod. + - Permissions + summary: delete_snapshot_permission + description: 'Delete a permission from a snapshot. - Returns updated pod permissions. - operationId: delete_pod_permission + + Returns updated snapshot permissions.' + operationId: delete_snapshot_permission parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path - - required: true - schema: - title: User - name: user - in: path + - required: true + schema: + title: Snapshot Id + name: snapshot_id + in: path + - required: true + schema: + title: User + name: user + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/PodPermissionsResponse' + $ref: '#/components/schemas/SnapshotPermissionsResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/{pod_id}/stop': + /pods/volumes: get: tags: - - Pods - summary: stop_pod - description: >- - Stop a pod. + - Volumes + summary: list_volumes + description: 'Get all volumes in your respective tenant and site that you have + READ or higher access to. - Note: + Returns a list of volumes.' + operationId: list_volumes + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/VolumesResponse' + post: + tags: + - Volumes + summary: create_volume + description: 'Create a volume with inputted information. - - Sets status_requested to OFF. Pod will attempt to get to STOPPED - status unless start_pod is ran. + Notes: - Returns updated pod object. - operationId: stop_pod - parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path + - Author will be given ADMIN level permissions to the volume. + + - status_requested defaults to "ON". So volume will immediately begin creation. + + + Returns new volume object.' + operationId: create_volume + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewVolume' + required: true responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/PodResponse' + $ref: '#/components/schemas/VolumeResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/{pod_id}/start': + /pods/volumes/{volume_id}: get: tags: - - Pods - summary: start_pod - description: |- - Start a pod. + - Volumes + summary: get_volume + description: 'Get a volume. - Note: - - Sets status_requested to ON. Pod will attempt to deploy. - Returns updated pod object. - operationId: start_pod + Returns retrieved volume object.' + operationId: get_volume parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path + - required: true + schema: + title: Volume Id + name: volume_id + in: path responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/PodResponse' + $ref: '#/components/schemas/VolumeResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - '/pods/{pod_id}/restart': - get: + put: tags: - - Pods - summary: restart_pod - description: >- - Restart a pod. + - Volumes + summary: update_volume + description: 'Update a volume. Note: - - Sets status_requested to RESTART. If pod status gets to STOPPED, - status_requested will be flipped to ON. Health should then create new - pod. + - Fields that change volume source or sink are not modifiable. Please recreate + your volume in that case. - Returns updated pod object. - operationId: restart_pod + Returns updated volume object.' + operationId: update_volume parameters: - - required: true - schema: - title: Pod Id - name: pod_id - in: path + - required: true + schema: + title: Volume Id + name: volume_id + in: path + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateVolume' + required: true responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/PodResponse' + $ref: '#/components/schemas/VolumeResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /traefik-config: - get: + delete: tags: - - Misc - summary: traefik_config - description: >- - Supplies traefik-config to service. Returns json traefik-config object - for + - Volumes + summary: delete_volume + description: 'Delete a volume. - traefik to use with the http provider. Dynamic configs don't work well - in - Kubernetes. - operationId: traefik_config + Returns "".' + operationId: delete_volume + parameters: + - required: true + schema: + title: Volume Id + name: volume_id + in: path responses: '200': description: Successful Response content: application/json: - schema: {} - /healthcheck: + schema: + $ref: '#/components/schemas/DeleteVolumeResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/volumes/{volume_id}/list: get: tags: - - Misc - summary: healthcheck - description: |- - Health check for service. Returns healthy when api is running. - Should add database health check, should add kubernetes health check - operationId: healthcheck + - Volumes + summary: list_volume_files + description: List files in volume. + operationId: list_volume_files + parameters: + - required: true + schema: + title: Volume Id + name: volume_id + in: path responses: '200': description: Successful Response content: application/json: - schema: {} - '/error-handler/{status}': + schema: + $ref: '#/components/schemas/FilesListResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/volumes/{volume_id}/contents/{path}: get: tags: - - Misc - summary: error_handler - description: Handles all error codes from Traefik. - operationId: error_handler + - Volumes + summary: get_volume_contents + description: 'Get file or directory contents as a stream of data from a Tapis + Volume. + + + Use the **zip** query parameter to request directories as a zip archive. This + is not allowed if path would result in all files in the volume being included. + Please download individual directories, files or objects.' + operationId: get_volume_contents parameters: - - required: true - schema: - title: Status - name: status - in: path + - description: Unique identifier for the volume. + required: true + schema: + type: string + title: Volume Id + description: Unique identifier for the volume. + name: volume_id + in: path + - description: Path relative to the volume's root directory. Cannot be empty + or /. + required: true + schema: + type: string + title: Path + description: Path relative to the volume's root directory. Cannot be empty + or /. + name: path + in: path + - description: If true, directory contents are compressed using ZIP format. + required: false + schema: + type: boolean + title: Zip + description: If true, directory contents are compressed using ZIP format. + default: false + name: zip + in: query responses: '200': - description: Successful Response + description: A streamed response of the file contents. content: application/json: schema: {} + application/octet-stream: {} + application/zip: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' -components: - schemas: - Body_upload_to_volume: - title: Body_upload_to_volume - required: - - file - type: object - properties: - file: - title: File - type: string - format: binary - CredentialsModel: - title: CredentialsModel - required: - - user_username - - user_password - type: object - properties: - user_username: - title: User Username - type: string - user_password: - title: User Password - type: string - additionalProperties: false - DeletePodResponse: - title: DeletePodResponse - required: - - message - - metadata - - result - - status - - version - type: object - properties: - message: - title: Message - type: string - metadata: - title: Metadata - type: object - result: - title: Result - type: string - status: - title: Status - type: string - version: - title: Version - type: string - additionalProperties: false - DeleteSnapshotResponse: - title: DeleteSnapshotResponse - required: - - message - - metadata - - result - - status - - version - type: object - properties: - message: - title: Message - type: string - metadata: - title: Metadata - type: object - result: - title: Result - type: string - status: - title: Status - type: string - version: - title: Version + /pods/volumes/{volume_id}/upload/{path}: + post: + tags: + - Volumes + summary: upload_to_volume + description: Upload to volume. + operationId: upload_to_volume + parameters: + - description: Unique identifier for the volume. + required: true + schema: type: string - additionalProperties: false - DeleteVolumeResponse: - title: DeleteVolumeResponse - required: - - message - - metadata - - result - - status - - version - type: object - properties: - message: - title: Message + title: Volume Id + description: Unique identifier for the volume. + name: volume_id + in: path + - description: Path within the volume where the file will be uploaded. Cannot + be empty or /. + required: true + schema: type: string - metadata: - title: Metadata + title: Path + description: Path within the volume where the file will be uploaded. Cannot + be empty or /. + name: path + in: path + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/Body_upload_to_volume' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/FilesUploadResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/volumes/{volume_id}/permissions: + get: + tags: + - Permissions + summary: get_volume_permissions + description: 'Get a volumes permissions. + + + Note: + + - There are 3 levels of permissions, READ, USER, and ADMIN. + + - Permissions are granted/revoked to individual TACC usernames. + + + Returns all volue permissions.' + operationId: get_volume_permissions + parameters: + - required: true + schema: + title: Volume Id + name: volume_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/VolumePermissionsResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + post: + tags: + - Permissions + summary: set_volume_permission + description: 'Set a permission for a volume. + + + Returns updated volume permissions.' + operationId: set_volume_permission + parameters: + - required: true + schema: + title: Volume Id + name: volume_id + in: path + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SetPermission' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/VolumePermissionsResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/volumes/{volume_id}/permissions/{user}: + delete: + tags: + - Permissions + summary: delete_volume_permission + description: 'Delete a permission from a volume. + + + Returns updated volume permissions.' + operationId: delete_volume_permission + parameters: + - required: true + schema: + title: Volume Id + name: volume_id + in: path + - required: true + schema: + title: User + name: user + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/VolumePermissionsResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods: + get: + tags: + - Pods + summary: list_pods + description: 'Get all pods in your respective tenant and site that you have + READ or higher access to. + + + Returns a list of pods.' + operationId: list_pods + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodsResponse' + post: + tags: + - Pods + summary: create_pod + description: 'Create a pod with inputted information. + + + Notes: + + - Author will be given ADMIN level permissions to the pod. + + - status_requested defaults to "ON". So pod will immediately begin creation. + + + Returns new pod object.' + operationId: create_pod + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewPod' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id}: + get: + tags: + - Pods + summary: get_pod + description: 'Get a pod. + + + Returns retrieved pod object.' + operationId: get_pod + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + put: + tags: + - Pods + summary: update_pod + description: 'Update a pod. + + + Note: + + - Pod will not be restarted, you must restart the pod for any pod-related + changes to proliferate. + + + Returns updated pod object.' + operationId: update_pod + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdatePod' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - Pods + summary: delete_pod + description: 'Delete a pod. + + + Returns "".' + operationId: delete_pod + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodDeleteResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id}/derived: + get: + tags: + - Pods + summary: get_derived_pod + description: 'Derive a pod''s final definition if templates are used. + + + Returns final pod definition to be used for pod creation.' + operationId: get_derived_pod + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id}/credentials: + get: + tags: + - Credentials + summary: get_pod_credentials + description: 'Get the credentials created for a pod. + + + Note: + + - These credentials are used in the case of templated pods, but for custom + pods they''re not. + + + Returns user accessible credentials.' + operationId: get_pod_credentials + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodCredentialsResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id}/logs: + get: + tags: + - Logs + summary: get_pod_logs + description: 'Get a pods stdout logs and action_logs. + + + Note: + + - Pod logs are only retrieved while pod is running. + + - If a pod is restarted or turned off and then on, the logs will be reset. + + - Action logs are detailed logs of actions taken on the pod. + + + Returns pod stdout logs and action logs.' + operationId: get_pod_logs + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodLogsResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id}/permissions: + get: + tags: + - Permissions + summary: get_pod_permissions + description: 'Get a pods permissions. + + + Note: + + - There are 3 levels of permissions, READ, USER, and ADMIN. + + - Permissions are granted/revoked to individual TACC usernames. + + + Returns all pod permissions.' + operationId: get_pod_permissions + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodPermissionsResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + post: + tags: + - Permissions + summary: set_pod_permission + description: 'Set a permission for a pod. + + + Returns updated pod permissions.' + operationId: set_pod_permission + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SetPermission' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodPermissionsResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id}/permissions/{user}: + delete: + tags: + - Permissions + summary: delete_pod_permission + description: 'Delete a permission from a pod. + + + Returns updated pod permissions.' + operationId: delete_pod_permission + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + - required: true + schema: + title: User + name: user + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodPermissionsResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id}/stop: + get: + tags: + - Pods + summary: stop_pod + description: 'Stop a pod. + + + Note: + + - Sets status_requested to OFF. Pod will attempt to get to STOPPED status + unless start_pod is ran. + + + Returns updated pod object.' + operationId: stop_pod + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id}/start: + get: + tags: + - Pods + summary: start_pod + description: 'Start a pod. + + + Note: + + - Sets status_requested to ON. Pod will attempt to deploy. + + + Returns updated pod object.' + operationId: start_pod + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id}/restart: + get: + tags: + - Pods + summary: restart_pod + description: 'Restart a pod. + + + Note: + + - Sets status_requested to RESTART. If pod status gets to STOPPED, status_requested + will be flipped to ON. Health should then create new pod. + + + Returns updated pod object.' + operationId: restart_pod + parameters: + - required: true + schema: + title: Pod Id + name: pod_id + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id_net}/auth: + get: + tags: + - Pods + summary: OAuth2 endpoint to act as middleware between pods and user traffic, + checking for authorization on a per pod basis. + description: "Write to session\n\nTraefik continues to user pod if 200, otherwise\ + \ goes to result.\nProcess a callback from a Tapis authorization server:\n\ + \ 1) Get the authorization code from the query parameters.\n 2) Exchange\ + \ the code for a token\n 3) Add the user and token to the sessionhttps\n\ + \ 4) Redirect to the /data endpoint." + operationId: pod_auth + parameters: + - required: true + schema: + title: Pod Id Net + name: pod_id_net + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /pods/{pod_id_net}/auth/callback: + get: + tags: + - Pods + summary: pod_auth_callback + operationId: pod_auth_callback + parameters: + - required: true + schema: + title: Pod Id Net + name: pod_id_net + in: path + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PodResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' +components: + schemas: + Body_upload_to_volume: + properties: + file: + type: string + format: binary + title: File + description: The file to upload. + type: object + required: + - file + title: Body_upload_to_volume + CredentialsModel: + properties: + user_username: + type: string + title: User Username + user_password: + type: string + title: User Password + additionalProperties: false + type: object + required: + - user_username + - user_password + title: CredentialsModel + DeleteSnapshotResponse: + properties: + message: + type: string + title: Message + metadata: type: object + title: Metadata result: - title: Result type: string + title: Result status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - FileModel: - title: FileModel + type: object required: - - name - - lastModified - - nativePermissions - - size - - path - - type + - message + - metadata + - result + - status + - version + title: DeleteSnapshotResponse + DeleteVolumeResponse: + properties: + message: + type: string + title: Message + metadata: + type: object + title: Metadata + result: + type: string + title: Result + status: + type: string + title: Status + version: + type: string + title: Version + additionalProperties: false type: object + required: + - message + - metadata + - result + - status + - version + title: DeleteVolumeResponse + FileModel: properties: + path: + type: string + title: Path + description: Path of object. name: - title: Name type: string + title: Name description: Name of object. + type: + type: string + title: Type + description: Type of object. + size: + type: integer + title: Size + description: Size of object in bytes. lastModified: - title: Lastmodified type: string + title: Lastmodified description: Last modified date of object. nativePermissions: - title: Nativepermissions type: string + title: Nativepermissions description: Native permissions of object. - size: - title: Size - type: integer - description: Size of object in bytes. - path: - title: Path + additionalProperties: false + type: object + required: + - path + - name + - type + - size + - lastModified + - nativePermissions + title: FileModel + FilesListResponse: + properties: + message: type: string - description: Path of object. - type: - title: Type + title: Message + metadata: + type: object + title: Metadata + result: + items: + $ref: '#/components/schemas/FileModel' + type: array + title: Result + status: + type: string + title: Status + version: type: string - description: Type of object. Either file or dir. + title: Version additionalProperties: false - FilesListResponse: + type: object + required: + - message + - metadata + - result + - status + - version title: FilesListResponse + FilesUploadResponse: + properties: + message: + type: string + title: Message + metadata: + type: object + title: Metadata + result: + type: string + title: Result + status: + type: string + title: Status + version: + type: string + title: Version + additionalProperties: false + type: object required: - - message - - metadata - - result - - status - - version + - message + - metadata + - result + - status + - version + title: FilesUploadResponse + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail type: object + title: HTTPValidationError + ImageDeleteResponse: properties: message: - title: Message type: string + title: Message metadata: + type: object title: Metadata + result: + type: string + title: Result + status: + type: string + title: Status + version: + type: string + title: Version + additionalProperties: false + type: object + required: + - message + - metadata + - result + - status + - version + title: ImageDeleteResponse + ImageResponse: + properties: + message: + type: string + title: Message + metadata: type: object + title: Metadata result: - title: Result - type: array - items: - $ref: '#/components/schemas/FileModel' + $ref: '#/components/schemas/ImageResponseModel' status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - FilesUploadResponse: - title: FilesUploadResponse + type: object required: - - message - - metadata - - result - - status - - version + - message + - metadata + - result + - status + - version + title: ImageResponse + ImageResponseModel: + properties: + image: + type: string + title: Image + description: Name of image to allow. + tenants: + items: + type: string + type: array + title: Tenants + description: Tenants that can use this image. + default: [] + description: + type: string + title: Description + description: Description of image. + default: '' + creation_ts: + type: string + format: date-time + title: Creation Ts + description: Time (UTC) that this image was created. + default: '2024-09-11T14:11:37.342300' + added_by: + type: string + title: Added By + description: User who added image to allow list. + default: '' + additionalProperties: false type: object + required: + - image + title: ImageResponseModel + description: Response object for Image class. + ImagesResponse: properties: message: - title: Message type: string + title: Message metadata: - title: Metadata type: object + title: Metadata result: + items: + $ref: '#/components/schemas/ImageResponseModel' + type: array title: Result - type: string status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - HTTPValidationError: - title: HTTPValidationError type: object - properties: - detail: - title: Detail - type: array - items: - $ref: '#/components/schemas/ValidationError' + required: + - message + - metadata + - result + - status + - version + title: ImagesResponse LogsModel: - title: LogsModel - type: object properties: logs: - title: Logs type: string - description: 'Logs from kubernetes pods, useful for debugging and reading results.' + title: Logs + description: Logs from kubernetes pods, useful for debugging and reading + results. default: '' + action_logs: + items: + type: string + type: array + title: Action Logs + description: Log of actions taken on this pod. + default: [] additionalProperties: false - Networking: - title: Networking type: object + title: LogsModel + NewImage: properties: - protocol: - title: Protocol + image: type: string - description: Whether to use http or tcp routing for requests to this pod. - default: http - port: - title: Port - type: integer - description: Pod port to expose via networking.url in this networking object. - default: 5000 - url: - title: Url + title: Image + description: Name of image to allow. + tenants: + items: + type: string + type: array + title: Tenants + description: Tenants that can use this image. + default: [] + description: type: string - description: >- - URL used to access the port of the pod defined in this networking - object. + title: Description + description: Description of image. default: '' additionalProperties: false - NewPod: - title: NewPod - required: - - pod_id - - pod_template type: object + required: + - image + title: NewImage + description: Object with fields that users are allowed to specify for the Image + class. + NewPod: properties: pod_id: - title: Pod Id type: string + title: Pod Id description: Name of this pod. - pod_template: - title: Pod Template + image: type: string - description: >- - Which pod template to use, or which custom image to run, must be on - allowlist. + title: Image + description: Which docker image to use, must be on allowlist, check /pods/images + for list. + default: '' + template: + type: string + title: Template + description: Which pod template to use as base of pod fields. User set attributes + will overwrite template fields. + default: '' description: - title: Description type: string + title: Description description: Description of this pod. default: '' command: - title: Command - type: array items: type: string - description: Command to run in pod. - environment_variables: - title: Environment Variables - type: object - description: Environment variables to inject into k8 pod; Only for custom pods. - default: {} - data_requests: - title: Data Requests type: array + title: Command + description: Command to run in pod. ex. `["sleep", "5000"]` or `["/bin/bash", + "-c", "(exec myscript.sh)"]` + arguments: items: type: string - description: Requested pod names. - default: [] - roles_required: - title: Roles Required type: array - items: - type: string - description: Roles required to view this pod. - default: [] + title: Arguments + description: Arguments for the Pod's command. + environment_variables: + type: object + title: Environment Variables + description: Environment variables to inject into k8 pod; Only for custom + pods. + default: {} status_requested: - title: Status Requested type: string - description: 'Status requested by user, ON or OFF.' + title: Status Requested + description: Status requested by user, `ON`, `OFF`, or `RESTART`. default: 'ON' volume_mounts: - title: Volume Mounts - type: object additionalProperties: - $ref: '#/components/schemas/VolumeMount' - description: >- - Key: Volume name. Value: List of strs specifying volume - folders/files to mount in pod + $ref: '#/components/schemas/models_pods__VolumeMount' + type: object + title: Volume Mounts + description: 'Key: Volume name. Value: List of strs specifying volume folders/files + to mount in pod' default: {} time_to_stop_default: - title: Time To Stop Default type: integer - description: >- - Default time (sec) for pod to run from instance start. -1 for + title: Time To Stop Default + description: Default time (sec) for pod to run from instance start. -1 for unlimited. 12 hour default. default: 43200 time_to_stop_instance: - title: Time To Stop Instance type: integer - description: >- - Time (sec) for pod to run from instance start. Reset each time + title: Time To Stop Instance + description: Time (sec) for pod to run from instance start. Reset each time instance is started. -1 for unlimited. None uses default. networking: - title: Networking - type: object additionalProperties: - $ref: '#/components/schemas/Networking' - description: >- - Networking information. {'url_suffix': {'protocol': 'http' 'tcp', - 'port': int}/} + $ref: '#/components/schemas/models_pods__Networking' + type: object + title: Networking + description: 'Networking information. `{"url_suffix": {"protocol": "http" "tcp", + "port": int}}`' default: default: protocol: http port: 5000 resources: - title: Resources allOf: - - $ref: '#/components/schemas/Resources' - description: Pod resource management + - $ref: '#/components/schemas/models_pods__Resources' + title: Resources + description: 'Pod resource management `{"cpu_limit": 3000, "mem_limit": + 3000, "cpu_request": 500, "mem_limit": 500, "gpus": 0}`' default: {} + compute_queue: + type: string + title: Compute Queue + description: Queue to run pod in. `default` is the default queue. + default: default additionalProperties: false - description: Object with fields that users are allowed to specify for the Pod class. - NewSnapshot: - title: NewSnapshot - required: - - snapshot_id - - source_volume_id - - source_volume_path type: object + required: + - pod_id + title: NewPod + description: Object with fields that users are allowed to specify for the Pod + class. + NewSnapshot: properties: snapshot_id: - title: Snapshot Id type: string + title: Snapshot Id description: Name of this snapshot. source_volume_id: - title: Source Volume Id type: string + title: Source Volume Id description: The volume_id to use as source of snapshot. source_volume_path: - title: Source Volume Path type: string + title: Source Volume Path description: Path in source volume_id to make snapshot of destination_path: - title: Destination Path type: string - description: >- - Path to copy to. Snapshots of singular files require - destination_path. + title: Destination Path + description: Path to copy to. Snapshots of singular files require destination_path. default: '' description: - title: Description type: string + title: Description description: Description of this snapshot. default: '' size_limit: - title: Size Limit type: integer - description: >- - Size in MB to limit snapshot to. We'll start warning if you've gone - past the limit. + title: Size Limit + description: Size in MB to limit snapshot to. We'll start warning if you've + gone past the limit. default: 1024 cron: - title: Cron type: string + title: Cron description: cron bits default: '' retention_policy: - title: Retention Policy type: string + title: Retention Policy description: retention_policy bits default: '' additionalProperties: false - description: >- - Object with fields that users are allowed to specify for the Snapshot - class. - NewVolume: - title: NewVolume + type: object required: - - volume_id + - snapshot_id + - source_volume_id + - source_volume_path + title: NewSnapshot + description: Object with fields that users are allowed to specify for the Snapshot + class. + NewTemplateTag: + properties: + pod_definition: + allOf: + - $ref: '#/components/schemas/TemplateTagPodDefinition' + title: Pod Definition + description: Pod definition for this template tag. + commit_message: + type: string + title: Commit Message + description: Commit message for this template tag. + tag: + type: string + title: Tag + description: Tag for this template. Default is 'latest'. + default: latest + additionalProperties: false type: object + required: + - pod_definition + - commit_message + title: NewTemplateTag + description: Object with fields that users are allowed to specify for the Template + class. + NewVolume: properties: volume_id: - title: Volume Id type: string + title: Volume Id description: Name of this volume. description: - title: Description type: string + title: Description description: Description of this volume. default: '' size_limit: - title: Size Limit type: integer - description: >- - Size in MB to limit volume to. We'll start warning if you've gone - past the limit. + title: Size Limit + description: Size in MB to limit volume to. We'll start warning if you've + gone past the limit. default: 1024 additionalProperties: false - description: >- - Object with fields that users are allowed to specify for the Volume + type: object + required: + - volume_id + title: NewVolume + description: Object with fields that users are allowed to specify for the Volume class. PermissionsModel: - title: PermissionsModel - type: object properties: permissions: - title: Permissions - type: array items: type: string + type: array + title: Permissions description: Pod permissions for each user. default: [] additionalProperties: false - PodCredentialsResponse: - title: PodCredentialsResponse - required: - - message - - metadata - - result - - status - - version type: object + title: PermissionsModel + PodCredentialsResponse: properties: message: - title: Message type: string + title: Message metadata: - title: Metadata type: object + title: Metadata result: $ref: '#/components/schemas/CredentialsModel' status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - PodLogsResponse: - title: PodLogsResponse - required: - - message - - metadata - - result - - status - - version type: object + required: + - message + - metadata + - result + - status + - version + title: PodCredentialsResponse + PodDeleteResponse: properties: message: - title: Message type: string + title: Message metadata: + type: object title: Metadata + result: + type: string + title: Result + status: + type: string + title: Status + version: + type: string + title: Version + additionalProperties: false + type: object + required: + - message + - metadata + - result + - status + - version + title: PodDeleteResponse + PodLogsResponse: + properties: + message: + type: string + title: Message + metadata: type: object + title: Metadata result: $ref: '#/components/schemas/LogsModel' status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - PodPermissionsResponse: - title: PodPermissionsResponse - required: - - message - - metadata - - result - - status - - version type: object + required: + - message + - metadata + - result + - status + - version + title: PodLogsResponse + PodPermissionsResponse: properties: message: - title: Message type: string + title: Message metadata: - title: Metadata type: object + title: Metadata result: $ref: '#/components/schemas/PermissionsModel' status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - PodResponse: - title: PodResponse - required: - - message - - metadata - - result - - status - - version type: object + required: + - message + - metadata + - result + - status + - version + title: PodPermissionsResponse + PodResponse: properties: message: - title: Message type: string + title: Message metadata: - title: Metadata type: object + title: Metadata result: $ref: '#/components/schemas/PodResponseModel' status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - PodResponseModel: - title: PodResponseModel - required: - - pod_id - - pod_template type: object + required: + - message + - metadata + - result + - status + - version + title: PodResponse + PodResponseModel: properties: pod_id: - title: Pod Id type: string + title: Pod Id description: Name of this pod. - pod_template: - title: Pod Template + image: + type: string + title: Image + description: Which docker image to use, must be on allowlist, check /pods/images + for list. + default: '' + template: type: string - description: >- - Which pod template to use, or which custom image to run, must be on - allowlist. + title: Template + description: Which pod template to use as base of pod fields. User set attributes + will overwrite template fields. + default: '' description: - title: Description type: string + title: Description description: Description of this pod. default: '' command: - title: Command - type: array items: type: string - description: Command to run in pod. - environment_variables: - title: Environment Variables - type: object - description: Environment variables to inject into k8 pod; Only for custom pods. - default: {} - data_requests: - title: Data Requests type: array + title: Command + description: Command to run in pod. ex. `["sleep", "5000"]` or `["/bin/bash", + "-c", "(exec myscript.sh)"]` + arguments: items: type: string - description: Requested pod names. - default: [] - roles_required: - title: Roles Required type: array - items: - type: string - description: Roles required to view this pod. - default: [] + title: Arguments + description: Arguments for the Pod's command. + environment_variables: + type: object + title: Environment Variables + description: Environment variables to inject into k8 pod; Only for custom + pods. + default: {} status_requested: - title: Status Requested type: string - description: 'Status requested by user, ON or OFF.' + title: Status Requested + description: Status requested by user, `ON`, `OFF`, or `RESTART`. default: 'ON' volume_mounts: - title: Volume Mounts - type: object additionalProperties: - $ref: '#/components/schemas/VolumeMount' - description: >- - Key: Volume name. Value: List of strs specifying volume - folders/files to mount in pod + $ref: '#/components/schemas/models_pods__VolumeMount' + type: object + title: Volume Mounts + description: 'Key: Volume name. Value: List of strs specifying volume folders/files + to mount in pod' default: {} time_to_stop_default: - title: Time To Stop Default type: integer - description: >- - Default time (sec) for pod to run from instance start. -1 for + title: Time To Stop Default + description: Default time (sec) for pod to run from instance start. -1 for unlimited. 12 hour default. default: 43200 time_to_stop_instance: - title: Time To Stop Instance type: integer - description: >- - Time (sec) for pod to run from instance start. Reset each time + title: Time To Stop Instance + description: Time (sec) for pod to run from instance start. Reset each time instance is started. -1 for unlimited. None uses default. networking: - title: Networking - type: object additionalProperties: - $ref: '#/components/schemas/Networking' - description: >- - Networking information. {'url_suffix': {'protocol': 'http' 'tcp', - 'port': int}/} + $ref: '#/components/schemas/models_pods__Networking' + type: object + title: Networking + description: 'Networking information. `{"url_suffix": {"protocol": "http" "tcp", + "port": int}}`' default: default: protocol: http port: 5000 resources: - title: Resources allOf: - - $ref: '#/components/schemas/Resources' - description: Pod resource management + - $ref: '#/components/schemas/models_pods__Resources' + title: Resources + description: 'Pod resource management `{"cpu_limit": 3000, "mem_limit": + 3000, "cpu_request": 500, "mem_limit": 500, "gpus": 0}`' default: {} + compute_queue: + type: string + title: Compute Queue + description: Queue to run pod in. `default` is the default queue. + default: default time_to_stop_ts: - title: Time To Stop Ts type: string - description: >- - Time (UTC) that this pod is scheduled to be stopped. Change with - time_to_stop_instance. format: date-time + title: Time To Stop Ts + description: Time (UTC) that this pod is scheduled to be stopped. Change + with time_to_stop_instance. status: - title: Status type: string + title: Status description: Current status of pod. default: STOPPED status_container: - title: Status Container type: object + title: Status Container description: Status of container if exists. Gives phase. default: {} - data_attached: - title: Data Attached - type: array - items: - type: string - description: Data attached. - default: [] - roles_inherited: - title: Roles Inherited - type: array - items: - type: string - description: Inherited roles required to view this pod - default: [] creation_ts: - title: Creation Ts type: string - description: Time (UTC) that this pod was created. format: date-time + title: Creation Ts + description: Time (UTC) that this pod was created. update_ts: - title: Update Ts type: string - description: Time (UTC) that this pod was updated. format: date-time + title: Update Ts + description: Time (UTC) that this pod was updated. start_instance_ts: - title: Start Instance Ts type: string - description: Time (UdTC) that this pod instance was started. format: date-time + title: Start Instance Ts + description: Time (UTC) that this pod instance was started. additionalProperties: false + type: object + required: + - pod_id + title: PodResponseModel description: Response object for Pod class. PodsResponse: - title: PodsResponse - required: - - message - - metadata - - result - - status - - version - type: object properties: message: - title: Message type: string + title: Message metadata: - title: Metadata type: object + title: Metadata result: - title: Result - type: array items: $ref: '#/components/schemas/PodResponseModel' + type: array + title: Result status: + type: string title: Status + version: + type: string + title: Version + additionalProperties: false + type: object + required: + - message + - metadata + - result + - status + - version + title: PodsResponse + SetPermission: + properties: + user: + type: string + title: User + description: User to modify permissions for. + level: + type: string + title: Level + description: Permission level to give the user. + additionalProperties: false + type: object + required: + - user + - level + title: SetPermission + description: Object with fields that users are allowed to specify for the Volume + class. + SnapshotPermissionsResponse: + properties: + message: type: string + title: Message + metadata: + type: object + title: Metadata + result: + $ref: '#/components/schemas/PermissionsModel' + status: + type: string + title: Status version: + type: string title: Version + additionalProperties: false + type: object + required: + - message + - metadata + - result + - status + - version + title: SnapshotPermissionsResponse + SnapshotResponse: + properties: + message: + type: string + title: Message + metadata: + type: object + title: Metadata + result: + $ref: '#/components/schemas/SnapshotResponseModel' + status: + type: string + title: Status + version: type: string + title: Version additionalProperties: false - Resources: - title: Resources type: object + required: + - message + - metadata + - result + - status + - version + title: SnapshotResponse + SnapshotResponseModel: properties: - cpu_request: - title: Cpu Request - type: integer - description: >- - CPU allocation pod requests at startup. In millicpus (m). 1000 = 1 - cpu. - default: 250 - cpu_limit: - title: Cpu Limit - type: integer - description: >- - CPU allocation pod is allowed to use. In millicpus (m). 1000 = 1 - cpu. - default: 2000 - mem_request: - title: Mem Request + snapshot_id: + type: string + title: Snapshot Id + description: Name of this snapshot. + source_volume_id: + type: string + title: Source Volume Id + description: The volume_id to use as source of snapshot. + source_volume_path: + type: string + title: Source Volume Path + description: Path in source volume_id to make snapshot of + destination_path: + type: string + title: Destination Path + description: Path to copy to. Snapshots of singular files require destination_path. + default: '' + description: + type: string + title: Description + description: Description of this snapshot. + default: '' + size_limit: type: integer - description: Memory allocation pod requests at startup. In megabytes (Mi) - default: 256 - mem_limit: - title: Mem Limit + title: Size Limit + description: Size in MB to limit snapshot to. We'll start warning if you've + gone past the limit. + default: 1024 + cron: + type: string + title: Cron + description: cron bits + default: '' + retention_policy: + type: string + title: Retention Policy + description: retention_policy bits + default: '' + size: type: integer - description: Memory allocation pod is allowed to use. In megabytes (Mi) - default: 3072 + title: Size + description: Size of snapshot currently in MB + default: 0 + status: + type: string + title: Status + description: Current status of snapshot. + default: REQUESTED + creation_ts: + type: string + format: date-time + title: Creation Ts + description: Time (UTC) that this snapshot was created. + update_ts: + type: string + format: date-time + title: Update Ts + description: Time (UTC) that this snapshot was updated. + additionalProperties: false + type: object + required: + - snapshot_id + - source_volume_id + - source_volume_path + title: SnapshotResponseModel + description: Response object for Snapshot class. + SnapshotsResponse: + properties: + message: + type: string + title: Message + metadata: + type: object + title: Metadata + result: + items: + $ref: '#/components/schemas/SnapshotResponseModel' + type: array + title: Result + status: + type: string + title: Status + version: + type: string + title: Version + additionalProperties: false + type: object + required: + - message + - metadata + - result + - status + - version + title: SnapshotsResponse + Template: + properties: + template_id: + type: string + title: Template Id + description: Name of template. + description: + type: string + title: Description + description: Description of template. + default: '' + metatags: + items: + type: string + type: array + title: Metatags + description: Metadata tags for additional search/listing functionality for + the template. + default: [] + archive_message: + type: string + title: Archive Message + description: If set, metadata message to give users of this template. + default: '' + creation_ts: + type: string + format: date-time + title: Creation Ts + description: Time (UTC) that this template was created. + update_ts: + type: string + format: date-time + title: Update Ts + description: Time (UTC) that this template was updated. + tenant_id: + type: string + title: Tenant Id + description: Tapis tenant used during creation of this template. + default: '' + site_id: + type: string + title: Site Id + description: Tapis site used during creation of this template. + default: '' + permissions: + items: + type: string + type: array + title: Permissions + description: Template permissions in user:level format. + default: [] additionalProperties: false - SetPermission: - title: SetPermission - required: - - user - - level type: object + required: + - template_id + title: Template + TemplateDeleteResponse: properties: - user: - title: User + message: type: string - description: User to modify permissions for. - level: - title: Level + title: Message + metadata: + type: object + title: Metadata + result: type: string - description: Permission level to give the user. + title: Result + status: + type: string + title: Status + version: + type: string + title: Version additionalProperties: false - description: >- - Object with fields that users are allowed to specify for the Volume - class. - SnapshotPermissionsResponse: - title: SnapshotPermissionsResponse - required: - - message - - metadata - - result - - status - - version type: object + required: + - message + - metadata + - result + - status + - version + title: TemplateDeleteResponse + TemplatePermissionsResponse: properties: message: - title: Message type: string + title: Message metadata: - title: Metadata type: object + title: Metadata result: $ref: '#/components/schemas/PermissionsModel' status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - SnapshotResponse: - title: SnapshotResponse - required: - - message - - metadata - - result - - status - - version type: object + required: + - message + - metadata + - result + - status + - version + title: TemplatePermissionsResponse + TemplateResponse: properties: message: - title: Message type: string + title: Message metadata: - title: Metadata type: object + title: Metadata result: - $ref: '#/components/schemas/SnapshotResponseModel' + $ref: '#/components/schemas/TemplateResponseModel' status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - SnapshotResponseModel: - title: SnapshotResponseModel - required: - - snapshot_id - - source_volume_id - - source_volume_path type: object + required: + - message + - metadata + - result + - status + - version + title: TemplateResponse + TemplateResponseModel: properties: - snapshot_id: - title: Snapshot Id - type: string - description: Name of this snapshot. - source_volume_id: - title: Source Volume Id - type: string - description: The volume_id to use as source of snapshot. - source_volume_path: - title: Source Volume Path - type: string - description: Path in source volume_id to make snapshot of - destination_path: - title: Destination Path + template_id: type: string - description: >- - Path to copy to. Snapshots of singular files require - destination_path. - default: '' + title: Template Id + description: Name of template. description: - title: Description - type: string - description: Description of this snapshot. - default: '' - size_limit: - title: Size Limit - type: integer - description: >- - Size in MB to limit snapshot to. We'll start warning if you've gone - past the limit. - default: 1024 - cron: - title: Cron type: string - description: cron bits + title: Description + description: Description of template. default: '' - retention_policy: - title: Retention Policy + metatags: + items: + type: string + type: array + title: Metatags + description: Metadata tags for additional search/listing functionality for + the template. + default: [] + archive_message: type: string - description: retention_policy bits + title: Archive Message + description: If set, metadata message to give users of this template. default: '' - size: - title: Size - type: integer - description: Size of snapshot currently in MB - default: 0 - status: - title: Status - type: string - description: Current status of snapshot. - default: REQUESTED creation_ts: - title: Creation Ts type: string - description: Time (UTC) that this snapshot was created. format: date-time + title: Creation Ts + description: Time (UTC) that this template was created. update_ts: + type: string + format: date-time title: Update Ts + description: Time (UTC) that this template was updated. + additionalProperties: false + type: object + required: + - template_id + title: TemplateResponseModel + TemplateTag: + properties: + template_id: + type: string + title: Template Id + description: template_id this tag is linked to + pod_definition: + allOf: + - $ref: '#/components/schemas/TemplateTagPodDefinition' + title: Pod Definition + description: Pod definition for this template. + default: {} + commit_message: + type: string + title: Commit Message + description: Commit message for this template tag. + default: '' + tag: + type: string + title: Tag + description: Tag for this template. Default is 'latest'. + default: latest + tag_timestamp: + type: string + title: Tag Timestamp + description: tag@timestamp for this template tag. + default: '' + added_by: + type: string + title: Added By + description: User who added this template tag. + default: '' + creation_ts: type: string - description: Time (UTC) that this snapshot was updated. format: date-time + title: Creation Ts + description: Time (UTC) that this template tag was created. additionalProperties: false - description: Response object for Snapshot class. - SnapshotsResponse: - title: SnapshotsResponse + type: object required: - - message - - metadata - - result - - status - - version + - template_id + title: TemplateTag + TemplateTagPodDefinition: + properties: + image: + type: string + title: Image + description: Which docker image to use, must be on allowlist, check /pods/images + for list. + template: + type: string + title: Template + description: Name of template to base this template off of. + description: + type: string + title: Description + description: Description of this pod. + command: + items: + type: string + type: array + title: Command + description: Command to run in pod. ex. `["sleep", "5000"]` or `["/bin/bash", + "-c", "(exec myscript.sh)"]` + arguments: + items: + type: string + type: array + title: Arguments + description: Arguments for the Pod's command. + environment_variables: + type: object + title: Environment Variables + description: Environment variables to inject into k8 pod; Only for custom + pods. + default: {} + volume_mounts: + additionalProperties: + $ref: '#/components/schemas/models_templates_tags__VolumeMount' + type: object + title: Volume Mounts + description: 'Key: Volume name. Value: List of strs specifying volume folders/files + to mount in pod' + default: {} + time_to_stop_default: + type: integer + title: Time To Stop Default + description: Default time (sec) for pod to run from instance start. -1 for + unlimited. 12 hour default. + time_to_stop_instance: + type: integer + title: Time To Stop Instance + description: Time (sec) for pod to run from instance start. Reset each time + instance is started. -1 for unlimited. None uses default. + networking: + additionalProperties: + $ref: '#/components/schemas/models_templates_tags__Networking' + type: object + title: Networking + description: 'Networking information. `{"url_suffix": {"protocol": "http" "tcp", + "port": int}}`' + default: {} + resources: + allOf: + - $ref: '#/components/schemas/models_templates_tags__Resources' + title: Resources + description: 'Pod resource management `{"cpu_limit": 3000, "mem_limit": + 3000, "cpu_request": 500, "mem_limit": 500, "gpus": 0}`' + default: {} + compute_queue: + type: string + title: Compute Queue + description: Queue to run pod in. `default` is the default queue. + default: default + additionalProperties: false type: object + title: TemplateTagPodDefinition + TemplateTagResponse: properties: message: - title: Message type: string + title: Message metadata: + type: object title: Metadata + result: + $ref: '#/components/schemas/TemplateTag' + status: + type: string + title: Status + version: + type: string + title: Version + additionalProperties: false + type: object + required: + - message + - metadata + - result + - status + - version + title: TemplateTagResponse + TemplateTagsResponse: + properties: + message: + type: string + title: Message + metadata: type: object + title: Metadata result: - title: Result - type: array items: - $ref: '#/components/schemas/SnapshotResponseModel' + $ref: '#/components/schemas/TemplateTag' + type: array + title: Result status: - title: Status type: string + title: Status version: + type: string title: Version + additionalProperties: false + type: object + required: + - message + - metadata + - result + - status + - version + title: TemplateTagsResponse + TemplatesResponse: + properties: + message: + type: string + title: Message + metadata: + type: object + title: Metadata + result: + items: + $ref: '#/components/schemas/TemplateResponseModel' + type: array + title: Result + status: type: string + title: Status + version: + type: string + title: Version additionalProperties: false - UpdatePod: - title: UpdatePod type: object + required: + - message + - metadata + - result + - status + - version + title: TemplatesResponse + UpdatePod: properties: description: - title: Description type: string + title: Description description: Description of this pod. default: '' command: - title: Command - type: array items: type: string - description: Command to run in pod. + type: array + title: Command + description: Command to run in pod. ex. ["sleep", "5000"] or ["/bin/bash", + "-c", "(exec myscript.sh)"] environment_variables: - title: Environment Variables type: object - description: Environment variables to inject into k8 pod; Only for custom pods. + title: Environment Variables + description: Environment variables to inject into k8 pod; Only for custom + pods. default: {} - data_requests: - title: Data Requests - type: array - items: - type: string - description: Requested pod names. - default: [] - roles_required: - title: Roles Required - type: array - items: - type: string - description: Roles required to view this pod. - default: [] status_requested: - title: Status Requested type: string - description: 'Status requested by user, ON or OFF.' + title: Status Requested + description: Status requested by user, `ON`, `OFF`, or `RESTART`. default: 'ON' volume_mounts: - title: Volume Mounts - type: object additionalProperties: - $ref: '#/components/schemas/VolumeMount' - description: >- - Key: Volume name. Value: List of strs specifying volume - folders/files to mount in pod + $ref: '#/components/schemas/models_pods__VolumeMount' + type: object + title: Volume Mounts + description: 'Key: Volume name. Value: List of strs specifying volume folders/files + to mount in pod' default: {} time_to_stop_default: - title: Time To Stop Default type: integer - description: >- - Default time (sec) for pod to run from instance start. -1 for + title: Time To Stop Default + description: Default time (sec) for pod to run from instance start. -1 for unlimited. 12 hour default. default: 43200 time_to_stop_instance: - title: Time To Stop Instance type: integer - description: >- - Time (sec) for pod to run from instance start. Reset each time + title: Time To Stop Instance + description: Time (sec) for pod to run from instance start. Reset each time instance is started. -1 for unlimited. None uses default. networking: - title: Networking - type: object additionalProperties: - $ref: '#/components/schemas/Networking' - description: >- - Networking information. {'url_suffix': {'protocol': 'http' 'tcp', - 'port': int}/} + $ref: '#/components/schemas/models_pods__Networking' + type: object + title: Networking + description: 'Networking information. {"url_suffix": {"protocol": "http" "tcp", + "port": int}}' default: default: protocol: http port: 5000 resources: - title: Resources allOf: - - $ref: '#/components/schemas/Resources' - description: Pod resource management + - $ref: '#/components/schemas/models_pods__Resources' + title: Resources + description: 'Pod resource management {"cpu_limit": 3000, "mem_limit": 3000, + "cpu_request": 500, "mem_limit": 500, "gpu": 0}' default: {} additionalProperties: false - description: Object with fields that users are allowed to specify for the Pod class. - UpdateSnapshot: - title: UpdateSnapshot type: object + title: UpdatePod + description: Object with fields that users are allowed to specify for the Pod + class. + UpdateSnapshot: properties: description: - title: Description type: string + title: Description description: Description of this snapshot. default: '' size_limit: - title: Size Limit type: integer - description: >- - Size in MB to limit snapshot to. We'll start warning if you've gone - past the limit. + title: Size Limit + description: Size in MB to limit snapshot to. We'll start warning if you've + gone past the limit. default: 1024 cron: - title: Cron type: string + title: Cron description: cron bits default: '' retention_policy: - title: Retention Policy type: string + title: Retention Policy description: retention_policy bits default: '' additionalProperties: false - description: >- - Object with fields that users are allowed to specify when updating the - Snapshot class. - UpdateVolume: - title: UpdateVolume type: object + title: UpdateSnapshot + description: Object with fields that users are allowed to specify when updating + the Snapshot class. + UpdateTemplate: properties: description: + type: string title: Description + description: Description of template. + default: '' + metatags: + items: + type: string + type: array + title: Metatags + description: Metadata tags for additional search/listing functionality for + the template. + default: [] + archive_message: + type: string + title: Archive Message + description: If set, metadata message to give users of this template. + default: '' + additionalProperties: false + type: object + title: UpdateTemplate + description: Object with fields that users are allowed to specify for the Pod + class. + UpdateVolume: + properties: + description: type: string + title: Description description: Description of this volume. default: '' size_limit: - title: Size Limit type: integer - description: >- - Size in MB to limit volume to. We'll start warning if you've gone - past the limit. + title: Size Limit + description: Size in MB to limit volume to. We'll start warning if you've + gone past the limit. default: 1024 additionalProperties: false - description: >- - Object with fields that users are allowed to specify when updating the - Volume class. - ValidationError: - title: ValidationError - required: - - loc - - msg - - type type: object + title: UpdateVolume + description: Object with fields that users are allowed to specify when updating + the Volume class. + ValidationError: properties: loc: - title: Location - type: array items: anyOf: - - type: string - - type: integer + - type: string + - type: integer + type: array + title: Location msg: - title: Message type: string + title: Message type: - title: Error Type type: string - VolumeMount: - title: VolumeMount + title: Error Type type: object - properties: - type: - title: Type - type: string - description: Type of volume to attach. - default: '' - mount_path: - title: Mount Path - type: string - description: Path to mount volume to. - default: /tapis_volume_mount - sub_path: - title: Sub Path - type: string - description: Path to mount volume to. - default: '' - additionalProperties: false - VolumePermissionsResponse: - title: VolumePermissionsResponse required: - - message - - metadata - - result - - status - - version - type: object + - loc + - msg + - type + title: ValidationError + VolumePermissionsResponse: properties: message: - title: Message type: string + title: Message metadata: - title: Metadata type: object + title: Metadata result: $ref: '#/components/schemas/PermissionsModel' status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - VolumeResponse: - title: VolumeResponse - required: - - message - - metadata - - result - - status - - version type: object + required: + - message + - metadata + - result + - status + - version + title: VolumePermissionsResponse + VolumeResponse: properties: message: - title: Message type: string + title: Message metadata: - title: Metadata type: object + title: Metadata result: $ref: '#/components/schemas/VolumeResponseModel' status: - title: Status type: string + title: Status version: - title: Version type: string + title: Version additionalProperties: false - VolumeResponseModel: - title: VolumeResponseModel - required: - - volume_id type: object + required: + - message + - metadata + - result + - status + - version + title: VolumeResponse + VolumeResponseModel: properties: volume_id: - title: Volume Id type: string + title: Volume Id description: Name of this volume. description: - title: Description type: string + title: Description description: Description of this volume. default: '' size_limit: - title: Size Limit type: integer - description: >- - Size in MB to limit volume to. We'll start warning if you've gone - past the limit. + title: Size Limit + description: Size in MB to limit volume to. We'll start warning if you've + gone past the limit. default: 1024 size: - title: Size type: integer + title: Size description: Size of volume currently in MB default: 0 status: - title: Status type: string + title: Status description: Current status of volume. default: REQUESTED creation_ts: - title: Creation Ts type: string - description: Time (UTC) that this volume was created. format: date-time + title: Creation Ts + description: Time (UTC) that this volume was created. update_ts: - title: Update Ts type: string - description: Time (UTC) that this volume was updated. format: date-time + title: Update Ts + description: Time (UTC) that this volume was updated. additionalProperties: false + type: object + required: + - volume_id + title: VolumeResponseModel description: Response object for Volume class. VolumesResponse: - title: VolumesResponse - required: - - message - - metadata - - result - - status - - version - type: object properties: message: - title: Message type: string + title: Message metadata: - title: Metadata type: object + title: Metadata result: - title: Result - type: array items: $ref: '#/components/schemas/VolumeResponseModel' + type: array + title: Result status: - title: Status type: string + title: Status version: + type: string title: Version + additionalProperties: false + type: object + required: + - message + - metadata + - result + - status + - version + title: VolumesResponse + models_pods__Networking: + properties: + protocol: + type: string + title: Protocol + description: Which network protocol to use. `http`, `tcp`, `postgres`, or + `local_only`. `local_only` is only accessible from within the cluster. + default: http + port: + type: integer + title: Port + description: Pod port to expose via networking.url in this networking object. + default: 5000 + url: + type: string + title: Url + description: URL used to access the port of the pod defined in this networking + object. Generated by service. + default: '' + ip_allow_list: + items: + type: string + type: array + title: Ip Allow List + description: List of IPs that are allowed to access this specific pod port. + If empty, all IPs are allowed. ex. ['127.0.0.1/32', '192.168.1.7'] + default: [] + tapis_auth: + type: boolean + title: Tapis Auth + description: If true, will require Tapis auth to access the pod. + default: false + tapis_auth_response_headers: + items: + type: string + type: array + title: Tapis Auth Response Headers + description: List of headers to forward to the pod when using Tapis auth. + default: [] + tapis_auth_allowed_users: + items: + type: string + type: array + title: Tapis Auth Allowed Users + description: List of users allowed to access the pod when using Tapis auth. + default: + - '*' + tapis_ui_uri: + type: string + title: Tapis Ui Uri + description: Path to redirect to when accessing the pod via Tapis UI. + default: '' + tapis_ui_uri_redirect: + type: boolean + title: Tapis Ui Uri Redirect + description: If true, will redirect to the tapis_ui_uri when accessing the + pod via Tapis UI. Otherwise, just read-only uri. + default: false + tapis_ui_uri_description: + type: string + title: Tapis Ui Uri Description + description: Describing where the tapis_ui_uri will redirect to. + default: '' + additionalProperties: false + type: object + title: Networking + models_pods__Resources: + properties: + cpu_request: + type: integer + title: Cpu Request + description: CPU allocation pod requests at startup. In millicpus (m). 1000 + = 1 cpu. + default: 250 + cpu_limit: + type: integer + title: Cpu Limit + description: CPU allocation pod is allowed to use. In millicpus (m). 1000 + = 1 cpu. + default: 2000 + mem_request: + type: integer + title: Mem Request + description: Memory allocation pod requests at startup. In megabytes (Mi) + default: 256 + mem_limit: + type: integer + title: Mem Limit + description: Memory allocation pod is allowed to use. In megabytes (Mi) + default: 3072 + gpus: + type: integer + title: Gpus + description: GPU allocation pod is allowed to use. In integers of GPUs. + (we only have 1 currently ;) ) + default: 0 + additionalProperties: false + type: object + title: Resources + models_pods__VolumeMount: + properties: + type: + type: string + title: Type + description: Type of volume to attach. + default: '' + mount_path: + type: string + title: Mount Path + description: Path to mount volume to. + default: /tapis_volume_mount + sub_path: + type: string + title: Sub Path + description: Path to mount volume to. + default: '' + additionalProperties: false + type: object + title: VolumeMount + models_templates_tags__Networking: + properties: + protocol: + type: string + title: Protocol + description: Which network protocol to use. `http`, `tcp`, `postgres`, or + `local_only`. `local_only` is only accessible from within the cluster. + default: http + port: + type: integer + title: Port + description: Pod port to expose via networking.url in this networking object. + default: 5000 + url: + type: string + title: Url + description: URL used to access the port of the pod defined in this networking + object. Generated by service. + default: '' + additionalProperties: false + type: object + title: Networking + models_templates_tags__Resources: + properties: + cpu_request: + type: integer + title: Cpu Request + description: CPU allocation pod requests at startup. In millicpus (m). 1000 + = 1 cpu. + cpu_limit: + type: integer + title: Cpu Limit + description: CPU allocation pod is allowed to use. In millicpus (m). 1000 + = 1 cpu. + mem_request: + type: integer + title: Mem Request + description: Memory allocation pod requests at startup. In megabytes (Mi) + mem_limit: + type: integer + title: Mem Limit + description: Memory allocation pod is allowed to use. In megabytes (Mi) + gpus: + type: integer + title: Gpus + description: GPU allocation pod is allowed to use. In integers of GPUs. + (we only have 1 currently ;) ) + additionalProperties: false + type: object + title: Resources + models_templates_tags__VolumeMount: + properties: + type: + type: string + title: Type + description: Type of volume to attach. + default: '' + mount_path: + type: string + title: Mount Path + description: Path to mount volume to. + default: /tapis_volume_mount + sub_path: type: string + title: Sub Path + description: Path to mount volume to. + default: '' additionalProperties: false + type: object + title: VolumeMount tags: - - name: Pods - description: Create and command pods. - - name: Credentials - description: Manage pod's credentials used. - - name: Logs - description: Manage pod logs. - - name: Permissions - description: >- - Manage pod permissions. Grant specific TACC users **READ**, **USER**, and - **ADMIN** level permissions. - - name: Volumes - description: Create and manage volumes. - - name: Snapshots - description: Create and manage snapshots. +- name: Pods + description: Create and command pods. +- name: Credentials + description: Manage pod's credentials used. +- name: Logs + description: Manage pod logs. +- name: Permissions + description: Manage pod permissions. Grant specific TACC users **READ**, **USER**, + and **ADMIN** level permissions. +- name: Volumes + description: Create and manage volumes. +- name: Snapshots + description: Create and manage snapshots. diff --git a/tapipy/resources/openapi_v3-systems.yml b/tapipy/resources/openapi_v3-systems.yml index abe787c..9b74cfe 100644 --- a/tapipy/resources/openapi_v3-systems.yml +++ b/tapipy/resources/openapi_v3-systems.yml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: Tapis Systems API description: The Tapis Systems API provides for management of Tapis Systems including permissions, credentials and Scheduler Profiles. - version: '1.6.0' + version: '1.6.3' termsOfService: "https://tapis-project.org" contact: name: "Systems API - CICSupport" @@ -21,8 +21,6 @@ servers: - url: 'https://dev.develop.tapis.io/' description: Development environment variables: {} -security: - - TapisJWT: [] tags: - name: General description: General service health and readiness @@ -47,6 +45,25 @@ tags: made through the Files service. Note that Tapis permissions and sharing are independent of native permissions enforced by the underlying system host. + - name: Child Systems + description: | + A system that has *allowChildren* set to *true* allows for the creation of child systems based on it. + This ability provides a way to easily clone and manage systems based on existing systems. + Child systems allow a user to set only a few fields, and use all other values from an existing parent system. + This can reduce the difficulty in managing systems. + It allows for all child systems to be updated when the parent is updated. + - name: Scheduler Profiles + description: | + The Systems service supports managing Tapis scheduler profiles. An HPC center often has certain conventions + and restrictions around the use of batch schedulers. A scheduler profile resource can be defined to provide the + Tapis Jobs service with additional site specific information to be used when executing applications using a + scheduler. A scheduler profile contains information on options that should be hidden from the scheduler, + the module load command to use and which modules should be loaded by default when running a job. Anyone in a + tenant may create a scheduler profile for use by all users in the tenant. The owner of a profile or a + tenant administrator may modify or delete a profile. A profile may referenced in a system definition using the + attribute *batchSchedulerProfile*. The profile to be used may also be set in the job submit request using the + special scheduler option *--tapis-profile*. The value in the job submit request takes precedence over any value + defined for the execution system. # ------------------------------------------------------------------------------ # --- Paths ------------------------------------------------------------------- @@ -90,13 +107,21 @@ paths: tags: - Systems description: | - Retrieve list of systems. Use *listType*, *search* and *select* query parameters to limit results. Query + Retrieve list of systems. + + Use *listType*, *search* and *select* query parameters to limit results. Query parameter *listType* allows for filtering results based on authorization. Options for *listType* are - *OWNED* Include only items owned by requester (Default) - *SHARED_PUBLIC* Include only items shared publicly - *ALL* Include all items requester is authorized to view. Includes check for READ or MODIFY permission. + + Certain Tapis services or a tenant administrator may use the query parameter *impersonationId* to be used in + place of the requesting Tapis user. Tapis will use this user Id when performing authorization and resolving + the *effectiveUserId*. operationId: getSystems + security: + - TapisJWT: [] parameters: - name: search in: query @@ -147,6 +172,11 @@ paths: schema: type: boolean default: false + - name: impersonationId + in: query + description: Restricted. Only certain Tapis services or a tenant administrator may impersonate a Tapis user. + schema: + type: string responses: '200': description: Success. @@ -192,7 +222,30 @@ paths: skipCredentialCheck=true to bypass initial verification of credentials. The attribute *rootDir* serves as an effective root directory when operating on files through the Tapis - Files service. When using Files to list, copy, move, mkdir, etc. all paths are relative to this directory. + Files service. All paths are relative to this directory when using Files to list, copy, move, mkdir, etc. + Required for systems of type LINUX or IRODS. Supports the following variables which are resolved at create + time: *${apiUserId}*, *${tenant}* and *${owner}*. May not be updated. Contact support to request a change. + + There is also a special macro available for *rootDir* that may be used under certain conditions when a system + is first created. The macro name is HOST_EVAL. + The syntax for the macro is HOST_EVAL($var), where *var* is the environment variable to be evaluated + on the system host when the create request is made. + Note that the $ character preceding the environment variable name is optional. + If after resolution the final path does not have the required leading slash (/) to make it an absolute path, + then one will be prepended. + The following conditions must be met in order to use the macro + + - System must be of type LINUX + - Credentials must be provided when system is created. + - Macro HOST_EVAL() must only appear once and must be the first element of the path. Including a leading slash is optional. + - The *effectiveUserId* for the system must be static. Note that *effectiveUserId* may be set to *${owner}*. + + Here are some examples + + - HOST_EVAL($SCRATCH) + - HOST_EVAL($HOME) + - /HOST_EVAL(MY_ROOT_DIR)/scratch + - /HOST_EVAL($PROJECT_HOME)/projects/${tenant}/${owner} Note that certain attributes in the request body (such as tenant) are allowed but ignored so that the JSON result returned by a GET may be modified and used when making a POST request to create a system. @@ -204,6 +257,8 @@ paths: - created - updated operationId: createSystem + security: + - TapisJWT: [] parameters: - name: skipCredentialCheck in: query @@ -255,16 +310,17 @@ paths: tags: - Child Systems description: | - Create a child system. The child system gets all of it's attributes from it's parent except for the following - fields: + Create a child system based on a parent system. The child system inherits most attributes from the parent. + The following fields are filled in when the child system is created: - *id* - *effectiveUserId* - *rootDir* - *owner* - The owner will be the user who is creating the system. The caller must have read permission to the parent. - system. + The owner will be the user who is creating the system. The caller must have read permission on the parent system. operationId: createChildSystem + security: + - TapisJWT: [] parameters: - name: parentId in: path @@ -323,6 +379,8 @@ paths: - *SHARED_PUBLIC* Include only items shared publicly - *ALL* Include all items requester is authorized to view. Includes check for READ or MODIFY permission. operationId: searchSystemsQueryParameters + security: + - TapisJWT: [] parameters: - name: freeFormParameterName in: query @@ -403,6 +461,8 @@ paths: - *SHARED_PUBLIC* Include only items shared publicly - *ALL* Include all items requester is authorized to view. Includes check for READ or MODIFY permission. operationId: searchSystemsRequestBody + security: + - TapisJWT: [] parameters: - name: listType in: query @@ -476,6 +536,8 @@ paths: *WARNING Capability constraint matching is not yet supported.* Retrieve details for systems. Use request body to specify constraint conditions as an SQL-like WHERE clause. operationId: matchConstraints + security: + - TapisJWT: [] requestBody: required: true description: A JSON object specifying SQL-like constraint conditions as an array of strings. Strings are concatenated to form full query. @@ -509,14 +571,17 @@ paths: description: | Retrieve information for a system given the system Id. - Use query parameter authnMethod= to override the default authentication method. + Use query parameter *authnMethod* to override the default authentication method. - Certain services may use the query parameter *impersonationId* to be used in place of the requesting - Tapis user. Tapis will use this user Id when performing authorization and resolving the *effectiveUserId*. + Certain Tapis services or a tenant administrator may use the query parameter *impersonationId* to be used in + place of the requesting Tapis user. Tapis will use this user Id when performing authorization and resolving + the *effectiveUserId*. - Certain services may use the query parameter *sharedAppCtx* to indicate that the request is in a shared + Certain Tapis services may use the query parameter *sharedAppCtx* to indicate that the request is in a shared application context. operationId: getSystem + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -549,12 +614,12 @@ paths: default: false - name: impersonationId in: query - description: Restricted. Only certain services may impersonate a Tapis user. + description: Restricted. Only certain Tapis services or a tenant administrator may impersonate a Tapis user. schema: type: string - name: sharedAppCtx in: query - description: Restricted. Only certain services may indicate that the request is in a shared context. Must be set to the grantor who shared the application. + description: Restricted. Only certain Tapis services may indicate that the request is in a shared context. Must be set to the grantor who shared the application. schema: $ref: '#/components/schemas/UserNameString' - name: resourceTenant @@ -607,11 +672,12 @@ paths: - enabled - bucketName - rootDir - - isDtn - canExec Note that the attributes owner and enabled may be modified using other endpoints. operationId: patchSystem + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -689,11 +755,12 @@ paths: - enabled - bucketName - rootDir - - isDtn - canExec Note that the attributes owner and enabled may be modified using other endpoints. operationId: putSystem + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -757,6 +824,8 @@ paths: description: | Check if a system is currently enabled, i.e. available for use. operationId: isEnabled + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -795,6 +864,8 @@ paths: description: | Mark a system available for use. operationId: enableSystem + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -833,6 +904,8 @@ paths: description: | Mark a system unavailable for use. operationId: disableSystem + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -871,6 +944,8 @@ paths: description: | Mark a system as deleted. System will not appear in queries unless explicitly requested. operationId: deleteSystem + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -909,6 +984,8 @@ paths: description: | Mark a system as not deleted. System will appear in queries. operationId: undeleteSystem + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -947,6 +1024,8 @@ paths: description: | Change owner of a system. operationId: changeSystemOwner + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -990,6 +1069,8 @@ paths: description: | Retrieve history of changes for a given systemId. operationId: getHistory + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1026,9 +1107,11 @@ paths: tags: - Child Systems description: | - Make a child system a standalone system. This will break the connection with it's parent, and from - this point on, the child system will not be connected to the parent. Warning, this cannot be undone. + Make a child system a standalone system. This will break the connection with it's parent. From + this point on, the child system will not be connected to the parent. **WARNING** This cannot be undone. operationId: unlinkFromParent + security: + - TapisJWT: [] parameters: - name: childSystemId in: path @@ -1070,6 +1153,8 @@ paths: this point on, the child system will not be connected to the parent. This is similar to unlinkFromParent, but permissions are required for the parent system rather than the child system. Warning, this cannot be undone. operationId: unlinkChildren + security: + - TapisJWT: [] parameters: - name: parentSystemId in: path @@ -1131,12 +1216,14 @@ paths: If the *effectiveUserId* for the system is static (i.e. not *${apiUserId}*) then *{userName}* is interpreted as the host *loginUser* that is used when accessing the host. - Desired authentication method may be specified using query parameter authnMethod=. If desired + Desired authentication method may be specified using query parameter *authnMethod*. If desired authentication method not specified then credentials for the system's default authentication method are returned. The result includes the attribute *authnMethod* indicating the authentication method associated with the returned credentials. operationId: getUserCredential + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1210,6 +1297,8 @@ paths: By default credentials for LINUX and S3 type systems are verified. Use query parameter *skipCredentialCheck=true* to bypass initial credential validation. operationId: createUserCredential + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1269,6 +1358,8 @@ paths: If the *effectiveUserId* for the system is dynamic (i.e. equal to *${apiUserId}*) then the operation is allowed if *{userName}* is the Tapis user making the request. operationId: removeUserCredential + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1325,9 +1416,11 @@ paths: If the *effectiveUserId* for the system is dynamic (i.e. equal to *${apiUserId}*) then the operation is allowed if *{userName}* is the Tapis user making the request. - Desired authentication method may be specified using query parameter authnMethod=. If not specified, + Desired authentication method may be specified using query parameter *authnMethod*. If not specified, then credentials for the system's default authentication method are verified. operationId: checkUserCredential + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1376,14 +1469,15 @@ paths: application/json: schema: $ref: '#/components/schemas/RespBasic' - '/v3/systems/credential/globus/authUrl': + '/v3/systems/credential/{systemId}/globus/authUrl': get: tags: - Credentials summary: Retrieve a Globus URL that can be used to generate an authorization code for an OAuth2 flow. description: | - Retrieve a Globus URL + Session Id that can be used to generate an oauth2 authorization code. - In Globus the code is referred to as a *Native App Authorization Code*. + Retrieve a Globus URL + Session Id that can be used to generate an oauth2 authorization code associated with + the given system. + In Globus, the code is referred to as a *Native App Authorization Code*. The host property of the system is used as the Globus Endpoint Id or Globus Collection Id. Once a user has obtained an authorization code, the corresponding Systems endpoint for generating Globus tokens should be called to exchange the code + sessionId for a pair of access and refresh tokens. @@ -1392,6 +1486,14 @@ paths: Please note that the Tapis installation for your site must be configured by the site administrator to support systems of type GLOBUS. operationId: getGlobusAuthUrl + security: + - TapisJWT: [] + parameters: + - name: systemId + in: path + required: true + schema: + $ref: '#/components/schemas/IdString' responses: '200': description: Success. @@ -1437,6 +1539,8 @@ paths: Please note that the Tapis installation for your site must be configured by the site administrator to support systems of type GLOBUS. operationId: generateGlobusTokens + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1488,6 +1592,8 @@ paths: description: | Retrieve all system related permissions for a given system and user. operationId: getUserPerms + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1537,6 +1643,8 @@ paths: Create permissions in the Security Kernel for a user. Requester must be owner of the system. Permissions are READ, MODIFY, EXECUTE. MODIFY implies READ. operationId: grantUserPerms + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1589,6 +1697,8 @@ paths: Remove permissions from the Security Kernel for a user. Requester must be owner of the system. Permissions are READ, MODIFY, EXECUTE. operationId: revokeUserPerms + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1635,6 +1745,8 @@ paths: Remove system user permission from the Security Kernel. Requester must be owner of the system. Permissions are READ, MODIFY, EXECUTE. operationId: revokeUserPerm + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1687,6 +1799,8 @@ paths: Retrieve all sharing information for a system. This includes all users with whom the system has been shared and whether or not the system has been made publicly available. operationId: getShareInfo + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1734,6 +1848,8 @@ paths: *effectiveUserId*, sharing also allows for MODIFY access to all paths for calls made through the Files service. Requester must be owner of the system. operationId: shareSystem + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1784,6 +1900,8 @@ paths: to all paths for calls made through the Files service. Requester must be owner of the system. operationId: shareSystemPublic + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1818,6 +1936,8 @@ paths: Create or update sharing information for a system. The system will be unshared with the list of users provided in the request body. Requester must be owner of the system. operationId: unShareSystem + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1865,6 +1985,8 @@ paths: description: | Remove public sharing for a system. Requester must be owner of the system. operationId: unShareSystemPublic + security: + - TapisJWT: [] parameters: - name: systemId in: path @@ -1898,6 +2020,8 @@ paths: - Scheduler Profiles description: Retrieve list of scheduler profiles. operationId: getSchedulerProfiles + security: + - TapisJWT: [] responses: '200': description: Success. @@ -1941,6 +2065,8 @@ paths: - created - updated operationId: createSchedulerProfile + security: + - TapisJWT: [] requestBody: required: true description: A JSON object specifying information for the profile to be created. @@ -1986,6 +2112,8 @@ paths: description: | Retrieve information for a scheduler profile given the profile name. operationId: getSchedulerProfile + security: + - TapisJWT: [] parameters: - name: name in: path @@ -2029,6 +2157,8 @@ paths: description: | Remove a scheduler profile given the profile name. Requester must be owner of the profile. operationId: deleteSchedulerProfile + security: + - TapisJWT: [] parameters: - name: name in: path @@ -2116,17 +2246,10 @@ components: format: int32 dtnSystemId: $ref: '#/components/schemas/DtnSysId' - dtnMountPoint: - type: string - dtnMountSourcePath: - type: string isPublic: type: boolean isDynamicEffectiveUser: type: boolean - isDtn: - type: boolean - default: false canExec: type: boolean description: Indicates if system can be used to execute jobs. @@ -2136,6 +2259,10 @@ components: enableCmdPrefix: type: boolean default: false + allowChildren: + type: boolean + default: false + description: Indicates if system allows for the creation of child systems. mpiCmd: type: string minLength: 1 @@ -2181,8 +2308,6 @@ components: $ref: '#/components/schemas/TagString' notes: type: object - importRefId: - type: string uuid: type: string format: uuid @@ -2484,16 +2609,6 @@ components: format: int32 dtnSystemId: $ref: '#/components/schemas/DtnSysId' - dtnMountPoint: - type: string - description: Mount point (aka target) used when running the mount command on this system. Used during job execution. - dtnMountSourcePath: - type: string - description: The path exported by *dtnSystemId* that matches the *dtnMountPoint* on this system. This will be relative to *rootDir* on *dtnSystemId*. Used during job execution. - isDtn: - type: boolean - default: false - description: Indicates if system will be used as a data transfer node (DTN). canExec: type: boolean description: Indicates if system can be used to execute jobs. @@ -2505,6 +2620,10 @@ components: type: boolean default: false description: Indicates if system allows a job submission request to specify a *cmdPrefix*. + allowChildren: + type: boolean + default: false + description: Indicates if system allows for the creation of child systems. mpiCmd: type: string minLength: 1 @@ -2546,8 +2665,6 @@ components: notes: type: object description: Metadata in the form of a Json object. Not used by Tapis. - importRefId: - type: string ReqPostChildSystem: type: object required: @@ -2586,16 +2703,16 @@ components: format: int32 dtnSystemId: $ref: '#/components/schemas/DtnSysId' - dtnMountPoint: - type: string - dtnMountSourcePath: - type: string canRunBatch: type: boolean description: Indicates if system supports running jobs using a batch scheduler. enableCmdPrefix: type: boolean default: false + allowChildren: + type: boolean + default: false + description: Indicates if system allows for the creation of child systems. mpiCmd: type: string minLength: 1 @@ -2635,8 +2752,6 @@ components: $ref: '#/components/schemas/TagString' notes: type: object - importRefId: - type: string ReqPatchSystem: type: object properties: @@ -2660,10 +2775,6 @@ components: format: int32 dtnSystemId: $ref: '#/components/schemas/DtnSysId' - dtnMountPoint: - type: string - dtnMountSourcePath: - type: string canRunBatch: type: boolean description: Indicates if system supports running jobs using a batch scheduler. @@ -2671,6 +2782,7 @@ components: type: boolean allowChildren: type: boolean + description: Indicates if system allows for the creation of child systems. mpiCmd: type: string minLength: 1 @@ -2710,8 +2822,6 @@ components: $ref: '#/components/schemas/TagString' notes: type: object - importRefId: - type: string ReqPostPutCredential: type: object properties: @@ -3078,6 +3188,8 @@ components: type: string sesssionId: type: string + systemId: + type: string RespShareInfo: type: object properties: diff --git a/tapipy/resources/resource_etags.json b/tapipy/resources/resource_etags.json index 7cd04ae..b52b096 100644 --- a/tapipy/resources/resource_etags.json +++ b/tapipy/resources/resource_etags.json @@ -1 +1 @@ -{"actors": "W/\"790d47ac8c8c65e702080db8e564c154ee3ba420\"", "authenticator": "W/\"209d4f68d29b92e19b8c550282ff1f88b64ea58f\"", "meta": "W/\"e49bc20f1b1e3296278db40fdd6b33d92c44beea\"", "sk": "W/\"1d60d95dc6576e129817e90c107bd4daf0200e4c\"", "streams": "W/\"1455851696f5beb5a505b652506c24ad924aea81\"", "systems": "W/\"abe787ca9c9bfbfd4086d988ae043147ee2e4eb3\"", "tenants": "W/\"19937e762737fba432a63c8f32e824c182d2e973\"", "tokens": "W/\"ab429c47e75f628ae31c7e0ba77a8320604fc178\"", "pgrest": "W/\"6494d146379036fdd95de0ed7da22d1bec1335cd\"", "pods": "W/\"da6d18aa800a7950aa7d49bce088a29ea99ad2d5\"", "jobs": "W/\"6d072a998ee9adbca4430d41a3b7a8faf76b5df0\"", "apps": "W/\"e8e10c6ddac84baeaa2df0594250d0db763ba113\"", "notifications": "W/\"a23be9cd9fab948fe4aefd9dcfa331b2452297dc\"", "files": "W/\"ee9feff01143ad76c0cc8cfd66c4c2706e0226a8\"", "workflows": "W/\"65d8905a3806bedb35b2df205f27c4ca17a0b592\"", "globus-proxy": "W/\"a2053ae9cc8c77b367005c77a93a24f13d8f096a\""} +{"actors": "W/\"790d47ac8c8c65e702080db8e564c154ee3ba420\"", "authenticator": "W/\"209d4f68d29b92e19b8c550282ff1f88b64ea58f\"", "meta": "W/\"e49bc20f1b1e3296278db40fdd6b33d92c44beea\"", "sk": "W/\"1d60d95dc6576e129817e90c107bd4daf0200e4c\"", "streams": "W/\"1455851696f5beb5a505b652506c24ad924aea81\"", "systems": "W/\"9b74cfef01a785fb1349f35a4ebc72147616ae9e\"", "tenants": "W/\"19937e762737fba432a63c8f32e824c182d2e973\"", "tokens": "W/\"ab429c47e75f628ae31c7e0ba77a8320604fc178\"", "pgrest": "W/\"6494d146379036fdd95de0ed7da22d1bec1335cd\"", "pods": "W/\"ce06400396fe6b69997e662a16306a5adb8d2331\"", "jobs": "W/\"5a1feda480b454344b4b76e7453d893b5984d3f7\"", "apps": "W/\"13ecbdbfca73c5c8938bb3c97161f44571ad7f18\"", "notifications": "W/\"a1c96a78c82a6ad9986f81a9dc13556adfa71516\"", "files": "W/\"ee9feff01143ad76c0cc8cfd66c4c2706e0226a8\"", "workflows": "W/\"65d8905a3806bedb35b2df205f27c4ca17a0b592\"", "globus-proxy": "W/\"71f32672ab00802ff9a3b87e5c0754e7a4cd2279\""} \ No newline at end of file diff --git a/tapipy/specs/v2-tapis-project-tapipy-main-tapipy-resources-openapi_v3-pods.pickle b/tapipy/specs/v2-tapis-project-tapipy-main-tapipy-resources-openapi_v3-pods.pickle index 1c1baa4ed3bd47e5d611feaa5eadeb6e028b3136..128a1bb09e31ad7d7b2f3f27a529e1766367130e 100644 GIT binary patch literal 106898 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 literal 60738 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 RaL&{he0z0-J?92p{|{?RGCu$S diff --git a/tapipy/specs/v2-tapis-project-tapipy-prod-tapipy-resources-openapi_v3-apps.pickle b/tapipy/specs/v2-tapis-project-tapipy-prod-tapipy-resources-openapi_v3-apps.pickle index ab78c233ffd9aaba777243dc0733dd97e199c741..44be60660629a1da9cc88d872aa3ae2dc3d9b9e6 100644 GIT binary patch literal 82025 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 KL?_5m;r|1Na=BIj diff --git a/tapipy/specs/v2-tapis-project-tapipy-prod-tapipy-resources-openapi_v3-globus-proxy.pickle b/tapipy/specs/v2-tapis-project-tapipy-prod-tapipy-resources-openapi_v3-globus-proxy.pickle index 3702949a1d952c13db0964fa5103e89036543922..33283a60a63ec5c441abb66b121776e5fcbfb2de 100644 GIT binary patch delta 4687 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 diff --git a/tapipy/specs/v2-tapis-project-tapipy-prod-tapipy-resources-openapi_v3-jobs.pickle b/tapipy/specs/v2-tapis-project-tapipy-prod-tapipy-resources-openapi_v3-jobs.pickle index 869d1c4b7d9fbea9e56f2c661299fda7aa032c15..133d92d8387a65f3e45849adde628ea339f94cef 100644 GIT binary patch literal 62672 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 literal 58674 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 QI&HYc##Ny}Cc)tU2dI-thX4Qo diff --git a/tapipy/specs/v2-tapis-project-tapipy-prod-tapipy-resources-openapi_v3-notifications.pickle b/tapipy/specs/v2-tapis-project-tapipy-prod-tapipy-resources-openapi_v3-notifications.pickle index 741a36439b025aeb803e15fa113e63f16bc7efd5..cf439ca11becb6221ce9fe2f20296ed6231042b0 100644 GIT binary patch delta 1880 zcmbtTO>9(E6z-dLXcpT7z6~)^NsWmn7$qSt2nnv-867A;F-AT2zF{n6Fj6-!=bU%W z{m%E@@7`F*d~z<6eZA+zhL4VA7rZT}zX>1r&Lms5y__+R#DivQe$2eEqc8dGiE9sJ zo84Vq`^*>H9yH(Xdo0 zy3T1tu}Ikbe0dfoNR3I?I%|LDY654tg2N$Ak?4#~nS>e<3gSs15YQ@{p$GvyR&0{_ zM6sBU!fSCJaEz*He@%za1sHhk&mwRbZ;wY~F-a>CUx$XE+I!UJewjKFRb z8zdFXN}VFbBA~)?wJL}igmOBdA_0b4}Q6qf2-6p148%&-#6m-w${>?PE{2?eFYb_E7vrv%Q z4{@E=VnhGkF*)n4C5}PE%-`V{_&<(8hc5M}dkJ^3vvVhExo%U7hSSx`ng*y*^0E5G zd$JI|Snn;THfx0MzOBQbjyABiLgLHfm4>YMSV0@N7o%0?z51g`GIM{%`_O##+QmW< zUq;E@g9G4XnvQ|BsPqM^S;v>$rJAp2M+Z99`;YADZ&{;MCX$x4s1^;GTl3InAKz=4 z_*0uZ&Aor*bjP`8w|D4vyCEHP@$R7We)r9-O)kuh_H9LFqSA`4u75;{2&OVBEJvsjYpJw^*QHdDvfnxW%6 z2foA1t^U0&M?c`#tggSg^WM2y>RLzrx5!^MYs{7WpICOww9q{Vlj%*M0p9AoL$_G# zFbXeO=w;AOemwD4_xfM^QmCkj0n*1)uiDZj$M0IQc=F*aZMOG0;L3}3?eHZRUw83J l7vFU8D=z-s#jm>f4;TCC@%K({*}7`2!%4Tnka_CV=-=o~k_!L; delta 800 zcmb7=O=uHA9L0CGO&U!VLsbG&JHbm1wHU0G=(?V&NDrlo(1WtwY^L2=(%mq-8;NwI z;6V=_lxZS*5ENsN5(Y$36nYR5Pu^@5dlp-%UqL<8*@<6=9CQxDZyx{m=FQxBE;r}p z$l1r(Y~AO(I6e@^hjg?y zLJ!uGbf_L|`E!+(_!aSjk}DsQP2NCc{|x&UHnP4)8Mp;OJRw5U+}=a`>*rhLCa$e@ z^u3I=eAXfdUX72u4y9M3!y8t)zCD88{tiDb$mD0o8fXTe4* U{=b_3^*+t_)GW=7UXvw0TF#u&L~x@M|NJ>5-L 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 HZ2A8WO+PST literal 87940 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 diff --git a/tapipy/specs/v2-tapis-project-tapipy-prod-tapipy-resources-openapi_v3-workflows.pickle b/tapipy/specs/v2-tapis-project-tapipy-prod-tapipy-resources-openapi_v3-workflows.pickle index f563fdb300da6dc82c54a456cf85ae06bf4764b7..f7c7a9894b1476849c71ae4dd76b414367ae36b8 100644 GIT binary patch literal 338448 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 literal 253213 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 diff --git a/tests/tapipy-tests.py b/tests/tapipy-tests.py index 846abf0..d2980d3 100644 --- a/tests/tapipy-tests.py +++ b/tests/tapipy-tests.py @@ -330,7 +330,7 @@ def test_download_service_dev_specs(): password=PASSWORD, resource_set="dev") t.get_tokens() - return t + assert(t) def test_download_service_local_specs(): t = Tapis(base_url=BASE_URL, @@ -338,4 +338,4 @@ def test_download_service_local_specs(): password=PASSWORD, resource_set="local") t.get_tokens() - return t + assert(t) From 0d58a4c4ea365b4898e448e5ae456839b0d3acce Mon Sep 17 00:00:00 2001 From: "Christian R. Garcia" Date: Tue, 17 Sep 2024 15:25:07 -0700 Subject: [PATCH 6/9] 1.7.0 ready, 3.7 compatible. --- pyproject-alpha3.toml | 33 +++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- tapipy/configuration.py | 3 ++- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 pyproject-alpha3.toml diff --git a/pyproject-alpha3.toml b/pyproject-alpha3.toml new file mode 100644 index 0000000..3e6b4b3 --- /dev/null +++ b/pyproject-alpha3.toml @@ -0,0 +1,33 @@ +[tool.poetry] +name = "tapipy" +version = "1.6.4a3" +description = "Python lib for interacting with an instance of the Tapis API Framework" +license = "BSD-4-Clause" +authors = ["Joe Stubbs "] +maintainers = ["Joe Stubbs ", + "Christian Garcia "] +readme = "README.md" +repository = "https://github.com/tapis-project/tapipy" +include = ["tapipy/specs", + "tapipy/resources"] + +[tool.poetry.dependencies] +python = "^3.8" +certifi = ">= 2020.11.8" +six = "^1.10" +python_dateutil = "^2.5.3" +setuptools = ">= 21.0.0" +urllib3 = "^1.26.5" +PyJWT = ">= 1.7.1" +openapi_core = "0.19.3" +requests = "^2.20.0" +atomicwrites = "^1.4.0" +cryptography = ">= 3.3.2" +# jsonschema from 4.0.0 -> 4.3.0 slows tapipy import to 8+ seconds. +jsonschema = "^4.18.0" +pyyaml = ">= 5.4" +cloudpickle = ">= 1.6.0" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/pyproject.toml b/pyproject.toml index 099fa52..4d71028 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tapipy" -version = "1.6.3" +version = "1.7.0" description = "Python lib for interacting with an instance of the Tapis API Framework" license = "BSD-4-Clause" authors = ["Joe Stubbs "] diff --git a/tapipy/configuration.py b/tapipy/configuration.py index d373469..1f44299 100644 --- a/tapipy/configuration.py +++ b/tapipy/configuration.py @@ -1,4 +1,5 @@ -from typing import List, Union, Literal +from typing import List, Union +from typing_extensions import Literal from tapipy import errors From e086884a936315504808690583b97f13bfb3d759 Mon Sep 17 00:00:00 2001 From: "Christian R. Garcia" Date: Tue, 17 Sep 2024 15:32:42 -0700 Subject: [PATCH 7/9] run.sh as main entrypoint --- Dockerfile-tests | 4 +++- tests/run.sh | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Dockerfile-tests b/Dockerfile-tests index 9ec8f3a..c803df2 100644 --- a/Dockerfile-tests +++ b/Dockerfile-tests @@ -14,6 +14,7 @@ RUN python -m pip install --upgrade pip # Moving files ADD tests/tapipy-tests.py /home/tapis/tapipy-tests.py +#ADD tests /home/tapis/tests # Add tapipy files and build with Poetry build. ADD . /home/tapis/tapipy-install-dir @@ -25,4 +26,5 @@ RUN pip install *.whl WORKDIR /home/tapis # Testing -ENTRYPOINT ["pytest", "--verbose", "/home/tapis/tapipy-tests.py"] \ No newline at end of file +ENTRYPOINT ["pytest", "--verbose", "/home/tapis/tapipy-tests.py"] +#ENTRYPOINT [ "/home/tapis/tests/run.sh" ] \ No newline at end of file diff --git a/tests/run.sh b/tests/run.sh index a304544..759e3a7 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -8,4 +8,6 @@ cd ../ python3 -m unittest -v tests.TestConfig python3 -m unittest -v tests.TestUtils -python3 -m unittest -v tests.TestRetriableDecorator \ No newline at end of file +python3 -m unittest -v tests.TestRetriableDecorator + +pytest --verbose /home/tapis/tapipy-tests.py \ No newline at end of file From b5cf4dd3332a05269ba8533af7ec45506d5c884a Mon Sep 17 00:00:00 2001 From: "Christian R. Garcia" Date: Tue, 17 Sep 2024 16:28:21 -0700 Subject: [PATCH 8/9] Fixed config init time. Set default retriable configs for init tapis client calls. --- Dockerfile-tests | 6 +++--- tapipy/tapis.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Dockerfile-tests b/Dockerfile-tests index c803df2..e300ccd 100644 --- a/Dockerfile-tests +++ b/Dockerfile-tests @@ -14,7 +14,7 @@ RUN python -m pip install --upgrade pip # Moving files ADD tests/tapipy-tests.py /home/tapis/tapipy-tests.py -#ADD tests /home/tapis/tests +ADD tests /home/tapis/tests # Add tapipy files and build with Poetry build. ADD . /home/tapis/tapipy-install-dir @@ -26,5 +26,5 @@ RUN pip install *.whl WORKDIR /home/tapis # Testing -ENTRYPOINT ["pytest", "--verbose", "/home/tapis/tapipy-tests.py"] -#ENTRYPOINT [ "/home/tapis/tests/run.sh" ] \ No newline at end of file +#ENTRYPOINT ["pytest", "--verbose", "/home/tapis/tapipy-tests.py"] +ENTRYPOINT [ "/home/tapis/tests/run.sh" ] \ No newline at end of file diff --git a/tapipy/tapis.py b/tapipy/tapis.py index d545d24..b552a95 100644 --- a/tapipy/tapis.py +++ b/tapipy/tapis.py @@ -341,8 +341,9 @@ def __init__(self, tapi_client): def reload_tenants(self): try: - sites = self.tapi_client.tenants.list_sites() - tenants = self.tapi_client.tenants.list_tenants() + # _config required to ensure user client-level config isn't applied during Tapipy initialization + sites = self.tapi_client.tenants.list_sites(_config=Config()) + tenants = self.tapi_client.tenants.list_tenants(_config=Config()) except Exception as e: raise errors.BaseTapyException(f"Unable to retrieve sites and tenants from the Tenants API. e: {e}") for t in tenants: @@ -495,6 +496,14 @@ def __init__(self, # method signature should be def fn(op: Opertaion, response: Response, **kwargs) self.plugin_on_call_post_request_callables = [] + self.config = config + if type(config) == dict: + self.config = Config(**config) + # Set the configuration object + if type(config) not in [Config, dict]: + raise TypeError("Tapis Client Config must be an instance of 'Config' or a dictionary") + + # we lazy-load the tenant_cache to prevent making a call to the Tenants API when not needed. if tenants: self.tenant_cache = tenants @@ -507,13 +516,7 @@ def __init__(self, if t.base_url == base_url: self.tenant_id = t.tenant_id - # Set the configuration object - if type(config) not in [Config, dict]: - raise TypeError("Tapis Client Config must be an instance of 'Config' or a dictionary") - self.config = config - if type(config) == dict: - self.config = Config(**config) for p in plugins: # call each plugin's on_tapis_client_instantiation() function with all args passed in. From 0fe830e93495148882fa7f72e6078e7b6ba23a94 Mon Sep 17 00:00:00 2001 From: "Christian R. Garcia" Date: Fri, 15 Nov 2024 19:42:50 -0800 Subject: [PATCH 9/9] Added _x_tapis_tracking_id --- CHANGELOG.md | 11 +++++++ pyproject.toml | 2 +- tapipy/__init__.py | 2 +- tapipy/tapis.py | 51 +++++++++++++++++++++++++++++++ tests/tapipy-tests.py | 70 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f23e74d..6755f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. +## 1.7.1 - 2024-11-15 +### Added +- Adding support for _x_tapis_tracking_id variable with validation. This sends the header when users call tapipy operations and specify the value in the call. + +### Changed +- No change. + +### Removed +- No change. + + ## 1.7.0 - 2024-09-13 ### Added - Poetry lock update diff --git a/pyproject.toml b/pyproject.toml index 4d71028..8be1134 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tapipy" -version = "1.7.0" +version = "1.7.1" description = "Python lib for interacting with an instance of the Tapis API Framework" license = "BSD-4-Clause" authors = ["Joe Stubbs "] diff --git a/tapipy/__init__.py b/tapipy/__init__.py index 0e1a38d..48c2f6b 100644 --- a/tapipy/__init__.py +++ b/tapipy/__init__.py @@ -1 +1 @@ -__version__ = '1.7.0' +__version__ = '1.7.1' diff --git a/tapipy/tapis.py b/tapipy/tapis.py index b552a95..be3d23e 100644 --- a/tapipy/tapis.py +++ b/tapipy/tapis.py @@ -1100,6 +1100,57 @@ def __call__(self, **kwargs): raise errors.InvalidInputError( msg="The headers argument, if passed, must be a dictionary-like object.") + # if X-Tapis-Tracking_ID (regardless of case) is in the headers we need to set tracking_id for validation + tracking_id = None + for k, v in headers.items(): + if k.lower() == 'x-tapis-tracking-id' or k.lower() == 'x_tapis_tracking_id': + tracking_id = headers.pop(k) + break + if '_x_tapis_tracking_id' in kwargs: + if tracking_id: + raise errors.InvalidInputError(msg="The _x_tapis_tracking_id argument and the X-Tapis-Tracking-ID header cannot both be set.") + else: + tracking_id = kwargs.pop('_x_tapis_tracking_id') + + # tracking_id header needs to be passed through to __call__ headers for splunk audit trails + if tracking_id: + try: + if not isinstance(tracking_id, str): + raise errors.InvalidInputError( + msg="The _x_tapis_tracking_id argument, if passed, must be a string.") + + if not tracking_id.isascii(): + raise errors.InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed be a single period, followed by an ASCII universally unique identifier string. Must be an entirely ASCII string.") + + if len(tracking_id) > 126: + raise errors.InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed be a single period, followed by an ASCII universally unique identifier string. Must be less than 126 characters.") + + # only one . in string + if tracking_id.count('.') != 1: + raise errors.InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed be a single period, followed by an ASCII universally unique identifier string. count('.') != 1.") + + # ensure doesn't start or end with . + if tracking_id.startswith('.') or tracking_id.endswith('.'): + raise errors.InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed be a single period, followed by an ASCII universally unique identifier string. Cannot start or end with '.'.") + + tracking_namespace, tracking_unique_identifier = tracking_id.split('.') + # check namespace is alphanumeric + underscores + if not all(c.isalnum() or c == '_' for c in tracking_namespace): + raise errors.InvalidInputError(msg="Error: tracking_namespace contains invalid characters. Alphanumeric + underscores only.") + + # check tracking_unique_identifier is alphanumeric + hyphens + if not all(c.isalnum() or c == '-' for c in tracking_unique_identifier): + raise errors.InvalidInputError(msg="Error: tracking_unique_identifier contains invalid characters. Alphanumeric + hyphens only.") + + headers.update({'X-Tapis-Tracking-ID': tracking_id}) + except ValueError: + raise errors.InvalidInputError( + msg=f"_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed be a single period, followed by an ASCII universally unique identifier string. Got x_tapis_tracking_id: {tracking_id}") + # construct the data - data = None files = None diff --git a/tests/tapipy-tests.py b/tests/tapipy-tests.py index d2980d3..606b4c4 100644 --- a/tests/tapipy-tests.py +++ b/tests/tapipy-tests.py @@ -6,6 +6,7 @@ import subprocess import pytest from tapipy.tapis import Tapis, TapisResult +from tapipy.errors import InvalidInputError BASE_URL = os.getenv("base_url", "https://dev.develop.tapis.io") @@ -24,6 +25,7 @@ def client(): # ----------------------------------------------------- # Tests to check parsing of different result structures - # ----------------------------------------------------- + def test_tapisresult_list_simple(): result = ['a', 1, 'b', True, None, 3.14159, b'some bytes'] tr = TapisResult(result) @@ -308,6 +310,74 @@ def test_debug_flag_tenants(client): assert hasattr(debug.request, 'url') assert hasattr(debug.response, 'content') +# ---------------- +# tracking_id tests - Confluence: Proposal for File Provenance Auditing +# ---------------- + +def validate_tracking_id(tracking_id): + if not isinstance(tracking_id, str): + raise InvalidInputError( + msg="The _x_tapis_tracking_id argument, if passed, must be a string.") + + if not tracking_id.isascii(): + raise InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed by a single period, followed by an ASCII universally unique identifier string. Must be an entirely ASCII string.") + + if len(tracking_id) > 126: + raise InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed by a single period, followed by an ASCII universally unique identifier string. Must be less than 126 characters.") + + if tracking_id.count('.') != 1: + raise InvalidInputError( + msg="_x_tapis_tracking_id validation error. .. namespace is a non-empty, ASCII string of alphanumeric characters and underscores, followed by a single period, followed by an ASCII universally unique identifier string. count('.') != 1.") + + tracking_namespace, tracking_id = tracking_id.split('.') + if not all(c.isalnum() or c == '_' for c in tracking_namespace): + raise InvalidInputError(msg="Error: tracking_namespace contains invalid characters. Alphanumeric + underscores only.") + + if not all(c.isalnum() or c == '-' for c in tracking_id): + raise InvalidInputError(msg="Error: tracking_id contains invalid characters. Alphanumeric + hyphens only.") + +def test_tracking_id_validation(client): + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id=True) # Not a string + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.iden.tifier") # More than one period + + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifier") # Should work + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifier-with-non-ascii-字符") # Non-ASCII characters + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifier" * 10) # Length > 126 + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespaceidentifier") # No period + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace..identifier") # More than one period + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifi.er") # More than one period + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifi_er") # id only allowed alphanumeric and hyphens after . + + result = client.tenants.list_tenants(_x_tapis_tracking_id="names_ace.identifi-er") # namespace only allowed alphanumeric and underscores (This is proper) + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace!@#.identifier") # Invalid characters in namespace + + with pytest.raises(InvalidInputError): + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifier!@#") # Invalid characters in identifier + + # Valid case + try: + result = client.tenants.list_tenants(_x_tapis_tracking_id="namespace.identifier") + except InvalidInputError: + pytest.fail("validate_tracking_id() raised InvalidInputError unexpectedly!") # ----------------------- # Tapipy import timing test -