sovereign 0.14.2__py3-none-any.whl → 1.0.0a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sovereign might be problematic. Click here for more details.

Files changed (99) hide show
  1. sovereign/__init__.py +17 -78
  2. sovereign/app.py +74 -59
  3. sovereign/cache/__init__.py +245 -0
  4. sovereign/cache/backends/__init__.py +110 -0
  5. sovereign/cache/backends/s3.py +161 -0
  6. sovereign/cache/filesystem.py +74 -0
  7. sovereign/cache/types.py +17 -0
  8. sovereign/configuration.py +607 -0
  9. sovereign/constants.py +1 -0
  10. sovereign/context.py +271 -100
  11. sovereign/dynamic_config/__init__.py +112 -0
  12. sovereign/dynamic_config/deser.py +78 -0
  13. sovereign/dynamic_config/loaders.py +120 -0
  14. sovereign/error_info.py +61 -0
  15. sovereign/events.py +49 -0
  16. sovereign/logging/access_logger.py +85 -0
  17. sovereign/logging/application_logger.py +54 -0
  18. sovereign/logging/base_logger.py +41 -0
  19. sovereign/logging/bootstrapper.py +36 -0
  20. sovereign/logging/types.py +10 -0
  21. sovereign/middlewares.py +8 -7
  22. sovereign/modifiers/lib.py +2 -1
  23. sovereign/rendering.py +124 -0
  24. sovereign/rendering_common.py +91 -0
  25. sovereign/response_class.py +18 -0
  26. sovereign/server.py +123 -28
  27. sovereign/statistics.py +19 -21
  28. sovereign/templates/base.html +59 -46
  29. sovereign/templates/resources.html +203 -102
  30. sovereign/testing/loaders.py +9 -0
  31. sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
  32. sovereign/tracing.py +103 -0
  33. sovereign/types.py +304 -0
  34. sovereign/utils/auth.py +27 -13
  35. sovereign/utils/crypto/__init__.py +0 -0
  36. sovereign/utils/crypto/crypto.py +135 -0
  37. sovereign/utils/crypto/suites/__init__.py +21 -0
  38. sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
  39. sovereign/utils/crypto/suites/base_cipher.py +21 -0
  40. sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
  41. sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
  42. sovereign/utils/dictupdate.py +3 -2
  43. sovereign/utils/eds.py +40 -22
  44. sovereign/utils/entry_point_loader.py +18 -0
  45. sovereign/utils/mock.py +60 -17
  46. sovereign/utils/resources.py +17 -0
  47. sovereign/utils/templates.py +4 -2
  48. sovereign/utils/timer.py +5 -3
  49. sovereign/utils/version_info.py +8 -0
  50. sovereign/utils/weighted_clusters.py +2 -1
  51. sovereign/v2/__init__.py +0 -0
  52. sovereign/v2/data/data_store.py +621 -0
  53. sovereign/v2/data/render_discovery_response.py +24 -0
  54. sovereign/v2/data/repositories.py +90 -0
  55. sovereign/v2/data/utils.py +33 -0
  56. sovereign/v2/data/worker_queue.py +273 -0
  57. sovereign/v2/jobs/refresh_context.py +117 -0
  58. sovereign/v2/jobs/render_discovery_job.py +145 -0
  59. sovereign/v2/logging.py +81 -0
  60. sovereign/v2/types.py +41 -0
  61. sovereign/v2/web.py +101 -0
  62. sovereign/v2/worker.py +199 -0
  63. sovereign/views/__init__.py +7 -0
  64. sovereign/views/api.py +82 -0
  65. sovereign/views/crypto.py +46 -15
  66. sovereign/views/discovery.py +52 -67
  67. sovereign/views/healthchecks.py +107 -20
  68. sovereign/views/interface.py +173 -117
  69. sovereign/worker.py +193 -0
  70. {sovereign-0.14.2.dist-info → sovereign-1.0.0a4.dist-info}/METADATA +81 -73
  71. sovereign-1.0.0a4.dist-info/RECORD +85 -0
  72. {sovereign-0.14.2.dist-info → sovereign-1.0.0a4.dist-info}/WHEEL +1 -1
  73. sovereign-1.0.0a4.dist-info/entry_points.txt +46 -0
  74. sovereign_files/__init__.py +0 -0
  75. sovereign_files/static/darkmode.js +51 -0
  76. sovereign_files/static/node_expression.js +42 -0
  77. sovereign_files/static/panel.js +76 -0
  78. sovereign_files/static/resources.css +246 -0
  79. sovereign_files/static/resources.js +642 -0
  80. sovereign_files/static/sass/style.scss +33 -0
  81. sovereign_files/static/style.css +16143 -0
  82. sovereign_files/static/style.css.map +1 -0
  83. sovereign/config_loader.py +0 -225
  84. sovereign/discovery.py +0 -175
  85. sovereign/logs.py +0 -131
  86. sovereign/schemas.py +0 -715
  87. sovereign/sources/__init__.py +0 -3
  88. sovereign/sources/file.py +0 -21
  89. sovereign/sources/inline.py +0 -38
  90. sovereign/sources/lib.py +0 -40
  91. sovereign/sources/poller.py +0 -298
  92. sovereign/static/sass/style.scss +0 -27
  93. sovereign/static/style.css +0 -13553
  94. sovereign/templates/ul_filter.html +0 -22
  95. sovereign/utils/crypto.py +0 -64
  96. sovereign/views/admin.py +0 -120
  97. sovereign-0.14.2.dist-info/LICENSE.txt +0 -13
  98. sovereign-0.14.2.dist-info/RECORD +0 -45
  99. sovereign-0.14.2.dist-info/entry_points.txt +0 -10
@@ -0,0 +1,91 @@
1
+ import importlib
2
+ from typing import Any
3
+
4
+ import yaml
5
+ from starlette.exceptions import HTTPException
6
+ from yaml.parser import ParserError
7
+ from yaml.scanner import ScannerError
8
+
9
+ from sovereign import config, logs
10
+
11
+ type_urls = {
12
+ "v2": {
13
+ "listeners": "type.googleapis.com/envoy.api.v2.Listener",
14
+ "clusters": "type.googleapis.com/envoy.api.v2.Cluster",
15
+ "endpoints": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
16
+ "secrets": "type.googleapis.com/envoy.api.v2.auth.Secret",
17
+ "routes": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
18
+ "scoped-routes": "type.googleapis.com/envoy.api.v2.ScopedRouteConfiguration",
19
+ },
20
+ "v3": {
21
+ "listeners": "type.googleapis.com/envoy.config.listener.v3.Listener",
22
+ "clusters": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
23
+ "endpoints": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
24
+ "secrets": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
25
+ "routes": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
26
+ "scoped-routes": "type.googleapis.com/envoy.config.route.v3.ScopedRouteConfiguration",
27
+ "runtime": "type.googleapis.com/envoy.service.runtime.v3.Runtime",
28
+ },
29
+ }
30
+
31
+
32
+ def add_type_urls(api_version, resource_type, resources):
33
+ type_url = type_urls.get(api_version, {}).get(resource_type)
34
+ if type_url is not None:
35
+ for resource in resources:
36
+ if not resource.get("@type"):
37
+ resource["@type"] = type_url
38
+
39
+
40
+ def deserialize_config(content: str) -> dict[str, Any]:
41
+ try:
42
+ envoy_configuration = yaml.safe_load(content)
43
+ except (ParserError, ScannerError) as e:
44
+ logs.access_logger.queue_log_fields(
45
+ error=repr(e),
46
+ YAML_CONTEXT=e.context,
47
+ YAML_CONTEXT_MARK=e.context_mark,
48
+ YAML_NOTE=e.note,
49
+ YAML_PROBLEM=e.problem,
50
+ YAML_PROBLEM_MARK=e.problem_mark,
51
+ )
52
+
53
+ if config.sentry_dsn:
54
+ mod = importlib.import_module("sentry_sdk")
55
+ mod.capture_exception(e)
56
+
57
+ raise HTTPException(
58
+ status_code=500,
59
+ detail=(
60
+ "Failed to load configuration, there may be "
61
+ "a syntax error in the configured templates. "
62
+ "Please check Sentry if you have configured Sentry DSN"
63
+ ),
64
+ )
65
+ if not isinstance(envoy_configuration, dict):
66
+ raise RuntimeError(
67
+ f"Deserialized configuration is of unexpected format: {envoy_configuration}"
68
+ )
69
+ return envoy_configuration
70
+
71
+
72
+ def filter_resources(
73
+ generated: list[dict[str, Any]], requested: list[str]
74
+ ) -> list[dict[str, Any]]:
75
+ """
76
+ If Envoy specifically requested a resource, this removes everything
77
+ that does not match the name of the resource.
78
+ If Envoy did not specifically request anything, every resource is retained.
79
+ """
80
+ if len(requested) == 0:
81
+ return generated
82
+ return [resource for resource in generated if resource_name(resource) in requested]
83
+
84
+
85
+ def resource_name(resource: dict[str, Any]) -> str:
86
+ name = resource.get("name") or resource.get("cluster_name")
87
+ if isinstance(name, str):
88
+ return name
89
+ raise KeyError(
90
+ f"Failed to determine the name or cluster_name of the following resource: {resource}"
91
+ )
@@ -0,0 +1,18 @@
1
+ from importlib.util import find_spec
2
+ from typing import Type
3
+
4
+ from fastapi.responses import JSONResponse
5
+
6
+ json_response_class: Type[JSONResponse] = JSONResponse
7
+ if find_spec("orjson"):
8
+ from fastapi.responses import ORJSONResponse
9
+
10
+ json_response_class = ORJSONResponse
11
+
12
+ elif find_spec("ujson"):
13
+ from fastapi.responses import UJSONResponse
14
+
15
+ json_response_class = UJSONResponse
16
+
17
+
18
+ __all__ = ["json_response_class"]
sovereign/server.py CHANGED
@@ -1,31 +1,126 @@
1
- import gunicorn.app.base
2
- from fastapi import FastAPI
3
- from typing import Optional, Dict, Any
4
- from sovereign import asgi_config
5
- from sovereign.app import app
6
-
7
-
8
- class StandaloneApplication(gunicorn.app.base.BaseApplication): # type: ignore
9
- def __init__(
10
- self, application: FastAPI, options: Optional[Dict[str, Any]] = None
11
- ) -> None:
12
- self.options = options or {}
13
- self.application = application
14
- super().__init__()
15
-
16
- def load_config(self) -> None:
17
- for key, value in self.options.items():
18
- self.cfg.set(key.lower(), value)
19
-
20
- def load(self) -> FastAPI:
21
- return self.application
22
-
23
-
24
- def main() -> None:
25
- asgi = StandaloneApplication(
26
- application=app, options=asgi_config.as_gunicorn_conf()
27
- )
28
- asgi.run()
1
+ import configparser
2
+ import tempfile
3
+ import warnings
4
+ from pathlib import Path
5
+
6
+ import uvicorn
7
+
8
+ from sovereign import application_logger as log
9
+ from sovereign.configuration import SovereignAsgiConfig, SupervisordConfig, config
10
+ from sovereign.v2.worker import Worker
11
+
12
+ # noinspection PyArgumentList
13
+ asgi_config = SovereignAsgiConfig()
14
+ # noinspection PyArgumentList
15
+ supervisord_config = SupervisordConfig()
16
+
17
+
18
+ def web(supervisor_enabled=True) -> None:
19
+ from sovereign.app import app
20
+
21
+ log.debug("Starting web server")
22
+
23
+ if not supervisor_enabled:
24
+ uvicorn.run(
25
+ app,
26
+ log_level=asgi_config.log_level,
27
+ access_log=False,
28
+ timeout_keep_alive=asgi_config.keepalive,
29
+ host=asgi_config.host,
30
+ port=asgi_config.port,
31
+ workers=1, # per managed supervisor proc
32
+ )
33
+ else:
34
+ uvicorn.run(
35
+ app,
36
+ fd=0,
37
+ log_level=asgi_config.log_level,
38
+ access_log=False,
39
+ timeout_keep_alive=asgi_config.keepalive,
40
+ host=asgi_config.host,
41
+ port=asgi_config.port,
42
+ workers=1, # per managed supervisor proc
43
+ )
44
+
45
+
46
+ def worker():
47
+ if config.worker_v2_enabled:
48
+ log.debug("Starting worker v2")
49
+ Worker().start()
50
+ else:
51
+ from sovereign.worker import worker as worker_app
52
+
53
+ log.debug("Starting worker")
54
+ uvicorn.run(
55
+ worker_app,
56
+ log_level=asgi_config.log_level,
57
+ access_log=False,
58
+ timeout_keep_alive=asgi_config.keepalive,
59
+ host="127.0.0.1",
60
+ port=9080,
61
+ workers=1, # per managed supervisor proc
62
+ )
63
+
64
+
65
+ def write_supervisor_conf() -> Path:
66
+ proc_env = {
67
+ "LANG": "en_US.UTF-8",
68
+ "LC_ALL": "en_US.UTF-8",
69
+ }
70
+ base = {
71
+ "autostart": "true",
72
+ "autorestart": "true",
73
+ "stdout_logfile": "/dev/stdout",
74
+ "stdout_logfile_maxbytes": "0",
75
+ "stderr_logfile": "/dev/stderr",
76
+ "stderr_logfile_maxbytes": "0",
77
+ "stopsignal": "QUIT",
78
+ "environment": ",".join(["=".join((k, v)) for k, v in proc_env.items()]),
79
+ }
80
+
81
+ conf = configparser.RawConfigParser()
82
+ conf["supervisord"] = supervisord = {
83
+ "nodaemon": str(supervisord_config.nodaemon).lower(),
84
+ "loglevel": supervisord_config.loglevel,
85
+ "pidfile": supervisord_config.pidfile,
86
+ "logfile": supervisord_config.logfile,
87
+ "directory": supervisord_config.directory,
88
+ }
89
+
90
+ conf["fcgi-program:web"] = web = {
91
+ **base,
92
+ "socket": f"tcp://{asgi_config.host}:{asgi_config.port}",
93
+ "numprocs": str(asgi_config.workers),
94
+ "process_name": "%(program_name)s-%(process_num)02d",
95
+ "command": "sovereign-web", # default niceness, higher CPU priority
96
+ }
97
+
98
+ conf["program:data"] = worker = {
99
+ **base,
100
+ "numprocs": "1",
101
+ "command": "nice -n 2 sovereign-worker", # run worker with reduced CPU priority (higher niceness value)
102
+ }
103
+
104
+ if user := asgi_config.user:
105
+ supervisord["user"] = user
106
+ web["user"] = user
107
+ worker["user"] = user
108
+
109
+ log.debug("Writing supervisor config")
110
+ with tempfile.NamedTemporaryFile("w", delete=False) as f:
111
+ conf.write(f)
112
+ log.debug("Supervisor config written out")
113
+ return Path(f.name)
114
+
115
+
116
+ def main():
117
+ path = write_supervisor_conf()
118
+ with warnings.catch_warnings():
119
+ warnings.simplefilter("ignore")
120
+ from supervisor import supervisord
121
+
122
+ log.debug("Starting processes")
123
+ supervisord.main(["-c", path])
29
124
 
30
125
 
31
126
  if __name__ == "__main__":
sovereign/statistics.py CHANGED
@@ -1,15 +1,15 @@
1
1
  import logging
2
- from typing import Optional, Any, Callable, Dict
3
2
  from functools import wraps
4
- from sovereign.schemas import StatsdConfig
3
+ from typing import Any, Callable, Dict, Optional
5
4
 
6
- emitted: Dict[str, Any] = dict()
5
+ from sovereign.configuration import config as sovereign_config
6
+
7
+ STATSD: Dict[str, Optional["StatsDProxy"]] = {"instance": None}
7
8
 
8
9
 
9
10
  class StatsDProxy:
10
11
  def __init__(self, statsd_instance: Optional[Any] = None) -> None:
11
12
  self.statsd = statsd_instance
12
- self.emitted = emitted
13
13
 
14
14
  def __getattr__(self, item: str) -> Any:
15
15
  if self.statsd is not None:
@@ -20,14 +20,12 @@ class StatsDProxy:
20
20
  return self.do_nothing
21
21
 
22
22
  def do_nothing(self, *args: Any, **kwargs: Any) -> None:
23
- k = args[0]
24
- emitted[k] = emitted.setdefault(k, 0) + 1
23
+ _ = args[0]
25
24
 
26
25
 
27
26
  class StatsdNoop:
28
- def __init__(self, *args: Any, **kwargs: Any) -> None:
29
- k = args[0]
30
- emitted[k] = emitted.setdefault(k, 0) + 1
27
+ def __init__(self, *args, **kwargs):
28
+ pass
31
29
 
32
30
  def __enter__(self): # type: ignore
33
31
  return self
@@ -43,21 +41,18 @@ class StatsdNoop:
43
41
  return wrapped
44
42
 
45
43
 
46
- def configure_statsd(config: StatsdConfig) -> StatsDProxy:
44
+ def configure_statsd() -> StatsDProxy:
45
+ if STATSD["instance"] is not None:
46
+ return STATSD["instance"]
47
+ config = sovereign_config.statsd
47
48
  try:
48
49
  from datadog import DogStatsd
49
50
 
50
- class CustomStatsd(DogStatsd): # type: ignore
51
- def _report(self, metric, metric_type, value, tags, sample_rate) -> None: # type: ignore
52
- super()._report(metric, metric_type, value, tags, sample_rate)
53
- self.emitted: Dict[str, Any] = dict()
54
- self.emitted[metric] = self.emitted.setdefault(metric, 0) + 1
55
-
56
- module: Optional[CustomStatsd]
57
- module = CustomStatsd()
58
- if config.enabled:
51
+ module: Optional[DogStatsd]
52
+ module = DogStatsd()
53
+ if config.enabled and module:
59
54
  module.host = config.host
60
- module.port = config.port
55
+ module.port = int(config.port)
61
56
  module.namespace = config.namespace
62
57
  module.use_ms = config.use_ms
63
58
  for tag, value in config.tags.items():
@@ -71,4 +66,7 @@ def configure_statsd(config: StatsdConfig) -> StatsDProxy:
71
66
  raise
72
67
  module = None
73
68
 
74
- return StatsDProxy(module)
69
+ ret = StatsDProxy(module)
70
+ if STATSD["instance"] is None:
71
+ STATSD["instance"] = ret
72
+ return ret
@@ -1,64 +1,77 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" class="has-navbar-fixed-top">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
7
7
  <link rel="stylesheet" type="text/css" href="/static/style.css">
8
8
  <title>{% block title %}{% endblock %} - Sovereign</title>
9
+ <script src="/static/darkmode.js"></script>
9
10
  {%- block head %}{% endblock %}
10
11
  </head>
11
12
  <body>
12
- <div class="columns">
13
- <div class="column"></div>
14
13
  {%- block nav %}
15
- <div class="column is-two-thirds">
16
- {%- set active_page = resource_type|default('redirect_to_docs') -%}
17
- <nav class="navbar is-primary is-fixed-top" role="navigation" aria-label="main navigation">
18
- <div class="navbar-brand">
19
- <a class="navbar-item" href="/ui">
20
- <h4 class="title is-4" style="color: white">
21
- sovereign
22
- </h4>
23
- </a>
24
- </div>
25
- <div id="navbar" class="navbar-menu">
26
- <div class="navbar-start">
27
- <div class="navbar-item has-dropdown is-hoverable">
28
- <a class="navbar-link">
29
- Resources
30
- </a>
31
- <div class="navbar-dropdown">
32
- {%- for type in all_types %}
33
- <a class="navbar-item{% if type == active_page %} is-active{% endif %}"
34
- href="/ui/resources/{{ type }}">
35
- {{ type }}
14
+ <div class="hero">
15
+ <div class="hero-body p-4">
16
+ <a href="#">
17
+ <h3 class="title is-3" style="margin-bottom: 0px">sovereign</h3>
18
+ <h7 class="title is-7">version {{ sovereign_version }}</h7>
19
+ </a>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="columns is-gapless" style="min-height: calc(100vh - 120px);">
24
+ <div class="column is-narrow" style="width: 250px;">
25
+ <aside class="menu p-4" style="height: 100%;">
26
+ <div class="mb-4">
27
+ <p class="menu-label">RESOURCES</p>
28
+ <ul class="menu-list">
29
+ {%- set active_page = resource_type|default('redirect_to_docs') -%}
30
+ {%- for type in all_types %}
31
+ <li style="padding: 2px">
32
+ <a href="/ui/resources/{{ type }}"
33
+ style="padding: 3px; border-radius: 4px"
34
+ {% if type == active_page %}class="is-active has-background-primary has-text-white"{% endif %}>
35
+ {{ type|capitalize }}
36
36
  </a>
37
- {%- endfor %}
38
- </div>
39
- </div>
37
+ </li>
38
+ {%- endfor %}
39
+ </ul>
40
40
  </div>
41
- <div class="navbar-end">
42
- <div class="navbar-item">
43
- <a class="button is-dark" href="/docs">
44
- <span>OpenAPI</span>
45
- </a>
46
- </div>
41
+
42
+ <div class="mb-4">
43
+ <p class="menu-label">LINKS</p>
44
+ <ul class="menu-list">
45
+ <li>
46
+ <a href="/docs">OpenAPI Spec</a>
47
+ </li>
48
+ <li>
49
+ <a href="https://developer.atlassian.com/platform/sovereign/">Documentation</a>
50
+ </li>
51
+ <li>
52
+ <a href="https://bitbucket.org/atlassian/sovereign">Repository</a>
53
+ </li>
54
+ </ul>
47
55
  </div>
56
+
57
+ <p class="menu-label">THEME</p>
58
+ <button id="dark-mode-toggle" class="button is-small">
59
+ <span>🌘</span>
60
+ </button>
61
+ </aside>
62
+ </div>
63
+
64
+ <div class="column">
65
+ <div class="p-4" style="max-width: 1000px">
66
+ {%- endblock %}
67
+ {%- block body %}
68
+ {% endblock -%}
48
69
  </div>
49
- </nav>
50
- {%- endblock %}
51
-
52
- {%- block subnav %}
53
- {%- endblock %}
54
-
55
- {%- block body %}
56
- {% endblock -%}
70
+ </div>
57
71
  </div>
58
- <div class="column"></div>
59
- </div>
60
- <footer class="footer">
61
- {% block footer %}{% endblock %}
62
- </footer>
72
+
73
+ <footer class="footer">
74
+ {% block footer %}{% endblock %}
75
+ </footer>
63
76
  </body>
64
77
  </html>