mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-16 20:08:09 +00:00
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:
@ -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")
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
"methods": [
|
"methods": [
|
||||||
"get",
|
"get",
|
||||||
"post",
|
"post",
|
||||||
"delete",
|
"delete"
|
||||||
"patch"
|
|
||||||
],
|
],
|
||||||
"route": "job_templates/manage"
|
"route": "job_templates/manage"
|
||||||
},
|
},
|
||||||
|
@ -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()]
|
||||||
|
@ -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),
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
Reference in New Issue
Block a user