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:
Sybren A. Stüvel 2022-07-29 21:13:37 +02:00
parent 48ca73f550
commit a6c935a634
22 changed files with 500 additions and 197 deletions

View File

@ -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

View File

@ -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()
job_types = validate_and_convert_types(
json_dict, (AvailableJobTypes,), ["job_types"], True, True, dummy_cfg try:
) job_types = validate_and_convert_types(
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

View File

@ -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)

View File

@ -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]

View File

@ -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)

View File

@ -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]

View File

@ -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 \

View File

@ -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
""" """

View File

@ -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
""" """

View File

@ -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
) )
submitted_job = job_submission.submit_job(self.job, api_client) try:
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]:

View File

@ -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))

View File

@ -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{

View File

@ -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 {
@ -40,8 +44,9 @@ 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
}

View File

@ -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
}

View File

@ -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
} }

View File

@ -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" }

View File

@ -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 {

View File

@ -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

View 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.

View File

@ -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;

View File

@ -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

View File

@ -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