Shaman: allow Manager to determine the final checkout path

The checkout request now responds with the final checkout path. This makes
it possible for the Manager to ensure the checkout is unique.
This commit is contained in:
Sybren A. Stüvel 2022-03-25 13:11:13 +01:00
parent 0e682282f0
commit 724938c7ae
24 changed files with 521 additions and 160 deletions

View File

@ -19,14 +19,14 @@ if TYPE_CHECKING:
from ..manager import ApiClient as _ApiClient from ..manager import ApiClient as _ApiClient
from ..manager.models import ( from ..manager.models import (
ShamanCheckoutResult as _ShamanCheckoutResult,
ShamanRequirementsRequest as _ShamanRequirementsRequest, ShamanRequirementsRequest as _ShamanRequirementsRequest,
ShamanRequirementsResponse as _ShamanRequirementsResponse,
ShamanFileSpec as _ShamanFileSpec, ShamanFileSpec as _ShamanFileSpec,
) )
else: else:
_ApiClient = object _ApiClient = object
_ShamanCheckoutResult = object
_ShamanRequirementsRequest = object _ShamanRequirementsRequest = object
_ShamanRequirementsResponse = object
_ShamanFileSpec = object _ShamanFileSpec = object
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -58,10 +58,14 @@ class Packer(bat_pack.Packer): # type: ignore
super().__init__(blendfile, project_root, target, **kwargs) super().__init__(blendfile, project_root, target, **kwargs)
self.checkout_path = checkout_path self.checkout_path = checkout_path
self.api_client = api_client self.api_client = api_client
self.shaman_transferrer: Optional[Transferrer] = None
# Mypy doesn't understand that bat_transfer.FileTransferer exists. # Mypy doesn't understand that bat_transfer.FileTransferer exists.
def _create_file_transferer(self) -> bat_transfer.FileTransferer: # type: ignore def _create_file_transferer(self) -> bat_transfer.FileTransferer: # type: ignore
return Transferrer(self.api_client, self.project, self.checkout_path) self.shaman_transferrer = Transferrer(
self.api_client, self.project, self.checkout_path
)
return self.shaman_transferrer
def _make_target_path(self, target: str) -> PurePath: def _make_target_path(self, target: str) -> PurePath:
return PurePosixPath("/") return PurePosixPath("/")
@ -70,10 +74,11 @@ class Packer(bat_pack.Packer): # type: ignore
def output_path(self) -> PurePath: def output_path(self) -> PurePath:
"""The path of the packed blend file in the target directory.""" """The path of the packed blend file in the target directory."""
assert self._output_path is not None assert self._output_path is not None
assert self.shaman_transferrer is not None
checkout_location = PurePosixPath(self.checkout_path) checkout_root = PurePosixPath(self.shaman_transferrer.checkout_path)
rel_output = self._output_path.relative_to(self._target_path) rel_output = self._output_path.relative_to(self._target_path)
out_path: PurePath = checkout_location / rel_output out_path: PurePath = checkout_root / rel_output
return out_path return out_path
def execute(self): def execute(self):
@ -149,7 +154,10 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore
return return
self.log.info("All files uploaded succesfully") self.log.info("All files uploaded succesfully")
self._request_checkout(shaman_file_specs) checkout_result = self._request_checkout(shaman_file_specs)
# Update our checkout path to match the one received from the Manager.
self.checkout_path = checkout_result.checkout_path
def _upload_missing_files( def _upload_missing_files(
self, shaman_file_specs: _ShamanRequirementsRequest self, shaman_file_specs: _ShamanRequirementsRequest
@ -407,14 +415,16 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore
raise self.AbortUpload("interrupting ongoing upload") raise self.AbortUpload("interrupting ongoing upload")
super().report_transferred(bytes_transferred) super().report_transferred(bytes_transferred)
def _request_checkout(self, shaman_file_specs: _ShamanRequirementsRequest) -> None: def _request_checkout(
self, shaman_file_specs: _ShamanRequirementsRequest
) -> Optional[_ShamanCheckoutResult]:
"""Ask the Shaman to create a checkout of this BAT pack.""" """Ask the Shaman to create a checkout of this BAT pack."""
if not self.checkout_path: if not self.checkout_path:
self.log.warning("NOT requesting checkout at Shaman") self.log.warning("NOT requesting checkout at Shaman")
return return None
from ..manager.models import ShamanCheckout from ..manager.models import ShamanCheckout, ShamanCheckoutResult
from ..manager.exceptions import ApiException from ..manager.exceptions import ApiException
self.log.info( self.log.info(
@ -427,7 +437,9 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore
) )
try: try:
self.shaman_api.shaman_checkout(checkoutRequest) result: ShamanCheckoutResult = self.shaman_api.shaman_checkout(
checkoutRequest
)
except ApiException as ex: except ApiException as ex:
match ex.status: match ex.status:
case 424: # Files were missing case 424: # Files were missing
@ -444,9 +456,10 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore
) )
self.log.error(msg) self.log.error(msg)
self.error_set(msg) self.error_set(msg)
return return None
self.log.info("Shaman created checkout at %s", self.checkout_path) self.log.info("Shaman created checkout at %s", result.checkout_path)
return result
def make_file_spec_hashable(spec: _ShamanFileSpec) -> HashableShamanFileSpec: def make_file_spec_hashable(spec: _ShamanFileSpec) -> HashableShamanFileSpec:

View File

@ -10,7 +10,7 @@
""" """
__version__ = "8a97cf50-dirty" __version__ = "f66594b0-dirty"
# import ApiClient # import ApiClient
from flamenco.manager.api_client import ApiClient from flamenco.manager.api_client import ApiClient

View File

@ -23,6 +23,7 @@ from flamenco.manager.model_utils import ( # noqa: F401
) )
from flamenco.manager.model.error import Error from flamenco.manager.model.error import Error
from flamenco.manager.model.shaman_checkout import ShamanCheckout from flamenco.manager.model.shaman_checkout import ShamanCheckout
from flamenco.manager.model.shaman_checkout_result import ShamanCheckoutResult
from flamenco.manager.model.shaman_requirements_request import ShamanRequirementsRequest from flamenco.manager.model.shaman_requirements_request import ShamanRequirementsRequest
from flamenco.manager.model.shaman_requirements_response import ShamanRequirementsResponse from flamenco.manager.model.shaman_requirements_response import ShamanRequirementsResponse
from flamenco.manager.model.shaman_single_file_status import ShamanSingleFileStatus from flamenco.manager.model.shaman_single_file_status import ShamanSingleFileStatus
@ -41,7 +42,7 @@ class ShamanApi(object):
self.api_client = api_client self.api_client = api_client
self.shaman_checkout_endpoint = _Endpoint( self.shaman_checkout_endpoint = _Endpoint(
settings={ settings={
'response_type': None, 'response_type': (ShamanCheckoutResult,),
'auth': [], 'auth': [],
'endpoint_path': '/shaman/checkout/create', 'endpoint_path': '/shaman/checkout/create',
'operation_id': 'shaman_checkout', 'operation_id': 'shaman_checkout',
@ -312,7 +313,7 @@ class ShamanApi(object):
async_req (bool): execute request asynchronously async_req (bool): execute request asynchronously
Returns: Returns:
None ShamanCheckoutResult
If the method is called asynchronously, returns the request If the method is called asynchronously, returns the request
thread. thread.
""" """

View File

@ -76,7 +76,7 @@ class ApiClient(object):
self.default_headers[header_name] = header_value self.default_headers[header_name] = header_value
self.cookie = cookie self.cookie = cookie
# Set default User-Agent. # Set default User-Agent.
self.user_agent = 'Flamenco/8a97cf50-dirty (Blender add-on)' self.user_agent = 'Flamenco/f66594b0-dirty (Blender add-on)'
def __enter__(self): def __enter__(self):
return self return self

View File

@ -404,7 +404,7 @@ conf = flamenco.manager.Configuration(
"OS: {env}\n"\ "OS: {env}\n"\
"Python Version: {pyversion}\n"\ "Python Version: {pyversion}\n"\
"Version of the API: 1.0.0\n"\ "Version of the API: 1.0.0\n"\
"SDK Package Version: 8a97cf50-dirty".\ "SDK Package Version: f66594b0-dirty".\
format(env=sys.platform, pyversion=sys.version) format(env=sys.platform, pyversion=sys.version)
def get_host_settings(self): def get_host_settings(self):

View File

@ -11,7 +11,7 @@ Method | HTTP request | Description
# **shaman_checkout** # **shaman_checkout**
> shaman_checkout(shaman_checkout) > ShamanCheckoutResult shaman_checkout(shaman_checkout)
Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint. Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint.
@ -24,6 +24,7 @@ import flamenco.manager
from flamenco.manager.api import shaman_api from flamenco.manager.api import shaman_api
from flamenco.manager.model.error import Error from flamenco.manager.model.error import Error
from flamenco.manager.model.shaman_checkout import ShamanCheckout from flamenco.manager.model.shaman_checkout import ShamanCheckout
from flamenco.manager.model.shaman_checkout_result import ShamanCheckoutResult
from pprint import pprint from pprint import pprint
# Defining the host is optional and defaults to http://localhost # Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters. # See configuration.py for a list of all supported configuration parameters.
@ -50,7 +51,8 @@ with flamenco.manager.ApiClient() as api_client:
# example passing only required values which don't have defaults set # example passing only required values which don't have defaults set
try: try:
# Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint. # Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint.
api_instance.shaman_checkout(shaman_checkout) api_response = api_instance.shaman_checkout(shaman_checkout)
pprint(api_response)
except flamenco.manager.ApiException as e: except flamenco.manager.ApiException as e:
print("Exception when calling ShamanApi->shaman_checkout: %s\n" % e) print("Exception when calling ShamanApi->shaman_checkout: %s\n" % e)
``` ```
@ -64,7 +66,7 @@ Name | Type | Description | Notes
### Return type ### Return type
void (empty response body) [**ShamanCheckoutResult**](ShamanCheckoutResult.md)
### Authorization ### Authorization
@ -80,7 +82,7 @@ No authorization required
| Status code | Description | Response headers | | Status code | Description | Response headers |
|-------------|-------------|------------------| |-------------|-------------|------------------|
**204** | Checkout was created succesfully. | - | **200** | Checkout was created succesfully. | - |
**424** | There were files missing. Use `shamanCheckoutRequirements` to figure out which ones. | - | **424** | There were files missing. Use `shamanCheckoutRequirements` to figure out which ones. | - |
**409** | Checkout already exists. | - | **409** | Checkout already exists. | - |
**0** | unexpected error | - | **0** | unexpected error | - |

View File

@ -6,7 +6,7 @@ Set of files with their SHA256 checksum, size in bytes, and desired location in
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**files** | [**[ShamanFileSpec]**](ShamanFileSpec.md) | | **files** | [**[ShamanFileSpec]**](ShamanFileSpec.md) | |
**checkout_path** | **str** | Path where the Manager should create this checkout, It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`. | **checkout_path** | **str** | Path where the Manager should create this checkout. It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`. |
**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] **any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -0,0 +1,13 @@
# ShamanCheckoutResult
The result of a Shaman checkout.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**checkout_path** | **str** | Path where the Manager created this checkout. This can be different than what was requested, as the Manager will ensure a unique directory. The path is relative to the Shaman checkout path as configured on the Manager. |
**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -113,7 +113,7 @@ class ShamanCheckout(ModelNormal):
Args: Args:
files ([ShamanFileSpec]): files ([ShamanFileSpec]):
checkout_path (str): Path where the Manager should create this checkout, It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`. checkout_path (str): Path where the Manager should create this checkout. It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`.
Keyword Args: Keyword Args:
_check_type (bool): if True, values for parameters in openapi_types _check_type (bool): if True, values for parameters in openapi_types
@ -200,7 +200,7 @@ class ShamanCheckout(ModelNormal):
Args: Args:
files ([ShamanFileSpec]): files ([ShamanFileSpec]):
checkout_path (str): Path where the Manager should create this checkout, It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`. checkout_path (str): Path where the Manager should create this checkout. It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`.
Keyword Args: Keyword Args:
_check_type (bool): if True, values for parameters in openapi_types _check_type (bool): if True, values for parameters in openapi_types

View File

@ -0,0 +1,261 @@
"""
Flamenco manager
Render Farm manager API # noqa: E501
The version of the OpenAPI document: 1.0.0
Generated by: https://openapi-generator.tech
"""
import re # noqa: F401
import sys # noqa: F401
from flamenco.manager.model_utils import ( # noqa: F401
ApiTypeError,
ModelComposed,
ModelNormal,
ModelSimple,
cached_property,
change_keys_js_to_python,
convert_js_args_to_python_args,
date,
datetime,
file_type,
none_type,
validate_get_composed_info,
OpenApiModel
)
from flamenco.manager.exceptions import ApiAttributeError
class ShamanCheckoutResult(ModelNormal):
"""NOTE: This class is auto generated by OpenAPI Generator.
Ref: https://openapi-generator.tech
Do not edit the class manually.
Attributes:
allowed_values (dict): The key is the tuple path to the attribute
and the for var_name this is (var_name,). The value is a dict
with a capitalized key describing the allowed value and an allowed
value. These dicts store the allowed enum values.
attribute_map (dict): The key is attribute name
and the value is json key in definition.
discriminator_value_class_map (dict): A dict to go from the discriminator
variable value to the discriminator class name.
validations (dict): The key is the tuple path to the attribute
and the for var_name this is (var_name,). The value is a dict
that stores validations for max_length, min_length, max_items,
min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
inclusive_minimum, and regex.
additional_properties_type (tuple): A tuple of classes accepted
as additional properties values.
"""
allowed_values = {
}
validations = {
}
@cached_property
def additional_properties_type():
"""
This must be a method because a model may have properties that are
of type self, this must run after the class is loaded
"""
return (bool, date, datetime, dict, float, int, list, str, none_type,) # noqa: E501
_nullable = False
@cached_property
def openapi_types():
"""
This must be a method because a model may have properties that are
of type self, this must run after the class is loaded
Returns
openapi_types (dict): The key is attribute name
and the value is attribute type.
"""
return {
'checkout_path': (str,), # noqa: E501
}
@cached_property
def discriminator():
return None
attribute_map = {
'checkout_path': 'checkoutPath', # noqa: E501
}
read_only_vars = {
}
_composed_schemas = {}
@classmethod
@convert_js_args_to_python_args
def _from_openapi_data(cls, checkout_path, *args, **kwargs): # noqa: E501
"""ShamanCheckoutResult - a model defined in OpenAPI
Args:
checkout_path (str): Path where the Manager created this checkout. This can be different than what was requested, as the Manager will ensure a unique directory. The path is relative to the Shaman checkout path as configured on the Manager.
Keyword Args:
_check_type (bool): if True, values for parameters in openapi_types
will be type checked and a TypeError will be
raised if the wrong type is input.
Defaults to True
_path_to_item (tuple/list): This is a list of keys or values to
drill down to the model in received_data
when deserializing a response
_spec_property_naming (bool): True if the variable names in the input data
are serialized names, as specified in the OpenAPI document.
False if the variable names in the input data
are pythonic names, e.g. snake case (default)
_configuration (Configuration): the instance to use when
deserializing a file_type parameter.
If passed, type conversion is attempted
If omitted no type conversion is done.
_visited_composed_classes (tuple): This stores a tuple of
classes that we have traveled through so that
if we see that class again we will not use its
discriminator again.
When traveling through a discriminator, the
composed schema that is
is traveled through is added to this set.
For example if Animal has a discriminator
petType and we pass in "Dog", and the class Dog
allOf includes Animal, we move through Animal
once using the discriminator, and pick Dog.
Then in Dog, we will make an instance of the
Animal class but this time we won't travel
through its discriminator because we passed in
_visited_composed_classes = (Animal,)
"""
_check_type = kwargs.pop('_check_type', True)
_spec_property_naming = kwargs.pop('_spec_property_naming', False)
_path_to_item = kwargs.pop('_path_to_item', ())
_configuration = kwargs.pop('_configuration', None)
_visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
self = super(OpenApiModel, cls).__new__(cls)
if args:
raise ApiTypeError(
"Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
args,
self.__class__.__name__,
),
path_to_item=_path_to_item,
valid_classes=(self.__class__,),
)
self._data_store = {}
self._check_type = _check_type
self._spec_property_naming = _spec_property_naming
self._path_to_item = _path_to_item
self._configuration = _configuration
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
self.checkout_path = checkout_path
for var_name, var_value in kwargs.items():
if var_name not in self.attribute_map and \
self._configuration is not None and \
self._configuration.discard_unknown_keys and \
self.additional_properties_type is None:
# discard variable.
continue
setattr(self, var_name, var_value)
return self
required_properties = set([
'_data_store',
'_check_type',
'_spec_property_naming',
'_path_to_item',
'_configuration',
'_visited_composed_classes',
])
@convert_js_args_to_python_args
def __init__(self, checkout_path, *args, **kwargs): # noqa: E501
"""ShamanCheckoutResult - a model defined in OpenAPI
Args:
checkout_path (str): Path where the Manager created this checkout. This can be different than what was requested, as the Manager will ensure a unique directory. The path is relative to the Shaman checkout path as configured on the Manager.
Keyword Args:
_check_type (bool): if True, values for parameters in openapi_types
will be type checked and a TypeError will be
raised if the wrong type is input.
Defaults to True
_path_to_item (tuple/list): This is a list of keys or values to
drill down to the model in received_data
when deserializing a response
_spec_property_naming (bool): True if the variable names in the input data
are serialized names, as specified in the OpenAPI document.
False if the variable names in the input data
are pythonic names, e.g. snake case (default)
_configuration (Configuration): the instance to use when
deserializing a file_type parameter.
If passed, type conversion is attempted
If omitted no type conversion is done.
_visited_composed_classes (tuple): This stores a tuple of
classes that we have traveled through so that
if we see that class again we will not use its
discriminator again.
When traveling through a discriminator, the
composed schema that is
is traveled through is added to this set.
For example if Animal has a discriminator
petType and we pass in "Dog", and the class Dog
allOf includes Animal, we move through Animal
once using the discriminator, and pick Dog.
Then in Dog, we will make an instance of the
Animal class but this time we won't travel
through its discriminator because we passed in
_visited_composed_classes = (Animal,)
"""
_check_type = kwargs.pop('_check_type', True)
_spec_property_naming = kwargs.pop('_spec_property_naming', False)
_path_to_item = kwargs.pop('_path_to_item', ())
_configuration = kwargs.pop('_configuration', None)
_visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
if args:
raise ApiTypeError(
"Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
args,
self.__class__.__name__,
),
path_to_item=_path_to_item,
valid_classes=(self.__class__,),
)
self._data_store = {}
self._check_type = _check_type
self._spec_property_naming = _spec_property_naming
self._path_to_item = _path_to_item
self._configuration = _configuration
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
self.checkout_path = checkout_path
for var_name, var_value in kwargs.items():
if var_name not in self.attribute_map and \
self._configuration is not None and \
self._configuration.discard_unknown_keys and \
self.additional_properties_type is None:
# discard variable.
continue
setattr(self, var_name, var_value)
if var_name in self.read_only_vars:
raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
f"class with read only attributes.")

View File

@ -27,6 +27,7 @@ from flamenco.manager.model.manager_configuration import ManagerConfiguration
from flamenco.manager.model.registered_worker import RegisteredWorker from flamenco.manager.model.registered_worker import RegisteredWorker
from flamenco.manager.model.security_error import SecurityError from flamenco.manager.model.security_error import SecurityError
from flamenco.manager.model.shaman_checkout import ShamanCheckout from flamenco.manager.model.shaman_checkout import ShamanCheckout
from flamenco.manager.model.shaman_checkout_result import ShamanCheckoutResult
from flamenco.manager.model.shaman_file_spec import ShamanFileSpec from flamenco.manager.model.shaman_file_spec import ShamanFileSpec
from flamenco.manager.model.shaman_file_spec_with_status import ShamanFileSpecWithStatus from flamenco.manager.model.shaman_file_spec_with_status import ShamanFileSpecWithStatus
from flamenco.manager.model.shaman_file_status import ShamanFileStatus from flamenco.manager.model.shaman_file_status import ShamanFileStatus

View File

@ -4,7 +4,7 @@ Render Farm manager API
The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.0.0 - API version: 1.0.0
- Package version: 8a97cf50-dirty - Package version: f66594b0-dirty
- Build package: org.openapitools.codegen.languages.PythonClientCodegen - Build package: org.openapitools.codegen.languages.PythonClientCodegen
For more information, please visit [https://flamenco.io/](https://flamenco.io/) For more information, please visit [https://flamenco.io/](https://flamenco.io/)
@ -102,6 +102,7 @@ Class | Method | HTTP request | Description
- [RegisteredWorker](flamenco/manager/docs/RegisteredWorker.md) - [RegisteredWorker](flamenco/manager/docs/RegisteredWorker.md)
- [SecurityError](flamenco/manager/docs/SecurityError.md) - [SecurityError](flamenco/manager/docs/SecurityError.md)
- [ShamanCheckout](flamenco/manager/docs/ShamanCheckout.md) - [ShamanCheckout](flamenco/manager/docs/ShamanCheckout.md)
- [ShamanCheckoutResult](flamenco/manager/docs/ShamanCheckoutResult.md)
- [ShamanFileSpec](flamenco/manager/docs/ShamanFileSpec.md) - [ShamanFileSpec](flamenco/manager/docs/ShamanFileSpec.md)
- [ShamanFileSpecWithStatus](flamenco/manager/docs/ShamanFileSpecWithStatus.md) - [ShamanFileSpecWithStatus](flamenco/manager/docs/ShamanFileSpecWithStatus.md)
- [ShamanFileStatus](flamenco/manager/docs/ShamanFileStatus.md) - [ShamanFileStatus](flamenco/manager/docs/ShamanFileStatus.md)

View File

@ -291,9 +291,8 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
assert self.job is not None assert self.job is not None
self.log.info("Sending BAT pack to Shaman") self.log.info("Sending BAT pack to Shaman")
# TODO: get project name from preferences/GUI. # TODO: get project name from preferences/GUI and insert that here too.
# TODO: update Shaman API to ensure this is a unique location. checkout_root = PurePosixPath(f"{self.job.name}")
checkout_root = PurePosixPath(f"project/{self.job.name}")
self.packthread = bat_interface.copy( self.packthread = bat_interface.copy(
base_blendfile=blendfile, base_blendfile=blendfile,

View File

@ -84,7 +84,8 @@ type Shaman interface {
// Checkout creates a directory, and symlinks the required files into it. The // Checkout creates a directory, and symlinks the required files into it. The
// files must all have been uploaded to Shaman before calling this. // files must all have been uploaded to Shaman before calling this.
Checkout(ctx context.Context, checkout api.ShamanCheckout) error // Returns the final checkout directory, as it may be modified to ensure uniqueness.
Checkout(ctx context.Context, checkout api.ShamanCheckout) (string, error)
// Requirements checks a Shaman Requirements file, and returns the subset // Requirements checks a Shaman Requirements file, and returns the subset
// containing the unknown files. // containing the unknown files.

View File

@ -412,11 +412,12 @@ func (m *MockShaman) EXPECT() *MockShamanMockRecorder {
} }
// Checkout mocks base method. // Checkout mocks base method.
func (m *MockShaman) Checkout(arg0 context.Context, arg1 api.ShamanCheckout) error { func (m *MockShaman) Checkout(arg0 context.Context, arg1 api.ShamanCheckout) (string, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Checkout", arg0, arg1) ret := m.ctrl.Call(m, "Checkout", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(string)
return ret0 ret1, _ := ret[1].(error)
return ret0, ret1
} }
// Checkout indicates an expected call of Checkout. // Checkout indicates an expected call of Checkout.

View File

@ -31,14 +31,16 @@ func (f *Flamenco) ShamanCheckout(e echo.Context) error {
return sendAPIError(e, http.StatusBadRequest, "invalid format") return sendAPIError(e, http.StatusBadRequest, "invalid format")
} }
err = f.shaman.Checkout(e.Request().Context(), api.ShamanCheckout(reqBody)) checkoutPath, err := f.shaman.Checkout(e.Request().Context(), api.ShamanCheckout(reqBody))
if err != nil { if err != nil {
// TODO: return 409 when checkout already exists. // TODO: return 409 when checkout already exists.
logger.Warn().Err(err).Msg("Shaman: creating checkout") logger.Warn().Err(err).Msg("Shaman: creating checkout")
return sendAPIError(e, http.StatusInternalServerError, "unexpected error: %v", err) return sendAPIError(e, http.StatusInternalServerError, "unexpected error: %v", err)
} }
return e.String(http.StatusNoContent, "") return e.JSON(http.StatusOK, api.ShamanCheckoutResult{
CheckoutPath: checkoutPath,
})
} }
// Checks a Shaman Requirements file, and reports which files are unknown. // Checks a Shaman Requirements file, and reports which files are unknown.

View File

@ -313,8 +313,12 @@ paths:
schema: schema:
$ref: "#/components/schemas/ShamanCheckout" $ref: "#/components/schemas/ShamanCheckout"
responses: responses:
"204": "200":
description: Checkout was created succesfully. description: Checkout was created succesfully.
content:
application/json:
schema:
$ref: '#/components/schemas/ShamanCheckoutResult'
"424": "424":
description: There were files missing. Use `shamanCheckoutRequirements` to figure out which ones. description: There were files missing. Use `shamanCheckoutRequirements` to figure out which ones.
content: content:
@ -795,6 +799,19 @@ components:
size: 127 size: 127
path: logging.go path: logging.go
ShamanCheckoutResult:
type: object
description: The result of a Shaman checkout.
properties:
"checkoutPath":
type: string
description: >
Path where the Manager created this checkout. This can be different
than what was requested, as the Manager will ensure a unique
directory. The path is relative to the Shaman checkout path as
configured on the Manager.
required: [checkoutPath]
ShamanFileStatus: ShamanFileStatus:
type: string type: string
enum: [unknown, uploading, stored] enum: [unknown, uploading, stored]

View File

@ -1397,6 +1397,7 @@ func (r TaskUpdateResponse) StatusCode() int {
type ShamanCheckoutResponse struct { type ShamanCheckoutResponse struct {
Body []byte Body []byte
HTTPResponse *http.Response HTTPResponse *http.Response
JSON200 *ShamanCheckoutResult
JSON409 *Error JSON409 *Error
JSON424 *Error JSON424 *Error
JSONDefault *Error JSONDefault *Error
@ -2054,6 +2055,13 @@ func ParseShamanCheckoutResponse(rsp *http.Response) (*ShamanCheckoutResponse, e
} }
switch { switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest ShamanCheckoutResult
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409:
var dest Error var dest Error
if err := json.Unmarshal(bodyBytes, &dest); err != nil { if err := json.Unmarshal(bodyBytes, &dest); err != nil {

View File

@ -18,94 +18,95 @@ import (
// Base64 encoded, gzipped, json marshaled Swagger object // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"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",
"FwAA//9HZfvdYFcAAA==", "qeL6WBbrQz9XoAWYUaeTb7TWGDXptY+ZANCjk+N+L2H3VE4VRSX9BRFn0oetqA14X94K+Hqi39HJ8QgX",
"QpFrme83hCUW9/eliptE1nTge37dvL35vwAAAP//0IX4BlJZAAA=",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

View File

@ -250,11 +250,17 @@ type SecurityError struct {
// Set of files with their SHA256 checksum, size in bytes, and desired location in the checkout directory. // Set of files with their SHA256 checksum, size in bytes, and desired location in the checkout directory.
type ShamanCheckout struct { type ShamanCheckout struct {
// Path where the Manager should create this checkout, It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the "checkout ID", but in this version it can be a path like `project-slug/scene-name/unique-ID`. // Path where the Manager should create this checkout. It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the "checkout ID", but in this version it can be a path like `project-slug/scene-name/unique-ID`.
CheckoutPath string `json:"checkoutPath"` CheckoutPath string `json:"checkoutPath"`
Files []ShamanFileSpec `json:"files"` Files []ShamanFileSpec `json:"files"`
} }
// The result of a Shaman checkout.
type ShamanCheckoutResult struct {
// Path where the Manager created this checkout. This can be different than what was requested, as the Manager will ensure a unique directory. The path is relative to the Shaman checkout path as configured on the Manager.
CheckoutPath string `json:"checkoutPath"`
}
// Specification of a file in the Shaman storage. // Specification of a file in the Shaman storage.
type ShamanFileSpec struct { type ShamanFileSpec struct {
// Location of the file in the checkout // Location of the file in the checkout

View File

@ -20,7 +20,7 @@ var (
validCheckoutRegexp = regexp.MustCompile(`^[^/?*:;{}\\][^?*:;{}\\]*$`) validCheckoutRegexp = regexp.MustCompile(`^[^/?*:;{}\\][^?*:;{}\\]*$`)
) )
func (m *Manager) Checkout(ctx context.Context, checkout api.ShamanCheckout) error { func (m *Manager) Checkout(ctx context.Context, checkout api.ShamanCheckout) (string, error) {
logger := (*zerolog.Ctx(ctx)).With(). logger := (*zerolog.Ctx(ctx)).With().
Str("checkoutPath", checkout.CheckoutPath).Logger() Str("checkoutPath", checkout.CheckoutPath).Logger()
logger.Debug().Msg("shaman: user requested checkout creation") logger.Debug().Msg("shaman: user requested checkout creation")
@ -28,7 +28,7 @@ func (m *Manager) Checkout(ctx context.Context, checkout api.ShamanCheckout) err
// Actually create the checkout. // Actually create the checkout.
resolvedCheckoutInfo, err := m.PrepareCheckout(checkout.CheckoutPath) resolvedCheckoutInfo, err := m.PrepareCheckout(checkout.CheckoutPath)
if err != nil { if err != nil {
return err return "", err
} }
// The checkout directory was created, so if anything fails now, it should be erased. // The checkout directory was created, so if anything fails now, it should be erased.
@ -46,17 +46,17 @@ func (m *Manager) Checkout(ctx context.Context, checkout api.ShamanCheckout) err
blobPath, status := m.fileStore.ResolveFile(fileSpec.Sha, int64(fileSpec.Size), filestore.ResolveStoredOnly) blobPath, status := m.fileStore.ResolveFile(fileSpec.Sha, int64(fileSpec.Size), filestore.ResolveStoredOnly)
if status != filestore.StatusStored { if status != filestore.StatusStored {
// Caller should upload this file before we can create the checkout. // Caller should upload this file before we can create the checkout.
return ErrMissingFiles return "", ErrMissingFiles
} }
if err := m.SymlinkToCheckout(blobPath, resolvedCheckoutInfo.absolutePath, fileSpec.Path); err != nil { if err := m.SymlinkToCheckout(blobPath, resolvedCheckoutInfo.absolutePath, fileSpec.Path); err != nil {
return fmt.Errorf("symlinking %q to checkout: %w", fileSpec.Path, err) return "", fmt.Errorf("symlinking %q to checkout: %w", fileSpec.Path, err)
} }
} }
checkoutOK = true // Prevent the checkout directory from being erased again. checkoutOK = true // Prevent the checkout directory from being erased again.
logger.Info().Msg("shaman: checkout created") logger.Info().Msg("shaman: checkout created")
return nil return resolvedCheckoutInfo.RelativePath, nil
} }
func isValidCheckoutPath(checkoutPath string) bool { func isValidCheckoutPath(checkoutPath string) bool {

View File

@ -28,13 +28,13 @@ func TestCheckout(t *testing.T) {
}, },
} }
err := manager.Checkout(ctx, checkout) actualCheckoutPath, err := manager.Checkout(ctx, checkout)
if err != nil { if err != nil {
t.Fatalf("fatal error: %v", err) t.Fatalf("fatal error: %v", err)
} }
// Check the symlinks of the checkout // Check the symlinks of the checkout
coPath := path.Join(manager.checkoutBasePath, checkout.CheckoutPath) coPath := path.Join(manager.checkoutBasePath, actualCheckoutPath)
assert.FileExists(t, path.Join(coPath, "subdir", "replacer.py")) assert.FileExists(t, path.Join(coPath, "subdir", "replacer.py"))
assert.FileExists(t, path.Join(coPath, "feed.py")) assert.FileExists(t, path.Join(coPath, "feed.py"))
assert.FileExists(t, path.Join(coPath, "httpstuff.py")) assert.FileExists(t, path.Join(coPath, "httpstuff.py"))

View File

@ -25,6 +25,7 @@ package checkout
import ( import (
"errors" "errors"
"fmt" "fmt"
"math/rand"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -43,7 +44,9 @@ type Manager struct {
checkoutBasePath string checkoutBasePath string
fileStore *filestore.Store fileStore *filestore.Store
wg sync.WaitGroup wg *sync.WaitGroup
checkoutUniquenessMutex *sync.Mutex
} }
// ResolvedCheckoutInfo contains the result of validating the Checkout ID and parsing it into a final path. // ResolvedCheckoutInfo contains the result of validating the Checkout ID and parsing it into a final path.
@ -78,7 +81,7 @@ func NewManager(conf config.Config, fileStore *filestore.Store) *Manager {
logger.Error().Err(err).Msg("unable to create checkout directory") logger.Error().Err(err).Msg("unable to create checkout directory")
} }
return &Manager{conf.CheckoutPath, fileStore, sync.WaitGroup{}} return &Manager{conf.CheckoutPath, fileStore, new(sync.WaitGroup), new(sync.Mutex)}
} }
// Close waits for still-running touch() calls to finish, then returns. // Close waits for still-running touch() calls to finish, then returns.
@ -101,7 +104,17 @@ func (m *Manager) pathForCheckout(requestedCheckoutPath string) (ResolvedCheckou
// PrepareCheckout creates the root directory for a specific checkout. // PrepareCheckout creates the root directory for a specific checkout.
// Returns the path relative to the checkout root directory. // Returns the path relative to the checkout root directory.
func (m *Manager) PrepareCheckout(checkoutPath string) (ResolvedCheckoutInfo, error) { func (m *Manager) PrepareCheckout(checkoutPath string) (ResolvedCheckoutInfo, error) {
checkoutPaths, err := m.pathForCheckout(checkoutPath) // This function checks the filesystem and tries to ensure uniqueness, so it's
// important that it doesn't run simultaneously in parallel threads.
m.checkoutUniquenessMutex.Lock()
defer m.checkoutUniquenessMutex.Unlock()
var lastErr error
// Just try 10 different random tokens. If that still doesn't work, fail.
for try := 0; try < 10; try++ {
randomisedPath := fmt.Sprintf("%s-%s", checkoutPath, randomisedToken())
checkoutPaths, err := m.pathForCheckout(randomisedPath)
if err != nil { if err != nil {
return ResolvedCheckoutInfo{}, err return ResolvedCheckoutInfo{}, err
} }
@ -113,25 +126,32 @@ func (m *Manager) PrepareCheckout(checkoutPath string) (ResolvedCheckoutInfo, er
if stat, err := os.Stat(checkoutPaths.absolutePath); !os.IsNotExist(err) { if stat, err := os.Stat(checkoutPaths.absolutePath); !os.IsNotExist(err) {
if err == nil { if err == nil {
// No error stat'ing this path, indicating it's an existing checkout.
// Just retry another random token.
lastErr = ErrCheckoutAlreadyExists
if stat.IsDir() { if stat.IsDir() {
logger.Debug().Msg("shaman: checkout path exists") logger.Debug().Msg("shaman: checkout path exists")
} else { } else {
logger.Error().Msg("shaman: checkout path exists but is not a directory") logger.Warn().Msg("shaman: checkout path exists but is not a directory")
} }
// No error stat'ing this path, indicating it's an existing checkout. continue
return ResolvedCheckoutInfo{}, ErrCheckoutAlreadyExists
} }
// If it's any other error, it's really a problem on our side. // If it's any other error, it's really a problem on our side. Don't retry.
logger.Error().Err(err).Msg("shaman: unable to stat checkout directory") logger.Error().Err(err).Msg("shaman: unable to stat checkout directory")
return ResolvedCheckoutInfo{}, err return ResolvedCheckoutInfo{}, err
} }
if err := os.MkdirAll(checkoutPaths.absolutePath, 0777); err != nil { if err := os.MkdirAll(checkoutPaths.absolutePath, 0777); err != nil {
logger.Error().Err(err).Msg("shaman: unable to create checkout directory") lastErr = err
logger.Warn().Err(err).Msg("shaman: unable to create checkout directory")
continue
} }
logger.Info().Str("relPath", checkoutPaths.RelativePath).Msg("shaman: created checkout directory") logger.Info().Str("relPath", checkoutPaths.RelativePath).Msg("shaman: created checkout directory")
return checkoutPaths, nil return checkoutPaths, nil
}
return ResolvedCheckoutInfo{}, lastErr
} }
// EraseCheckout removes the checkout directory structure identified by the ID. // EraseCheckout removes the checkout directory structure identified by the ID.
@ -231,3 +251,17 @@ func touchFile(blobPath string) error {
logger.Debug().Msg("done touching") logger.Debug().Msg("done touching")
return nil return nil
} }
// randomisedToken generates a random 4-character string.
// It is intended to add to a checkout path, to create some randomness and thus
// a higher chance of the path not yet existing.
func randomisedToken() string {
var runes = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
n := 4
s := make([]rune, n)
for i := range s {
s[i] = runes[rand.Intn(len(runes))]
}
return string(s)
}

View File

@ -112,7 +112,7 @@ func (s *Server) IsEnabled() bool {
// Checkout creates a directory, and symlinks the required files into it. The // Checkout creates a directory, and symlinks the required files into it. The
// files must all have been uploaded to Shaman before calling this. // files must all have been uploaded to Shaman before calling this.
func (s *Server) Checkout(ctx context.Context, checkout api.ShamanCheckout) error { func (s *Server) Checkout(ctx context.Context, checkout api.ShamanCheckout) (string, error) {
return s.checkoutMan.Checkout(ctx, checkout) return s.checkoutMan.Checkout(ctx, checkout)
} }