diff --git a/FEATURES.md b/FEATURES.md
index 95800e76..1285c867 100644
--- a/FEATURES.md
+++ b/FEATURES.md
@@ -25,6 +25,7 @@ Note that list is **not** in any specific order.
- [ ] Worker sleep schedule
- [ ] Web frontend for Worker management
- [ ] Web frontend for Job & Task management
+- [ ] API: only allow valid job & task status changes. Currently any change is allowed by the API.
- [ ] Web frontend for configuration management
- [ ] Frontend authentication
- [x] Worker API authentication
diff --git a/addon/flamenco/manager/__init__.py b/addon/flamenco/manager/__init__.py
index 5d602986..c64e2a85 100644
--- a/addon/flamenco/manager/__init__.py
+++ b/addon/flamenco/manager/__init__.py
@@ -10,7 +10,7 @@
"""
-__version__ = "d79fde17-dirty"
+__version__ = "b699647e-dirty"
# import ApiClient
from flamenco.manager.api_client import ApiClient
diff --git a/addon/flamenco/manager/api/jobs_api.py b/addon/flamenco/manager/api/jobs_api.py
index b9c6fd9e..2d54d4a4 100644
--- a/addon/flamenco/manager/api/jobs_api.py
+++ b/addon/flamenco/manager/api/jobs_api.py
@@ -25,6 +25,7 @@ from flamenco.manager.model.available_job_type import AvailableJobType
from flamenco.manager.model.available_job_types import AvailableJobTypes
from flamenco.manager.model.error import Error
from flamenco.manager.model.job import Job
+from flamenco.manager.model.job_status_change import JobStatusChange
from flamenco.manager.model.jobs_query import JobsQuery
from flamenco.manager.model.jobs_query_result import JobsQueryResult
from flamenco.manager.model.submitted_job import SubmittedJob
@@ -231,6 +232,62 @@ class JobsApi(object):
},
api_client=api_client
)
+ self.set_job_status_endpoint = _Endpoint(
+ settings={
+ 'response_type': None,
+ 'auth': [],
+ 'endpoint_path': '/api/jobs/{job_id}/setstatus',
+ 'operation_id': 'set_job_status',
+ 'http_method': 'POST',
+ 'servers': None,
+ },
+ params_map={
+ 'all': [
+ 'job_id',
+ 'job_status_change',
+ ],
+ 'required': [
+ 'job_id',
+ 'job_status_change',
+ ],
+ 'nullable': [
+ ],
+ 'enum': [
+ ],
+ 'validation': [
+ ]
+ },
+ root_map={
+ 'validations': {
+ },
+ 'allowed_values': {
+ },
+ 'openapi_types': {
+ 'job_id':
+ (str,),
+ 'job_status_change':
+ (JobStatusChange,),
+ },
+ 'attribute_map': {
+ 'job_id': 'job_id',
+ },
+ 'location_map': {
+ 'job_id': 'path',
+ 'job_status_change': 'body',
+ },
+ 'collection_format_map': {
+ }
+ },
+ headers_map={
+ 'accept': [
+ 'application/json'
+ ],
+ 'content_type': [
+ 'application/json'
+ ]
+ },
+ api_client=api_client
+ )
self.submit_job_endpoint = _Endpoint(
settings={
'response_type': (Job,),
@@ -585,6 +642,87 @@ class JobsApi(object):
jobs_query
return self.query_jobs_endpoint.call_with_http_info(**kwargs)
+ def set_job_status(
+ self,
+ job_id,
+ job_status_change,
+ **kwargs
+ ):
+ """set_job_status # noqa: E501
+
+ This method makes a synchronous HTTP request by default. To make an
+ asynchronous HTTP request, please pass async_req=True
+
+ >>> thread = api.set_job_status(job_id, job_status_change, async_req=True)
+ >>> result = thread.get()
+
+ Args:
+ job_id (str):
+ job_status_change (JobStatusChange): The status change to request.
+
+ Keyword Args:
+ _return_http_data_only (bool): response data without head status
+ code and headers. Default is True.
+ _preload_content (bool): if False, the urllib3.HTTPResponse object
+ will be returned without reading/decoding response data.
+ Default is True.
+ _request_timeout (int/float/tuple): timeout setting for this request. If
+ one number provided, it will be total request timeout. It can also
+ be a pair (tuple) of (connection, read) timeouts.
+ Default is None.
+ _check_input_type (bool): specifies if type checking
+ should be done one the data sent to the server.
+ Default is True.
+ _check_return_type (bool): specifies if type checking
+ should be done one the data received from the server.
+ Default is True.
+ _spec_property_naming (bool): True if the variable names in the input data
+ are serialized names, as specified in the OpenAPI document.
+ False if the variable names in the input data
+ are pythonic names, e.g. snake case (default)
+ _content_type (str/None): force body content-type.
+ Default is None and content-type will be predicted by allowed
+ content-types and body.
+ _host_index (int/None): specifies the index of the server
+ that we want to use.
+ Default is read from the configuration.
+ async_req (bool): execute request asynchronously
+
+ Returns:
+ None
+ If the method is called asynchronously, returns the request
+ thread.
+ """
+ kwargs['async_req'] = kwargs.get(
+ 'async_req', False
+ )
+ kwargs['_return_http_data_only'] = kwargs.get(
+ '_return_http_data_only', True
+ )
+ kwargs['_preload_content'] = kwargs.get(
+ '_preload_content', True
+ )
+ kwargs['_request_timeout'] = kwargs.get(
+ '_request_timeout', None
+ )
+ kwargs['_check_input_type'] = kwargs.get(
+ '_check_input_type', True
+ )
+ kwargs['_check_return_type'] = kwargs.get(
+ '_check_return_type', True
+ )
+ kwargs['_spec_property_naming'] = kwargs.get(
+ '_spec_property_naming', False
+ )
+ kwargs['_content_type'] = kwargs.get(
+ '_content_type')
+ kwargs['_host_index'] = kwargs.get('_host_index')
+ kwargs['job_id'] = \
+ job_id
+ kwargs['job_status_change'] = \
+ job_status_change
+ return self.set_job_status_endpoint.call_with_http_info(**kwargs)
+
def submit_job(
self,
submitted_job,
diff --git a/addon/flamenco/manager/api_client.py b/addon/flamenco/manager/api_client.py
index daea4294..aa785e5b 100644
--- a/addon/flamenco/manager/api_client.py
+++ b/addon/flamenco/manager/api_client.py
@@ -76,7 +76,7 @@ class ApiClient(object):
self.default_headers[header_name] = header_value
self.cookie = cookie
# Set default User-Agent.
- self.user_agent = 'Flamenco/d79fde17-dirty (Blender add-on)'
+ self.user_agent = 'Flamenco/b699647e-dirty (Blender add-on)'
def __enter__(self):
return self
diff --git a/addon/flamenco/manager/configuration.py b/addon/flamenco/manager/configuration.py
index d9479bae..fc712b51 100644
--- a/addon/flamenco/manager/configuration.py
+++ b/addon/flamenco/manager/configuration.py
@@ -404,7 +404,7 @@ conf = flamenco.manager.Configuration(
"OS: {env}\n"\
"Python Version: {pyversion}\n"\
"Version of the API: 1.0.0\n"\
- "SDK Package Version: d79fde17-dirty".\
+ "SDK Package Version: b699647e-dirty".\
format(env=sys.platform, pyversion=sys.version)
def get_host_settings(self):
diff --git a/addon/flamenco/manager/docs/JobStatusChange.md b/addon/flamenco/manager/docs/JobStatusChange.md
new file mode 100644
index 00000000..bcef2e1c
--- /dev/null
+++ b/addon/flamenco/manager/docs/JobStatusChange.md
@@ -0,0 +1,13 @@
+# JobStatusChange
+
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**status** | [**JobStatus**](JobStatus.md) | |
+**reason** | **str** | The reason for this status change. |
+**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/JobsApi.md b/addon/flamenco/manager/docs/JobsApi.md
index ade0d60d..c9606119 100644
--- a/addon/flamenco/manager/docs/JobsApi.md
+++ b/addon/flamenco/manager/docs/JobsApi.md
@@ -8,6 +8,7 @@ Method | HTTP request | Description
[**get_job_type**](JobsApi.md#get_job_type) | **GET** /api/jobs/type/{typeName} | Get single job type and its parameters.
[**get_job_types**](JobsApi.md#get_job_types) | **GET** /api/jobs/types | Get list of job types and their parameters.
[**query_jobs**](JobsApi.md#query_jobs) | **POST** /api/jobs/query | Fetch list of jobs.
+[**set_job_status**](JobsApi.md#set_job_status) | **POST** /api/jobs/{job_id}/setstatus |
[**submit_job**](JobsApi.md#submit_job) | **POST** /api/jobs | Submit a new job for Flamenco Manager to execute.
@@ -283,6 +284,77 @@ No authorization required
[[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)
+# **set_job_status**
+> set_job_status(job_id, job_status_change)
+
+
+
+### Example
+
+
+```python
+import time
+import flamenco.manager
+from flamenco.manager.api import jobs_api
+from flamenco.manager.model.error import Error
+from flamenco.manager.model.job_status_change import JobStatusChange
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = flamenco.manager.Configuration(
+ host = "http://localhost"
+)
+
+
+# Enter a context with an instance of the API client
+with flamenco.manager.ApiClient() as api_client:
+ # Create an instance of the API class
+ api_instance = jobs_api.JobsApi(api_client)
+ job_id = "job_id_example" # str |
+ job_status_change = JobStatusChange(
+ status=JobStatus("active"),
+ reason="reason_example",
+ ) # JobStatusChange | The status change to request.
+
+ # example passing only required values which don't have defaults set
+ try:
+ api_instance.set_job_status(job_id, job_status_change)
+ except flamenco.manager.ApiException as e:
+ print("Exception when calling JobsApi->set_job_status: %s\n" % e)
+```
+
+
+### Parameters
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+ **job_id** | **str**| |
+ **job_status_change** | [**JobStatusChange**](JobStatusChange.md)| The status change to request. |
+
+### Return type
+
+void (empty response body)
+
+### Authorization
+
+No authorization required
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**204** | Status change was accepted. | - |
+**422** | The requested status change is not valid for the current status of the job. | - |
+**0** | Unexpected error. | - |
+
+[[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)
+
# **submit_job**
> Job submit_job(submitted_job)
diff --git a/addon/flamenco/manager/model/job_status_change.py b/addon/flamenco/manager/model/job_status_change.py
new file mode 100644
index 00000000..a4870450
--- /dev/null
+++ b/addon/flamenco/manager/model/job_status_change.py
@@ -0,0 +1,273 @@
+"""
+ Flamenco manager
+
+ Render Farm manager API # noqa: E501
+
+ The version of the OpenAPI document: 1.0.0
+ Generated by: https://openapi-generator.tech
+"""
+
+
+import re # noqa: F401
+import sys # noqa: F401
+
+from flamenco.manager.model_utils import ( # noqa: F401
+ ApiTypeError,
+ ModelComposed,
+ ModelNormal,
+ ModelSimple,
+ cached_property,
+ change_keys_js_to_python,
+ convert_js_args_to_python_args,
+ date,
+ datetime,
+ file_type,
+ none_type,
+ validate_get_composed_info,
+ OpenApiModel
+)
+from flamenco.manager.exceptions import ApiAttributeError
+
+
+def lazy_import():
+ from flamenco.manager.model.job_status import JobStatus
+ globals()['JobStatus'] = JobStatus
+
+
+class JobStatusChange(ModelNormal):
+ """NOTE: This class is auto generated by OpenAPI Generator.
+ Ref: https://openapi-generator.tech
+
+ Do not edit the class manually.
+
+ Attributes:
+ allowed_values (dict): The key is the tuple path to the attribute
+ and the for var_name this is (var_name,). The value is a dict
+ with a capitalized key describing the allowed value and an allowed
+ value. These dicts store the allowed enum values.
+ attribute_map (dict): The key is attribute name
+ and the value is json key in definition.
+ discriminator_value_class_map (dict): A dict to go from the discriminator
+ variable value to the discriminator class name.
+ validations (dict): The key is the tuple path to the attribute
+ and the for var_name this is (var_name,). The value is a dict
+ that stores validations for max_length, min_length, max_items,
+ min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
+ inclusive_minimum, and regex.
+ additional_properties_type (tuple): A tuple of classes accepted
+ as additional properties values.
+ """
+
+ allowed_values = {
+ }
+
+ validations = {
+ }
+
+ @cached_property
+ def additional_properties_type():
+ """
+ This must be a method because a model may have properties that are
+ of type self, this must run after the class is loaded
+ """
+ lazy_import()
+ return (bool, date, datetime, dict, float, int, list, str, none_type,) # noqa: E501
+
+ _nullable = False
+
+ @cached_property
+ def openapi_types():
+ """
+ This must be a method because a model may have properties that are
+ of type self, this must run after the class is loaded
+
+ Returns
+ openapi_types (dict): The key is attribute name
+ and the value is attribute type.
+ """
+ lazy_import()
+ return {
+ 'status': (JobStatus,), # noqa: E501
+ 'reason': (str,), # noqa: E501
+ }
+
+ @cached_property
+ def discriminator():
+ return None
+
+
+ attribute_map = {
+ 'status': 'status', # noqa: E501
+ 'reason': 'reason', # noqa: E501
+ }
+
+ read_only_vars = {
+ }
+
+ _composed_schemas = {}
+
+ @classmethod
+ @convert_js_args_to_python_args
+ def _from_openapi_data(cls, status, reason, *args, **kwargs): # noqa: E501
+ """JobStatusChange - a model defined in OpenAPI
+
+ Args:
+ status (JobStatus):
+ reason (str): The reason for this status change.
+
+ Keyword Args:
+ _check_type (bool): if True, values for parameters in openapi_types
+ will be type checked and a TypeError will be
+ raised if the wrong type is input.
+ Defaults to True
+ _path_to_item (tuple/list): This is a list of keys or values to
+ drill down to the model in received_data
+ when deserializing a response
+ _spec_property_naming (bool): True if the variable names in the input data
+ are serialized names, as specified in the OpenAPI document.
+ False if the variable names in the input data
+ are pythonic names, e.g. snake case (default)
+ _configuration (Configuration): the instance to use when
+ deserializing a file_type parameter.
+ If passed, type conversion is attempted
+ If omitted no type conversion is done.
+ _visited_composed_classes (tuple): This stores a tuple of
+ classes that we have traveled through so that
+ if we see that class again we will not use its
+ discriminator again.
+ When traveling through a discriminator, the
+ composed schema that is
+ is traveled through is added to this set.
+ For example if Animal has a discriminator
+ petType and we pass in "Dog", and the class Dog
+ allOf includes Animal, we move through Animal
+ once using the discriminator, and pick Dog.
+ Then in Dog, we will make an instance of the
+ Animal class but this time we won't travel
+ through its discriminator because we passed in
+ _visited_composed_classes = (Animal,)
+ """
+
+ _check_type = kwargs.pop('_check_type', True)
+ _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+ _path_to_item = kwargs.pop('_path_to_item', ())
+ _configuration = kwargs.pop('_configuration', None)
+ _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+ self = super(OpenApiModel, cls).__new__(cls)
+
+ if args:
+ raise ApiTypeError(
+ "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+ args,
+ self.__class__.__name__,
+ ),
+ path_to_item=_path_to_item,
+ valid_classes=(self.__class__,),
+ )
+
+ self._data_store = {}
+ self._check_type = _check_type
+ self._spec_property_naming = _spec_property_naming
+ self._path_to_item = _path_to_item
+ self._configuration = _configuration
+ self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+ self.status = status
+ self.reason = reason
+ for var_name, var_value in kwargs.items():
+ if var_name not in self.attribute_map and \
+ self._configuration is not None and \
+ self._configuration.discard_unknown_keys and \
+ self.additional_properties_type is None:
+ # discard variable.
+ continue
+ setattr(self, var_name, var_value)
+ return self
+
+ required_properties = set([
+ '_data_store',
+ '_check_type',
+ '_spec_property_naming',
+ '_path_to_item',
+ '_configuration',
+ '_visited_composed_classes',
+ ])
+
+ @convert_js_args_to_python_args
+ def __init__(self, status, reason, *args, **kwargs): # noqa: E501
+ """JobStatusChange - a model defined in OpenAPI
+
+ Args:
+ status (JobStatus):
+ reason (str): The reason for this status change.
+
+ Keyword Args:
+ _check_type (bool): if True, values for parameters in openapi_types
+ will be type checked and a TypeError will be
+ raised if the wrong type is input.
+ Defaults to True
+ _path_to_item (tuple/list): This is a list of keys or values to
+ drill down to the model in received_data
+ when deserializing a response
+ _spec_property_naming (bool): True if the variable names in the input data
+ are serialized names, as specified in the OpenAPI document.
+ False if the variable names in the input data
+ are pythonic names, e.g. snake case (default)
+ _configuration (Configuration): the instance to use when
+ deserializing a file_type parameter.
+ If passed, type conversion is attempted
+ If omitted no type conversion is done.
+ _visited_composed_classes (tuple): This stores a tuple of
+ classes that we have traveled through so that
+ if we see that class again we will not use its
+ discriminator again.
+ When traveling through a discriminator, the
+ composed schema that is
+ is traveled through is added to this set.
+ For example if Animal has a discriminator
+ petType and we pass in "Dog", and the class Dog
+ allOf includes Animal, we move through Animal
+ once using the discriminator, and pick Dog.
+ Then in Dog, we will make an instance of the
+ Animal class but this time we won't travel
+ through its discriminator because we passed in
+ _visited_composed_classes = (Animal,)
+ """
+
+ _check_type = kwargs.pop('_check_type', True)
+ _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+ _path_to_item = kwargs.pop('_path_to_item', ())
+ _configuration = kwargs.pop('_configuration', None)
+ _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+ if args:
+ raise ApiTypeError(
+ "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+ args,
+ self.__class__.__name__,
+ ),
+ path_to_item=_path_to_item,
+ valid_classes=(self.__class__,),
+ )
+
+ self._data_store = {}
+ self._check_type = _check_type
+ self._spec_property_naming = _spec_property_naming
+ self._path_to_item = _path_to_item
+ self._configuration = _configuration
+ self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+ self.status = status
+ self.reason = reason
+ for var_name, var_value in kwargs.items():
+ if var_name not in self.attribute_map and \
+ self._configuration is not None and \
+ self._configuration.discard_unknown_keys and \
+ self.additional_properties_type is None:
+ # discard variable.
+ continue
+ setattr(self, var_name, var_value)
+ if var_name in self.read_only_vars:
+ raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
+ f"class with read only attributes.")
diff --git a/addon/flamenco/manager/models/__init__.py b/addon/flamenco/manager/models/__init__.py
index a77f7a09..2557c62d 100644
--- a/addon/flamenco/manager/models/__init__.py
+++ b/addon/flamenco/manager/models/__init__.py
@@ -23,6 +23,7 @@ from flamenco.manager.model.job_all_of import JobAllOf
from flamenco.manager.model.job_metadata import JobMetadata
from flamenco.manager.model.job_settings import JobSettings
from flamenco.manager.model.job_status import JobStatus
+from flamenco.manager.model.job_status_change import JobStatusChange
from flamenco.manager.model.job_update import JobUpdate
from flamenco.manager.model.jobs_query import JobsQuery
from flamenco.manager.model.jobs_query_result import JobsQueryResult
diff --git a/addon/flamenco/manager_README.md b/addon/flamenco/manager_README.md
index 8f4c94f3..f8e66d5c 100644
--- a/addon/flamenco/manager_README.md
+++ b/addon/flamenco/manager_README.md
@@ -4,7 +4,7 @@ Render Farm manager API
The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.0.0
-- Package version: d79fde17-dirty
+- Package version: b699647e-dirty
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
For more information, please visit [https://flamenco.io/](https://flamenco.io/)
@@ -36,6 +36,7 @@ from flamenco.manager.model.available_job_type import AvailableJobType
from flamenco.manager.model.available_job_types import AvailableJobTypes
from flamenco.manager.model.error import Error
from flamenco.manager.model.job import Job
+from flamenco.manager.model.job_status_change import JobStatusChange
from flamenco.manager.model.jobs_query import JobsQuery
from flamenco.manager.model.jobs_query_result import JobsQueryResult
from flamenco.manager.model.submitted_job import SubmittedJob
@@ -71,6 +72,7 @@ Class | Method | HTTP request | Description
*JobsApi* | [**get_job_type**](flamenco/manager/docs/JobsApi.md#get_job_type) | **GET** /api/jobs/type/{typeName} | Get single job type and its parameters.
*JobsApi* | [**get_job_types**](flamenco/manager/docs/JobsApi.md#get_job_types) | **GET** /api/jobs/types | Get list of job types and their parameters.
*JobsApi* | [**query_jobs**](flamenco/manager/docs/JobsApi.md#query_jobs) | **POST** /api/jobs/query | Fetch list of jobs.
+*JobsApi* | [**set_job_status**](flamenco/manager/docs/JobsApi.md#set_job_status) | **POST** /api/jobs/{job_id}/setstatus |
*JobsApi* | [**submit_job**](flamenco/manager/docs/JobsApi.md#submit_job) | **POST** /api/jobs | Submit a new job for Flamenco Manager to execute.
*MetaApi* | [**get_configuration**](flamenco/manager/docs/MetaApi.md#get_configuration) | **GET** /api/configuration | Get the configuration of this Manager.
*MetaApi* | [**get_version**](flamenco/manager/docs/MetaApi.md#get_version) | **GET** /api/version | Get the Flamenco version of this Manager
@@ -103,6 +105,7 @@ Class | Method | HTTP request | Description
- [JobMetadata](flamenco/manager/docs/JobMetadata.md)
- [JobSettings](flamenco/manager/docs/JobSettings.md)
- [JobStatus](flamenco/manager/docs/JobStatus.md)
+ - [JobStatusChange](flamenco/manager/docs/JobStatusChange.md)
- [JobUpdate](flamenco/manager/docs/JobUpdate.md)
- [JobsQuery](flamenco/manager/docs/JobsQuery.md)
- [JobsQueryResult](flamenco/manager/docs/JobsQueryResult.md)
diff --git a/internal/manager/api_impl/jobs.go b/internal/manager/api_impl/jobs.go
index 5b6fedb1..fb6916e9 100644
--- a/internal/manager/api_impl/jobs.go
+++ b/internal/manager/api_impl/jobs.go
@@ -4,6 +4,7 @@ package api_impl
import (
"context"
+ "errors"
"fmt"
"net/http"
@@ -119,6 +120,42 @@ func (f *Flamenco) FetchJob(e echo.Context, jobId string) error {
return e.JSON(http.StatusOK, apiJob)
}
+func (f *Flamenco) SetJobStatus(e echo.Context, jobID string) error {
+ logger := requestLogger(e)
+ ctx := e.Request().Context()
+
+ logger = logger.With().Str("job", jobID).Logger()
+
+ var statusChange api.SetJobStatusJSONRequestBody
+ if err := e.Bind(&statusChange); err != nil {
+ logger.Warn().Err(err).Msg("bad request received")
+ return sendAPIError(e, http.StatusBadRequest, "invalid format")
+ }
+
+ dbJob, err := f.persist.FetchJob(ctx, jobID)
+ if err != nil {
+ if errors.Is(err, persistence.ErrJobNotFound) {
+ return sendAPIError(e, http.StatusNotFound, "no such job")
+ }
+ logger.Error().Err(err).Msg("error fetching job")
+ return sendAPIError(e, http.StatusInternalServerError, "error fetching job")
+ }
+
+ logger = logger.With().
+ Str("currentstatus", string(dbJob.Status)).
+ Str("requestedStatus", string(statusChange.Status)).
+ Str("reason", statusChange.Reason).
+ Logger()
+ logger.Info().Msg("job status change requested")
+
+ err = f.stateMachine.JobStatusChange(ctx, dbJob, statusChange.Status, statusChange.Reason)
+ if err != nil {
+ logger.Error().Err(err).Msg("error changing job status")
+ return sendAPIError(e, http.StatusInternalServerError, "unexpected error changing job status")
+ }
+ return e.String(http.StatusNoContent, "")
+}
+
func (f *Flamenco) TaskUpdate(e echo.Context, taskID string) error {
logger := requestLogger(e)
worker := requestWorkerOrPanic(e)
diff --git a/internal/manager/api_impl/jobs_test.go b/internal/manager/api_impl/jobs_test.go
index 2ae59305..78ce8de1 100644
--- a/internal/manager/api_impl/jobs_test.go
+++ b/internal/manager/api_impl/jobs_test.go
@@ -195,3 +195,57 @@ func TestGetJobTypeError(t *testing.T) {
assert.NoError(t, err)
assertResponseAPIError(t, echoCtx, http.StatusInternalServerError, "error getting job type")
}
+
+func TestSetJobStatus_nonexistentJob(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ mf := newMockedFlamenco(mockCtrl)
+
+ jobID := "18a9b096-d77e-438c-9be2-74397038298b"
+ statusUpdate := api.JobStatusChange{
+ Status: api.JobStatusCancelRequested,
+ Reason: "someone pushed a button",
+ }
+
+ mf.persistence.EXPECT().FetchJob(gomock.Any(), jobID).Return(nil, persistence.ErrJobNotFound)
+
+ // Do the call.
+ echoCtx := mf.prepareMockedJSONRequest(statusUpdate)
+ err := mf.flamenco.SetJobStatus(echoCtx, jobID)
+ assert.NoError(t, err)
+
+ assertResponseAPIError(t, echoCtx, http.StatusNotFound, "no such job")
+}
+
+func TestSetJobStatus_happy(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ mf := newMockedFlamenco(mockCtrl)
+
+ jobID := "18a9b096-d77e-438c-9be2-74397038298b"
+ statusUpdate := api.JobStatusChange{
+ Status: api.JobStatusCancelRequested,
+ Reason: "someone pushed a button",
+ }
+ dbJob := persistence.Job{
+ UUID: jobID,
+ Name: "test job",
+ Status: api.JobStatusActive,
+ Settings: persistence.StringInterfaceMap{},
+ Metadata: persistence.StringStringMap{},
+ }
+
+ // Set up expectations.
+ ctx := gomock.Any()
+ mf.persistence.EXPECT().FetchJob(ctx, jobID).Return(&dbJob, nil)
+ mf.stateMachine.EXPECT().JobStatusChange(ctx, &dbJob, statusUpdate.Status, "someone pushed a button")
+
+ // Do the call.
+ echoCtx := mf.prepareMockedJSONRequest(statusUpdate)
+ err := mf.flamenco.SetJobStatus(echoCtx, jobID)
+ assert.NoError(t, err)
+
+ assertResponseEmpty(t, echoCtx)
+}
diff --git a/internal/manager/api_impl/support_test.go b/internal/manager/api_impl/support_test.go
index a69aa06a..bc145141 100644
--- a/internal/manager/api_impl/support_test.go
+++ b/internal/manager/api_impl/support_test.go
@@ -78,13 +78,17 @@ func (mf *mockedFlamenco) prepareMockedRequest(body io.Reader) echo.Context {
return c
}
-func getRecordedResponse(echoCtx echo.Context) *http.Response {
+func getRecordedResponseRecorder(echoCtx echo.Context) *httptest.ResponseRecorder {
writer := echoCtx.Response().Writer
resp, ok := writer.(*httptest.ResponseRecorder)
if !ok {
panic(fmt.Sprintf("response writer was not a `*httptest.ResponseRecorder` but a %T", writer))
}
- return resp.Result()
+ return resp
+}
+
+func getRecordedResponse(echoCtx echo.Context) *http.Response {
+ return getRecordedResponseRecorder(echoCtx).Result()
}
// assertResponseJSON asserts that a recorded response is JSON with the given HTTP status code.
@@ -118,6 +122,13 @@ func assertResponseAPIError(t *testing.T, echoCtx echo.Context, expectStatusCode
})
}
+// assertResponseEmpty asserts the response is an empty 204 No Content response.
+func assertResponseEmpty(t *testing.T, echoCtx echo.Context) {
+ resp := getRecordedResponseRecorder(echoCtx)
+ assert.Equal(t, http.StatusNoContent, resp.Code, "Unexpected status: %v", resp.Result().Status)
+ assert.Zero(t, resp.Body.Len(), "HTTP 204 No Content should have no content, got %v", resp.Body.String())
+}
+
func testWorker() persistence.Worker {
return persistence.Worker{
Model: gorm.Model{ID: 1},
diff --git a/internal/worker/mocks/client.gen.go b/internal/worker/mocks/client.gen.go
index af9965bc..c7386d3b 100644
--- a/internal/worker/mocks/client.gen.go
+++ b/internal/worker/mocks/client.gen.go
@@ -236,6 +236,46 @@ func (mr *MockFlamencoClientMockRecorder) ScheduleTaskWithResponse(arg0 interfac
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScheduleTaskWithResponse", reflect.TypeOf((*MockFlamencoClient)(nil).ScheduleTaskWithResponse), varargs...)
}
+// SetJobStatusWithBodyWithResponse mocks base method.
+func (m *MockFlamencoClient) SetJobStatusWithBodyWithResponse(arg0 context.Context, arg1, arg2 string, arg3 io.Reader, arg4 ...api.RequestEditorFn) (*api.SetJobStatusResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2, arg3}
+ for _, a := range arg4 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SetJobStatusWithBodyWithResponse", varargs...)
+ ret0, _ := ret[0].(*api.SetJobStatusResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// SetJobStatusWithBodyWithResponse indicates an expected call of SetJobStatusWithBodyWithResponse.
+func (mr *MockFlamencoClientMockRecorder) SetJobStatusWithBodyWithResponse(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetJobStatusWithBodyWithResponse", reflect.TypeOf((*MockFlamencoClient)(nil).SetJobStatusWithBodyWithResponse), varargs...)
+}
+
+// SetJobStatusWithResponse mocks base method.
+func (m *MockFlamencoClient) SetJobStatusWithResponse(arg0 context.Context, arg1 string, arg2 api.SetJobStatusJSONRequestBody, arg3 ...api.RequestEditorFn) (*api.SetJobStatusResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SetJobStatusWithResponse", varargs...)
+ ret0, _ := ret[0].(*api.SetJobStatusResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// SetJobStatusWithResponse indicates an expected call of SetJobStatusWithResponse.
+func (mr *MockFlamencoClientMockRecorder) SetJobStatusWithResponse(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetJobStatusWithResponse", reflect.TypeOf((*MockFlamencoClient)(nil).SetJobStatusWithResponse), varargs...)
+}
+
// ShamanCheckoutRequirementsWithBodyWithResponse mocks base method.
func (m *MockFlamencoClient) ShamanCheckoutRequirementsWithBodyWithResponse(arg0 context.Context, arg1 string, arg2 io.Reader, arg3 ...api.RequestEditorFn) (*api.ShamanCheckoutRequirementsResponse, error) {
m.ctrl.T.Helper()
diff --git a/pkg/api/flamenco-manager.yaml b/pkg/api/flamenco-manager.yaml
index b0bf0b53..8b1d3247 100644
--- a/pkg/api/flamenco-manager.yaml
+++ b/pkg/api/flamenco-manager.yaml
@@ -322,6 +322,36 @@ paths:
application/json:
schema: {$ref: "#/components/schemas/Job"}
+ /api/jobs/{job_id}/setstatus:
+ summary: Request a status change for the given job.
+ post:
+ operationId: setJobStatus
+ tags: [jobs]
+ parameters:
+ - name: job_id
+ in: path
+ required: true
+ schema: {type: string, format: uuid}
+ requestBody:
+ description: The status change to request.
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/JobStatusChange"
+ responses:
+ "204":
+ description: Status change was accepted.
+ "422":
+ description: The requested status change is not valid for the current status of the job.
+ default:
+ description: Unexpected error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+
## Shaman
/shaman/checkout/requirements:
@@ -791,6 +821,13 @@ components:
items: {$ref: "#/components/schemas/Job"}
required: [jobs]
+ JobStatusChange:
+ type: object
+ properties:
+ status: {$ref: "#/components/schemas/JobStatus"}
+ reason: {type: string, description: The reason for this status change.}
+ required: [status, reason]
+
Error:
description: Generic error response.
type: object
diff --git a/pkg/api/openapi_client.gen.go b/pkg/api/openapi_client.gen.go
index 7aeab204..12ff9910 100644
--- a/pkg/api/openapi_client.gen.go
+++ b/pkg/api/openapi_client.gen.go
@@ -112,6 +112,11 @@ type ClientInterface interface {
// FetchJob request
FetchJob(ctx context.Context, jobId string, reqEditors ...RequestEditorFn) (*http.Response, error)
+ // SetJobStatus request with any body
+ SetJobStatusWithBody(ctx context.Context, jobId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ SetJobStatus(ctx context.Context, jobId string, body SetJobStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
+
// GetVersion request
GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -257,6 +262,30 @@ func (c *Client) FetchJob(ctx context.Context, jobId string, reqEditors ...Reque
return c.Client.Do(req)
}
+func (c *Client) SetJobStatusWithBody(ctx context.Context, jobId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewSetJobStatusRequestWithBody(c.Server, jobId, contentType, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Client) SetJobStatus(ctx context.Context, jobId string, body SetJobStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewSetJobStatusRequest(c.Server, jobId, body)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
func (c *Client) GetVersion(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewGetVersionRequest(c.Server)
if err != nil {
@@ -675,6 +704,53 @@ func NewFetchJobRequest(server string, jobId string) (*http.Request, error) {
return req, nil
}
+// NewSetJobStatusRequest calls the generic SetJobStatus builder with application/json body
+func NewSetJobStatusRequest(server string, jobId string, body SetJobStatusJSONRequestBody) (*http.Request, error) {
+ var bodyReader io.Reader
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader = bytes.NewReader(buf)
+ return NewSetJobStatusRequestWithBody(server, jobId, "application/json", bodyReader)
+}
+
+// NewSetJobStatusRequestWithBody generates requests for SetJobStatus with any type of body
+func NewSetJobStatusRequestWithBody(server string, jobId string, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "job_id", runtime.ParamLocationPath, jobId)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/api/jobs/%s/setstatus", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
// NewGetVersionRequest generates requests for GetVersion
func NewGetVersionRequest(server string) (*http.Request, error) {
var err error
@@ -1201,6 +1277,11 @@ type ClientWithResponsesInterface interface {
// FetchJob request
FetchJobWithResponse(ctx context.Context, jobId string, reqEditors ...RequestEditorFn) (*FetchJobResponse, error)
+ // SetJobStatus request with any body
+ SetJobStatusWithBodyWithResponse(ctx context.Context, jobId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SetJobStatusResponse, error)
+
+ SetJobStatusWithResponse(ctx context.Context, jobId string, body SetJobStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*SetJobStatusResponse, error)
+
// GetVersion request
GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error)
@@ -1384,6 +1465,28 @@ func (r FetchJobResponse) StatusCode() int {
return 0
}
+type SetJobStatusResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSONDefault *Error
+}
+
+// Status returns HTTPResponse.Status
+func (r SetJobStatusResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r SetJobStatusResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
type GetVersionResponse struct {
Body []byte
HTTPResponse *http.Response
@@ -1728,6 +1831,23 @@ func (c *ClientWithResponses) FetchJobWithResponse(ctx context.Context, jobId st
return ParseFetchJobResponse(rsp)
}
+// SetJobStatusWithBodyWithResponse request with arbitrary body returning *SetJobStatusResponse
+func (c *ClientWithResponses) SetJobStatusWithBodyWithResponse(ctx context.Context, jobId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SetJobStatusResponse, error) {
+ rsp, err := c.SetJobStatusWithBody(ctx, jobId, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseSetJobStatusResponse(rsp)
+}
+
+func (c *ClientWithResponses) SetJobStatusWithResponse(ctx context.Context, jobId string, body SetJobStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*SetJobStatusResponse, error) {
+ rsp, err := c.SetJobStatus(ctx, jobId, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseSetJobStatusResponse(rsp)
+}
+
// GetVersionWithResponse request returning *GetVersionResponse
func (c *ClientWithResponses) GetVersionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVersionResponse, error) {
rsp, err := c.GetVersion(ctx, reqEditors...)
@@ -2054,6 +2174,32 @@ func ParseFetchJobResponse(rsp *http.Response) (*FetchJobResponse, error) {
return response, nil
}
+// ParseSetJobStatusResponse parses an HTTP response from a SetJobStatusWithResponse call
+func ParseSetJobStatusResponse(rsp *http.Response) (*SetJobStatusResponse, error) {
+ bodyBytes, err := ioutil.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &SetJobStatusResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true:
+ var dest Error
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSONDefault = &dest
+
+ }
+
+ return response, nil
+}
+
// ParseGetVersionResponse parses an HTTP response from a GetVersionWithResponse call
func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) {
bodyBytes, err := ioutil.ReadAll(rsp.Body)
diff --git a/pkg/api/openapi_server.gen.go b/pkg/api/openapi_server.gen.go
index 28ace4df..ea914967 100644
--- a/pkg/api/openapi_server.gen.go
+++ b/pkg/api/openapi_server.gen.go
@@ -31,6 +31,9 @@ type ServerInterface interface {
// Fetch info about the job.
// (GET /api/jobs/{job_id})
FetchJob(ctx echo.Context, jobId string) error
+
+ // (POST /api/jobs/{job_id}/setstatus)
+ SetJobStatus(ctx echo.Context, jobId string) error
// Get the Flamenco version of this Manager
// (GET /api/version)
GetVersion(ctx echo.Context) error
@@ -143,6 +146,22 @@ func (w *ServerInterfaceWrapper) FetchJob(ctx echo.Context) error {
return err
}
+// SetJobStatus converts echo context to params.
+func (w *ServerInterfaceWrapper) SetJobStatus(ctx echo.Context) error {
+ var err error
+ // ------------- Path parameter "job_id" -------------
+ var jobId string
+
+ err = runtime.BindStyledParameterWithLocation("simple", false, "job_id", runtime.ParamLocationPath, ctx.Param("job_id"), &jobId)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter job_id: %s", err))
+ }
+
+ // Invoke the callback with all the unmarshalled arguments
+ err = w.Handler.SetJobStatus(ctx, jobId)
+ return err
+}
+
// GetVersion converts echo context to params.
func (w *ServerInterfaceWrapper) GetVersion(ctx echo.Context) error {
var err error
@@ -369,6 +388,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
router.GET(baseURL+"/api/jobs/type/:typeName", wrapper.GetJobType)
router.GET(baseURL+"/api/jobs/types", wrapper.GetJobTypes)
router.GET(baseURL+"/api/jobs/:job_id", wrapper.FetchJob)
+ router.POST(baseURL+"/api/jobs/:job_id/setstatus", wrapper.SetJobStatus)
router.GET(baseURL+"/api/version", wrapper.GetVersion)
router.POST(baseURL+"/api/worker/register-worker", wrapper.RegisterWorker)
router.POST(baseURL+"/api/worker/sign-off", wrapper.SignOff)
diff --git a/pkg/api/openapi_spec.gen.go b/pkg/api/openapi_spec.gen.go
index 8584c74b..a9ecf5bb 100644
--- a/pkg/api/openapi_spec.gen.go
+++ b/pkg/api/openapi_spec.gen.go
@@ -18,107 +18,109 @@ import (
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "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",
- "AAD//zdTo0YPZgAA",
+ "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",
+ "vqPX9dn1/wQAAP//GzfZKqRpAAA=",
}
// 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 50f83ada..64ebb470 100644
--- a/pkg/api/openapi_types.gen.go
+++ b/pkg/api/openapi_types.gen.go
@@ -230,6 +230,13 @@ type JobSettings struct {
// JobStatus defines model for JobStatus.
type JobStatus string
+// JobStatusChange defines model for JobStatusChange.
+type JobStatusChange struct {
+ // The reason for this status change.
+ Reason string `json:"reason"`
+ Status JobStatus `json:"status"`
+}
+
// Subset of a Job, sent over SocketIO when a job changes. For new jobs, `previous_status` will be excluded.
type JobUpdate struct {
// UUID of the Job
@@ -416,6 +423,9 @@ type SubmitJobJSONBody SubmittedJob
// QueryJobsJSONBody defines parameters for QueryJobs.
type QueryJobsJSONBody JobsQuery
+// SetJobStatusJSONBody defines parameters for SetJobStatus.
+type SetJobStatusJSONBody JobStatusChange
+
// RegisterWorkerJSONBody defines parameters for RegisterWorker.
type RegisterWorkerJSONBody WorkerRegistration
@@ -449,6 +459,9 @@ type SubmitJobJSONRequestBody SubmitJobJSONBody
// QueryJobsJSONRequestBody defines body for QueryJobs for application/json ContentType.
type QueryJobsJSONRequestBody QueryJobsJSONBody
+// SetJobStatusJSONRequestBody defines body for SetJobStatus for application/json ContentType.
+type SetJobStatusJSONRequestBody SetJobStatusJSONBody
+
// RegisterWorkerJSONRequestBody defines body for RegisterWorker for application/json ContentType.
type RegisterWorkerJSONRequestBody RegisterWorkerJSONBody
diff --git a/web/app/src/manager-api/ApiClient.js b/web/app/src/manager-api/ApiClient.js
index 2083a715..327bcec9 100644
--- a/web/app/src/manager-api/ApiClient.js
+++ b/web/app/src/manager-api/ApiClient.js
@@ -55,7 +55,7 @@ class ApiClient {
* @default {}
*/
this.defaultHeaders = {
- 'User-Agent': 'Flamenco/d79fde17-dirty / webbrowser'
+ 'User-Agent': 'Flamenco/b699647e-dirty / webbrowser'
};
/**
diff --git a/web/app/src/manager-api/index.js b/web/app/src/manager-api/index.js
index 4ff6bb2b..7549a99c 100644
--- a/web/app/src/manager-api/index.js
+++ b/web/app/src/manager-api/index.js
@@ -25,6 +25,7 @@ import FlamencoVersion from './model/FlamencoVersion';
import Job from './model/Job';
import JobAllOf from './model/JobAllOf';
import JobStatus from './model/JobStatus';
+import JobStatusChange from './model/JobStatusChange';
import JobUpdate from './model/JobUpdate';
import JobsQuery from './model/JobsQuery';
import JobsQueryResult from './model/JobsQueryResult';
@@ -163,6 +164,12 @@ export {
*/
JobStatus,
+ /**
+ * The JobStatusChange model constructor.
+ * @property {module:model/JobStatusChange}
+ */
+ JobStatusChange,
+
/**
* The JobUpdate model constructor.
* @property {module:model/JobUpdate}
diff --git a/web/app/src/manager-api/manager/JobsApi.js b/web/app/src/manager-api/manager/JobsApi.js
index 3a3b3d0f..bcbfb787 100644
--- a/web/app/src/manager-api/manager/JobsApi.js
+++ b/web/app/src/manager-api/manager/JobsApi.js
@@ -17,6 +17,7 @@ import AvailableJobType from '../model/AvailableJobType';
import AvailableJobTypes from '../model/AvailableJobTypes';
import Error from '../model/Error';
import Job from '../model/Job';
+import JobStatusChange from '../model/JobStatusChange';
import JobsQuery from '../model/JobsQuery';
import JobsQueryResult from '../model/JobsQueryResult';
import SubmittedJob from '../model/SubmittedJob';
@@ -217,6 +218,56 @@ export default class JobsApi {
}
+ /**
+ * @param {String} jobId
+ * @param {module:model/JobStatusChange} jobStatusChange The status change to request.
+ * @return {Promise} a {@link https://www.promisejs.org/|Promise}, with an object containing HTTP response
+ */
+ setJobStatusWithHttpInfo(jobId, jobStatusChange) {
+ let postBody = jobStatusChange;
+ // verify the required parameter 'jobId' is set
+ if (jobId === undefined || jobId === null) {
+ throw new Error("Missing the required parameter 'jobId' when calling setJobStatus");
+ }
+ // verify the required parameter 'jobStatusChange' is set
+ if (jobStatusChange === undefined || jobStatusChange === null) {
+ throw new Error("Missing the required parameter 'jobStatusChange' when calling setJobStatus");
+ }
+
+ let pathParams = {
+ 'job_id': jobId
+ };
+ let queryParams = {
+ };
+ let headerParams = {
+ };
+ let formParams = {
+ };
+
+ let authNames = [];
+ let contentTypes = ['application/json'];
+ let accepts = ['application/json'];
+ let returnType = null;
+ return this.apiClient.callApi(
+ '/api/jobs/{job_id}/setstatus', 'POST',
+ pathParams, queryParams, headerParams, formParams, postBody,
+ authNames, contentTypes, accepts, returnType, null
+ );
+ }
+
+ /**
+ * @param {String} jobId
+ * @param {module:model/JobStatusChange} jobStatusChange The status change to request.
+ * @return {Promise} a {@link https://www.promisejs.org/|Promise}
+ */
+ setJobStatus(jobId, jobStatusChange) {
+ return this.setJobStatusWithHttpInfo(jobId, jobStatusChange)
+ .then(function(response_and_data) {
+ return response_and_data.data;
+ });
+ }
+
+
/**
* Submit a new job for Flamenco Manager to execute.
* @param {module:model/SubmittedJob} submittedJob Job to submit
diff --git a/web/app/src/manager-api/model/JobStatusChange.js b/web/app/src/manager-api/model/JobStatusChange.js
new file mode 100644
index 00000000..ddb082f9
--- /dev/null
+++ b/web/app/src/manager-api/model/JobStatusChange.js
@@ -0,0 +1,85 @@
+/**
+ * Flamenco manager
+ * Render Farm manager API
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ *
+ */
+
+import ApiClient from '../ApiClient';
+import JobStatus from './JobStatus';
+
+/**
+ * The JobStatusChange model module.
+ * @module model/JobStatusChange
+ * @version 0.0.0
+ */
+class JobStatusChange {
+ /**
+ * Constructs a new JobStatusChange
.
+ * @alias module:model/JobStatusChange
+ * @param status {module:model/JobStatus}
+ * @param reason {String} The reason for this status change.
+ */
+ constructor(status, reason) {
+
+ JobStatusChange.initialize(this, status, reason);
+ }
+
+ /**
+ * Initializes the fields of this object.
+ * This method is used by the constructors of any subclasses, in order to implement multiple inheritance (mix-ins).
+ * Only for internal use.
+ */
+ static initialize(obj, status, reason) {
+ obj['status'] = status;
+ obj['reason'] = reason;
+ }
+
+ /**
+ * Constructs a JobStatusChange
from a plain JavaScript object, optionally creating a new instance.
+ * Copies all relevant properties from data
to obj
if supplied or a new instance if not.
+ * @param {Object} data The plain JavaScript object bearing properties of interest.
+ * @param {module:model/JobStatusChange} obj Optional instance to populate.
+ * @return {module:model/JobStatusChange} The populated JobStatusChange
instance.
+ */
+ static constructFromObject(data, obj) {
+ if (data) {
+ obj = obj || new JobStatusChange();
+
+ if (data.hasOwnProperty('status')) {
+ obj['status'] = JobStatus.constructFromObject(data['status']);
+ }
+ if (data.hasOwnProperty('reason')) {
+ obj['reason'] = ApiClient.convertToType(data['reason'], 'String');
+ }
+ }
+ return obj;
+ }
+
+
+}
+
+/**
+ * @member {module:model/JobStatus} status
+ */
+JobStatusChange.prototype['status'] = undefined;
+
+/**
+ * The reason for this status change.
+ * @member {String} reason
+ */
+JobStatusChange.prototype['reason'] = undefined;
+
+
+
+
+
+
+export default JobStatusChange;
+