From a6c935a6344741793d90cc2852753097081c6bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 29 Jul 2022 21:13:37 +0200 Subject: [PATCH] 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. --- addon/flamenco/job_submission.py | 1 + addon/flamenco/job_types.py | 14 +- .../flamenco/manager/docs/AvailableJobType.md | 1 + addon/flamenco/manager/docs/Job.md | 1 + addon/flamenco/manager/docs/JobsApi.md | 2 + addon/flamenco/manager/docs/SubmittedJob.md | 1 + .../manager/model/available_job_type.py | 10 +- addon/flamenco/manager/model/job.py | 4 + addon/flamenco/manager/model/submitted_job.py | 4 + addon/flamenco/operators.py | 15 +- internal/manager/api_impl/jobs.go | 10 +- internal/manager/api_impl/jobs_test.go | 66 ++++ .../manager/job_compilers/job_compilers.go | 57 ++- .../job_compilers/job_compilers_test.go | 54 +++ internal/manager/job_compilers/scripts.go | 15 +- pkg/api/flamenco-openapi.yaml | 26 +- pkg/api/openapi_client.gen.go | 8 + pkg/api/openapi_spec.gen.go | 362 +++++++++--------- pkg/api/openapi_types.gen.go | 6 + .../src/manager-api/model/AvailableJobType.js | 17 +- web/app/src/manager-api/model/Job.js | 14 + web/app/src/manager-api/model/SubmittedJob.js | 9 + 22 files changed, 500 insertions(+), 197 deletions(-) diff --git a/addon/flamenco/job_submission.py b/addon/flamenco/job_submission.py index 43a53979..f201f665 100644 --- a/addon/flamenco/job_submission.py +++ b/addon/flamenco/job_submission.py @@ -48,6 +48,7 @@ def job_for_scene(scene: bpy.types.Scene) -> Optional[_SubmittedJob]: settings=settings, metadata=metadata, submitter_platform=platform.system().lower(), + type_etag=propgroup.job_type.etag, ) return job diff --git a/addon/flamenco/job_types.py b/addon/flamenco/job_types.py index 4822ed34..db60e8a7 100644 --- a/addon/flamenco/job_types.py +++ b/addon/flamenco/job_types.py @@ -95,9 +95,17 @@ def _available_job_types_from_json(job_types_json: str) -> None: json_dict = json.loads(job_types_json) 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( job_types, AvailableJobTypes diff --git a/addon/flamenco/manager/docs/AvailableJobType.md b/addon/flamenco/manager/docs/AvailableJobType.md index 11026c80..d2e6b305 100644 --- a/addon/flamenco/manager/docs/AvailableJobType.md +++ b/addon/flamenco/manager/docs/AvailableJobType.md @@ -8,6 +8,7 @@ Name | Type | Description | Notes **name** | **str** | | **label** | **str** | | **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] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/addon/flamenco/manager/docs/Job.md b/addon/flamenco/manager/docs/Job.md index 5ad93a8b..702667fe 100644 --- a/addon/flamenco/manager/docs/Job.md +++ b/addon/flamenco/manager/docs/Job.md @@ -13,6 +13,7 @@ Name | Type | Description | Notes **status** | [**JobStatus**](JobStatus.md) | | **activity** | **str** | Description of the last activity on this job. | **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] **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] diff --git a/addon/flamenco/manager/docs/JobsApi.md b/addon/flamenco/manager/docs/JobsApi.md index 12eb3a79..a755f627 100644 --- a/addon/flamenco/manager/docs/JobsApi.md +++ b/addon/flamenco/manager/docs/JobsApi.md @@ -1010,6 +1010,7 @@ with flamenco.manager.ApiClient() as api_client: submitted_job = SubmittedJob( name="name_example", type="type_example", + type_etag="type_etag_example", priority=50, settings=JobSettings(), metadata=JobMetadata( @@ -1053,6 +1054,7 @@ No authorization required | Status code | Description | Response headers | |-------------|-------------|------------------| **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 | - | [[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) diff --git a/addon/flamenco/manager/docs/SubmittedJob.md b/addon/flamenco/manager/docs/SubmittedJob.md index d2923f8c..be4c6edd 100644 --- a/addon/flamenco/manager/docs/SubmittedJob.md +++ b/addon/flamenco/manager/docs/SubmittedJob.md @@ -9,6 +9,7 @@ Name | Type | Description | Notes **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. | **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] **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] diff --git a/addon/flamenco/manager/model/available_job_type.py b/addon/flamenco/manager/model/available_job_type.py index 181020a4..2a4015c8 100644 --- a/addon/flamenco/manager/model/available_job_type.py +++ b/addon/flamenco/manager/model/available_job_type.py @@ -90,6 +90,7 @@ class AvailableJobType(ModelNormal): 'name': (str,), # noqa: E501 'label': (str,), # noqa: E501 'settings': ([AvailableJobSetting],), # noqa: E501 + 'etag': (str,), # noqa: E501 } @cached_property @@ -101,6 +102,7 @@ class AvailableJobType(ModelNormal): 'name': 'name', # noqa: E501 'label': 'label', # noqa: E501 'settings': 'settings', # noqa: E501 + 'etag': 'etag', # noqa: E501 } read_only_vars = { @@ -110,13 +112,14 @@ class AvailableJobType(ModelNormal): @classmethod @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 Args: name (str): label (str): 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: _check_type (bool): if True, values for parameters in openapi_types @@ -179,6 +182,7 @@ class AvailableJobType(ModelNormal): self.name = name self.label = label self.settings = settings + self.etag = etag for var_name, var_value in kwargs.items(): if var_name not in self.attribute_map and \ self._configuration is not None and \ @@ -199,13 +203,14 @@ class AvailableJobType(ModelNormal): ]) @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 Args: name (str): label (str): 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: _check_type (bool): if True, values for parameters in openapi_types @@ -266,6 +271,7 @@ class AvailableJobType(ModelNormal): self.name = name self.label = label self.settings = settings + self.etag = etag for var_name, var_value in kwargs.items(): if var_name not in self.attribute_map and \ self._configuration is not None and \ diff --git a/addon/flamenco/manager/model/job.py b/addon/flamenco/manager/model/job.py index f4ae3d95..e4d6e7d7 100644 --- a/addon/flamenco/manager/model/job.py +++ b/addon/flamenco/manager/model/job.py @@ -104,6 +104,7 @@ class Job(ModelComposed): 'updated': (datetime,), # noqa: E501 'status': (JobStatus,), # noqa: E501 'activity': (str,), # noqa: E501 + 'type_etag': (str,), # noqa: E501 'settings': (JobSettings,), # noqa: E501 'metadata': (JobMetadata,), # noqa: E501 } @@ -123,6 +124,7 @@ class Job(ModelComposed): 'updated': 'updated', # noqa: E501 'status': 'status', # noqa: E501 'activity': 'activity', # noqa: E501 + 'type_etag': 'type_etag', # noqa: E501 'settings': 'settings', # noqa: E501 'metadata': 'metadata', # noqa: E501 } @@ -175,6 +177,7 @@ class Job(ModelComposed): Animal class but this time we won't travel through its discriminator because we passed in _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 metadata (JobMetadata): [optional] # noqa: E501 """ @@ -286,6 +289,7 @@ class Job(ModelComposed): Animal class but this time we won't travel through its discriminator because we passed in _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 metadata (JobMetadata): [optional] # noqa: E501 """ diff --git a/addon/flamenco/manager/model/submitted_job.py b/addon/flamenco/manager/model/submitted_job.py index 18afacc1..24128174 100644 --- a/addon/flamenco/manager/model/submitted_job.py +++ b/addon/flamenco/manager/model/submitted_job.py @@ -93,6 +93,7 @@ class SubmittedJob(ModelNormal): 'type': (str,), # noqa: E501 'priority': (int,), # noqa: E501 'submitter_platform': (str,), # noqa: E501 + 'type_etag': (str,), # noqa: E501 'settings': (JobSettings,), # noqa: E501 'metadata': (JobMetadata,), # noqa: E501 } @@ -107,6 +108,7 @@ class SubmittedJob(ModelNormal): 'type': 'type', # noqa: E501 'priority': 'priority', # noqa: E501 'submitter_platform': 'submitter_platform', # noqa: E501 + 'type_etag': 'type_etag', # noqa: E501 'settings': 'settings', # noqa: E501 'metadata': 'metadata', # noqa: E501 } @@ -158,6 +160,7 @@ class SubmittedJob(ModelNormal): Animal class but this time we won't travel through its discriminator because we passed in _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 metadata (JobMetadata): [optional] # noqa: E501 """ @@ -252,6 +255,7 @@ class SubmittedJob(ModelNormal): Animal class but this time we won't travel through its discriminator because we passed in _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 metadata (JobMetadata): [optional] # noqa: E501 """ diff --git a/addon/flamenco/operators.py b/addon/flamenco/operators.py index 935e42db..09203a7a 100644 --- a/addon/flamenco/operators.py +++ b/addon/flamenco/operators.py @@ -350,6 +350,8 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): assert self.job is not None assert self.blendfile_on_farm is not None + from flamenco.manager import ApiException + api_client = self.get_api_client(context) 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 ) - 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) def _quit(self, context: bpy.types.Context) -> set[str]: diff --git a/internal/manager/api_impl/jobs.go b/internal/manager/api_impl/jobs.go index df1c6837..87fb2e5d 100644 --- a/internal/manager/api_impl/jobs.go +++ b/internal/manager/api_impl/jobs.go @@ -83,12 +83,20 @@ func (f *Flamenco) SubmitJob(e echo.Context) error { 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 // the tasks also use those. replaceTwoWayVariables(f.config, 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") // TODO: make this a more specific error object for this API call. return sendAPIError(e, http.StatusBadRequest, fmt.Sprintf("error compiling job: %v", err)) diff --git a/internal/manager/api_impl/jobs_test.go b/internal/manager/api_impl/jobs_test.go index a6bbe11a..4a2f567c 100644 --- a/internal/manager/api_impl/jobs_test.go +++ b/internal/manager/api_impl/jobs_test.go @@ -176,6 +176,71 @@ func TestSubmitJobWithSettings(t *testing.T) { err := mf.flamenco.SubmitJob(echoCtx) 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) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -183,6 +248,7 @@ func TestGetJobTypeHappy(t *testing.T) { // Get an existing job type. jt := api.AvailableJobType{ + Etag: "some etag", Name: "test-job-type", Label: "Test Job Type", Settings: []api.AvailableJobSetting{ diff --git a/internal/manager/job_compilers/job_compilers.go b/internal/manager/job_compilers/job_compilers.go index 9ed335d5..c36804fb 100644 --- a/internal/manager/job_compilers/job_compilers.go +++ b/internal/manager/job_compilers/job_compilers.go @@ -6,7 +6,10 @@ package job_compilers import ( "context" + "crypto/sha1" + "encoding/json" "errors" + "fmt" "os" "sort" "sync" @@ -22,6 +25,7 @@ import ( var ErrJobTypeUnknown = errors.New("job type unknown") 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. type Service struct { @@ -40,8 +44,9 @@ type Compiler struct { } type VM struct { - runtime *goja.Runtime // Goja VM containing the job compiler script. - compiler Compiler // Program loaded into this VM. + runtime *goja.Runtime // Goja VM containing the job compiler script. + compiler Compiler // Program loaded into this VM. + jobTypeEtag string // Etag for this particular job type. } // 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 } + if err := vm.checkJobTypeEtag(sj); err != nil { + return nil, err + } + // Create an AuthoredJob from this SubmittedJob. aj := AuthoredJob{ JobID: uuid.New(), @@ -203,5 +212,49 @@ func (vm *VM) getJobTypeInfo() (api.AvailableJobType, error) { } ajt.Name = vm.compiler.jobType + ajt.Etag = vm.jobTypeEtag 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 +} diff --git a/internal/manager/job_compilers/job_compilers_test.go b/internal/manager/job_compilers/job_compilers_test.go index 188139c7..119bf9de 100644 --- a/internal/manager/job_compilers/job_compilers_test.go +++ b/internal/manager/job_compilers/job_compilers_test.go @@ -255,3 +255,57 @@ func TestSimpleBlenderRenderOutputPathFieldReplacement(t *testing.T) { }, 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 +} diff --git a/internal/manager/job_compilers/scripts.go b/internal/manager/job_compilers/scripts.go index 48901c09..efba10e9 100644 --- a/internal/manager/job_compilers/scripts.go +++ b/internal/manager/job_compilers/scripts.go @@ -158,13 +158,18 @@ func (s *Service) compilerVMForJobType(jobTypeName string) (*VM, error) { return nil, ErrJobTypeUnknown } - vm := newGojaVM(s.registry) - if _, err := vm.RunProgram(program.program); err != nil { + runtime := newGojaVM(s.registry) + if _, err := runtime.RunProgram(program.program); err != nil { return nil, err } - return &VM{ - runtime: vm, + vm := &VM{ + runtime: runtime, compiler: program, - }, nil + } + if err := vm.updateEtag(); err != nil { + return nil, err + } + + return vm, nil } diff --git a/pkg/api/flamenco-openapi.yaml b/pkg/api/flamenco-openapi.yaml index 542fce90..bffde533 100644 --- a/pkg/api/flamenco-openapi.yaml +++ b/pkg/api/flamenco-openapi.yaml @@ -591,6 +591,14 @@ paths: content: application/json: 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: description: Error message content: @@ -1355,7 +1363,13 @@ components: "settings": type: array 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: type: object @@ -1428,6 +1442,16 @@ components: properties: "name": { 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 } "settings": { $ref: "#/components/schemas/JobSettings" } "metadata": { $ref: "#/components/schemas/JobMetadata" } diff --git a/pkg/api/openapi_client.gen.go b/pkg/api/openapi_client.gen.go index 20534dd6..02b43607 100644 --- a/pkg/api/openapi_client.gen.go +++ b/pkg/api/openapi_client.gen.go @@ -2804,6 +2804,7 @@ type SubmitJobResponse struct { Body []byte HTTPResponse *http.Response JSON200 *Job + JSON412 *Error JSONDefault *Error } @@ -4300,6 +4301,13 @@ func ParseSubmitJobResponse(rsp *http.Response) (*SubmitJobResponse, error) { } 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: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { diff --git a/pkg/api/openapi_spec.gen.go b/pkg/api/openapi_spec.gen.go index b8e5cdc3..3e44bab4 100644 --- a/pkg/api/openapi_spec.gen.go +++ b/pkg/api/openapi_spec.gen.go @@ -18,185 +18,189 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "AAAA//9yIC5tYNcAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/api/openapi_types.gen.go b/pkg/api/openapi_types.gen.go index 05b83343..1a99359f 100644 --- a/pkg/api/openapi_types.gen.go +++ b/pkg/api/openapi_types.gen.go @@ -210,6 +210,8 @@ type AvailableJobSettingVisibility string // Job type supported by this Manager, and its parameters. 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"` Name string `json:"name"` 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. SubmitterPlatform string `json:"submitter_platform"` 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. diff --git a/web/app/src/manager-api/model/AvailableJobType.js b/web/app/src/manager-api/model/AvailableJobType.js index 24b547f8..938c7b3a 100644 --- a/web/app/src/manager-api/model/AvailableJobType.js +++ b/web/app/src/manager-api/model/AvailableJobType.js @@ -27,10 +27,11 @@ class AvailableJobType { * @param name {String} * @param label {String} * @param settings {Array.} + * @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). * Only for internal use. */ - static initialize(obj, name, label, settings) { + static initialize(obj, name, label, settings, etag) { obj['name'] = name; obj['label'] = label; obj['settings'] = settings; + obj['etag'] = etag; } /** @@ -64,6 +66,9 @@ class AvailableJobType { if (data.hasOwnProperty('settings')) { obj['settings'] = ApiClient.convertToType(data['settings'], [AvailableJobSetting]); } + if (data.hasOwnProperty('etag')) { + obj['etag'] = ApiClient.convertToType(data['etag'], 'String'); + } } return obj; } @@ -86,6 +91,12 @@ AvailableJobType.prototype['label'] = 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; + diff --git a/web/app/src/manager-api/model/Job.js b/web/app/src/manager-api/model/Job.js index d6eed4ac..9ee8f1ed 100644 --- a/web/app/src/manager-api/model/Job.js +++ b/web/app/src/manager-api/model/Job.js @@ -78,6 +78,9 @@ class Job { if (data.hasOwnProperty('type')) { 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')) { obj['priority'] = ApiClient.convertToType(data['priority'], 'Number'); } @@ -122,6 +125,12 @@ Job.prototype['name'] = 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 * @default 50 @@ -184,6 +193,11 @@ SubmittedJob.prototype['name'] = undefined; * @member {String} type */ 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 * @default 50 diff --git a/web/app/src/manager-api/model/SubmittedJob.js b/web/app/src/manager-api/model/SubmittedJob.js index 4cdd0763..599b794f 100644 --- a/web/app/src/manager-api/model/SubmittedJob.js +++ b/web/app/src/manager-api/model/SubmittedJob.js @@ -62,6 +62,9 @@ class SubmittedJob { if (data.hasOwnProperty('type')) { 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')) { obj['priority'] = ApiClient.convertToType(data['priority'], 'Number'); } @@ -91,6 +94,12 @@ SubmittedJob.prototype['name'] = 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 * @default 50