Fix T99421: Introducing an etag for job types
The etag prevents job submissions with old settings, when the job compiler script has been edited. The etag is the SHA1 hash of the `JOB_TYPE` dictionary (as defined by the JavaScript file). The hash is computed in a way that's independent of the exact formatting in the JavaScript file. Also the actual JS code itself is irrelevant, just the `JOB_TYPE` dictionary is used.
This commit is contained in:
parent
48ca73f550
commit
a6c935a634
@ -48,6 +48,7 @@ def job_for_scene(scene: bpy.types.Scene) -> Optional[_SubmittedJob]:
|
|||||||
settings=settings,
|
settings=settings,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
submitter_platform=platform.system().lower(),
|
submitter_platform=platform.system().lower(),
|
||||||
|
type_etag=propgroup.job_type.etag,
|
||||||
)
|
)
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
@ -95,9 +95,17 @@ def _available_job_types_from_json(job_types_json: str) -> None:
|
|||||||
json_dict = json.loads(job_types_json)
|
json_dict = json.loads(job_types_json)
|
||||||
|
|
||||||
dummy_cfg = Configuration()
|
dummy_cfg = Configuration()
|
||||||
|
|
||||||
|
try:
|
||||||
job_types = validate_and_convert_types(
|
job_types = validate_and_convert_types(
|
||||||
json_dict, (AvailableJobTypes,), ["job_types"], True, True, dummy_cfg
|
json_dict, (AvailableJobTypes,), ["job_types"], True, True, dummy_cfg
|
||||||
)
|
)
|
||||||
|
except TypeError:
|
||||||
|
_log.warn(
|
||||||
|
"Flamenco: could not restore cached job types, refresh them from Flamenco Manager"
|
||||||
|
)
|
||||||
|
_store_available_job_types(AvailableJobTypes(job_types=[]))
|
||||||
|
return
|
||||||
|
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
job_types, AvailableJobTypes
|
job_types, AvailableJobTypes
|
||||||
|
@ -8,6 +8,7 @@ Name | Type | Description | Notes
|
|||||||
**name** | **str** | |
|
**name** | **str** | |
|
||||||
**label** | **str** | |
|
**label** | **str** | |
|
||||||
**settings** | [**[AvailableJobSetting]**](AvailableJobSetting.md) | |
|
**settings** | [**[AvailableJobSetting]**](AvailableJobSetting.md) | |
|
||||||
|
**etag** | **str** | Hash of the job type. If the job settings or the label change, this etag will change. This is used on job submission to ensure that the submitted job settings are up to date. |
|
||||||
**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)
|
||||||
|
@ -13,6 +13,7 @@ Name | Type | Description | Notes
|
|||||||
**status** | [**JobStatus**](JobStatus.md) | |
|
**status** | [**JobStatus**](JobStatus.md) | |
|
||||||
**activity** | **str** | Description of the last activity on this job. |
|
**activity** | **str** | Description of the last activity on this job. |
|
||||||
**priority** | **int** | | defaults to 50
|
**priority** | **int** | | defaults to 50
|
||||||
|
**type_etag** | **str** | Hash of the job type, copied from the `AvailableJobType.etag` property of the job type. The job will be rejected if this field doesn't match the actual job type on the Manager. This prevents job submission with old settings, after the job compiler script has been updated. If this field is ommitted, the check is bypassed. | [optional]
|
||||||
**settings** | [**JobSettings**](JobSettings.md) | | [optional]
|
**settings** | [**JobSettings**](JobSettings.md) | | [optional]
|
||||||
**metadata** | [**JobMetadata**](JobMetadata.md) | | [optional]
|
**metadata** | [**JobMetadata**](JobMetadata.md) | | [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]
|
**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]
|
||||||
|
@ -1010,6 +1010,7 @@ with flamenco.manager.ApiClient() as api_client:
|
|||||||
submitted_job = SubmittedJob(
|
submitted_job = SubmittedJob(
|
||||||
name="name_example",
|
name="name_example",
|
||||||
type="type_example",
|
type="type_example",
|
||||||
|
type_etag="type_etag_example",
|
||||||
priority=50,
|
priority=50,
|
||||||
settings=JobSettings(),
|
settings=JobSettings(),
|
||||||
metadata=JobMetadata(
|
metadata=JobMetadata(
|
||||||
@ -1053,6 +1054,7 @@ No authorization required
|
|||||||
| Status code | Description | Response headers |
|
| Status code | Description | Response headers |
|
||||||
|-------------|-------------|------------------|
|
|-------------|-------------|------------------|
|
||||||
**200** | Job was succesfully compiled into individual tasks. | - |
|
**200** | Job was succesfully compiled into individual tasks. | - |
|
||||||
|
**412** | The given job type etag does not match the job type etag on the Manager. This is likely due to the client caching the job type for too long. | - |
|
||||||
**0** | Error message | - |
|
**0** | Error message | - |
|
||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
@ -9,6 +9,7 @@ Name | Type | Description | Notes
|
|||||||
**type** | **str** | |
|
**type** | **str** | |
|
||||||
**submitter_platform** | **str** | Operating system of the submitter. This is used to recognise two-way variables. This should be a lower-case version of the platform, like \"linux\", \"windows\", \"darwin\", \"openbsd\", etc. Should be ompatible with Go's `runtime.GOOS`; run `go tool dist list` to get a list of possible platforms. As a special case, the platform \"manager\" can be given, which will be interpreted as \"the Manager's platform\". This is mostly to make test/debug scripts easier, as they can use a static document on all platforms. |
|
**submitter_platform** | **str** | Operating system of the submitter. This is used to recognise two-way variables. This should be a lower-case version of the platform, like \"linux\", \"windows\", \"darwin\", \"openbsd\", etc. Should be ompatible with Go's `runtime.GOOS`; run `go tool dist list` to get a list of possible platforms. As a special case, the platform \"manager\" can be given, which will be interpreted as \"the Manager's platform\". This is mostly to make test/debug scripts easier, as they can use a static document on all platforms. |
|
||||||
**priority** | **int** | | defaults to 50
|
**priority** | **int** | | defaults to 50
|
||||||
|
**type_etag** | **str** | Hash of the job type, copied from the `AvailableJobType.etag` property of the job type. The job will be rejected if this field doesn't match the actual job type on the Manager. This prevents job submission with old settings, after the job compiler script has been updated. If this field is ommitted, the check is bypassed. | [optional]
|
||||||
**settings** | [**JobSettings**](JobSettings.md) | | [optional]
|
**settings** | [**JobSettings**](JobSettings.md) | | [optional]
|
||||||
**metadata** | [**JobMetadata**](JobMetadata.md) | | [optional]
|
**metadata** | [**JobMetadata**](JobMetadata.md) | | [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]
|
**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]
|
||||||
|
@ -90,6 +90,7 @@ class AvailableJobType(ModelNormal):
|
|||||||
'name': (str,), # noqa: E501
|
'name': (str,), # noqa: E501
|
||||||
'label': (str,), # noqa: E501
|
'label': (str,), # noqa: E501
|
||||||
'settings': ([AvailableJobSetting],), # noqa: E501
|
'settings': ([AvailableJobSetting],), # noqa: E501
|
||||||
|
'etag': (str,), # noqa: E501
|
||||||
}
|
}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@ -101,6 +102,7 @@ class AvailableJobType(ModelNormal):
|
|||||||
'name': 'name', # noqa: E501
|
'name': 'name', # noqa: E501
|
||||||
'label': 'label', # noqa: E501
|
'label': 'label', # noqa: E501
|
||||||
'settings': 'settings', # noqa: E501
|
'settings': 'settings', # noqa: E501
|
||||||
|
'etag': 'etag', # noqa: E501
|
||||||
}
|
}
|
||||||
|
|
||||||
read_only_vars = {
|
read_only_vars = {
|
||||||
@ -110,13 +112,14 @@ class AvailableJobType(ModelNormal):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@convert_js_args_to_python_args
|
@convert_js_args_to_python_args
|
||||||
def _from_openapi_data(cls, name, label, settings, *args, **kwargs): # noqa: E501
|
def _from_openapi_data(cls, name, label, settings, etag, *args, **kwargs): # noqa: E501
|
||||||
"""AvailableJobType - a model defined in OpenAPI
|
"""AvailableJobType - a model defined in OpenAPI
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str):
|
name (str):
|
||||||
label (str):
|
label (str):
|
||||||
settings ([AvailableJobSetting]):
|
settings ([AvailableJobSetting]):
|
||||||
|
etag (str): Hash of the job type. If the job settings or the label change, this etag will change. This is used on job submission to ensure that the submitted job settings are up to date.
|
||||||
|
|
||||||
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
|
||||||
@ -179,6 +182,7 @@ class AvailableJobType(ModelNormal):
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.label = label
|
self.label = label
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
self.etag = etag
|
||||||
for var_name, var_value in kwargs.items():
|
for var_name, var_value in kwargs.items():
|
||||||
if var_name not in self.attribute_map and \
|
if var_name not in self.attribute_map and \
|
||||||
self._configuration is not None and \
|
self._configuration is not None and \
|
||||||
@ -199,13 +203,14 @@ class AvailableJobType(ModelNormal):
|
|||||||
])
|
])
|
||||||
|
|
||||||
@convert_js_args_to_python_args
|
@convert_js_args_to_python_args
|
||||||
def __init__(self, name, label, settings, *args, **kwargs): # noqa: E501
|
def __init__(self, name, label, settings, etag, *args, **kwargs): # noqa: E501
|
||||||
"""AvailableJobType - a model defined in OpenAPI
|
"""AvailableJobType - a model defined in OpenAPI
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str):
|
name (str):
|
||||||
label (str):
|
label (str):
|
||||||
settings ([AvailableJobSetting]):
|
settings ([AvailableJobSetting]):
|
||||||
|
etag (str): Hash of the job type. If the job settings or the label change, this etag will change. This is used on job submission to ensure that the submitted job settings are up to date.
|
||||||
|
|
||||||
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
|
||||||
@ -266,6 +271,7 @@ class AvailableJobType(ModelNormal):
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.label = label
|
self.label = label
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
self.etag = etag
|
||||||
for var_name, var_value in kwargs.items():
|
for var_name, var_value in kwargs.items():
|
||||||
if var_name not in self.attribute_map and \
|
if var_name not in self.attribute_map and \
|
||||||
self._configuration is not None and \
|
self._configuration is not None and \
|
||||||
|
@ -104,6 +104,7 @@ class Job(ModelComposed):
|
|||||||
'updated': (datetime,), # noqa: E501
|
'updated': (datetime,), # noqa: E501
|
||||||
'status': (JobStatus,), # noqa: E501
|
'status': (JobStatus,), # noqa: E501
|
||||||
'activity': (str,), # noqa: E501
|
'activity': (str,), # noqa: E501
|
||||||
|
'type_etag': (str,), # noqa: E501
|
||||||
'settings': (JobSettings,), # noqa: E501
|
'settings': (JobSettings,), # noqa: E501
|
||||||
'metadata': (JobMetadata,), # noqa: E501
|
'metadata': (JobMetadata,), # noqa: E501
|
||||||
}
|
}
|
||||||
@ -123,6 +124,7 @@ class Job(ModelComposed):
|
|||||||
'updated': 'updated', # noqa: E501
|
'updated': 'updated', # noqa: E501
|
||||||
'status': 'status', # noqa: E501
|
'status': 'status', # noqa: E501
|
||||||
'activity': 'activity', # noqa: E501
|
'activity': 'activity', # noqa: E501
|
||||||
|
'type_etag': 'type_etag', # noqa: E501
|
||||||
'settings': 'settings', # noqa: E501
|
'settings': 'settings', # noqa: E501
|
||||||
'metadata': 'metadata', # noqa: E501
|
'metadata': 'metadata', # noqa: E501
|
||||||
}
|
}
|
||||||
@ -175,6 +177,7 @@ class Job(ModelComposed):
|
|||||||
Animal class but this time we won't travel
|
Animal class but this time we won't travel
|
||||||
through its discriminator because we passed in
|
through its discriminator because we passed in
|
||||||
_visited_composed_classes = (Animal,)
|
_visited_composed_classes = (Animal,)
|
||||||
|
type_etag (str): Hash of the job type, copied from the `AvailableJobType.etag` property of the job type. The job will be rejected if this field doesn't match the actual job type on the Manager. This prevents job submission with old settings, after the job compiler script has been updated. If this field is ommitted, the check is bypassed. . [optional] # noqa: E501
|
||||||
settings (JobSettings): [optional] # noqa: E501
|
settings (JobSettings): [optional] # noqa: E501
|
||||||
metadata (JobMetadata): [optional] # noqa: E501
|
metadata (JobMetadata): [optional] # noqa: E501
|
||||||
"""
|
"""
|
||||||
@ -286,6 +289,7 @@ class Job(ModelComposed):
|
|||||||
Animal class but this time we won't travel
|
Animal class but this time we won't travel
|
||||||
through its discriminator because we passed in
|
through its discriminator because we passed in
|
||||||
_visited_composed_classes = (Animal,)
|
_visited_composed_classes = (Animal,)
|
||||||
|
type_etag (str): Hash of the job type, copied from the `AvailableJobType.etag` property of the job type. The job will be rejected if this field doesn't match the actual job type on the Manager. This prevents job submission with old settings, after the job compiler script has been updated. If this field is ommitted, the check is bypassed. . [optional] # noqa: E501
|
||||||
settings (JobSettings): [optional] # noqa: E501
|
settings (JobSettings): [optional] # noqa: E501
|
||||||
metadata (JobMetadata): [optional] # noqa: E501
|
metadata (JobMetadata): [optional] # noqa: E501
|
||||||
"""
|
"""
|
||||||
|
@ -93,6 +93,7 @@ class SubmittedJob(ModelNormal):
|
|||||||
'type': (str,), # noqa: E501
|
'type': (str,), # noqa: E501
|
||||||
'priority': (int,), # noqa: E501
|
'priority': (int,), # noqa: E501
|
||||||
'submitter_platform': (str,), # noqa: E501
|
'submitter_platform': (str,), # noqa: E501
|
||||||
|
'type_etag': (str,), # noqa: E501
|
||||||
'settings': (JobSettings,), # noqa: E501
|
'settings': (JobSettings,), # noqa: E501
|
||||||
'metadata': (JobMetadata,), # noqa: E501
|
'metadata': (JobMetadata,), # noqa: E501
|
||||||
}
|
}
|
||||||
@ -107,6 +108,7 @@ class SubmittedJob(ModelNormal):
|
|||||||
'type': 'type', # noqa: E501
|
'type': 'type', # noqa: E501
|
||||||
'priority': 'priority', # noqa: E501
|
'priority': 'priority', # noqa: E501
|
||||||
'submitter_platform': 'submitter_platform', # noqa: E501
|
'submitter_platform': 'submitter_platform', # noqa: E501
|
||||||
|
'type_etag': 'type_etag', # noqa: E501
|
||||||
'settings': 'settings', # noqa: E501
|
'settings': 'settings', # noqa: E501
|
||||||
'metadata': 'metadata', # noqa: E501
|
'metadata': 'metadata', # noqa: E501
|
||||||
}
|
}
|
||||||
@ -158,6 +160,7 @@ class SubmittedJob(ModelNormal):
|
|||||||
Animal class but this time we won't travel
|
Animal class but this time we won't travel
|
||||||
through its discriminator because we passed in
|
through its discriminator because we passed in
|
||||||
_visited_composed_classes = (Animal,)
|
_visited_composed_classes = (Animal,)
|
||||||
|
type_etag (str): Hash of the job type, copied from the `AvailableJobType.etag` property of the job type. The job will be rejected if this field doesn't match the actual job type on the Manager. This prevents job submission with old settings, after the job compiler script has been updated. If this field is ommitted, the check is bypassed. . [optional] # noqa: E501
|
||||||
settings (JobSettings): [optional] # noqa: E501
|
settings (JobSettings): [optional] # noqa: E501
|
||||||
metadata (JobMetadata): [optional] # noqa: E501
|
metadata (JobMetadata): [optional] # noqa: E501
|
||||||
"""
|
"""
|
||||||
@ -252,6 +255,7 @@ class SubmittedJob(ModelNormal):
|
|||||||
Animal class but this time we won't travel
|
Animal class but this time we won't travel
|
||||||
through its discriminator because we passed in
|
through its discriminator because we passed in
|
||||||
_visited_composed_classes = (Animal,)
|
_visited_composed_classes = (Animal,)
|
||||||
|
type_etag (str): Hash of the job type, copied from the `AvailableJobType.etag` property of the job type. The job will be rejected if this field doesn't match the actual job type on the Manager. This prevents job submission with old settings, after the job compiler script has been updated. If this field is ommitted, the check is bypassed. . [optional] # noqa: E501
|
||||||
settings (JobSettings): [optional] # noqa: E501
|
settings (JobSettings): [optional] # noqa: E501
|
||||||
metadata (JobMetadata): [optional] # noqa: E501
|
metadata (JobMetadata): [optional] # noqa: E501
|
||||||
"""
|
"""
|
||||||
|
@ -350,6 +350,8 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
|||||||
assert self.job is not None
|
assert self.job is not None
|
||||||
assert self.blendfile_on_farm is not None
|
assert self.blendfile_on_farm is not None
|
||||||
|
|
||||||
|
from flamenco.manager import ApiException
|
||||||
|
|
||||||
api_client = self.get_api_client(context)
|
api_client = self.get_api_client(context)
|
||||||
|
|
||||||
propgroup = getattr(context.scene, "flamenco_job_settings", None)
|
propgroup = getattr(context.scene, "flamenco_job_settings", None)
|
||||||
@ -362,7 +364,18 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
|||||||
propgroup.job_type, self.job, self.blendfile_on_farm
|
propgroup.job_type, self.job, self.blendfile_on_farm
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
submitted_job = job_submission.submit_job(self.job, api_client)
|
submitted_job = job_submission.submit_job(self.job, api_client)
|
||||||
|
except ApiException as ex:
|
||||||
|
if ex.status == 412:
|
||||||
|
self.report(
|
||||||
|
{"ERROR"},
|
||||||
|
"Cached job type is old. Refresh the job types and submit again, please",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.report({"ERROR"}, f"Could not submit job: {ex.reason}")
|
||||||
|
return
|
||||||
|
|
||||||
self.report({"INFO"}, "Job %s submitted" % submitted_job.name)
|
self.report({"INFO"}, "Job %s submitted" % submitted_job.name)
|
||||||
|
|
||||||
def _quit(self, context: bpy.types.Context) -> set[str]:
|
def _quit(self, context: bpy.types.Context) -> set[str]:
|
||||||
|
@ -83,12 +83,20 @@ func (f *Flamenco) SubmitJob(e echo.Context) error {
|
|||||||
submittedJob.SubmitterPlatform = runtime.GOOS
|
submittedJob.SubmitterPlatform = runtime.GOOS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if submittedJob.TypeEtag == nil || *submittedJob.TypeEtag == "" {
|
||||||
|
logger.Warn().Msg("job submitted without job type etag, refresh the job types in the Blender add-on")
|
||||||
|
}
|
||||||
|
|
||||||
// Before compiling the job, replace the two-way variables. This ensures all
|
// Before compiling the job, replace the two-way variables. This ensures all
|
||||||
// the tasks also use those.
|
// the tasks also use those.
|
||||||
replaceTwoWayVariables(f.config, submittedJob)
|
replaceTwoWayVariables(f.config, submittedJob)
|
||||||
|
|
||||||
authoredJob, err := f.jobCompiler.Compile(ctx, submittedJob)
|
authoredJob, err := f.jobCompiler.Compile(ctx, submittedJob)
|
||||||
if err != nil {
|
switch {
|
||||||
|
case errors.Is(err, job_compilers.ErrJobTypeBadEtag):
|
||||||
|
logger.Warn().Err(err).Msg("rejecting submitted job, job type etag does not match")
|
||||||
|
return sendAPIError(e, http.StatusPreconditionFailed, "rejecting job, job type etag does not match")
|
||||||
|
case err != nil:
|
||||||
logger.Warn().Err(err).Msg("error compiling job")
|
logger.Warn().Err(err).Msg("error compiling job")
|
||||||
// TODO: make this a more specific error object for this API call.
|
// TODO: make this a more specific error object for this API call.
|
||||||
return sendAPIError(e, http.StatusBadRequest, fmt.Sprintf("error compiling job: %v", err))
|
return sendAPIError(e, http.StatusBadRequest, fmt.Sprintf("error compiling job: %v", err))
|
||||||
|
@ -176,6 +176,71 @@ func TestSubmitJobWithSettings(t *testing.T) {
|
|||||||
err := mf.flamenco.SubmitJob(echoCtx)
|
err := mf.flamenco.SubmitJob(echoCtx)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSubmitJobWithEtag(t *testing.T) {
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
|
||||||
|
mf := newMockedFlamenco(mockCtrl)
|
||||||
|
|
||||||
|
submittedJob := api.SubmittedJob{
|
||||||
|
Name: "поднео посао",
|
||||||
|
Type: "test",
|
||||||
|
Priority: 50,
|
||||||
|
SubmitterPlatform: "linux",
|
||||||
|
TypeEtag: ptr("bad etag"),
|
||||||
|
}
|
||||||
|
|
||||||
|
mf.jobCompiler.EXPECT().Compile(gomock.Any(), submittedJob).
|
||||||
|
Return(nil, job_compilers.ErrJobTypeBadEtag)
|
||||||
|
mf.expectConvertTwoWayVariables(t, config.VariableAudienceWorkers, "linux", map[string]string{}).AnyTimes()
|
||||||
|
|
||||||
|
// Expect the job to be rejected.
|
||||||
|
{
|
||||||
|
echoCtx := mf.prepareMockedJSONRequest(submittedJob)
|
||||||
|
err := mf.flamenco.SubmitJob(echoCtx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assertResponseAPIError(t, echoCtx,
|
||||||
|
http.StatusPreconditionFailed, "rejecting job, job type etag does not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect the job compiler to be called.
|
||||||
|
authoredJob := job_compilers.AuthoredJob{
|
||||||
|
JobID: "afc47568-bd9d-4368-8016-e91d945db36d",
|
||||||
|
Name: submittedJob.Name,
|
||||||
|
JobType: submittedJob.Type,
|
||||||
|
Priority: submittedJob.Priority,
|
||||||
|
Status: api.JobStatusUnderConstruction,
|
||||||
|
Created: mf.clock.Now(),
|
||||||
|
}
|
||||||
|
mf.jobCompiler.EXPECT().Compile(gomock.Any(), gomock.Any()).Return(&authoredJob, nil)
|
||||||
|
|
||||||
|
// Expect the job to be saved with 'queued' status:
|
||||||
|
mf.persistence.EXPECT().StoreAuthoredJob(gomock.Any(), gomock.Any()).Return(nil)
|
||||||
|
|
||||||
|
// Expect the job to be fetched from the database again:
|
||||||
|
dbJob := persistence.Job{
|
||||||
|
UUID: authoredJob.JobID,
|
||||||
|
Name: authoredJob.Name,
|
||||||
|
JobType: authoredJob.JobType,
|
||||||
|
Priority: authoredJob.Priority,
|
||||||
|
Status: api.JobStatusQueued,
|
||||||
|
Settings: persistence.StringInterfaceMap{},
|
||||||
|
Metadata: persistence.StringStringMap{},
|
||||||
|
}
|
||||||
|
mf.persistence.EXPECT().FetchJob(gomock.Any(), authoredJob.JobID).Return(&dbJob, nil)
|
||||||
|
|
||||||
|
// Expect the new job to be broadcast.
|
||||||
|
mf.broadcaster.EXPECT().BroadcastNewJob(gomock.Any())
|
||||||
|
|
||||||
|
{ // Expect the job with the right etag to be accepted.
|
||||||
|
submittedJob.TypeEtag = ptr("correct etag")
|
||||||
|
echoCtx := mf.prepareMockedJSONRequest(submittedJob)
|
||||||
|
err := mf.flamenco.SubmitJob(echoCtx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetJobTypeHappy(t *testing.T) {
|
func TestGetJobTypeHappy(t *testing.T) {
|
||||||
mockCtrl := gomock.NewController(t)
|
mockCtrl := gomock.NewController(t)
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
@ -183,6 +248,7 @@ func TestGetJobTypeHappy(t *testing.T) {
|
|||||||
|
|
||||||
// Get an existing job type.
|
// Get an existing job type.
|
||||||
jt := api.AvailableJobType{
|
jt := api.AvailableJobType{
|
||||||
|
Etag: "some etag",
|
||||||
Name: "test-job-type",
|
Name: "test-job-type",
|
||||||
Label: "Test Job Type",
|
Label: "Test Job Type",
|
||||||
Settings: []api.AvailableJobSetting{
|
Settings: []api.AvailableJobSetting{
|
||||||
|
@ -6,7 +6,10 @@ package job_compilers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
@ -22,6 +25,7 @@ import (
|
|||||||
|
|
||||||
var ErrJobTypeUnknown = errors.New("job type unknown")
|
var ErrJobTypeUnknown = errors.New("job type unknown")
|
||||||
var ErrScriptIncomplete = errors.New("job compiler script incomplete")
|
var ErrScriptIncomplete = errors.New("job compiler script incomplete")
|
||||||
|
var ErrJobTypeBadEtag = errors.New("job type etag does not match")
|
||||||
|
|
||||||
// Service contains job compilers defined in JavaScript.
|
// Service contains job compilers defined in JavaScript.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@ -42,6 +46,7 @@ type Compiler struct {
|
|||||||
type VM struct {
|
type VM struct {
|
||||||
runtime *goja.Runtime // Goja VM containing the job compiler script.
|
runtime *goja.Runtime // Goja VM containing the job compiler script.
|
||||||
compiler Compiler // Program loaded into this VM.
|
compiler Compiler // Program loaded into this VM.
|
||||||
|
jobTypeEtag string // Etag for this particular job type.
|
||||||
}
|
}
|
||||||
|
|
||||||
// jobCompileFunc is a function that fills job.Tasks.
|
// jobCompileFunc is a function that fills job.Tasks.
|
||||||
@ -91,6 +96,10 @@ func (s *Service) Compile(ctx context.Context, sj api.SubmittedJob) (*AuthoredJo
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := vm.checkJobTypeEtag(sj); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Create an AuthoredJob from this SubmittedJob.
|
// Create an AuthoredJob from this SubmittedJob.
|
||||||
aj := AuthoredJob{
|
aj := AuthoredJob{
|
||||||
JobID: uuid.New(),
|
JobID: uuid.New(),
|
||||||
@ -203,5 +212,49 @@ func (vm *VM) getJobTypeInfo() (api.AvailableJobType, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ajt.Name = vm.compiler.jobType
|
ajt.Name = vm.compiler.jobType
|
||||||
|
ajt.Etag = vm.jobTypeEtag
|
||||||
return ajt, nil
|
return ajt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getEtag gets the job type etag hash.
|
||||||
|
func (vm *VM) getEtag() (string, error) {
|
||||||
|
jobTypeInfo, err := vm.getJobTypeInfo()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to JSON, then compute the SHA256sum to get the Etag.
|
||||||
|
asBytes, err := json.Marshal(&jobTypeInfo)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher := sha1.New()
|
||||||
|
hasher.Write(asBytes)
|
||||||
|
hashsum := hasher.Sum(nil)
|
||||||
|
return fmt.Sprintf("%x", hashsum), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateEtag sets vm.jobTypeEtag based on the job type info it contains.
|
||||||
|
func (vm *VM) updateEtag() error {
|
||||||
|
etag, err := vm.getEtag()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.jobTypeEtag = etag
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *VM) checkJobTypeEtag(sj api.SubmittedJob) error {
|
||||||
|
if sj.TypeEtag == nil || *sj.TypeEtag == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if vm.jobTypeEtag != *sj.TypeEtag {
|
||||||
|
return fmt.Errorf("%w: expecting %q, submitted job has %q",
|
||||||
|
ErrJobTypeBadEtag, vm.jobTypeEtag, *sj.TypeEtag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -255,3 +255,57 @@ func TestSimpleBlenderRenderOutputPathFieldReplacement(t *testing.T) {
|
|||||||
}, tVideo.Commands[0].Parameters)
|
}, tVideo.Commands[0].Parameters)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEtag(t *testing.T) {
|
||||||
|
c := mockedClock(t)
|
||||||
|
|
||||||
|
s, err := Load(c)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Etags should be computed when the compiler VM is obtained.
|
||||||
|
vm, err := s.compilerVMForJobType("echo-sleep-test")
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
const expectEtag = "eba586e16d6b55baaa43e32f9e78ae514b457fee"
|
||||||
|
assert.Equal(t, expectEtag, vm.jobTypeEtag)
|
||||||
|
|
||||||
|
// A mismatching Etag should prevent job compilation.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sj := api.SubmittedJob{
|
||||||
|
Name: "job name",
|
||||||
|
Type: "echo-sleep-test",
|
||||||
|
Priority: 50,
|
||||||
|
SubmitterPlatform: "linux",
|
||||||
|
Settings: &api.JobSettings{AdditionalProperties: map[string]interface{}{
|
||||||
|
"message": "hey",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // Test without etag.
|
||||||
|
aj, err := s.Compile(ctx, sj)
|
||||||
|
if assert.NoError(t, err, "job without etag should always be accepted") {
|
||||||
|
assert.NotNil(t, aj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // Test with bad etag.
|
||||||
|
sj.TypeEtag = ptr("this is not the right etag")
|
||||||
|
_, err := s.Compile(ctx, sj)
|
||||||
|
assert.ErrorIs(t, err, ErrJobTypeBadEtag)
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // Test with correct etag.
|
||||||
|
sj.TypeEtag = ptr(expectEtag)
|
||||||
|
aj, err := s.Compile(ctx, sj)
|
||||||
|
if assert.NoError(t, err, "job with correct etag should be accepted") {
|
||||||
|
assert.NotNil(t, aj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptr[T any](value T) *T {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
@ -158,13 +158,18 @@ func (s *Service) compilerVMForJobType(jobTypeName string) (*VM, error) {
|
|||||||
return nil, ErrJobTypeUnknown
|
return nil, ErrJobTypeUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
vm := newGojaVM(s.registry)
|
runtime := newGojaVM(s.registry)
|
||||||
if _, err := vm.RunProgram(program.program); err != nil {
|
if _, err := runtime.RunProgram(program.program); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &VM{
|
vm := &VM{
|
||||||
runtime: vm,
|
runtime: runtime,
|
||||||
compiler: program,
|
compiler: program,
|
||||||
}, nil
|
}
|
||||||
|
if err := vm.updateEtag(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return vm, nil
|
||||||
}
|
}
|
||||||
|
@ -591,6 +591,14 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema: { $ref: "#/components/schemas/Job" }
|
schema: { $ref: "#/components/schemas/Job" }
|
||||||
|
"412":
|
||||||
|
description: >
|
||||||
|
The given job type etag does not match the job type etag on the
|
||||||
|
Manager. This is likely due to the client caching the job type for
|
||||||
|
too long.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/Error" }
|
||||||
default:
|
default:
|
||||||
description: Error message
|
description: Error message
|
||||||
content:
|
content:
|
||||||
@ -1355,7 +1363,13 @@ components:
|
|||||||
"settings":
|
"settings":
|
||||||
type: array
|
type: array
|
||||||
items: { $ref: "#/components/schemas/AvailableJobSetting" }
|
items: { $ref: "#/components/schemas/AvailableJobSetting" }
|
||||||
required: [name, label, settings]
|
"etag":
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Hash of the job type. If the job settings or the label change, this
|
||||||
|
etag will change. This is used on job submission to ensure that the
|
||||||
|
submitted job settings are up to date.
|
||||||
|
required: [name, label, settings, etag]
|
||||||
|
|
||||||
AvailableJobSetting:
|
AvailableJobSetting:
|
||||||
type: object
|
type: object
|
||||||
@ -1428,6 +1442,16 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
"name": { type: string }
|
"name": { type: string }
|
||||||
"type": { type: string }
|
"type": { type: string }
|
||||||
|
"type_etag":
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Hash of the job type, copied from the `AvailableJobType.etag`
|
||||||
|
property of the job type. The job will be rejected if this field
|
||||||
|
doesn't match the actual job type on the Manager. This prevents job
|
||||||
|
submission with old settings, after the job compiler script has been
|
||||||
|
updated.
|
||||||
|
|
||||||
|
If this field is ommitted, the check is bypassed.
|
||||||
"priority": { type: integer, default: 50 }
|
"priority": { type: integer, default: 50 }
|
||||||
"settings": { $ref: "#/components/schemas/JobSettings" }
|
"settings": { $ref: "#/components/schemas/JobSettings" }
|
||||||
"metadata": { $ref: "#/components/schemas/JobMetadata" }
|
"metadata": { $ref: "#/components/schemas/JobMetadata" }
|
||||||
|
@ -2804,6 +2804,7 @@ type SubmitJobResponse struct {
|
|||||||
Body []byte
|
Body []byte
|
||||||
HTTPResponse *http.Response
|
HTTPResponse *http.Response
|
||||||
JSON200 *Job
|
JSON200 *Job
|
||||||
|
JSON412 *Error
|
||||||
JSONDefault *Error
|
JSONDefault *Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4300,6 +4301,13 @@ func ParseSubmitJobResponse(rsp *http.Response) (*SubmitJobResponse, error) {
|
|||||||
}
|
}
|
||||||
response.JSON200 = &dest
|
response.JSON200 = &dest
|
||||||
|
|
||||||
|
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 412:
|
||||||
|
var dest Error
|
||||||
|
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response.JSON412 = &dest
|
||||||
|
|
||||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true:
|
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true:
|
||||||
var dest Error
|
var dest Error
|
||||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||||
|
@ -18,185 +18,189 @@ 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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",
|
||||||
|
"AAAA//9yIC5tYNcAAA==",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
@ -210,6 +210,8 @@ type AvailableJobSettingVisibility string
|
|||||||
|
|
||||||
// Job type supported by this Manager, and its parameters.
|
// Job type supported by this Manager, and its parameters.
|
||||||
type AvailableJobType struct {
|
type AvailableJobType struct {
|
||||||
|
// Hash of the job type. If the job settings or the label change, this etag will change. This is used on job submission to ensure that the submitted job settings are up to date.
|
||||||
|
Etag string `json:"etag"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Settings []AvailableJobSetting `json:"settings"`
|
Settings []AvailableJobSetting `json:"settings"`
|
||||||
@ -588,6 +590,10 @@ type SubmittedJob struct {
|
|||||||
// As a special case, the platform "manager" can be given, which will be interpreted as "the Manager's platform". This is mostly to make test/debug scripts easier, as they can use a static document on all platforms.
|
// As a special case, the platform "manager" can be given, which will be interpreted as "the Manager's platform". This is mostly to make test/debug scripts easier, as they can use a static document on all platforms.
|
||||||
SubmitterPlatform string `json:"submitter_platform"`
|
SubmitterPlatform string `json:"submitter_platform"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
// Hash of the job type, copied from the `AvailableJobType.etag` property of the job type. The job will be rejected if this field doesn't match the actual job type on the Manager. This prevents job submission with old settings, after the job compiler script has been updated.
|
||||||
|
// If this field is ommitted, the check is bypassed.
|
||||||
|
TypeEtag *string `json:"type_etag,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// The task as it exists in the Manager database, i.e. before variable replacement.
|
// The task as it exists in the Manager database, i.e. before variable replacement.
|
||||||
|
@ -27,10 +27,11 @@ class AvailableJobType {
|
|||||||
* @param name {String}
|
* @param name {String}
|
||||||
* @param label {String}
|
* @param label {String}
|
||||||
* @param settings {Array.<module:model/AvailableJobSetting>}
|
* @param settings {Array.<module:model/AvailableJobSetting>}
|
||||||
|
* @param etag {String} Hash of the job type. If the job settings or the label change, this etag will change. This is used on job submission to ensure that the submitted job settings are up to date.
|
||||||
*/
|
*/
|
||||||
constructor(name, label, settings) {
|
constructor(name, label, settings, etag) {
|
||||||
|
|
||||||
AvailableJobType.initialize(this, name, label, settings);
|
AvailableJobType.initialize(this, name, label, settings, etag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,10 +39,11 @@ class AvailableJobType {
|
|||||||
* This method is used by the constructors of any subclasses, in order to implement multiple inheritance (mix-ins).
|
* This method is used by the constructors of any subclasses, in order to implement multiple inheritance (mix-ins).
|
||||||
* Only for internal use.
|
* Only for internal use.
|
||||||
*/
|
*/
|
||||||
static initialize(obj, name, label, settings) {
|
static initialize(obj, name, label, settings, etag) {
|
||||||
obj['name'] = name;
|
obj['name'] = name;
|
||||||
obj['label'] = label;
|
obj['label'] = label;
|
||||||
obj['settings'] = settings;
|
obj['settings'] = settings;
|
||||||
|
obj['etag'] = etag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,6 +66,9 @@ class AvailableJobType {
|
|||||||
if (data.hasOwnProperty('settings')) {
|
if (data.hasOwnProperty('settings')) {
|
||||||
obj['settings'] = ApiClient.convertToType(data['settings'], [AvailableJobSetting]);
|
obj['settings'] = ApiClient.convertToType(data['settings'], [AvailableJobSetting]);
|
||||||
}
|
}
|
||||||
|
if (data.hasOwnProperty('etag')) {
|
||||||
|
obj['etag'] = ApiClient.convertToType(data['etag'], 'String');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
@ -86,6 +91,12 @@ AvailableJobType.prototype['label'] = undefined;
|
|||||||
*/
|
*/
|
||||||
AvailableJobType.prototype['settings'] = undefined;
|
AvailableJobType.prototype['settings'] = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of the job type. If the job settings or the label change, this etag will change. This is used on job submission to ensure that the submitted job settings are up to date.
|
||||||
|
* @member {String} etag
|
||||||
|
*/
|
||||||
|
AvailableJobType.prototype['etag'] = undefined;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,6 +78,9 @@ class Job {
|
|||||||
if (data.hasOwnProperty('type')) {
|
if (data.hasOwnProperty('type')) {
|
||||||
obj['type'] = ApiClient.convertToType(data['type'], 'String');
|
obj['type'] = ApiClient.convertToType(data['type'], 'String');
|
||||||
}
|
}
|
||||||
|
if (data.hasOwnProperty('type_etag')) {
|
||||||
|
obj['type_etag'] = ApiClient.convertToType(data['type_etag'], 'String');
|
||||||
|
}
|
||||||
if (data.hasOwnProperty('priority')) {
|
if (data.hasOwnProperty('priority')) {
|
||||||
obj['priority'] = ApiClient.convertToType(data['priority'], 'Number');
|
obj['priority'] = ApiClient.convertToType(data['priority'], 'Number');
|
||||||
}
|
}
|
||||||
@ -122,6 +125,12 @@ Job.prototype['name'] = undefined;
|
|||||||
*/
|
*/
|
||||||
Job.prototype['type'] = undefined;
|
Job.prototype['type'] = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of the job type, copied from the `AvailableJobType.etag` property of the job type. The job will be rejected if this field doesn't match the actual job type on the Manager. This prevents job submission with old settings, after the job compiler script has been updated. If this field is ommitted, the check is bypassed.
|
||||||
|
* @member {String} type_etag
|
||||||
|
*/
|
||||||
|
Job.prototype['type_etag'] = undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @member {Number} priority
|
* @member {Number} priority
|
||||||
* @default 50
|
* @default 50
|
||||||
@ -184,6 +193,11 @@ SubmittedJob.prototype['name'] = undefined;
|
|||||||
* @member {String} type
|
* @member {String} type
|
||||||
*/
|
*/
|
||||||
SubmittedJob.prototype['type'] = undefined;
|
SubmittedJob.prototype['type'] = undefined;
|
||||||
|
/**
|
||||||
|
* Hash of the job type, copied from the `AvailableJobType.etag` property of the job type. The job will be rejected if this field doesn't match the actual job type on the Manager. This prevents job submission with old settings, after the job compiler script has been updated. If this field is ommitted, the check is bypassed.
|
||||||
|
* @member {String} type_etag
|
||||||
|
*/
|
||||||
|
SubmittedJob.prototype['type_etag'] = undefined;
|
||||||
/**
|
/**
|
||||||
* @member {Number} priority
|
* @member {Number} priority
|
||||||
* @default 50
|
* @default 50
|
||||||
|
@ -62,6 +62,9 @@ class SubmittedJob {
|
|||||||
if (data.hasOwnProperty('type')) {
|
if (data.hasOwnProperty('type')) {
|
||||||
obj['type'] = ApiClient.convertToType(data['type'], 'String');
|
obj['type'] = ApiClient.convertToType(data['type'], 'String');
|
||||||
}
|
}
|
||||||
|
if (data.hasOwnProperty('type_etag')) {
|
||||||
|
obj['type_etag'] = ApiClient.convertToType(data['type_etag'], 'String');
|
||||||
|
}
|
||||||
if (data.hasOwnProperty('priority')) {
|
if (data.hasOwnProperty('priority')) {
|
||||||
obj['priority'] = ApiClient.convertToType(data['priority'], 'Number');
|
obj['priority'] = ApiClient.convertToType(data['priority'], 'Number');
|
||||||
}
|
}
|
||||||
@ -91,6 +94,12 @@ SubmittedJob.prototype['name'] = undefined;
|
|||||||
*/
|
*/
|
||||||
SubmittedJob.prototype['type'] = undefined;
|
SubmittedJob.prototype['type'] = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of the job type, copied from the `AvailableJobType.etag` property of the job type. The job will be rejected if this field doesn't match the actual job type on the Manager. This prevents job submission with old settings, after the job compiler script has been updated. If this field is ommitted, the check is bypassed.
|
||||||
|
* @member {String} type_etag
|
||||||
|
*/
|
||||||
|
SubmittedJob.prototype['type_etag'] = undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @member {Number} priority
|
* @member {Number} priority
|
||||||
* @default 50
|
* @default 50
|
||||||
|
Loading…
x
Reference in New Issue
Block a user