Fix: Tag Interface Delete Button #104256
@ -21,6 +21,7 @@ bugs in actually-released versions.
|
||||
- The webapp automatically reloads after a disconnect, when it reconnects to Flamenco Manager and sees the Manager version changed [#104235](https://projects.blender.org/studio/flamenco/pulls/104235).
|
||||
- Show the configured Flamenco Manager name in the webapp's browser window title.
|
||||
- Workers can be marked as 'restartable' by using the `-restart-exit-code N` commandline option. More info in the [Worker Actions documentation](https://flamenco.blender.org/usage/worker-actions/).
|
||||
- The `{timestamp}` placeholder in the render output path is now replaced with a local timestamp (rather than UTC).
|
||||
|
||||
|
||||
## 3.2 - released 2023-02-21
|
||||
|
3
Makefile
3
Makefile
@ -71,6 +71,9 @@ stresser:
|
||||
job-creator:
|
||||
go build -v ${BUILD_FLAGS} ${PKG}/cmd/job-creator
|
||||
|
||||
flamenco-addon.zip: addon-packer
|
||||
./addon-packer -filename ./flamenco-addon.zip
|
||||
|
||||
addon-packer: cmd/addon-packer/addon-packer.go
|
||||
go build -v ${BUILD_FLAGS} ${PKG}/cmd/addon-packer
|
||||
|
||||
|
@ -5,7 +5,7 @@ What kind of thing to subscribe to / unsubscribe from.
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**value** | **str** | What kind of thing to subscribe to / unsubscribe from. | must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", ]
|
||||
**value** | **str** | What kind of thing to subscribe to / unsubscribe from. | must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", "allWorkerTags", ]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
14
addon/flamenco/manager/docs/SocketIOWorkerTagUpdate.md
generated
Normal file
14
addon/flamenco/manager/docs/SocketIOWorkerTagUpdate.md
generated
Normal file
@ -0,0 +1,14 @@
|
||||
# SocketIOWorkerTagUpdate
|
||||
|
||||
Worker Tag, sent over SocketIO when it changes.
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**tag** | [**WorkerTag**](WorkerTag.md) | |
|
||||
**was_deleted** | **bool** | When a tag was just deleted, this is set to `true`. | [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)
|
||||
|
||||
|
@ -57,6 +57,7 @@ class SocketIOSubscriptionType(ModelSimple):
|
||||
'JOB': "job",
|
||||
'TASKLOG': "tasklog",
|
||||
'ALLLASTRENDERED': "allLastRendered",
|
||||
'ALLWORKERTAGS': "allWorkerTags",
|
||||
},
|
||||
}
|
||||
|
||||
@ -108,10 +109,10 @@ class SocketIOSubscriptionType(ModelSimple):
|
||||
Note that value can be passed either in args or in kwargs, but not in both.
|
||||
|
||||
Args:
|
||||
args[0] (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", ] # noqa: E501
|
||||
args[0] (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", "allWorkerTags", ] # noqa: E501
|
||||
|
||||
Keyword Args:
|
||||
value (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", ] # noqa: E501
|
||||
value (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", "allWorkerTags", ] # noqa: E501
|
||||
_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.
|
||||
@ -198,10 +199,10 @@ class SocketIOSubscriptionType(ModelSimple):
|
||||
Note that value can be passed either in args or in kwargs, but not in both.
|
||||
|
||||
Args:
|
||||
args[0] (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", ] # noqa: E501
|
||||
args[0] (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", "allWorkerTags", ] # noqa: E501
|
||||
|
||||
Keyword Args:
|
||||
value (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", ] # noqa: E501
|
||||
value (str): What kind of thing to subscribe to / unsubscribe from.., must be one of ["allJobs", "allWorkers", "job", "tasklog", "allLastRendered", "allWorkerTags", ] # noqa: E501
|
||||
_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.
|
||||
|
271
addon/flamenco/manager/model/socket_io_worker_tag_update.py
generated
Normal file
271
addon/flamenco/manager/model/socket_io_worker_tag_update.py
generated
Normal file
@ -0,0 +1,271 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
def lazy_import():
|
||||
from flamenco.manager.model.worker_tag import WorkerTag
|
||||
globals()['WorkerTag'] = WorkerTag
|
||||
|
||||
|
||||
class SocketIOWorkerTagUpdate(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
|
||||
"""
|
||||
lazy_import()
|
||||
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.
|
||||
"""
|
||||
lazy_import()
|
||||
return {
|
||||
'tag': (WorkerTag,), # noqa: E501
|
||||
'was_deleted': (bool,), # noqa: E501
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def discriminator():
|
||||
return None
|
||||
|
||||
|
||||
attribute_map = {
|
||||
'tag': 'tag', # noqa: E501
|
||||
'was_deleted': 'was_deleted', # noqa: E501
|
||||
}
|
||||
|
||||
read_only_vars = {
|
||||
}
|
||||
|
||||
_composed_schemas = {}
|
||||
|
||||
@classmethod
|
||||
@convert_js_args_to_python_args
|
||||
def _from_openapi_data(cls, tag, *args, **kwargs): # noqa: E501
|
||||
"""SocketIOWorkerTagUpdate - a model defined in OpenAPI
|
||||
|
||||
Args:
|
||||
tag (WorkerTag):
|
||||
|
||||
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,)
|
||||
was_deleted (bool): When a tag was just deleted, this is set to `true`.. [optional] # noqa: E501
|
||||
"""
|
||||
|
||||
_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.tag = tag
|
||||
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, tag, *args, **kwargs): # noqa: E501
|
||||
"""SocketIOWorkerTagUpdate - a model defined in OpenAPI
|
||||
|
||||
Args:
|
||||
tag (WorkerTag):
|
||||
|
||||
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,)
|
||||
was_deleted (bool): When a tag was just deleted, this is set to `true`.. [optional] # noqa: E501
|
||||
"""
|
||||
|
||||
_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.tag = tag
|
||||
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.")
|
1
addon/flamenco/manager/models/__init__.py
generated
1
addon/flamenco/manager/models/__init__.py
generated
@ -64,6 +64,7 @@ from flamenco.manager.model.socket_io_subscription_operation import SocketIOSubs
|
||||
from flamenco.manager.model.socket_io_subscription_type import SocketIOSubscriptionType
|
||||
from flamenco.manager.model.socket_io_task_log_update import SocketIOTaskLogUpdate
|
||||
from flamenco.manager.model.socket_io_task_update import SocketIOTaskUpdate
|
||||
from flamenco.manager.model.socket_io_worker_tag_update import SocketIOWorkerTagUpdate
|
||||
from flamenco.manager.model.socket_io_worker_update import SocketIOWorkerUpdate
|
||||
from flamenco.manager.model.submitted_job import SubmittedJob
|
||||
from flamenco.manager.model.task import Task
|
||||
|
7
go.mod
7
go.mod
@ -7,8 +7,8 @@ require (
|
||||
github.com/benbjohnson/clock v1.3.0
|
||||
github.com/deepmap/oapi-codegen v1.9.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dop251/goja v0.0.0-20211217115348-3f9136fa235d
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7
|
||||
github.com/dop251/goja v0.0.0-20230812105242-81d76064690d
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d
|
||||
github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e
|
||||
github.com/gertd/go-pluralize v0.2.1
|
||||
github.com/getkin/kin-openapi v0.88.0
|
||||
@ -34,13 +34,14 @@ require (
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/swag v0.19.5 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
|
15
go.sum
15
go.sum
@ -2,6 +2,9 @@ github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
@ -17,10 +20,17 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||
github.com/dop251/goja v0.0.0-20211217115348-3f9136fa235d h1:XT7Qdmcuwgsgz4GXejX7R5Morysk2GOpeguYJ9JoF5c=
|
||||
github.com/dop251/goja v0.0.0-20211217115348-3f9136fa235d/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||
github.com/dop251/goja v0.0.0-20230812105242-81d76064690d h1:9aaGwVf4q+kknu+mROAXUApJ1DoOwhE8dGj/XLBYzWg=
|
||||
github.com/dop251/goja v0.0.0-20230812105242-81d76064690d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7 h1:tYwu/z8Y0NkkzGEh3z21mSWggMg4LwLRFucLS7TjARg=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d h1:W1n4DvpzZGOISgp7wWNtraLcHtnmnTwBlJidqtMIuwQ=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
@ -71,6 +81,8 @@ github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGS
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
@ -81,6 +93,7 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/graarh/golang-socketio v0.0.0-20170510162725-2c44953b9b5f h1:utzdm9zUvVWGRtIpkdE4+36n+Gv60kNb7mFvgGxLElY=
|
||||
github.com/graarh/golang-socketio v0.0.0-20170510162725-2c44953b9b5f/go.mod h1:8gudiNCFh3ZfvInknmoXzPeV17FSH+X2J5k2cUPIwnA=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
@ -241,6 +254,7 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -255,6 +269,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -120,6 +120,9 @@ type ChangeBroadcaster interface {
|
||||
|
||||
BroadcastWorkerUpdate(workerUpdate api.SocketIOWorkerUpdate)
|
||||
BroadcastNewWorker(workerUpdate api.SocketIOWorkerUpdate)
|
||||
|
||||
BroadcastWorkerTagUpdate(workerTagUpdate api.SocketIOWorkerTagUpdate)
|
||||
BroadcastNewWorkerTag(workerTagUpdate api.SocketIOWorkerTagUpdate)
|
||||
}
|
||||
|
||||
// ChangeBroadcaster should be a subset of webupdates.BiDirComms.
|
||||
|
24
internal/manager/api_impl/mocks/api_impl_mock.gen.go
generated
24
internal/manager/api_impl/mocks/api_impl_mock.gen.go
generated
@ -632,6 +632,30 @@ func (mr *MockChangeBroadcasterMockRecorder) BroadcastNewWorker(arg0 interface{}
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastNewWorker", reflect.TypeOf((*MockChangeBroadcaster)(nil).BroadcastNewWorker), arg0)
|
||||
}
|
||||
|
||||
// BroadcastNewWorkerTag mocks base method.
|
||||
func (m *MockChangeBroadcaster) BroadcastNewWorkerTag(arg0 api.SocketIOWorkerTagUpdate) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "BroadcastNewWorkerTag", arg0)
|
||||
}
|
||||
|
||||
// BroadcastNewWorkerTag indicates an expected call of BroadcastNewWorkerTag.
|
||||
func (mr *MockChangeBroadcasterMockRecorder) BroadcastNewWorkerTag(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastNewWorkerTag", reflect.TypeOf((*MockChangeBroadcaster)(nil).BroadcastNewWorkerTag), arg0)
|
||||
}
|
||||
|
||||
// BroadcastWorkerTagUpdate mocks base method.
|
||||
func (m *MockChangeBroadcaster) BroadcastWorkerTagUpdate(arg0 api.SocketIOWorkerTagUpdate) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "BroadcastWorkerTagUpdate", arg0)
|
||||
}
|
||||
|
||||
// BroadcastWorkerTagUpdate indicates an expected call of BroadcastWorkerTagUpdate.
|
||||
func (mr *MockChangeBroadcasterMockRecorder) BroadcastWorkerTagUpdate(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastWorkerTagUpdate", reflect.TypeOf((*MockChangeBroadcaster)(nil).BroadcastWorkerTagUpdate), arg0)
|
||||
}
|
||||
|
||||
// BroadcastWorkerUpdate mocks base method.
|
||||
func (m *MockChangeBroadcaster) BroadcastWorkerUpdate(arg0 api.SocketIOWorkerUpdate) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -266,7 +266,9 @@ func (f *Flamenco) DeleteWorkerTag(e echo.Context, tagUUID string) error {
|
||||
return sendAPIError(e, http.StatusInternalServerError, "error deleting worker tag: %v", err)
|
||||
}
|
||||
|
||||
// TODO: SocketIO broadcast of tag deletion.
|
||||
// SocketIO broadcast of tag deletion.
|
||||
update := webupdates.NewWorkerTagDeletedUpdate(tagUUID)
|
||||
f.broadcaster.BroadcastWorkerTagUpdate(update)
|
||||
|
||||
logger.Info().Msg("worker tag deleted")
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
@ -344,7 +346,10 @@ func (f *Flamenco) UpdateWorkerTag(e echo.Context, tagUUID string) error {
|
||||
return sendAPIError(e, http.StatusInternalServerError, "error saving worker tag")
|
||||
}
|
||||
|
||||
// TODO: SocketIO broadcast of tag update.
|
||||
// SocketIO broadcast of tag update.
|
||||
sioUpdate := webupdates.NewWorkerTagUpdate(dbTag)
|
||||
f.broadcaster.BroadcastWorkerTagUpdate(sioUpdate)
|
||||
|
||||
logger.Info().Msg("worker tag updated")
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
}
|
||||
@ -412,7 +417,10 @@ func (f *Flamenco) CreateWorkerTag(e echo.Context) error {
|
||||
}
|
||||
|
||||
logger.Info().Msg("created new worker tag")
|
||||
// TODO: SocketIO broadcast of tag creation.
|
||||
|
||||
// SocketIO broadcast of tag creation.
|
||||
sioUpdate := webupdates.NewWorkerTagUpdate(&dbTag)
|
||||
f.broadcaster.BroadcastNewWorkerTag(sioUpdate)
|
||||
|
||||
return e.JSON(http.StatusOK, workerTagDBtoAPI(dbTag))
|
||||
}
|
||||
|
@ -281,7 +281,9 @@ func TestWorkerTagCRUDHappyFlow(t *testing.T) {
|
||||
Description: *apiTag.Description,
|
||||
}
|
||||
mf.persistence.EXPECT().CreateWorkerTag(gomock.Any(), &expectDBTag)
|
||||
// TODO: expect SocketIO broadcast of the tag creation.
|
||||
mf.broadcaster.EXPECT().BroadcastNewWorkerTag(api.SocketIOWorkerTagUpdate{
|
||||
Tag: apiTag,
|
||||
})
|
||||
echo := mf.prepareMockedJSONRequest(apiTag)
|
||||
require.NoError(t, mf.flamenco.CreateWorkerTag(echo))
|
||||
assertResponseJSON(t, echo, http.StatusOK, &apiTag)
|
||||
@ -303,7 +305,13 @@ func TestWorkerTagCRUDHappyFlow(t *testing.T) {
|
||||
Name: newAPITag.Name,
|
||||
Description: *apiTag.Description, // Not mentioning new description should keep old one.
|
||||
}
|
||||
// TODO: expect SocketIO broadcast of the tag update.
|
||||
mf.broadcaster.EXPECT().BroadcastWorkerTagUpdate(api.SocketIOWorkerTagUpdate{
|
||||
Tag: api.WorkerTag{
|
||||
Id: &UUID,
|
||||
Name: newAPITag.Name,
|
||||
Description: apiTag.Description,
|
||||
},
|
||||
})
|
||||
mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil)
|
||||
mf.persistence.EXPECT().SaveWorkerTag(gomock.Any(), &expectNewDBTag)
|
||||
echo = mf.prepareMockedJSONRequest(newAPITag)
|
||||
@ -320,7 +328,13 @@ func TestWorkerTagCRUDHappyFlow(t *testing.T) {
|
||||
Name: newAPITag.Name,
|
||||
Description: "",
|
||||
}
|
||||
// TODO: expect SocketIO broadcast of the tag update.
|
||||
mf.broadcaster.EXPECT().BroadcastWorkerTagUpdate(api.SocketIOWorkerTagUpdate{
|
||||
Tag: api.WorkerTag{
|
||||
Id: &UUID,
|
||||
Name: newAPITag.Name,
|
||||
Description: newAPITag.Description,
|
||||
},
|
||||
})
|
||||
mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil)
|
||||
mf.persistence.EXPECT().SaveWorkerTag(gomock.Any(), &expectNewDBTag)
|
||||
echo = mf.prepareMockedJSONRequest(newAPITag)
|
||||
@ -337,7 +351,13 @@ func TestWorkerTagCRUDHappyFlow(t *testing.T) {
|
||||
Name: newAPITag.Name,
|
||||
Description: *newAPITag.Description,
|
||||
}
|
||||
// TODO: expect SocketIO broadcast of the tag update.
|
||||
mf.broadcaster.EXPECT().BroadcastWorkerTagUpdate(api.SocketIOWorkerTagUpdate{
|
||||
Tag: api.WorkerTag{
|
||||
Id: &UUID,
|
||||
Name: newAPITag.Name,
|
||||
Description: newAPITag.Description,
|
||||
},
|
||||
})
|
||||
mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil)
|
||||
mf.persistence.EXPECT().SaveWorkerTag(gomock.Any(), &expectNewDBTag)
|
||||
echo = mf.prepareMockedJSONRequest(newAPITag)
|
||||
@ -347,7 +367,10 @@ func TestWorkerTagCRUDHappyFlow(t *testing.T) {
|
||||
// Delete.
|
||||
mf.persistence.EXPECT().FetchWorkerTag(gomock.Any(), UUID).Return(&expectDBTag, nil)
|
||||
mf.persistence.EXPECT().DeleteWorkerTag(gomock.Any(), UUID)
|
||||
// TODO: expect SocketIO broadcast of the tag deletion.
|
||||
mf.broadcaster.EXPECT().BroadcastWorkerTagUpdate(api.SocketIOWorkerTagUpdate{
|
||||
Tag: api.WorkerTag{Id: &UUID},
|
||||
WasDeleted: ptr(true),
|
||||
})
|
||||
echo = mf.prepareMockedJSONRequest(newAPITag)
|
||||
require.NoError(t, mf.flamenco.DeleteWorkerTag(echo, UUID))
|
||||
assertResponseNoContent(t, echo)
|
||||
|
@ -57,7 +57,7 @@ func exampleSubmittedJob() api.SubmittedJob {
|
||||
|
||||
func mockedClock(t *testing.T) clock.Clock {
|
||||
c := clock.NewMock()
|
||||
now, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
|
||||
now, err := time.ParseInLocation("2006-01-02T15:04:05", "2006-01-02T15:04:05", time.Local)
|
||||
assert.NoError(t, err)
|
||||
c.Set(now)
|
||||
return c
|
||||
@ -250,13 +250,13 @@ func TestSimpleBlenderRenderOutputPathFieldReplacement(t *testing.T) {
|
||||
require.NotNil(t, aj)
|
||||
|
||||
// The job compiler should have replaced the {timestamp} and {ext} fields.
|
||||
assert.Equal(t, "/root/2006-01-02_090405/jobname/######", aj.Settings["render_output_path"])
|
||||
assert.Equal(t, "/root/2006-01-02_150405/jobname/######", aj.Settings["render_output_path"])
|
||||
|
||||
// Tasks should have been created to render the frames: 1-3, 4-6, 7-9, 10, and video-encoding
|
||||
require.Len(t, aj.Tasks, 5)
|
||||
t0 := aj.Tasks[0]
|
||||
expectCliArgs := []interface{}{ // They are strings, but Goja doesn't know that and will produce an []interface{}.
|
||||
"--render-output", "/root/2006-01-02_090405/jobname/######",
|
||||
"--render-output", "/root/2006-01-02_150405/jobname/######",
|
||||
"--render-format", sj.Settings.AdditionalProperties["format"].(string),
|
||||
"--render-frame", "1..3",
|
||||
}
|
||||
@ -271,8 +271,8 @@ func TestSimpleBlenderRenderOutputPathFieldReplacement(t *testing.T) {
|
||||
tVideo := aj.Tasks[4] // This should be a video encoding task
|
||||
assert.EqualValues(t, AuthoredCommandParameters{
|
||||
"exe": "ffmpeg",
|
||||
"inputGlob": "/root/2006-01-02_090405/jobname/*.png",
|
||||
"outputFile": "/root/2006-01-02_090405/jobname/scene123-1-10.mp4",
|
||||
"inputGlob": "/root/2006-01-02_150405/jobname/*.png",
|
||||
"outputFile": "/root/2006-01-02_150405/jobname/scene123-1-10.mp4",
|
||||
"fps": int64(24),
|
||||
"args": expectedFramesToVideoArgs,
|
||||
}, tVideo.Commands[0].Parameters)
|
||||
@ -327,6 +327,57 @@ func TestEtag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplexFrameRange(t *testing.T) {
|
||||
s, err := Load(mockedClock(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Compiling a job should be really fast.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
sj := exampleSubmittedJob()
|
||||
|
||||
// Use a series of ranges, where each range is smaller than the chunk size.
|
||||
sj.Settings.AdditionalProperties["frames"] = "0-12,34-56,78-90"
|
||||
sj.Settings.AdditionalProperties["chunk_size"] = 20
|
||||
|
||||
aj, err := s.Compile(ctx, sj)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, aj)
|
||||
|
||||
// Expected chunks:
|
||||
// - 0-12, 34-40
|
||||
// - 41-56, 78-81
|
||||
// - 82-90
|
||||
taskNames := []string{}
|
||||
for _, task := range aj.Tasks {
|
||||
taskNames = append(taskNames, task.Name)
|
||||
}
|
||||
require.Equal(t, []string{
|
||||
"render-0-12,34-40",
|
||||
"render-41-56,78-81",
|
||||
"render-82-90",
|
||||
"preview-video",
|
||||
}, taskNames)
|
||||
|
||||
// Check the Blender CLI matches the expected frame ranges.
|
||||
frameRangesFromCLI := []string{}
|
||||
for _, task := range aj.Tasks[0:3] {
|
||||
args := task.Commands[0].Parameters["args"].([]interface{})
|
||||
require.Equal(t, "--render-frame", args[4])
|
||||
frameRangesFromCLI = append(frameRangesFromCLI, args[5].(string))
|
||||
}
|
||||
|
||||
assert.Equal(t,
|
||||
[]string{
|
||||
"0..12,34..40",
|
||||
"41..56,78..81",
|
||||
"82..90",
|
||||
},
|
||||
frameRangesFromCLI,
|
||||
)
|
||||
}
|
||||
|
||||
func ptr[T any](value T) *T {
|
||||
return &value
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ function authorRenderTasks(settings, renderDir, renderOutput) {
|
||||
args: [
|
||||
"--render-output", path.join(renderDir, path.basename(renderOutput)),
|
||||
"--render-format", settings.format,
|
||||
"--render-frame", chunk.replace("-", ".."), // Convert to Blender frame range notation.
|
||||
"--render-frame", chunk.replaceAll("-", ".."), // Convert to Blender frame range notation.
|
||||
]
|
||||
});
|
||||
task.addCommand(command);
|
||||
|
@ -109,7 +109,7 @@ function authorRenderTasks(settings, renderDir, renderOutput) {
|
||||
args: [
|
||||
"--render-output", path.join(renderDir, path.basename(renderOutput)),
|
||||
"--render-format", settings.format,
|
||||
"--render-frame", chunk.replace("-", ".."), // Convert to Blender frame range notation.
|
||||
"--render-frame", chunk.replaceAll("-", ".."), // Convert to Blender frame range notation.
|
||||
]
|
||||
});
|
||||
task.addCommand(command);
|
||||
|
@ -60,7 +60,7 @@ func calculateNextCheck(now time.Time, schedule *persistence.SleepSchedule) time
|
||||
// calcNext returns the given time of day on "today" if that hasn't passed
|
||||
// yet, otherwise on "tomorrow".
|
||||
calcNext := func(tod persistence.TimeOfDay) time.Time {
|
||||
nextCheck := tod.OnDate(now)
|
||||
nextCheck := tod.OnDate(now).In(time.Local)
|
||||
if nextCheck.Before(now) {
|
||||
nextCheck = nextCheck.AddDate(0, 0, 1)
|
||||
}
|
||||
@ -99,5 +99,6 @@ func earliestTime(timestamps []time.Time) time.Time {
|
||||
|
||||
// endOfDay returns the next midnight at UTC.
|
||||
func endOfDay(now time.Time) time.Time {
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, 1)
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
return startOfDay.AddDate(0, 0, 1)
|
||||
}
|
||||
|
@ -256,13 +256,14 @@ type TestMocks struct {
|
||||
// to the given time. Seconds and sub-seconds are set to zero.
|
||||
func (m *TestMocks) todayAt(hour, minute int) time.Time {
|
||||
now := m.clock.Now()
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
|
||||
todayAt := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, time.Local)
|
||||
return todayAt
|
||||
}
|
||||
|
||||
// endOfDay returns midnight of the day after whatever the mocked clock's "now" is set to.
|
||||
func (m *TestMocks) endOfDay() time.Time {
|
||||
now := m.clock.Now().UTC()
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, 1)
|
||||
startOfToday := m.todayAt(0, 0)
|
||||
return startOfToday.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
func testFixtures(t *testing.T) (*SleepScheduler, TestMocks, context.Context) {
|
||||
|
@ -21,9 +21,10 @@ const (
|
||||
// Predefined SocketIO rooms. There will be others, but those will have a
|
||||
// dynamic name like `job-fa48930a-105c-4125-a7f7-0aa1651dcd57` and cannot be
|
||||
// listed here as constants. See `roomXXX()` functions for those.
|
||||
SocketIORoomChat SocketIORoomName = "Chat" // For chat messages.
|
||||
SocketIORoomJobs SocketIORoomName = "Jobs" // For job updates.
|
||||
SocketIORoomWorkers SocketIORoomName = "Workers" // For worker updates.
|
||||
SocketIORoomChat SocketIORoomName = "Chat" // For chat messages.
|
||||
SocketIORoomJobs SocketIORoomName = "Jobs" // For job updates.
|
||||
SocketIORoomWorkers SocketIORoomName = "Workers" // For worker updates.
|
||||
SocketIORoomWorkerTags SocketIORoomName = "WorkerTags" // For worker tag updates.
|
||||
|
||||
// For updates about ALL last-rendered images. Normally these are sent to a
|
||||
// room specific to a particular job, but for the global "last rendered image"
|
||||
@ -40,6 +41,7 @@ const (
|
||||
SIOEventTaskUpdate SocketIOEventType = "/task" // sends api.SocketIOTaskUpdate
|
||||
SIOEventTaskLogUpdate SocketIOEventType = "/tasklog" // sends api.SocketIOTaskLogUpdate
|
||||
SIOEventWorkerUpdate SocketIOEventType = "/workers" // sends api.SocketIOWorkerUpdate
|
||||
SIOEventWorkerTagUpdate SocketIOEventType = "/workertags" // sends api.SocketIOWorkerTagUpdate
|
||||
SIOEventSubscription SocketIOEventType = "/subscription" // clients send api.SocketIOSubscription
|
||||
)
|
||||
|
||||
@ -74,6 +76,8 @@ func (b *BiDirComms) handleRoomSubscription(c *gosocketio.Channel, subs api.Sock
|
||||
sioRoom = SocketIORoomWorkers
|
||||
case api.SocketIOSubscriptionTypeAllLastRendered:
|
||||
sioRoom = SocketIORoomLastRendered
|
||||
case api.SocketIOSubscriptionTypeAllWorkerTags:
|
||||
sioRoom = SocketIORoomWorkerTags
|
||||
case api.SocketIOSubscriptionTypeJob:
|
||||
if subs.Uuid == nil {
|
||||
logger.Warn().Msg("socketIO: trying to (un)subscribe to job without UUID")
|
||||
|
48
internal/manager/webupdates/workertag_updates.go
Normal file
48
internal/manager/webupdates/workertag_updates.go
Normal file
@ -0,0 +1,48 @@
|
||||
package webupdates
|
||||
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"projects.blender.org/studio/flamenco/internal/manager/persistence"
|
||||
"projects.blender.org/studio/flamenco/pkg/api"
|
||||
)
|
||||
|
||||
// NewWorkerTagUpdate returns a partial SocketIOWorkerTagUpdate struct for the
|
||||
// given worker tag. It only fills in the fields that represent the current
|
||||
// state of the tag.
|
||||
func NewWorkerTagUpdate(tag *persistence.WorkerTag) api.SocketIOWorkerTagUpdate {
|
||||
tagUpdate := api.SocketIOWorkerTagUpdate{
|
||||
Tag: api.WorkerTag{
|
||||
Id: &tag.UUID,
|
||||
Name: tag.Name,
|
||||
Description: &tag.Description,
|
||||
},
|
||||
}
|
||||
return tagUpdate
|
||||
}
|
||||
|
||||
// NewWorkerTagDeletedUpdate returns a SocketIOWorkerTagUpdate struct that indicates
|
||||
// the worker tag has been deleted.
|
||||
func NewWorkerTagDeletedUpdate(tagUUID string) api.SocketIOWorkerTagUpdate {
|
||||
wasDeleted := true
|
||||
tagUpdate := api.SocketIOWorkerTagUpdate{
|
||||
Tag: api.WorkerTag{
|
||||
Id: &tagUUID,
|
||||
},
|
||||
WasDeleted: &wasDeleted,
|
||||
}
|
||||
return tagUpdate
|
||||
}
|
||||
|
||||
// BroadcastWorkerTagUpdate sends the worker tag update to clients.
|
||||
func (b *BiDirComms) BroadcastWorkerTagUpdate(WorkerTagUpdate api.SocketIOWorkerTagUpdate) {
|
||||
log.Debug().Interface("WorkerTagUpdate", WorkerTagUpdate).Msg("socketIO: broadcasting worker tag update")
|
||||
b.BroadcastTo(SocketIORoomWorkerTags, SIOEventWorkerTagUpdate, WorkerTagUpdate)
|
||||
}
|
||||
|
||||
// BroadcastNewWorkerTag sends a "new worker tag" notification to clients.
|
||||
func (b *BiDirComms) BroadcastNewWorkerTag(WorkerTagUpdate api.SocketIOWorkerTagUpdate) {
|
||||
log.Debug().Interface("WorkerTagUpdate", WorkerTagUpdate).Msg("socketIO: broadcasting new worker tag")
|
||||
b.BroadcastTo(SocketIORoomWorkerTags, SIOEventWorkerTagUpdate, WorkerTagUpdate)
|
||||
}
|
@ -12,7 +12,7 @@ import (
|
||||
|
||||
func mockedClock(t *testing.T) *clock.Mock {
|
||||
c := clock.NewMock()
|
||||
now, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
|
||||
now, err := time.ParseInLocation("2006-01-02T15:04:05", "2006-01-02T15:04:05", time.Local)
|
||||
assert.NoError(t, err)
|
||||
c.Set(now)
|
||||
return c
|
||||
|
@ -2320,6 +2320,17 @@ components:
|
||||
description: Whether this Worker can auto-restart.
|
||||
required: [id, name, updated, status, version, can_restart]
|
||||
|
||||
SocketIOWorkerTagUpdate:
|
||||
type: object
|
||||
description: >
|
||||
Worker Tag, sent over SocketIO when it changes.
|
||||
properties:
|
||||
"tag": { $ref: "#/components/schemas/WorkerTag" }
|
||||
"was_deleted":
|
||||
type: boolean
|
||||
description: When a tag was just deleted, this is set to `true`.
|
||||
required: [tag]
|
||||
|
||||
SocketIOSubscription:
|
||||
type: object
|
||||
description: >
|
||||
@ -2343,7 +2354,7 @@ components:
|
||||
|
||||
SocketIOSubscriptionType:
|
||||
type: string
|
||||
enum: [allJobs, allWorkers, job, tasklog, allLastRendered]
|
||||
enum: [allJobs, allWorkers, job, tasklog, allLastRendered, allWorkerTags]
|
||||
description: What kind of thing to subscribe to / unsubscribe from.
|
||||
|
||||
# Worker Management
|
||||
|
257
pkg/api/openapi_spec.gen.go
generated
257
pkg/api/openapi_spec.gen.go
generated
@ -108,134 +108,135 @@ var swaggerSpec = []string{
|
||||
"mAA3biD4eIs15HLs6ejbc8IuwQoDZQ+MdOnOXkyO3rQPLTDdVRyTJ35MzNKeMRM/R9sb+PrsxfYX2P9d",
|
||||
"yJnGuAbBmMtcKwuecVOs/LQThlwJPOv20WoYNpJRFw4T3rVjSIFxat8ZCetpTD31KPNeTr4H5c2+bl+5",
|
||||
"o+16CHgt7WVNsTZZbpT6Ekfzyvsuty3skBrEp8N6T0w/l8J8LSObUNkjlah/sJLaeDMvayGqLNfVf1i/",
|
||||
"9UhtD8uAcNP6r6TG3geKBK2khlxwkbtbvzUMQlBtUfwsJ5CMURS/hiADx6upvijkDB/G13rtqk+pvngh",
|
||||
"Z31U7NRdApLNK3HhhDQI9wh3Vkm5IDlDjpzjQ5evaJcEt5VeSp7bj3PcdJNdpvDY7qTrtLKLCEjkljYm",
|
||||
"L+kqZCsuqsLwElIABUNLPPtgkq5gT8vWouopOvt2w8KaStptrMNEO/w2EvIpQLJfRAZgdGRkF3V6NSE5",
|
||||
"Ti7bWQ7dDmzDXbjaZpnVOWY/V2ht1gi8yjfXJYulRJvAmp0Pe23m1hpMRHKyDS7im+uw0cX+eHxMIFdG",
|
||||
"xZkV6KgyG6Ief3VjUQGVrkbuq3QshpM8k/qcF/uRHTNTh4y75VrxK0qE+jIKmYvK2OIqWHQ404ylrCi0",
|
||||
"DvPkOl6vfd8n0UdVLrZb++YbtfSr/9w71Yn3+IyvzrKQ0LHtx42Ip+vVlvpzolO3t//S1gnF8V1J3uA4",
|
||||
"SThZL6cODogKyxhZ50U0jcHbRPh/fiKUe3D4+/8g//Gvv//b7//++//6/d/+419//9+///vv/zPWzMBG",
|
||||
"EEe7u1nOskU+OBp8dH9+AvdzJS7O0B58aPdkrAp+RqucSx8PP+UFc2EMe6iM7enp3ns50ehOv3twOIYh",
|
||||
"4yN//cuP9s9SD44O7g0HUJFPD44Gd0d39wfDAehy+kyqs0ueMzk4cr8MhgNZmbIyWI+LfTBMuAz9celC",
|
||||
"82Ar7q3uunCmsLK9NLhc4bDOeEpKs3Y8Vw0Oy1Cd1ZbIQcFF9SHCb4gaHjlQOyW2W0YgxpwNimfIDdy2",
|
||||
"5O8Gk1GMIJusKf7VOvZoKxtMnXnVA7VOeDbqFmJG9EobtqgTOd23rdJQkJGVyZngmnXN2+5lZ+KCuJBC",
|
||||
"LpkaZVSzEDbipvCLciH+7/BA3w2G5N1gyUUulxr/yKlacoH/liUTE53bP5jJxuQkTCUXJTU8lPH9Ud7R",
|
||||
"5FxVAtTQH1+9Ojn/E1GVIOcQ3yoLknNtIDUKAsqtkktDppSvoBkWaUWEx9rb/2lB7I6GjX2QdwNU+dW7",
|
||||
"gQ/OcNWI0eDqRVqoS1YqyHOmmrwbNK39frx3gxr2C6mtOg9WhQtGDNNmL2eTaubKnWnCqOZQWMwZA3wK",
|
||||
"HUYP84zkMoOCkpDAXhSNnSV1jz4znv3hbPvaZEOSyZLHDr7zdoWqsR3tPNSr7FY3O3V/1UnaluKznHBn",
|
||||
"m0JbXC6ZFncMWVCTYdo2zUxFizBSJzDqFOtkguVGt4ueAR7JIo9ykJr1jds150J9W28ieyeOGwu0st0C",
|
||||
"mduwjlWAOjerkmrdKmzaSdNPAt2Jn4bO0Cjjbp+vX1QnKv7qFbEZOX4aUiOcWdPp6ui+o4aECnETRiyJ",
|
||||
"yasCr79dCgZtgJkUs2ukijZmsctXVbBo6L8IK2l6E7bSV50U0jWJJohcSgJJ16w/9Uo6VqmH/CLtPZg+",
|
||||
"XMqXIxoSPmZjMmFTqVidphClqYx301C/ZKX76yiGgtmNZ5PVmc8W2SXP06kVibVuqU3voHiDYmJkZfF0",
|
||||
"g8CM+p9YBRXF/l8e0NPnfeymnnz9RgDXVYPFk55dTnzbui1tu0CqB0HcaSBcpg1NB5zBcGPhEXB4SNdw",
|
||||
"ILIHfpZnIx0dZgkNBDi1LIPDRsRTF1MiA+DGmStVpCd+++ZF7PWtZyfcaFZMQySpXIpC0nybDJDafhhO",
|
||||
"EWt5wP77TuUzijCEnGstp2bUrs2Qsh/XE96m8grxrb5CfYU4g76rSFfaENatGlOjO9Yxko1CvbXnGcTf",
|
||||
"LvbvaP28TcTwqibLLSmSn6nvpNb5LPBZ8PJD4rMX5aSj0qiKIea5MCNw9wHFghODuoMo6mHXAivZh9OD",
|
||||
"CDtZYsLmn4h0dpXWC3wmIPDjO5BvpM94Pff01hnjhTSEKeoyC0OZtrbUbpf1/SZrfTdHuODC1Zh3kRMQ",
|
||||
"yX5HkywUMscEXx6XZQJyTV5dMrVU3DCU5bmsNBhQRVRNzpfkSYoPKU/OCzlzHppAA9BZ5KViX//cLhpO",
|
||||
"BSZkVBW8p+KsaZDAHahEErnqbLqkPqAYpAVkDHRCUN65wKxoHCcRbL0uEe/zqMCaS+YnTV2ieo/bVSN0",
|
||||
"FtVQYqSTqF6eRXtsSQaviXvWsYyvDYjbzqDSP9bnJxYamuolcUqRUni+X3sqoLr/gi0miKdbifT46Snt",
|
||||
"WQBqV9sMoC+2I7nRUTVC+6Kqlsmcxk+/dQp0uVI2TXboqW2NZi+2qRPYvTS7KkdtHF0foetH778dmF8b",
|
||||
"RQjUZnJnwHa/jIKvJGFF1SxTDDilHAlpRoYVxYiKlRQsziQ9GhyOD/pgf/Q3b/i2ktt0UbKZa/0wqmv/",
|
||||
"D4aDBddZomjOFVN93cI/fvmb1ZbPcKZmoGlqCofM/Ud2wmfiVfuwGg5H14nFHeDj18fQISk6ibO6kq5e",
|
||||
"0tmMqVHFr+lgWq7QrlOzvwZvZ7XXf0yekKRPprOiNadUMFaeONtXwuFsHwfbmA8PRzXSVxo5sTCDCCkm",
|
||||
"cvSPBvnGh+WGCls5XTX1tDC2JdigKI3J47IsOHM+YvQPS/shB7vVeU5X+kxOz5aMXZxDuhW80/zdvuzD",
|
||||
"ExMrBJlQkIN7o7msFPnpp6OXL+uCS9hEo0bbeOTB0WAhiakIxLFDWFB+BlL30eDuD0f7+1g0wCl9ztsF",
|
||||
"eOXf2n9k3+pgYHOSbk4azdhIs5IqDPBZylHBoG2Jr4PpoG75sh0LCDpjFz1gJt+9GywkehxM5Z0N34/J",
|
||||
"M7B2LhgVmrwbsEumVnY8X+2y2wgu7D8SnQCgPZUfPGg+pmNxA6A2D9fmsWHsYROajXGjFa+5F4Ya1qdT",
|
||||
"Owe4isubbO9AT2rE0WBbLSpvEdaQcUOX9IJ1kesqnv7t01Aa38UBfBbqmGyH6xoOqLYkxR4CFF8YDgzT",
|
||||
"7hU5nVplBIwDbT97jUD9AQWJQmgY+I9kq1Y8XZGZOiUTwqJdVFbCNqDPCvqP1fq0j2b9GuefQG0ubikG",
|
||||
"5Kr2sKC0UmuATuHVZMoF1/O+JnDDL3iew7C/NSfbZ435M9U8WyN4jj8j5Gi5S8jRLkb0rxLd86UqtHyx",
|
||||
"2JttOgOEHJmWZqVCTaMr2Jm2D6mp9bGU4hcrLOQxOiupCKagYuXSGlZe2qAzwk3kuIcClmDbGAfXoDMT",
|
||||
"l1ZgkNM6jtWqn0Rz+zcVDIwvXSmho5E16q7boXNJfnz9lmDgRrDyPHv212fPxnWviR9fvx3Bbwkhodkm",
|
||||
"dedAOENnY/LENaB03sxWNVjqglbRcO9S3im42RUVuVwQGDCYiFxP7K08ntvaTjboFqd0tiXpr6l9QALd",
|
||||
"sRO4HVhEaJ6oobMznoNuce/w7kH+4IdsxOiDfHTv/oMHo0eT6YMRezTdfzRh937I2CShVoQRIlF/c+bI",
|
||||
"OtHfj7gWOl7N7yxmVxU+aQz5tGZqNJJsZ8lqlsr9eFWHVDpLJmEkOUU3eDjtiE19Qi0bykJZdWgR2z3O",
|
||||
"aJUq0PBWMwUF/Fz+oWMZx0+HpKRaL6XKQ2sUUKtdnUar/3j7ZW3WsKgHgAHOZvlqvdO5MeXg0yfoFIYO",
|
||||
"Pwi0z0xkAAm0+pTRhXNV4Zf6aG9v6mMMudzrFifEHAXynKqFS+mBlNXBcFDwjLks+kCcXlwedsZfLpfj",
|
||||
"majGUs323Dd6b1YWo8Px/piJ8dwssO46N0VjtYvQUqdW9u+O98egIMmSCVpysMjYn7AOBJzMHi353uXh",
|
||||
"XtYu6zpDQ0moA3icQ/8o06z/CjImpODDaAf7+x6qTMD31OqgmIG799550BBvt0xAbs4Hh9cEurBYXYRS",
|
||||
"AIiCXtCyK8bomWaFsGmnlR5e6r9B0B8QoHqMZyIvJXfplTPXSrkzYCcR1kI+Cd49COXZ82aWPmA/5yL/",
|
||||
"cyjq9Rord1wbuNON3BLwfi4rUdf4AvU4tM5rtrb/IuvC4nKJdZyEVllLK/EvlYTu942Te85dwplUZCEV",
|
||||
"I09eHPvGbeisgbg3TZYUIuZAhvLbSSFFKXXipKAAVOKogHf+WearLwaNViHLBFh8yzqpnK8PIo+weKPE",
|
||||
"IDLMSb5+PGoUxuuu9JfmxR3iIjHMDY50ygW7fTj1V1pwcLjSGJuugkwtPHVe28t6fN9Atz7IjUQFy0SM",
|
||||
"okDgNSjbKHvxVbH29Y3h5z8FYmJ1kBojm8VDNrC7HcbpRUYoiLWtFPEcq2d91pHv0OPl07Ax1oouiuZY",
|
||||
"bbl4E4K0D+INNIW8ZGnBoysnrD2Nx1nGdOjsn6pmnxgyBIMLaQhu7A749F+VTDx+fezzxItCLl3bQN8B",
|
||||
"e89Jku5Az0lJswt72O9E/3FrZqpyRH191X6yc0IvWbKk6/UQnuRUSaYZg9XSbnqJ6N1CynuJDLMWMkAE",
|
||||
"+pJNaFl6I0luVaRpVRR1KQ/jKj1bufL2kZK3dUhRT2khrPjqrE7QvFLADldkWglsgl9Az6oN6G0RIoXZ",
|
||||
"vZV7+3Gwwfn2PvpqP5/2Pnon7Kd1JKnBDJsddq0Czi3sXPk8p8JF9YRqxdk5qnZRcbo1lqwWn5gwcib3",
|
||||
"T9imXr9dIzNN183anWJ6La1V5Kpo1Ntq9MSPK23ZL51JwBfassgZqmyh7X9H/W7dchptmHqLb/WjakiC",
|
||||
"2h1L6w4L/4mhV9iA/gzkrCuztc0H5K32/flZENppno+QmazJgkMyGpozsAlmfE0ptGq0jCOVPEImVNfV",
|
||||
"cydKLnUjHezqGF/vcXcc962Iejg/JN9gRa9rYfWN5sLdQ/5ZTlx9kgU3HfS8To1jzYLALVZZCQ95p8sS",
|
||||
"s6KaC2+N6nRpgPa9uwfXLyOcBooa0uGYoTPImnMtzX3aXPOFZNIc15C2WaxIXrFW2/OMZnOPfGEouA9S",
|
||||
"ksKKJu/EjYpH8ID4lgRNSoA45jw7ULNfqs4dwbJKkFAXyz7YV6sx3M/NHELmLmXnUqFqv8XVAr32696v",
|
||||
"LFrCuut1L53bv+OFCNmelopiy8K5FSh/eXWK2ZWuTqFLX6jT88xcVrP5f16oP8qFArTacJ0A+8O+7Uhg",
|
||||
"SoMKZktuT9zU3lmeuGaNinb9ZnlmsvmPhZzQRl0qSCG7Xi7SV91uC4FmmL5yp75Yn0+HhttDxSrZ6blH",
|
||||
"LoL+0JBNzNQl033FAfWG43sFXVuwkWidhTQDQPcsp3V+f/edPtNkEvoouopj10Eh62ajKa27XQsf/ejQ",
|
||||
"VxJLCoxvWihpNJbsxyKAamQMdaEdmMwNRRD41JIwoDpAxlw/R/hwfGtoDdzbULXBAn47hKxbf06h2yjE",
|
||||
"doicaAkxeV00tBR376P97y90wdZqc66owVa6nB/w1qhW7dIMvVIBPmuTDhf+HHiUhSn07wuQ2HA+UXpu",
|
||||
"VGs7VIJInove4jT04AaBllRIw0thNzoBwAiV8R2UgqCG6NZArKcKbDeM1wXhRwwK+VSXne4C8in8jore",
|
||||
"ZqwOKcH9OL0pbOW3bYTLp0iCIjoWSmGHUhxG8dnMMpibJVpvBftQYo0SCObtuhMwBiss2BfHGBIusqLK",
|
||||
"UZ5xFaGxNarl4HKG/RlQSnblTcIgC7oKcbXOjkCzi5mSlcjH5BcZepLpEKLmysmR71bMfN+0MQTM6heZ",
|
||||
"vipG3Ig2z32x4TbTack07+VkC80QPxI5idJu+u7j3qSQ2UURktLSN/MNdJH/WU7+HN6+yQO5Fomr3kpK",
|
||||
"66pKi7/fLV2NR0xpX5Xse1fTvNFXH+6AH25L54+/mzTLWAkVbZgwijOnhwJZcZPcNqJiFxVW61q42Dsf",
|
||||
"gWDX+/118Or6Lvpa5AL1Zw2CWY1oJg3CMyobA7f/NqEC0ijQ2pr5q3U3Hr8HQJNcQvyb66AetqybO1wv",
|
||||
"daBTO6BaXDi+X+rYRUFvq8uonX8LSPkHtwI0j/oKFoHkoKHuwnoE0szEFUZ6zKmgCbyuy3j8wVmk34lL",
|
||||
"w+uxTgq2JB4246sZcP1EIVWA6sAY0dR6cNBXQcf3KfdL8MEr+H0IffvKRHMNsgZJoN6CA0PTRb0RQeuE",
|
||||
"pnXoeRLKzfyxkbNRdakHNZs5geBQhbVcEU1PGsNdBUmbC3KYCsbmcNg+EVGHnmdB8v+DoHFzk7sgcehz",
|
||||
"tJY9n8Jb3wZPhr2EFJy0rIgw5kzH1Y90R/K5ZWIhdeuGmk3QgKpedQMbtpH30jtOI9FyTs0IOlONUJ8d",
|
||||
"5bIXp4LN6dc5Nb/aj47N029F4HvqTDZ9ct7PcV+3hA3CIl8kQ2HXZ5+86W06kLuIo4Dz0BeC9Q5WLP83",
|
||||
"BDtTIWcucKVXHgOTketRVM9SD4eGJSg5JopVWEUmhQ/jLVZ+Cq5JOG3vffAFprGRNAqesjI9RqkvA4sY",
|
||||
"V7Fp4J7vH7yHNSvXMO1m2/1rctE3J0l5oeImu96tSlwP8ptzPiXbpqfCcn3rcMukfX/zKDwA+fX+o+sn",
|
||||
"lmEltFCM5itX/9cJDPduJIBAMbK0/8HTg6gRMYPYM3KuWxCtO/GeR9cEUZ5ncyKFM+/fGLupWuymRaSg",
|
||||
"9jAjtG4uj9dfrxYFFxeuZx4iqIMAhoQYJCoOKJUVXYoisr5h61ykFq6nqCvLnNGiCBe8Dr6p6QcCtR2w",
|
||||
"7BZEiY4vEywmbiluiRtdSzPifsnbUo74ZK+ViqR6dm9LUL4CLUm2rE6tN3T+gXL8EsT5+CCGcfkf+47r",
|
||||
"8excKbfqykBLdEI9WscwcI32MUa/lMpod/Frxus2thHhH2OSCPUBRoFttAcMXXl90BK29sZV1GQH3tXG",
|
||||
"CghhCd1bAsPuffRt3z/tfYRf+D/WONTjDtBSMR8N15IBt27oD/UOuwKjf3UnP/ywM29U4dn3wg7FnROz",
|
||||
"+t1vM2uomHvdsf+prt9bGiJv1SWKKw/V3cmTfeobAmZ0X9YR74CR/9zIOEwZVRxR4c0eyNyVeWFTpkho",
|
||||
"fu/bYxQuyerd4GD/h3eDgFh17WFQKsC/ZyolvEhfb08HOQ7DTJHEOw7eOHDMlKOFljiGlgsmBSOs0DBO",
|
||||
"XXI4tUzAFgDgnFHMAnYg/G8jnGb0hIrRU7vP0VsYYJCAYdQNOAVDqfiMC1rAnHZ86LaBNY0LGddAdvKC",
|
||||
"VeOiFjPYNdGHAeC+nZLny9YIQjm8AZ1kZhzDSDft7ZVb2Oi5W9hgY6zSNvKMzAwzI20Uo4smhQia+oQL",
|
||||
"e7+Hm3M5n+AcOsb/q9kVvRjaNSke7P+w6XWHjg1EdCQHg5QfJkdQ7nOrDmAI8YSZJXPI7ju210QnaO0u",
|
||||
"HAQWgN0GVIfuBNHZ4zIoO/cTjUMa7co33Fp/A+ub4xCvVDJzFZUnzH4Y5p+sGvcOJYrz3it0RKA1tys6",
|
||||
"BtQlBsdNB0Bv4EDAGVwIdD/fIb9Iw+rm242HcD+nUmV8UqxIVkhXd/2n09PXJJNCsAx7/mM/Ewm14Rzh",
|
||||
"dQXydOO8GGEfaGaIpgvmJEkjfe8jksvKCnn4gR6/E/5UMTsIb1NdHixxAmQi81UvK43TUO0UtXbRBUss",
|
||||
"OYJ1ce+jazfxab0B2rV03SLsMnSvuJ0GQlclO+k4wSqIYipvqWW52Udljdku8cWak99zRfrXn75v+/Kt",
|
||||
"IIHfzzpcgEYuHh96ApraEhN8OKeaCOhdQFbM3C50iiMQOj1zMFJ7wbD8D+59gwPMFW9ohR2EBt0bEM9A",
|
||||
"C9EtkO/Uvnh7kM+wD2avLCgXOxbDOG0D51vBqyguimpDpmwZtWF3G7ijcdtbUK/4kzCebxyyFqu2CwqI",
|
||||
"+oDcKFZ9eQtkpxvTNx8XgCzwGwgMwCY7EFCGAeaXjLDplGXGi7XQOBNHoJosWVG4970FHnqYMuqS0+fV",
|
||||
"ggqNMdAgnIIL+ZLTbsJ8XX3W3hGoRe1vFAY0wsWq79U54UIbRvNWaZuoom9vFYZQe/faWLpPx/BTXbny",
|
||||
"YcjraPTUrasXrK8UgKqdDj1isbmRNwEbl42K2mSxIrSeLiGh4zGMFjOzZ+jMnsRsu2ySuojqtoq4obM6",
|
||||
"seM2R2DHVbKhqjBchkpgfVXd6JAawtTt7tC2b8fQkM1aH2MN5g0h22vA+uUQOSqAmybj0eYTKByE/vi1",
|
||||
"3r1uw/dmX4DtlVUCplg1qQnUL88dN8LTdZprAeyKBi2Laa7BXLhOmFR/ezI7XbUqKtArD6WttkGWBqIN",
|
||||
"3TahswBSI0KbuNlHyDbEuoUD0zdyzV705CvUvZ/1eE024TJ+rf+epYtKghP/q1+A3RD/BikdNMquQ1nQ",
|
||||
"HurjWqCuvg4uiyHRsrb3ZbQonKHvQsglhGG9fXv89PZcwhDAIdhy1+uHkkgT9dK3LWqgtunC3cBt67tq",
|
||||
"fwErvl/rprumt4KRS4bwn3pRt+EwSFWe7gJv76Mrx76D6LWVShmGvf503k6JVoc7gUe5WL7bKfF5bWnp",
|
||||
"Wn8dG7z5mVwsQp9Q8GFmEHILDhRXVrE2oCxD5wUuyLnr+nMOyhV6AJsvYciFazkytEy8JNyQKVfajMlj",
|
||||
"sUKLDL4WV/ePhvE+QyDrVWirczW586vi1JcmBWs47rZpwcvQ6mcbeYXkzEBn63DE3q673c3fxqrkdP5u",
|
||||
"/5ubPrrrEiKSPX1ug7HpltiBehFwO2uQx+idkNIL1L2GzoY8/U2gYacPTw8OdmV0cvxUN0wItd/Vt+0l",
|
||||
"cvrPiaNREWMLKYSGnvMyWMB+3R0/C8bKkY4afW7ics3OoN8Sy2vubJs6+hDU0miFui4pmcVCnZCpL28n",
|
||||
"Cm6gXF8VI66Nk25CBp9j3D7FK1umQivWr2qXuiJtsgKcVN6y1mhhmUDzlhsD210x5Zt0r5Hf8MUgb1/f",
|
||||
"+Tc6i6+xPkniV3+jphkPCZb3i+sdd8rtiRHzy2+YVzqKQkdGq4/Esrz6S51AKqvvjeR0ukb04jPxajrd",
|
||||
"ygVz+2DpmtIBiW20o/sbdLiLjVHqItZ5qSZ1R921AH9CiwKjFb11xkhSODecL2YK5jszZ6s7ipEZlFJx",
|
||||
"w497T0VsOBRxrVfbTdF/qRfM0Jwa+hWMrXF/6T/Eld4aDR9XZs6Ewf7vrjWUxQYfStlnLfhsnMRAZCNh",
|
||||
"BpeDKyNOxesDT2KscYmwScE4OrXB10YOWKnXbuq+4X0CqZCk/4vbjVW7Y4jP8AqNuRVmTYhVDxB6UWGU",
|
||||
"1Y3W0yQs0ZT9um0+YaKU1lL7L3TA050l1D8w5XFU3Z2btydDWEIWjAua0MySjYLlWJsQE6ccRRk1Y6I8",
|
||||
"uoBvlYs6YcdRGaZGhcxoAQSOFvpLU7VL1thNlXIvQXDQGj7r5HEXN3599WGd4b03rBvKrUUdBvrI1S/S",
|
||||
"1wMNaZmhSFZk97i3f/gFu20hivUi5mumfLODp0xwJJ0ufz9tOscQOsfyaGb4JVpiGbhHfY2oopBL9FU4",
|
||||
"sLitKz6bGyLk0gXwHd4sg/k1dN6HnDR04GFTcH2hMbMMMtZnEroou8wMvHA7XlrnHqRh/Agam24T4JRX",
|
||||
"OFW6D0Uygq7/utgh0f72LQSjup30XUcnG3GBS/SBgVeyarixutGnqVtS53joZj9uh0m+LKWWLp8rjF2X",
|
||||
"Vrtpg8lnMqeGUVdfDIlZlTyD2EPXIAQE5lLJmWJaD6GDCNbGAe4zpbyoFNvIYTxf0UzkDUedBbcfHapH",
|
||||
"M8U235S9BV2N+EhV/WGlL+nKmVIq8U0kpbykq78wVr5Bj/M3pp5h4LcTY+rs5UhijlzvEYNSlSB75IKx",
|
||||
"0rvi6wBw8qr0tY8gkY5yoQkl6GqPZdLglEn533sQuSPRg7IXray1Jq7rqPT1qC0rU1ZmVCqZV9k6Qd8S",
|
||||
"y1fw8mv/7q1gDlCzau99yWa7ZhMP3belmH2tROSDLRORQfpzKba+bcW9u3ev/6K9YGJm5qF4z5/iZkU5",
|
||||
"z7FFraWylDgQjNwnmFfuVnp4/St9TVeQbwqdkqhyLWbu3b1/E24EXZWlVPagXrKcU3K6Kp3HDFCMIEZ5",
|
||||
"YXIS0qXrxoNx9Ne9g0c309TK129ATgmkQ0qyoGJFpvZiu0Jxzi1t5koaUzBXTu4PJXlgnrYF9EJqQxTL",
|
||||
"MHs9lL6D/aI8EGVrcwBOVfpIqtoRwoTG2nWYQwHSuztl++UdTXI+Yxob+LfOmDwJ2fMQJ/b6lx8Bzj+/",
|
||||
"fvYjcahkBy0LKkQ6TmudwGPm1WIiKC/0XqnYJWdLT5a4woJ/ntoTpP5eDAKIqktPzStVDI4Ge4PICNUm",
|
||||
"VsfNIKhO8y+PKYEdQJJKtxDGz3LizaQgo/29Yopb9Ks77A1b7RTGjSqQOjHo49fHzZZksYlMLhaVQHET",
|
||||
"CmykGns3HLiJCRw2vAxrItCdu7chKDZjstuwd0XJwq+oMxk4HROlXjB9PswCfKLO/XcQDG3S3stJqGgW",
|
||||
"z+HS9T/99un/BQAA//+MoGf5BQYBAA==",
|
||||
"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",
|
||||
"AP//gDoBPDoHAQA=",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
11
pkg/api/openapi_types.gen.go
generated
11
pkg/api/openapi_types.gen.go
generated
@ -105,6 +105,8 @@ const (
|
||||
|
||||
SocketIOSubscriptionTypeAllLastRendered SocketIOSubscriptionType = "allLastRendered"
|
||||
|
||||
SocketIOSubscriptionTypeAllWorkerTags SocketIOSubscriptionType = "allWorkerTags"
|
||||
|
||||
SocketIOSubscriptionTypeAllWorkers SocketIOSubscriptionType = "allWorkers"
|
||||
|
||||
SocketIOSubscriptionTypeJob SocketIOSubscriptionType = "job"
|
||||
@ -604,6 +606,15 @@ type SocketIOTaskUpdate struct {
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
// Worker Tag, sent over SocketIO when it changes.
|
||||
type SocketIOWorkerTagUpdate struct {
|
||||
// Tag of workers. A job can optionally specify which tag it should be limited to. Workers can be part of multiple tags simultaneously.
|
||||
Tag WorkerTag `json:"tag"`
|
||||
|
||||
// When a tag was just deleted, this is set to `true`.
|
||||
WasDeleted *bool `json:"was_deleted,omitempty"`
|
||||
}
|
||||
|
||||
// Subset of a Worker, sent over SocketIO when a worker changes.
|
||||
type SocketIOWorkerUpdate struct {
|
||||
// Whether this Worker can auto-restart.
|
||||
|
@ -13,12 +13,14 @@ const websocketURL = ws();
|
||||
export default {
|
||||
emits: [
|
||||
// Data from Flamenco Manager:
|
||||
"jobUpdate", "taskUpdate", "taskLogUpdate", "message", "workerUpdate", "lastRenderedUpdate",
|
||||
"jobUpdate", "taskUpdate", "taskLogUpdate", "message", "workerUpdate",
|
||||
"lastRenderedUpdate", "workerTagUpdate",
|
||||
// SocketIO events:
|
||||
"sioReconnected", "sioDisconnected"
|
||||
],
|
||||
props: [
|
||||
"mainSubscription", // One of the 'allXXX' subscription types, see `SocketIOSubscriptionType` in `flamenco-openapi.yaml`.
|
||||
"extraSubscription", // One of the 'allXXX' subscription types, see `SocketIOSubscriptionType` in `flamenco-openapi.yaml`.
|
||||
"subscribedJobID",
|
||||
"subscribedTaskID",
|
||||
],
|
||||
@ -66,6 +68,14 @@ export default {
|
||||
this._updateMainSubscription("subscribe", newType);
|
||||
}
|
||||
},
|
||||
extraSubscription(newType, oldType) {
|
||||
if (oldType) {
|
||||
this._updateMainSubscription("unsubscribe", oldType);
|
||||
}
|
||||
if (newType) {
|
||||
this._updateMainSubscription("subscribe", newType);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
connectToWebsocket() {
|
||||
@ -160,6 +170,13 @@ export default {
|
||||
this.$emit("workerUpdate", apiWorkerUpdate);
|
||||
});
|
||||
|
||||
this.socket.on("/workertags", (workerTagUpdate) => {
|
||||
// Convert to API object, in order to have the same parsing of data as
|
||||
// when we'd do an API call.
|
||||
const apiWorkerTagUpdate = API.SocketIOWorkerTagUpdate.constructFromObject(workerTagUpdate)
|
||||
this.$emit("workerTagUpdate", apiWorkerTagUpdate);
|
||||
});
|
||||
|
||||
// Chat system, useful for debugging.
|
||||
this.socket.on("/message", (message) => {
|
||||
this.$emit("message", message);
|
||||
@ -219,6 +236,7 @@ export default {
|
||||
if (this.subscribedJobID) this._updateJobSubscription("subscribe", this.subscribedJobID);
|
||||
if (this.subscribedTaskID) this._updateTaskLogSubscription("subscribe", this.subscribedTaskID);
|
||||
if (this.mainSubscription) this._updateMainSubscription("subscribe", this.mainSubscription);
|
||||
if (this.extraSubscription) this._updateMainSubscription("subscribe", this.extraSubscription);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -97,6 +97,7 @@ import TabsWrapper from '@/components/TabsWrapper.vue'
|
||||
import PopoverEditableJobPriority from '@/components/PopoverEditableJobPriority.vue'
|
||||
import { copyElementText, copyElementData } from '@/clipboard';
|
||||
import { useWorkers } from '@/stores/workers'
|
||||
import { useNotifs } from '@/stores/notifications';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
@ -123,6 +124,7 @@ export default {
|
||||
jobTypeSettings: null, // Mapping from setting key to its definition in the job type.
|
||||
showAllSettings: false,
|
||||
workers: useWorkers(),
|
||||
notifs: useNotifs(),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -123,17 +123,20 @@
|
||||
|
||||
<section class="worker-maintenance">
|
||||
<h3 class="sub-title">Maintenance</h3>
|
||||
<p>{{ workerData.name }} is <span class="worker-status">{{ workerData.status }}</span>, which means
|
||||
<template v-if="workerData.status == 'offline'">can be safely removed.</template>
|
||||
<template v-else>removing it now can cause the Worker to log errors. It
|
||||
is adviced to shut down the Worker before removing it from the
|
||||
system.</template>
|
||||
</p>
|
||||
<p><button @click="deleteWorker">Remove {{ workerData.name }}</button></p>
|
||||
<p class="hint">
|
||||
When a Worker is removed from the system, any active task still assigned
|
||||
to it will be requeued. Restarting the Worker after removing it from the
|
||||
system will simply register it anew.
|
||||
<p>
|
||||
{{ workerData.name }} is
|
||||
<template v-if="workerData.status == 'offline'">
|
||||
<span class="worker-status">offline</span>, which means it can be safely removed.
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="workerData.status == 'error'">
|
||||
in <span class="worker-status">error</span> state
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="worker-status">{{ workerData.status }}</span>
|
||||
</template>, which means removing it now can cause it to log errors. It
|
||||
is advised to shut down the Worker before removing it from the system.
|
||||
</template>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
7
web/app/src/manager-api/index.js
generated
7
web/app/src/manager-api/index.js
generated
@ -63,6 +63,7 @@ import SocketIOSubscriptionOperation from './model/SocketIOSubscriptionOperation
|
||||
import SocketIOSubscriptionType from './model/SocketIOSubscriptionType';
|
||||
import SocketIOTaskLogUpdate from './model/SocketIOTaskLogUpdate';
|
||||
import SocketIOTaskUpdate from './model/SocketIOTaskUpdate';
|
||||
import SocketIOWorkerTagUpdate from './model/SocketIOWorkerTagUpdate';
|
||||
import SocketIOWorkerUpdate from './model/SocketIOWorkerUpdate';
|
||||
import SubmittedJob from './model/SubmittedJob';
|
||||
import Task from './model/Task';
|
||||
@ -433,6 +434,12 @@ export {
|
||||
*/
|
||||
SocketIOTaskUpdate,
|
||||
|
||||
/**
|
||||
* The SocketIOWorkerTagUpdate model constructor.
|
||||
* @property {module:model/SocketIOWorkerTagUpdate}
|
||||
*/
|
||||
SocketIOWorkerTagUpdate,
|
||||
|
||||
/**
|
||||
* The SocketIOWorkerUpdate model constructor.
|
||||
* @property {module:model/SocketIOWorkerUpdate}
|
||||
|
@ -54,6 +54,13 @@ export default class SocketIOSubscriptionType {
|
||||
"allLastRendered" = "allLastRendered";
|
||||
|
||||
|
||||
/**
|
||||
* value: "allWorkerTags"
|
||||
* @const
|
||||
*/
|
||||
"allWorkerTags" = "allWorkerTags";
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns a <code>SocketIOSubscriptionType</code> enum value from a Javascript object name.
|
||||
|
84
web/app/src/manager-api/model/SocketIOWorkerTagUpdate.js
generated
Normal file
84
web/app/src/manager-api/model/SocketIOWorkerTagUpdate.js
generated
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Flamenco manager
|
||||
* Render Farm manager API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*
|
||||
*/
|
||||
|
||||
import ApiClient from '../ApiClient';
|
||||
import WorkerTag from './WorkerTag';
|
||||
|
||||
/**
|
||||
* The SocketIOWorkerTagUpdate model module.
|
||||
* @module model/SocketIOWorkerTagUpdate
|
||||
* @version 0.0.0
|
||||
*/
|
||||
class SocketIOWorkerTagUpdate {
|
||||
/**
|
||||
* Constructs a new <code>SocketIOWorkerTagUpdate</code>.
|
||||
* Worker Tag, sent over SocketIO when it changes.
|
||||
* @alias module:model/SocketIOWorkerTagUpdate
|
||||
* @param tag {module:model/WorkerTag}
|
||||
*/
|
||||
constructor(tag) {
|
||||
|
||||
SocketIOWorkerTagUpdate.initialize(this, tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the fields of this object.
|
||||
* This method is used by the constructors of any subclasses, in order to implement multiple inheritance (mix-ins).
|
||||
* Only for internal use.
|
||||
*/
|
||||
static initialize(obj, tag) {
|
||||
obj['tag'] = tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a <code>SocketIOWorkerTagUpdate</code> from a plain JavaScript object, optionally creating a new instance.
|
||||
* Copies all relevant properties from <code>data</code> to <code>obj</code> if supplied or a new instance if not.
|
||||
* @param {Object} data The plain JavaScript object bearing properties of interest.
|
||||
* @param {module:model/SocketIOWorkerTagUpdate} obj Optional instance to populate.
|
||||
* @return {module:model/SocketIOWorkerTagUpdate} The populated <code>SocketIOWorkerTagUpdate</code> instance.
|
||||
*/
|
||||
static constructFromObject(data, obj) {
|
||||
if (data) {
|
||||
obj = obj || new SocketIOWorkerTagUpdate();
|
||||
|
||||
if (data.hasOwnProperty('tag')) {
|
||||
obj['tag'] = WorkerTag.constructFromObject(data['tag']);
|
||||
}
|
||||
if (data.hasOwnProperty('was_deleted')) {
|
||||
obj['was_deleted'] = ApiClient.convertToType(data['was_deleted'], 'Boolean');
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @member {module:model/WorkerTag} tag
|
||||
*/
|
||||
SocketIOWorkerTagUpdate.prototype['tag'] = undefined;
|
||||
|
||||
/**
|
||||
* When a tag was just deleted, this is set to `true`.
|
||||
* @member {Boolean} was_deleted
|
||||
*/
|
||||
SocketIOWorkerTagUpdate.prototype['was_deleted'] = undefined;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export default SocketIOWorkerTagUpdate;
|
||||
|
@ -7,7 +7,9 @@
|
||||
</div>
|
||||
<footer class="app-footer">
|
||||
<notification-bar />
|
||||
<update-listener ref="updateListener" mainSubscription="allWorkers" @workerUpdate="onSIOWorkerUpdate"
|
||||
<update-listener ref="updateListener"
|
||||
mainSubscription="allWorkers" extraSubscription="allWorkerTags"
|
||||
@workerUpdate="onSIOWorkerUpdate" @workerTagUpdate="onSIOWorkerTagsUpdate"
|
||||
@sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
|
||||
</footer>
|
||||
</template>
|
||||
@ -85,6 +87,10 @@ export default {
|
||||
|
||||
this._fetchWorker(this.workerID);
|
||||
},
|
||||
onSIOWorkerTagsUpdate(workerTagsUpdate) {
|
||||
this.workers.refreshTags()
|
||||
.then(() => this._fetchWorker(this.workerID));
|
||||
},
|
||||
|
||||
onTableWorkerClicked(rowData) {
|
||||
if (rowData.id == this.workerID) return;
|
||||
|
@ -61,6 +61,18 @@ file][workercfg].
|
||||
|
||||
[workercfg]: {{< ref "usage/worker-configuration" >}}
|
||||
|
||||
## My Worker cannot find Blender, what do I do?
|
||||
|
||||
When installing and starting the Flamenco Worker you may see a warning in the logs that says
|
||||
the Worker cannot find Blender.
|
||||
|
||||
```
|
||||
WRN Blender could not be found. Flamenco Manager will have to supply the full path to Blender when Tasks are sent to this Worker. For more help see https://flamenco.blender.org/usage/variables/blender/
|
||||
```
|
||||
|
||||
If Flamenco cannot locate Blender on the system it is possible to use a [two-way variable named `blender`][blendervar] for each platform (eg: Windows, Linux, or MacOS). This path to Blender is then sent to the Worker for each render task. Note that the Worker will still show the warning at startup, as it cannot find Blender by itself; this is fine, because you now have configured the Manager to provide this path.
|
||||
|
||||
[blendervar]: {{< ref "usage/variables/blender" >}}
|
||||
|
||||
## Can I change the paths/names of the rendered files?
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user