Skip to content

Commit

Permalink
Event logging on invalid template variable. (#272)
Browse files Browse the repository at this point in the history
* Replaced pystache by chevron. Error rising on unknown template variable.

* Warning event on invalid template variable during application installation.
  • Loading branch information
vadim-zabolotniy authored Feb 7, 2023
1 parent 5f8dc0c commit 1563547
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 11 deletions.
2 changes: 1 addition & 1 deletion application/api/v1/endpoints/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ async def get_application_health_status(

@router.get(
'/{application_id}/outputs',
response_model=Outputs,
response_model=Outputs | None,
dependencies=[Depends(AuthorizedUser(OperatorRolePermission))]
)
async def get_application_outputs(
Expand Down
5 changes: 3 additions & 2 deletions application/managers/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,13 +570,14 @@ async def get_components_manifests(self, application: Application, skip_absent:
return manifests

def render_manifest(self, template: TemplateRevision, *, application: Application | None = None,
user_inputs: dict | None = None, components_manifests: dict[str, list] | None = None) -> str:
user_inputs: dict | None = None, components_manifests: dict[str, list] | None = None,
skip_context_error: bool = True) -> str:
"""
Renders new application's manifest using existing user input.
"""
inputs = self.get_inputs(template, application=application, user_inputs=user_inputs)

manifest = render_template(template.template, inputs, components_manifests)
manifest = render_template(template.template, inputs, components_manifests, skip_context_error=skip_context_error)

return manifest

Expand Down
2 changes: 1 addition & 1 deletion application/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
aiosmtplib
alembic
asyncpg
chevron
deepdiff
email-validator
Faker # This dependency temporary must be in production requirements until Organization title change will be implemented.
Expand All @@ -13,7 +14,6 @@ kubernetes_asyncio==22.6.5 # Version locked because of issue https://github.com
mergedeep
procrastinate
pydantic[dotenv]
pystache
pyyaml
sqlalchemy[asyncio]==1.4.46
uvicorn[standard]
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from exceptions.application import ApplicationException
from exceptions.application import ApplicationHookLaunchException
from exceptions.application import ApplicationHookTimeoutException
from exceptions.templates import TemlateVariableNotFoundException
from schemas.events import EventSchema
from schemas.templates import TemplateSchema
from services.procrastinate.application import procrastinate
Expand Down Expand Up @@ -129,6 +130,24 @@ async def install_applicatoin_components(application_id: int):
raise

await application_manager.set_health_status(application, ApplicationHealthStatuses.healthy)
components_manifests = await application_manager.get_components_manifests(application)
try:
raw_manifest = application_manager.render_manifest(
application.template, application=application, components_manifests=components_manifests,
skip_context_error=False
)
except TemlateVariableNotFoundException as error:
await application_manager.event_manager.create(EventSchema(
title='Application deployment',
message=f'Application deployment failed. {error.message.strip(".")}.',
organization_id=application.organization.id,
category=EventCategory.application,
severity=EventSeverityLevel.warning,
data={'application_id': application.id}
))
raw_manifest = application_manager.render_manifest(
application.template, application=application, components_manifests=components_manifests
)
application.manifest = raw_manifest
await application_manager.db.save(application)

Expand Down
78 changes: 71 additions & 7 deletions application/utils/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
"""
import logging
import re
from typing import Any

import pystache
import chevron
import yaml
from pydantic import ValidationError
from pydantic.error_wrappers import display_errors
from yaml import YAMLError

from exceptions.templates import InvalidTemplateException
from exceptions.templates import InvalidUserInputsException
from exceptions.templates import TemlateVariableNotFoundException
from schemas.templates import TemplateSchema


Expand All @@ -25,6 +27,62 @@
PLACEHOLDERS = re.compile(r'.*{{(.*)}}.*')


class TemplateContextDictionaryProxy(dict):
"""
Wrapper to track absent keys in context.
"""

def __init__(self, object, skip_error: bool = True) -> None:
self.skip_error = skip_error
super().__init__(object)

def __getitem__(self, key: str) -> Any:
try:
next_scope = super().__getitem__(key)

return context_factory(next_scope, skip_error=self.skip_error)
except KeyError:
if self.skip_error:
raise
raise TemlateVariableNotFoundException(
f'Unable to find key "{key}" in context provided for template rendering', variable=key
)


class TemplateContextListProxy(list):
"""
Wrapper to track absent indexes in context.
"""

def __init__(self, object, skip_error: bool = True) -> None:
self.skip_error = skip_error
super().__init__(object)

def __getitem__(self, index: str) -> Any:
try:
next_scope = super().__getitem__(index)

return context_factory(next_scope, skip_error=self.skip_error)
except IndexError:
if self.skip_error:
raise
raise TemlateVariableNotFoundException(
f'Unable to find index "{index}" in context provided for template rendering', variable=index
)


def context_factory(object: Any, skip_error: bool = True):
"""
Create and returns template context proxy.
"""
if isinstance(object, dict):
return TemplateContextDictionaryProxy(object, skip_error=skip_error)
elif isinstance(object, list):
return TemplateContextListProxy(object, skip_error=skip_error)
else:
return object


def make_template_yaml_safe(raw_template: str) -> str:
"""
Transforms template YAML into valid, by replacing `{{` to `"{{{` and `}}` to
Expand Down Expand Up @@ -74,7 +132,9 @@ def load_template(raw_template: str) -> TemplateSchema:
return validate_template(parsed_template_data)


def render_template(template: str, inputs: dict, components_manifests: dict[str, list] | None = None) -> str:
def render_template(
template: str, inputs: dict, components_manifests: dict[str, list] | None = None, skip_context_error: bool = True
) -> str:
"""
Renders template with provided context.
"""
Expand All @@ -86,11 +146,15 @@ def render_template(template: str, inputs: dict, components_manifests: dict[str,
for entity in entities:
grouped_entites.setdefault(entity.kind, {})[entity.metadata['name']] = entity.raw_representation
components_context[component_name] = {'manifest': grouped_entites}
context = {
'inputs': inputs,
'components': components_context
}
return pystache.render(template, context)
context = context_factory(
{
'inputs': inputs,
'components': components_context
},
skip_error=skip_context_error
)

return chevron.render(template, context)


def validate_inputs(template: str, inputs: dict) -> None:
Expand Down

0 comments on commit 1563547

Please sign in to comment.