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
from onefuzztypes.enums import ErrorCode
from onefuzztypes.job_templates import (
JobTemplateCreate,
JobTemplateDelete,
JobTemplateUpdate,
JobTemplateGet,
JobTemplateUpload,
)
from onefuzztypes.models import Error
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:
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()
return ok(templates)
def post(req: func.HttpRequest) -> func.HttpResponse:
request = parse_request(JobTemplateCreate, req)
request = parse_request(JobTemplateUpload, req)
if isinstance(request, Error):
return not_ok(request, context="JobTemplateCreate")
return not_ok(request, context="JobTemplateUpload")
entry = JobTemplateIndex(name=request.name, template=request.template)
result = entry.save(new=True)
result = entry.save()
if isinstance(result, Error):
return not_ok(result, context="JobTemplateCreate")
return not_ok(result, context="JobTemplateUpload")
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:
request = parse_request(JobTemplateDelete, req)
if isinstance(request, Error):
@ -68,7 +64,5 @@ def main(req: func.HttpRequest) -> func.HttpResponse:
return post(req)
elif req.method == "DELETE":
return delete(req)
elif req.method == "PATCH":
return patch(req)
else:
raise Exception("invalid method")

View File

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

View File

@ -25,6 +25,18 @@ class JobTemplateIndex(BASE_INDEX, ORMMixin):
def key_fields(cls) -> Tuple[str, Optional[str]]:
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
def get_index(cls) -> List[BASE_INDEX]:
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 (
JobTemplate,
JobTemplateCreate,
JobTemplateDelete,
JobTemplateGet,
JobTemplateIndex,
JobTemplateUpdate,
JobTemplateUpload,
)
from onefuzztypes.responses import BoolResult
@ -27,31 +27,29 @@ class Manage(Endpoint):
self.onefuzz._warn_preview(PreviewFeature.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:
""" Create a Job Template """
def get(self, name: str) -> JobTemplate:
""" Get an existing Job Template """
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(
"POST",
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:
""" 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:
def delete(self, name: str) -> BoolResult:
""" Delete a Job Template """
self.onefuzz._warn_preview(PreviewFeature.job_templates)
@ -59,5 +57,5 @@ class Manage(Endpoint):
return self._req_model(
"DELETE",
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 .requests import BaseRequest
from .responses import BaseResponse
from .validators import check_template_name
from .validators import check_template_name, check_template_name_optional
class UserFieldLocation(BaseModel):
@ -44,7 +44,7 @@ class JobTemplateNotification(BaseModel):
notification: NotificationConfig
class JobTemplate(BaseModel):
class JobTemplate(BaseResponse):
os: OS
job: JobConfig
tasks: List[TaskConfig]
@ -151,7 +151,7 @@ TEMPLATE_BASE_FIELDS = [
]
class JobTemplateCreate(BaseRequest):
class JobTemplateUpload(BaseRequest):
name: str
template: JobTemplate
@ -164,13 +164,6 @@ class JobTemplateDelete(BaseRequest):
_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):
name: str
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):
user_fields: TemplateUserFields

View File

@ -4,6 +4,7 @@
# Licensed under the MIT License.
from string import ascii_letters, digits
from typing import Optional
ALPHA_NUM = ascii_letters + digits
ALPHA_NUM_DASH = ALPHA_NUM + "-"
@ -36,3 +37,10 @@ def check_template_name(value: str) -> str:
raise ValueError("invalid value: %s" % 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)