Make the GET /api/jobs/types endpoint work

This commit is contained in:
Sybren A. Stüvel 2022-01-10 17:43:30 +01:00 committed by Sybren A. Stüvel
parent 6520dc2d66
commit d0fafb5063
9 changed files with 210 additions and 133 deletions

View File

@ -49,7 +49,6 @@ func main() {
log.Info().Str("version", appinfo.ApplicationVersion).Msgf("starting %v", appinfo.ApplicationName)
gojaPoC()
echoOpenAPIPoC()
}
@ -113,7 +112,12 @@ func echoOpenAPIPoC() {
})
e.Use(validator)
flamenco := api_impl.NewFlamenco()
compiler, err := job_compilers.Load()
if err != nil {
log.Fatal().Err(err).Msg("error loading job compilers")
}
flamenco := api_impl.NewFlamenco(compiler)
api.RegisterHandlers(e, flamenco)
// Log available routes

View File

@ -27,12 +27,20 @@ import (
)
type Flamenco struct {
jobCompiler JobCompiler
}
type JobCompiler interface {
ListJobTypes() api.AvailableJobTypes
}
var _ api.ServerInterface = (*Flamenco)(nil)
func NewFlamenco() *Flamenco {
return &Flamenco{}
// NewFlamenco creates a new Flamenco service, using the given JobCompiler.
func NewFlamenco(jc JobCompiler) *Flamenco {
return &Flamenco{
jobCompiler: jc,
}
}
// sendPetstoreError wraps sending of an error in the Error format, and

View File

@ -24,52 +24,15 @@ import (
"net/http"
"github.com/labstack/echo/v4"
"gitlab.com/blender/flamenco-goja-test/pkg/api"
"github.com/rs/zerolog/log"
)
func (f *Flamenco) GetJobTypes(e echo.Context) error {
// Some helper functions because Go doesn't allow taking the address of a literal.
defaultString := func(s string) *interface{} {
var iValue interface{} = s
return &iValue
}
defaultInt32 := func(i int32) *interface{} {
var iValue interface{} = i
return &iValue
}
defaultBool := func(b bool) *interface{} {
var iValue interface{} = b
return &iValue
}
boolPtr := func(b bool) *bool {
return &b
}
choicesStr := func(choices ...string) *[]string {
return &choices
if f.jobCompiler == nil {
log.Error().Msg("Flamenco is running without job compiler")
return sendAPIError(e, http.StatusInternalServerError, "no job types available")
}
// TODO: dynamically build based on the actually registered job types.
types := api.AvailableJobTypes{
JobTypes: []api.AvailableJobType{{
Name: "simple-blender-render",
Settings: []api.AvailableJobSetting{
{Key: "blender_cmd", Type: "string", Default: defaultString("{blender}")},
{Key: "chunk_size", Type: "int32", Default: defaultInt32(1)},
{Key: "frames", Type: "string", Required: boolPtr(true)},
{Key: "render_output", Type: "string", Required: boolPtr(true)},
{Key: "fps", Type: "int32"},
{Key: "extract_audio", Type: "bool", Default: defaultBool(true)},
{Key: "images_or_video",
Type: "string",
Required: boolPtr(true),
Choices: choicesStr("images", "video"),
Visible: boolPtr(false),
},
{Key: "format", Type: "string", Required: boolPtr(true)},
{Key: "output_file_extension", Type: "string", Required: boolPtr(true)},
},
}},
}
return e.JSON(http.StatusOK, &types)
jobTypes := f.jobCompiler.ListJobTypes()
return e.JSON(http.StatusOK, &jobTypes)
}

View File

@ -1,3 +1,5 @@
// Package job_compilers contains functionality to convert a Flamenco job
// definition into concrete tasks and commands to execute by Workers.
package job_compilers
/* ***** BEGIN GPL LICENSE BLOCK *****
@ -28,24 +30,32 @@ import (
"github.com/dop251/goja_nodejs/require"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"gitlab.com/blender/flamenco-goja-test/pkg/api"
)
var ErrJobTypeUnknown = errors.New("job type unknown")
var ErrScriptIncomplete = errors.New("job compiler script incomplete")
type GojaJobCompiler struct {
jobtypes map[string]JobType // Mapping from job type name to jobType struct.
registry *require.Registry // Goja module registry.
// Service contains job compilers defined in JavaScript.
type Service struct {
compilers map[string]Compiler // Mapping from job type name to the job compiler of that type.
registry *require.Registry // Goja module registry.
}
type JobType struct {
type Compiler struct {
jobType string
program *goja.Program // Compiled JavaScript file.
filename string // The filename of that JS file.
}
func Load() (*GojaJobCompiler, error) {
compiler := GojaJobCompiler{
jobtypes: map[string]JobType{},
type VM struct {
runtime *goja.Runtime // Goja VM containing the job compiler script.
compiler Compiler // Program loaded into this VM.
}
func Load() (*Service, error) {
compiler := Service{
compilers: map[string]Compiler{},
}
if err := compiler.loadScripts(); err != nil {
@ -53,7 +63,7 @@ func Load() (*GojaJobCompiler, error) {
}
staticFileLoader := func(path string) ([]byte, error) {
content, err := compiler.loadScript(path)
content, err := compiler.loadScriptBytes(path)
if err != nil {
// The 'require' module uses this to try different variations of the path
// in order to find it (without .js, with .js, etc.), so don't log any of
@ -71,34 +81,15 @@ func Load() (*GojaJobCompiler, error) {
return &compiler, nil
}
func (c *GojaJobCompiler) newGojaVM() *goja.Runtime {
vm := goja.New()
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())
mustSet := func(name string, value interface{}) {
err := vm.Set(name, value)
if err != nil {
log.Panic().Err(err).Msgf("unable to register '%s' in Goja VM", name)
}
func (s *Service) Run(jobTypeName string) error {
vm, err := s.compilerForJobType(jobTypeName)
if err != nil {
return err
}
// Set some global functions for script debugging purposes.
mustSet("print", jsPrint)
mustSet("alert", jsAlert)
// Pre-import some useful modules.
c.registry.Enable(vm)
mustSet("author", require.Require(vm, "author"))
mustSet("path", require.Require(vm, "path"))
mustSet("process", require.Require(vm, "process"))
return vm
}
func (c *GojaJobCompiler) Run(jobTypeName string) error {
jobType, ok := c.jobtypes[jobTypeName]
if !ok {
return ErrJobTypeUnknown
compileJob, err := vm.getCompileJob()
if err != nil {
return err
}
created, err := time.Parse(time.RFC3339, "2022-01-03T18:53:00+01:00")
@ -130,24 +121,7 @@ func (c *GojaJobCompiler) Run(jobTypeName string) error {
},
}
vm := c.newGojaVM()
// This should register the `compileJob()` function called below:
if _, err := vm.RunProgram(jobType.program); err != nil {
return err
}
compileJob, isCallable := goja.AssertFunction(vm.Get("compileJob"))
if !isCallable {
log.Error().
Str("jobType", jobTypeName).
Str("script", jobType.filename).
Msg("script does not define a compileJob(job) function")
return ErrScriptIncomplete
}
if _, err := compileJob(nil, vm.ToValue(&job)); err != nil {
if err := compileJob(&job); err != nil {
return err
}
@ -159,3 +133,60 @@ func (c *GojaJobCompiler) Run(jobTypeName string) error {
return nil
}
// ListJobTypes returns the list of available job types.
func (s *Service) ListJobTypes() api.AvailableJobTypes {
jobTypes := make([]api.AvailableJobType, 0)
for typeName := range s.compilers {
compiler, err := s.compilerForJobType(typeName)
if err != nil {
log.Warn().Err(err).Str("jobType", typeName).Msg("unable to determine job type settings")
continue
}
description, err := compiler.getJobTypeInfo()
if err != nil {
log.Warn().Err(err).Str("jobType", typeName).Msg("unable to determine job type settings")
continue
}
jobTypes = append(jobTypes, *description)
}
return api.AvailableJobTypes{JobTypes: jobTypes}
}
func (vm *VM) getCompileJob() (func(job *AuthoredJob) error, error) {
compileJob, isCallable := goja.AssertFunction(vm.runtime.Get("compileJob"))
if !isCallable {
// TODO: construct a more elaborate Error object that contains this info, instead of logging here.
log.Error().
Str("jobType", vm.compiler.jobType).
Str("script", vm.compiler.filename).
Msg("script does not define a compileJob(job) function")
return nil, ErrScriptIncomplete
}
// TODO: wrap this in a nicer way.
return func(job *AuthoredJob) error {
_, err := compileJob(nil, vm.runtime.ToValue(job))
return err
}, nil
}
func (vm *VM) getJobTypeInfo() (*api.AvailableJobType, error) {
jtValue := vm.runtime.Get("JOB_TYPE")
var ajt api.AvailableJobType
if err := vm.runtime.ExportTo(jtValue, &ajt); err != nil {
// TODO: construct a more elaborate Error object that contains this info, instead of logging here.
log.Error().
Err(err).
Str("jobType", vm.compiler.jobType).
Str("script", vm.compiler.filename).
Msg("script does not define a proper JOB_TYPE object")
return nil, ErrScriptIncomplete
}
ajt.Name = vm.compiler.jobType
return &ajt, nil
}

View File

@ -28,13 +28,14 @@ import (
"strings"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/require"
"github.com/rs/zerolog/log"
)
//go:embed scripts
var scriptsFS embed.FS
func (c *GojaJobCompiler) loadScripts() error {
func (s *Service) loadScripts() error {
scripts, err := scriptsFS.ReadDir("scripts")
if err != nil {
return fmt.Errorf("failed to find scripts: %w", err)
@ -46,7 +47,7 @@ func (c *GojaJobCompiler) loadScripts() error {
}
filename := path.Join("scripts", script.Name())
script_bytes, err := c.loadScript(filename)
script_bytes, err := s.loadScriptBytes(filename)
if err != nil {
log.Error().Err(err).Str("filename", filename).Msg("failed to read script")
continue
@ -59,7 +60,7 @@ func (c *GojaJobCompiler) loadScripts() error {
}
jobTypeName := filenameToJobType(script.Name())
c.jobtypes[jobTypeName] = JobType{
s.compilers[jobTypeName] = Compiler{
program: program,
filename: script.Name(),
}
@ -70,7 +71,7 @@ func (c *GojaJobCompiler) loadScripts() error {
return nil
}
func (c *GojaJobCompiler) loadScript(path string) ([]byte, error) {
func (s *Service) loadScriptBytes(path string) ([]byte, error) {
file, err := scriptsFS.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open embedded script: %w", err)
@ -83,3 +84,46 @@ func filenameToJobType(filename string) string {
stem := filename[:len(filename)-len(extension)]
return strings.ReplaceAll(stem, "_", "-")
}
func (s *Service) newGojaVM() *goja.Runtime {
vm := goja.New()
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())
mustSet := func(name string, value interface{}) {
err := vm.Set(name, value)
if err != nil {
log.Panic().Err(err).Msgf("unable to register '%s' in Goja VM", name)
}
}
// Set some global functions for script debugging purposes.
mustSet("print", jsPrint)
mustSet("alert", jsAlert)
// Pre-import some useful modules.
s.registry.Enable(vm)
mustSet("author", require.Require(vm, "author"))
mustSet("path", require.Require(vm, "path"))
mustSet("process", require.Require(vm, "process"))
return vm
}
// compilerForJobType returns a Goja *Runtime that has the job compiler script for
// the given job type loaded up.
func (s *Service) compilerForJobType(jobTypeName string) (*VM, error) {
program, ok := s.compilers[jobTypeName]
if !ok {
return nil, ErrJobTypeUnknown
}
vm := s.newGojaVM()
if _, err := vm.RunProgram(program.program); err != nil {
return nil, err
}
return &VM{
runtime: vm,
compiler: program,
}, nil
}

View File

@ -18,6 +18,28 @@
*
* ***** END GPL LICENSE BLOCK ***** */
const JOB_TYPE = {
label: "Simple Blender Render",
settings: [
{ key: "blender_cmd", type: "string", default: "{blender}" },
{ key: "chunk_size", type: "int32", default: 1 },
{ key: "frames", type: "string", required: true },
{ key: "render_output", type: "string", required: true },
{ key: "fps", type: "int32" },
{ key: "extract_audio", type: "bool", default: true },
{
key: "images_or_video",
type: "string",
required: true,
choices: ["images", "video"],
visible: false,
},
{ key: "format", type: "string", required: true },
{ key: "output_file_extension", type: "string", required: true },
]
};
// Set of scene.render.image_settings.file_format values that produce
// files which FFmpeg is known not to handle as input.
const ffmpegIncompatibleImageFormats = new Set([

View File

@ -77,6 +77,8 @@ paths:
tags:
- name: worker
description: API for Flamenco Workers to communicate with Flamenco Manager.
- name: jobs
description: Blablabla for users to get job metadata balbla
components:
schemas:
@ -154,10 +156,11 @@ components:
description: Job type supported by this Manager, and its parameters.
properties:
"name": {type: string}
"label": {type: string}
"settings":
type: array
items: {$ref: "#/components/schemas/AvailableJobSetting"}
required: [name, settings]
required: [name, label, settings]
AvailableJobSetting:
type: object

View File

@ -18,35 +18,36 @@ import (
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/7xY32/jNhL+Vwa6A64FFDu36ZPftt3rIUWvGzQp+rAbpLQ4tplQpJYc2jUC/++HIakf",
"tuRkr7ftSyJLJGfmm29mPum5qGzdWIOGfLF4Lny1wVrEy7feq7VBeSf8E/+W6CunGlLWFIujp6A8CCC+",
"Eh4U8W+HFaotSljugTYIv1r3hG5WlEXjbIOOFEYrD0ryP9o3WCwKT06ZdXEo2a1aGBnXKMI6Xvzd4apY",
"FH+b907Ps8fz79IG3psPE86JPf9+tMtJG492+dA4ZZ2i/WCBMoRrdO2KdHdiuxH19IOXz/QkKLwaDuN6",
"m1ZyRMI/nXckeHQTDw5l4fBTUA5lsfgQkU5Y5B05gs6jgeMn2AyAGPoyyNJ9h7pdPmJF7NbbrVBaLDX+",
"YJe3SMROjXh0q8xaI/j0HOwKBPxgl8Cn+TFdqo1VVbo8PufXDRpYqy2aErSqFUXWbYVWkv8G9ECW73mE",
"fMgM3hu9h+DZR9gp2kCCLhpn2x3xRpCfUkziSgRNY7/uNgj5YfID/MbuTHYGOBGwY98lErpamWh/o3wL",
"yYyPf8L9+OhriYbUSqGDlXXxuLynhDp4giVCMOpTSMEpE5c8ZnA5vlFYPV+e+5BWQnssx3jTBh3Hoeoa",
"pRKEeg8OOfsgohmJK2UUbyg5sTEqNllGf2ygdKsRjlQVtHB9zJ1vS2s1CtNj/nLZTHDujrcdymKrvFpq",
"PAqNXHgpMk7VUTIgg/jLdWIqR+nDslZE6OArrZ4QBHyr0Uh0IKS8sObrGdwi8XG/RSR/S/lObVIY4Nbg",
"jNCdDdoIYtNBS/OPmMWOMmhkpIyffTQTGJ1UPLMmL/rM+rzLEJ9weN8gxxujHTEslY/wcP3uJtXqPmaZ",
"scpIzOAnCwY9oeQSCxUFhx6+iuXjS5CqYlPCKfRfg3AIPjSNdYSSqYAm1BxP5mnJ3fTqTVEWK20FFWUE",
"YBBiz+dhiNOxtb2mt5gmlvLwH2HEGl0JwkhQFIkqai7TicZ0dhhkwD5/kE21zVHHOcl028hbW6+lm7GY",
"aKI/Kk9tomMDPo/KGIF2RPyxSNsqfTHM3sRUgK0CGIWVH4DDxqFnF0CAT4MnT7BYXL9jFQhfUyyflekT",
"5/5Auv7lnI1T/WQAWhmtr6yrBSVlEWthLDRq9F6s8XVlEM/s10958zOulSd0KBMsY8fOCTkhpUM/PUW1",
"8PQgKlLbY7U00Fiqejqvs7QgxmE6G3ZFO+HOpKrTYONHLeMfOqFzzOhXtMCk6urC6PEYyq42jrKognNo",
"KNouTgEaBHXGz6nU3WIVWMWdIdRns+QlegzU6uK569fR8SgThalQo0yKsdFI8XolVLr5KWCIFxzfRXc7",
"bbtgJ+LsyFuObjQi+HThbIWeq3pyEiTaJho7kRrDKRT/B9mwckh/BZ+ypSPSTJoYkG6cseRypMUtN+Pk",
"EiuLBxFoM26h798G2rwBfsiis4oIwkrbXdSew/tmnSUKfJfIrPdgLIHixNdoCGUJ3oKxO6itixpYsn4Q",
"8LEgK+3HgiWQAVFREPrEZhI+cYJE6SO8qvrutyFqGMJdzPaZWH7xrLdqZP3FjT4thut3JTTC+511sn2U",
"0E7vFSCoXeoGNJq96g6jrczKpvZtSFTUz5Hiey1qNJWFOxSczOB03ukX8/kqP50pO0/vGcNIfk4683vh",
"aqjTXIa3N9fcOFSFxuPAzr9vftxejc7f7XaztQkz69bzvMfP142+uJpdztDMNlTryElF+sjbbK4oiy06",
"n9z55+xydsmrbYNGNKpYFFfxFpcpbSLH5qJR80e79POuEtapcrgSI6LXkt1F6nQKV4BvLPvGC99cXrZQ",
"oolbRdPozI/5o0+lnUTG/ypBfErXybeOdlEvjFIJhboWbp+8BT0ST1FXbFC5E+VIgkVClDO+uD86qDfV",
"If3Yvw0fyoRfouHc5aF8setnsvUTYLbTO8/u1FHQ07dW7r8YlBMddgLLtIrlVut9MWxw/EJ2+BPzPdIx",
"Ey4allYaWh9OXu+/iBtpFk/YDgZ/b7BiwY15zZAerfsgwOAut6MBo/KN+4lNKSVM0H6nP2UU5W990zTi",
"USGDxrskTv68ohx+eZwAKX5z7AV7rIs3l9+MO/1PNn6V9CC6skpfSpTv5P2hLL65vPpivh+rrQnnb9DV",
"ynPHhHdoFMqjaVwsPjwfT68P94f7YTbfL0kokwlAx0i8xoQInM9ZdIBGNlYZmmUXHLfy6EEaEvOCTecT",
"Rx+Ab64jmF2fSoDGz2z8UhUMA5i/q3WLBm+PeS5lRw/3h/8GAAD//4xnB3uMFgAA",
"H4sIAAAAAAAC/7xYYW/jNtL+KwO9L3AtoNi5TT/52273ekjR6wZNin7YDdKxOLaZUKSWHMU1Av/3w5C0",
"JFtKsr3bHrDYyBLJmXnmmZlHeioqVzfOkuVQLJ6KUG2oxnj5NgS9tqRuMDzIb0Wh8rph7WyxOHoKOgAC",
"yxUG0Cy/PVWkH0nBcge8IfjN+Qfys6IsGu8a8qwpWrnTSv7wrqFiUQT22q6LfSlu1WhVXKOZ6njx/55W",
"xaL4v3nv9Dx7PP8+bZC9+TD0Hnfy+94tJ23cu+Vd47XzmneDBdoyrckfVqS7E9st1tMPXj4zMHL7ajiC",
"63VaKRFheHjekTaQn3iwLwtPn1vtSRWLjxHphEXekSPoPBo4foLNAIihL4Ms3Xaou+U9VSxuvX1EbXBp",
"6Ee3vCZmcWrEo2tt14YgpOfgVoDwo1uCnBbGdKk2Tlfp8vic3zZkYa0fyZZgdK05su4RjVbyf0sB2Mm9",
"QJAPmcEHa3bQBvERtpo3kKCLxsV2R7wR5KcUU7TC1vDYr5sNQX6Y/ICwcVubnQFJBGzFd0VMvtY22t/o",
"cIBkJsc/0G589KUiy3qlycPK+Xhc3lNC3QaGJUFr9ec2BadtXHKfwZX4RmH1fHnqQ1qhCVSO8eYNeYlD",
"1zUpjUxmB54k+4DRjKKVtlo2lJLYGJWYLKM/ruV0q0HPumoN+j7mzrelc4bQ9pi/XDYTnLuRbfuyeNRB",
"Lw0dhca+fSkySdVRMiCD+OtlYqpEGdplrZnJwzdGPxAgvDNkFXlApc6c/XYG18Ry3O8Ryd9TvlObRAvS",
"GrxF09ngDbKYbo2yf4tZ7ChDVkXKhNknO4HRScULa/KiL6zPmwzxCYd3DUm8MdoRw1L5YIDL91epVncx",
"y4JVRmIGPzuwFJiUlFhbcespwDexfEIJSldiCr2m8C2gJwht0zjPpIQKZNta4sk8LaWbXrwpymJlHHJR",
"RgAGIfZ8HoY4Hduh1/QW08TSAf6FFtfkS0CrQHMkKtZSphONyeCSzJ8bExnKLx9xUw111ItOOJBbfHJv",
"YPM1QghaE232Jx34QIXYop/HbYzRYYj8ZxEf6vjFcHsTUwEeNMIorPwAPDWegrgACCGNpjzjYvn9QVXL",
"9Jqm+aKMnzg3nbYX0/UP712c+ycj0qlofeV8jZy0R6yWsRSpKQRc0+vaIZ7Zr5/y5hda68DkSSVYxo49",
"J/VQKU9hes4aDHyHFevHYz01KC9dPTyvxAyy4DCdDbfiLfpnUtWptPGjA+PvOil0zOhX1MKkLuvC6PEY",
"CrNDHGVRtd6T5Wi7OAVoENQzfk6l7pqqVnTeM4T6Ypa8RI+Bnl08dR09Oh6FJNqKDKmkKRtDHK9XqNPN",
"zy218ULiO+tup21n4kScLnnL0Y0G25AuvKsoSFVPzopE20Rjj6kxnELxX5CNKk/8v+BTtnREmkkTA9KN",
"M5ZcjrS4lmacXBLtcYctb8Yt9MPbljdvQB6KLK0igrAybhvV6fC+XWcRA98nMpsdWMegJfE1WSZVQnBg",
"3RZq56NKVqIwED4V7JT7VIhIsoAVt2hObCZpFCdIFEcYdNV3vw1zIxBuY7afieXXIIqsJlFo0ujTYrh8",
"X0KDIWydV4dHCe305gHIh6V+QKPZq+4I2tquXGrflrHifo4UPxisyVYObgglma03eWdYzOer/HSm3Ty9",
"iQwj+SUp0R/Q11CnuQxvry6lceiKbKCBnX9e/fR4MTp/u93O1radOb+e5z1hvm7M2cXsfEZ2tuHaRE5q",
"NkfeZnNFWTySD8mdv8/OZ+ey2jVksdHForiIt6RMeRM5NsdGz+/dMsy7SlinypFKjIheKnGXuNMpUgGh",
"ceKbLHxzfn6Akmzcik1jMj/m9yGVdhIZf1aChJSuk68hh0W9MEol1NY1+l3yFsxIPEVdsSHtT7Qlo4iE",
"KGdCcXt0UG+qQ/q+f1/elwm/RMO5z0P5bNvPZBcmwDxM7zy7U0ehwO+c2n01KCc67ASWaZXIrYP3xbDB",
"ySvb/i/M90jHTLhoRVoZOPhw8gHgq7iRZvGE7dbSHw1VIrgprxnS4+A+IFja5nY0YFS+cTuxKaVECNrv",
"DKeM4vw1cJpGMipUa+gmiZO/riiH3yYnQIpfJXvBHuvizfl3407/s4vfLQNgV1bpW4oOnbzfl8V35xdf",
"zfdjtTXh/BX5WgfpmPCerCZ1NI2Lxcen4+n18XZ/O8zmhyWjtpkAfIzEa0yIwIWcRQ9kVeO05Vl2wUsr",
"jx6kITEvxHQ+cfSJ+Ooygtn1qQRo/BAnL1WtFQDzl7du0eDtMc+l7Oi+PDXwThIm/6KZKCnk7DVxbIk1",
"MSpkhCWapcH+wNhV97f7fwcAAP//KSYvdP0WAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file

View File

@ -81,6 +81,7 @@ type AvailableJobSettingType string
// Job type supported by this Manager, and its parameters.
type AvailableJobType struct {
Label string `json:"label"`
Name string `json:"name"`
Settings []AvailableJobSetting `json:"settings"`
}