Example #1
0
 def wrapper(*args, **kwargs):
     key = f'{decorated.__name__}{args}{kwargs}'
     ret = cache.get(key)
     metrics_tags = [f'function:{decorated.__name__}']
     if ret is None:
         stats.increment('cache.miss', tags=metrics_tags)
         ret = decorated(*args, **kwargs)
         try:
             cache.set(key, ret, timeout=timeout)
         except AttributeError:
             stats.increment('cache.fail', tags=metrics_tags)
             LOG.warn(event='failed to write result to cache', key=key)
     else:
         stats.increment('cache.hit', tags=metrics_tags)
     return ret
Example #2
0
def match_node(node_value: Any, modify=True, sources: SourceData = None, discovery_type: str = 'default') -> SourceData:
    """
    Checks a node against all sources, using the node_match_key and source_match_key
    to determine if the node should receive the source in its configuration.

    :param discovery_type: type of XDS request, used to determine if a scoped source should be evaluated
    :param node_value: value from the node portion of the envoy discovery request
    :param modify: switch to enable or disable modifications via Modifiers
    :param sources: the data sources to match the node against
    """
    if source_metadata.is_stale:
        # Log/emit metric and manually refresh sources.
        stats.increment('sources.stale')
        LOG.warn(
            'Sources have not been refreshed in 2 minutes',
            last_update=source_metadata.updated.isoformat(),
            instance_count=source_metadata.count
        )
        sources_refresh()

    if sources is None:
        sources: SourceData = read_sources()

    ret = SourceData()
    for scope, instances in sources.scopes.items():
        if config.node_matching is False:
            ret.scopes[scope] = instances
            continue

        for instance in instances:
            source_value = extract_source_key(instance)

            # If a single expression evaluates true, the remaining are not evaluated/executed.
            # This saves (a small amount of) computation, which helps when the server starts
            # to receive thousands of requests. The list has been ordered descending by what
            # we think will more commonly be true.
            match = (
                    contains(source_value, node_value)
                    or node_value == source_value
                    or is_wildcard(node_value)
                    or is_wildcard(source_value)
                    or is_debug_request(node_value)
            )
            if match:
                ret.scopes[scope].append(instance)
    if modify:
        return apply_modifications(ret)
    return ret
def deserialize_config(content):
    try:
        envoy_configuration = yaml.safe_load(content)
    except ParserError as e:
        LOG.msg(
            error=repr(e),
            context=e.context,
            context_mark=e.context_mark,
            note=e.note,
            problem=e.problem,
            problem_mark=e.problem_mark,
        )
        raise HTTPException(
            status_code=500,
            detail='Failed to load configuration, there may be '
                   'a syntax error in the configured templates.'
        )
    return envoy_configuration
def init_app() -> FastAPI:
    # Warm the sources once before starting
    sources_refresh()
    LOG.info('Initial fetch of Sources completed')

    application = FastAPI(
        title='Sovereign',
        version=__versionstr__,
        debug=config.debug_enabled
    )
    application.include_router(discovery.router, tags=['Configuration Discovery'], prefix='/v2')
    application.include_router(crypto.router, tags=['Cryptographic Utilities'], prefix='/crypto')
    application.include_router(admin.router, tags=['Debugging Endpoints'], prefix='/admin')
    application.include_router(interface.router, tags=['User Interface'], prefix='/ui')
    application.include_router(healthchecks.router, tags=['Healthchecks'])

    application.add_middleware(RequestContextLogMiddleware)
    application.add_middleware(LoggingMiddleware)
    application.add_middleware(ScheduledTasksMiddleware)

    if config.sentry_dsn and sentry_sdk:
        sentry_sdk.init(config.sentry_dsn)
        application.add_middleware(SentryAsgiMiddleware)
        LOG.info('Sentry middleware enabled')

    @application.exception_handler(500)
    async def exception_handler(_, exc: Exception) -> json_response_class:
        """
        We cannot incur the execution of this function from unit tests
        because the starlette test client simply returns exceptions and does
        not run them through the exception handler.
        Ergo, this is a facade function for `generic_error_response`
        """
        return generic_error_response(exc)  # pragma: no cover

    @application.get('/')
    def redirect_to_docs():
        return RedirectResponse('/ui')

    @application.get('/static/{filename}', summary='Return a static asset')
    def static(filename: str):
        return FileResponse(resource_filename('sovereign', f'static/{filename}'))

    return application
Example #5
0
def sources_refresh():
    """
    All source data is stored in ``sovereign.sources._source_data``.
    Since the variable is outside this functions scope, we can only make
    in-place modifications to it via its methods.

    This function retrieves all sources, puts them into a temporary list,
    and then clears and re-fills ``_source_data`` with the new data.

    The process is done in two steps to avoid ``_source_data`` being empty
    for any significant amount of time.
    """
    stats.increment('sources.attempt')
    try:
        new_source_data = SourceData()
        for configured_source in config.sources:
            source = setup_sources(configured_source)
            new_source_data.scopes[source.scope].extend(source.get())
    except Exception as e:
        LOG.error(
            'Error while refreshing sources',
            traceback=[line for line in traceback.format_exc().split('\n')],
            error=e.__class__.__name__,
            detail=getattr(e, 'detail', '-'),
            request_id=get_request_id()
        )
        stats.increment('sources.error')
        raise

    if new_source_data == _source_data:
        stats.increment('sources.unchanged')
        source_metadata.update_date()
        return
    else:
        stats.increment('sources.refreshed')

    _source_data.scopes.clear()
    _source_data.scopes.update(new_source_data.scopes)
    source_metadata.update_date()
    source_metadata.update_count([
        instance
        for scope in _source_data.scopes.values()
        for instance in scope
    ])
Example #6
0
 async def dispatch(self, request: Request,
                    call_next: RequestResponseEndpoint):
     start_time = time.time()
     response = Response("Internal server error", status_code=500)
     new_log_context()
     add_log_context(env=config.environment,
                     site=request.headers.get('host', '-'),
                     method=request.method,
                     uri_path=request.url.path,
                     uri_query=dict(request.query_params.items()),
                     src_ip=request.client.host,
                     src_port=request.client.port,
                     pid=os.getpid(),
                     user_agent=request.headers.get('user-agent', '-'),
                     bytes_in=request.headers.get('content-length', '-'))
     try:
         response: Response = await call_next(request)
     finally:
         duration = time.time() - start_time
         LOG.info(bytes_out=response.headers.get('content-length', '-'),
                  status=response.status_code,
                  duration=duration,
                  request_id=response.headers.get('X-Request-Id', '-'))
         if 'discovery' in str(request.url):
             tags = {
                 'path':
                 request.url.path,
                 'xds_type':
                 response.headers.get("X-Sovereign-Requested-Type"),
                 'client_version':
                 response.headers.get("X-Sovereign-Client-Build"),
                 'response_code':
                 response.status_code,
             }
             tags = [
                 ':'.join(map(str, [k, v])) for k, v in tags.items()
                 if v is not None
             ]
             stats.increment('discovery.rq_total', tags=tags)
             stats.timing('discovery.rq_ms',
                          value=duration * 1000,
                          tags=tags)
     return response
        application.add_middleware(SentryAsgiMiddleware)
        LOG.info('Sentry middleware enabled')

    @application.exception_handler(500)
    async def exception_handler(_, exc: Exception) -> json_response_class:
        """
        We cannot incur the execution of this function from unit tests
        because the starlette test client simply returns exceptions and does
        not run them through the exception handler.
        Ergo, this is a facade function for `generic_error_response`
        """
        return generic_error_response(exc)  # pragma: no cover

    @application.get('/')
    def redirect_to_docs():
        return RedirectResponse('/ui')

    @application.get('/static/{filename}', summary='Return a static asset')
    def static(filename: str):
        return FileResponse(resource_filename('sovereign', f'static/{filename}'))

    return application


app = init_app()
LOG.info(f'Sovereign started and listening on {asgi_config.host}:{asgi_config.port}')


if __name__ == '__main__':  # pragma: no cover
    uvicorn.run(app, host='0.0.0.0', port=8000, access_log=False)
from sovereign import config
from sovereign.logs import LOG

disabled_suite = namedtuple('DisabledSuite', ['encrypt', 'decrypt'])
disabled_suite.encrypt = lambda x: 'Unavailable (No Secret Key)'
disabled_suite.decrypt = lambda x: 'Unavailable (No Secret Key)'

try:
    _cipher_suite = Fernet(config.encryption_key)
    KEY_AVAILABLE = True
except TypeError:
    KEY_AVAILABLE = False
    _cipher_suite = disabled_suite
except ValueError as e:
    if config.encryption_key != '':
        LOG.warn(
            f'Fernet key was provided, but appears to be invalid: {repr(e)}')
        _cipher_suite = disabled_suite
    KEY_AVAILABLE = False


def encrypt(data: str, key=None) -> str:
    _local_cipher_suite = _cipher_suite
    if key is not None:
        _local_cipher_suite = Fernet(key)
    try:
        encrypted: bytes = _local_cipher_suite.encrypt(data.encode())
    except (InvalidToken, AttributeError):
        raise HTTPException(status_code=400, detail='Encryption failed')
    return encrypted.decode()