Simplify job template management workflow (#354)

1. Merge 'create' and 'update' to a single 'save' operation.
2. Allow fetching a single template.

This enables the following workflow:

```
$ onefuzz job_templates manage get libfuzzer_linux > template.json
$ <... update template as desired ...>
$ onefuzz job_templates manage save libfuzzer_linux @./template.json
$
```
This commit is contained in:
bmc-msft
2020-12-02 09:27:42 -05:00
committed by GitHub
parent 9b3ccf37ea
commit e6b55ab95a
6 changed files with 69 additions and 57 deletions

View File

@ -6,9 +6,9 @@
import azure.functions as func import azure.functions as func
from onefuzztypes.enums import ErrorCode from onefuzztypes.enums import ErrorCode
from onefuzztypes.job_templates import ( from onefuzztypes.job_templates import (
JobTemplateCreate,
JobTemplateDelete, JobTemplateDelete,
JobTemplateUpdate, JobTemplateGet,
JobTemplateUpload,
) )
from onefuzztypes.models import Error from onefuzztypes.models import Error
from onefuzztypes.responses import BoolResult from onefuzztypes.responses import BoolResult
@ -18,40 +18,36 @@ from ..onefuzzlib.request import not_ok, ok, parse_request
def get(req: func.HttpRequest) -> func.HttpResponse: def get(req: func.HttpRequest) -> func.HttpResponse:
request = parse_request(JobTemplateGet, req)
if isinstance(request, Error):
return not_ok(request, context="JobTemplateGet")
if request.name:
entry = JobTemplateIndex.get_base_entry(request.name)
if entry is None:
return not_ok(
Error(code=ErrorCode.INVALID_REQUEST, errors=["no such job template"]),
context="JobTemplateGet",
)
return ok(entry.template)
templates = JobTemplateIndex.get_index() templates = JobTemplateIndex.get_index()
return ok(templates) return ok(templates)
def post(req: func.HttpRequest) -> func.HttpResponse: def post(req: func.HttpRequest) -> func.HttpResponse:
request = parse_request(JobTemplateCreate, req) request = parse_request(JobTemplateUpload, req)
if isinstance(request, Error): if isinstance(request, Error):
return not_ok(request, context="JobTemplateCreate") return not_ok(request, context="JobTemplateUpload")
entry = JobTemplateIndex(name=request.name, template=request.template) entry = JobTemplateIndex(name=request.name, template=request.template)
result = entry.save(new=True) result = entry.save()
if isinstance(result, Error): if isinstance(result, Error):
return not_ok(result, context="JobTemplateCreate") return not_ok(result, context="JobTemplateUpload")
return ok(BoolResult(result=True)) return ok(BoolResult(result=True))
def patch(req: func.HttpRequest) -> func.HttpResponse:
request = parse_request(JobTemplateUpdate, req)
if isinstance(request, Error):
return not_ok(request, context="JobTemplateUpdate")
entry = JobTemplateIndex.get(request.name)
if entry is None:
return not_ok(
Error(code=ErrorCode.UNABLE_TO_UPDATE, errors=["no such job template"]),
context="JobTemplateUpdate",
)
entry.template = request.template
entry.save()
return ok(BoolResult(result=True))
def delete(req: func.HttpRequest) -> func.HttpResponse: def delete(req: func.HttpRequest) -> func.HttpResponse:
request = parse_request(JobTemplateDelete, req) request = parse_request(JobTemplateDelete, req)
if isinstance(request, Error): if isinstance(request, Error):
@ -68,7 +64,5 @@ def main(req: func.HttpRequest) -> func.HttpResponse:
return post(req) return post(req)
elif req.method == "DELETE": elif req.method == "DELETE":
return delete(req) return delete(req)
elif req.method == "PATCH":
return patch(req)
else: else:
raise Exception("invalid method") raise Exception("invalid method")

View File

@ -9,8 +9,7 @@
"methods": [ "methods": [
"get", "get",
"post", "post",
"delete", "delete"
"patch"
], ],
"route": "job_templates/manage" "route": "job_templates/manage"
}, },

View File

@ -25,6 +25,18 @@ class JobTemplateIndex(BASE_INDEX, ORMMixin):
def key_fields(cls) -> Tuple[str, Optional[str]]: def key_fields(cls) -> Tuple[str, Optional[str]]:
return ("name", None) return ("name", None)
@classmethod
def get_base_entry(cls, name: str) -> Optional[BASE_INDEX]:
result = cls.get(name)
if result is not None:
return BASE_INDEX(name=name, template=result.template)
template = TEMPLATES.get(name)
if template is None:
return None
return BASE_INDEX(name=name, template=template)
@classmethod @classmethod
def get_index(cls) -> List[BASE_INDEX]: def get_index(cls) -> List[BASE_INDEX]:
entries = [BASE_INDEX(name=x.name, template=x.template) for x in cls.search()] entries = [BASE_INDEX(name=x.name, template=x.template) for x in cls.search()]

View File

@ -7,10 +7,10 @@ from typing import List
from onefuzztypes.job_templates import ( from onefuzztypes.job_templates import (
JobTemplate, JobTemplate,
JobTemplateCreate,
JobTemplateDelete, JobTemplateDelete,
JobTemplateGet,
JobTemplateIndex, JobTemplateIndex,
JobTemplateUpdate, JobTemplateUpload,
) )
from onefuzztypes.responses import BoolResult from onefuzztypes.responses import BoolResult
@ -27,31 +27,29 @@ class Manage(Endpoint):
self.onefuzz._warn_preview(PreviewFeature.job_templates) self.onefuzz._warn_preview(PreviewFeature.job_templates)
self.onefuzz.logger.debug("listing job templates") self.onefuzz.logger.debug("listing job templates")
return self._req_model_list("GET", JobTemplateIndex) return self._req_model_list(
"GET", JobTemplateIndex, data=JobTemplateGet(name=None)
)
def create(self, domain: str, name: str, template: JobTemplate) -> BoolResult: def get(self, name: str) -> JobTemplate:
""" Create a Job Template """ """ Get an existing Job Template """
self.onefuzz._warn_preview(PreviewFeature.job_templates) self.onefuzz._warn_preview(PreviewFeature.job_templates)
self.onefuzz.logger.debug("creating job templates") self.onefuzz.logger.debug("get job template")
return self._req_model("GET", JobTemplate, data=JobTemplateGet(name=name))
def upload(self, name: str, template: JobTemplate) -> BoolResult:
""" Upload a Job Template """
self.onefuzz._warn_preview(PreviewFeature.job_templates)
self.onefuzz.logger.debug("upload job template")
return self._req_model( return self._req_model(
"POST", "POST",
BoolResult, BoolResult,
data=JobTemplateCreate(domain=domain, name=name, template=template), data=JobTemplateUpload(name=name, template=template),
) )
def update(self, domain: str, name: str, template: JobTemplate) -> BoolResult: def delete(self, name: str) -> BoolResult:
""" Update an existing Job Template """
self.onefuzz._warn_preview(PreviewFeature.job_templates)
self.onefuzz.logger.debug("update job templates")
return self._req_model(
"POST",
BoolResult,
data=JobTemplateUpdate(domain=domain, name=name, template=template),
)
def delete(self, domain: str, name: str) -> BoolResult:
""" Delete a Job Template """ """ Delete a Job Template """
self.onefuzz._warn_preview(PreviewFeature.job_templates) self.onefuzz._warn_preview(PreviewFeature.job_templates)
@ -59,5 +57,5 @@ class Manage(Endpoint):
return self._req_model( return self._req_model(
"DELETE", "DELETE",
BoolResult, BoolResult,
data=JobTemplateDelete(domain=domain, name=name), data=JobTemplateDelete(name=name),
) )

View File

@ -12,7 +12,7 @@ from .models import JobConfig, NotificationConfig, TaskConfig, TaskContainers
from .primitives import File from .primitives import File
from .requests import BaseRequest from .requests import BaseRequest
from .responses import BaseResponse from .responses import BaseResponse
from .validators import check_template_name from .validators import check_template_name, check_template_name_optional
class UserFieldLocation(BaseModel): class UserFieldLocation(BaseModel):
@ -44,7 +44,7 @@ class JobTemplateNotification(BaseModel):
notification: NotificationConfig notification: NotificationConfig
class JobTemplate(BaseModel): class JobTemplate(BaseResponse):
os: OS os: OS
job: JobConfig job: JobConfig
tasks: List[TaskConfig] tasks: List[TaskConfig]
@ -151,7 +151,7 @@ TEMPLATE_BASE_FIELDS = [
] ]
class JobTemplateCreate(BaseRequest): class JobTemplateUpload(BaseRequest):
name: str name: str
template: JobTemplate template: JobTemplate
@ -164,13 +164,6 @@ class JobTemplateDelete(BaseRequest):
_verify_name: classmethod = validator("name", allow_reuse=True)(check_template_name) _verify_name: classmethod = validator("name", allow_reuse=True)(check_template_name)
class JobTemplateUpdate(BaseRequest):
name: str
template: JobTemplate
_verify_name: classmethod = validator("name", allow_reuse=True)(check_template_name)
class JobTemplateRequest(BaseRequest): class JobTemplateRequest(BaseRequest):
name: str name: str
user_fields: TemplateUserFields user_fields: TemplateUserFields
@ -181,5 +174,13 @@ class JobTemplateRequest(BaseRequest):
) )
class JobTemplateGet(BaseRequest):
name: Optional[str]
_validate_name: classmethod = validator("name", allow_reuse=True)(
check_template_name_optional
)
class JobTemplateRequestParameters(BaseRequest): class JobTemplateRequestParameters(BaseRequest):
user_fields: TemplateUserFields user_fields: TemplateUserFields

View File

@ -4,6 +4,7 @@
# Licensed under the MIT License. # Licensed under the MIT License.
from string import ascii_letters, digits from string import ascii_letters, digits
from typing import Optional
ALPHA_NUM = ascii_letters + digits ALPHA_NUM = ascii_letters + digits
ALPHA_NUM_DASH = ALPHA_NUM + "-" ALPHA_NUM_DASH = ALPHA_NUM + "-"
@ -36,3 +37,10 @@ def check_template_name(value: str) -> str:
raise ValueError("invalid value: %s" % value) raise ValueError("invalid value: %s" % value)
return check_alnum_underscore(value) return check_alnum_underscore(value)
def check_template_name_optional(value: Optional[str]) -> Optional[str]:
if value is None:
return value
return check_template_name(value)