sovereign 0.19.3__py3-none-any.whl → 1.0.0b148__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 (80) hide show
  1. sovereign/__init__.py +13 -81
  2. sovereign/app.py +59 -48
  3. sovereign/cache/__init__.py +172 -0
  4. sovereign/cache/backends/__init__.py +110 -0
  5. sovereign/cache/backends/s3.py +143 -0
  6. sovereign/cache/filesystem.py +73 -0
  7. sovereign/cache/types.py +15 -0
  8. sovereign/configuration.py +573 -0
  9. sovereign/constants.py +1 -0
  10. sovereign/context.py +271 -104
  11. sovereign/dynamic_config/__init__.py +113 -0
  12. sovereign/dynamic_config/deser.py +78 -0
  13. sovereign/dynamic_config/loaders.py +120 -0
  14. sovereign/events.py +49 -0
  15. sovereign/logging/access_logger.py +85 -0
  16. sovereign/logging/application_logger.py +54 -0
  17. sovereign/logging/base_logger.py +41 -0
  18. sovereign/logging/bootstrapper.py +36 -0
  19. sovereign/logging/types.py +10 -0
  20. sovereign/middlewares.py +8 -7
  21. sovereign/modifiers/lib.py +1 -0
  22. sovereign/rendering.py +192 -0
  23. sovereign/response_class.py +18 -0
  24. sovereign/server.py +93 -35
  25. sovereign/sources/file.py +1 -1
  26. sovereign/sources/inline.py +1 -0
  27. sovereign/sources/lib.py +1 -0
  28. sovereign/sources/poller.py +296 -53
  29. sovereign/statistics.py +17 -20
  30. sovereign/templates/base.html +59 -46
  31. sovereign/templates/resources.html +203 -102
  32. sovereign/testing/loaders.py +8 -0
  33. sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
  34. sovereign/tracing.py +102 -0
  35. sovereign/types.py +299 -0
  36. sovereign/utils/auth.py +26 -13
  37. sovereign/utils/crypto/__init__.py +0 -0
  38. sovereign/utils/crypto/crypto.py +135 -0
  39. sovereign/utils/crypto/suites/__init__.py +21 -0
  40. sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
  41. sovereign/utils/crypto/suites/base_cipher.py +21 -0
  42. sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
  43. sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
  44. sovereign/utils/dictupdate.py +2 -1
  45. sovereign/utils/eds.py +37 -21
  46. sovereign/utils/mock.py +54 -16
  47. sovereign/utils/resources.py +17 -0
  48. sovereign/utils/version_info.py +8 -0
  49. sovereign/views/__init__.py +4 -0
  50. sovereign/views/api.py +61 -0
  51. sovereign/views/crypto.py +46 -15
  52. sovereign/views/discovery.py +37 -116
  53. sovereign/views/healthchecks.py +87 -18
  54. sovereign/views/interface.py +112 -112
  55. sovereign/worker.py +204 -0
  56. {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/METADATA +79 -76
  57. sovereign-1.0.0b148.dist-info/RECORD +77 -0
  58. {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/WHEEL +1 -1
  59. sovereign-1.0.0b148.dist-info/entry_points.txt +38 -0
  60. sovereign_files/__init__.py +0 -0
  61. sovereign_files/static/darkmode.js +51 -0
  62. sovereign_files/static/node_expression.js +42 -0
  63. sovereign_files/static/panel.js +76 -0
  64. sovereign_files/static/resources.css +246 -0
  65. sovereign_files/static/resources.js +642 -0
  66. sovereign_files/static/sass/style.scss +33 -0
  67. sovereign_files/static/style.css +16143 -0
  68. sovereign_files/static/style.css.map +1 -0
  69. sovereign/config_loader.py +0 -225
  70. sovereign/discovery.py +0 -175
  71. sovereign/logs.py +0 -131
  72. sovereign/schemas.py +0 -780
  73. sovereign/static/sass/style.scss +0 -27
  74. sovereign/static/style.css +0 -13553
  75. sovereign/templates/ul_filter.html +0 -22
  76. sovereign/utils/crypto.py +0 -103
  77. sovereign/views/admin.py +0 -120
  78. sovereign-0.19.3.dist-info/LICENSE.txt +0 -13
  79. sovereign-0.19.3.dist-info/RECORD +0 -47
  80. sovereign-0.19.3.dist-info/entry_points.txt +0 -10
sovereign/statistics.py CHANGED
@@ -1,15 +1,14 @@
1
1
  import logging
2
2
  from typing import Optional, Any, Callable, Dict
3
3
  from functools import wraps
4
- from sovereign.schemas import StatsdConfig
4
+ from sovereign.configuration import config as sovereign_config
5
5
 
6
- emitted: Dict[str, Any] = dict()
6
+ STATSD: Dict[str, Optional["StatsDProxy"]] = {"instance": None}
7
7
 
8
8
 
9
9
  class StatsDProxy:
10
10
  def __init__(self, statsd_instance: Optional[Any] = None) -> None:
11
11
  self.statsd = statsd_instance
12
- self.emitted = emitted
13
12
 
14
13
  def __getattr__(self, item: str) -> Any:
15
14
  if self.statsd is not None:
@@ -20,14 +19,12 @@ class StatsDProxy:
20
19
  return self.do_nothing
21
20
 
22
21
  def do_nothing(self, *args: Any, **kwargs: Any) -> None:
23
- k = args[0]
24
- emitted[k] = emitted.setdefault(k, 0) + 1
22
+ _ = args[0]
25
23
 
26
24
 
27
25
  class StatsdNoop:
28
- def __init__(self, *args: Any, **kwargs: Any) -> None:
29
- k = args[0]
30
- emitted[k] = emitted.setdefault(k, 0) + 1
26
+ def __init__(self, *args, **kwargs):
27
+ pass
31
28
 
32
29
  def __enter__(self): # type: ignore
33
30
  return self
@@ -43,21 +40,18 @@ class StatsdNoop:
43
40
  return wrapped
44
41
 
45
42
 
46
- def configure_statsd(config: StatsdConfig) -> StatsDProxy:
43
+ def configure_statsd() -> StatsDProxy:
44
+ if STATSD["instance"] is not None:
45
+ return STATSD["instance"]
46
+ config = sovereign_config.statsd
47
47
  try:
48
48
  from datadog import DogStatsd
49
49
 
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:
50
+ module: Optional[DogStatsd]
51
+ module = DogStatsd()
52
+ if config.enabled and module:
59
53
  module.host = config.host
60
- module.port = config.port
54
+ module.port = int(config.port)
61
55
  module.namespace = config.namespace
62
56
  module.use_ms = config.use_ms
63
57
  for tag, value in config.tags.items():
@@ -71,4 +65,7 @@ def configure_statsd(config: StatsdConfig) -> StatsDProxy:
71
65
  raise
72
66
  module = None
73
67
 
74
- return StatsDProxy(module)
68
+ ret = StatsDProxy(module)
69
+ if STATSD["instance"] is None:
70
+ STATSD["instance"] = ret
71
+ 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>
@@ -1,66 +1,82 @@
1
1
  {%- extends 'base.html' %}
2
- {%- import 'ul_filter.html' as filter %}
3
2
  {% block title %}{{ resource_type|capitalize }}{% endblock %}
4
3
 
5
4
  {% block head %}
6
- <meta name="last_update" content="{{ last_update }}">
7
- {{ filter }}
5
+ <link rel="preconnect" href="https://fonts.googleapis.com">
6
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
7
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
8
+ <link rel="stylesheet" href="/static/resources.css">
9
+ <script src="/static/resources.js"></script>
10
+ <script src="/static/panel.js"></script>
8
11
  {% endblock %}
9
12
 
10
13
 
11
14
  {%- block body %}
12
15
  <div class="content">
16
+ {% if error %}
17
+ <span class="panel-icon">
18
+ <i class="fas fa-arrow-right" aria-hidden="true"></i>
19
+ </span>
20
+ <div class="notification is-danger">
21
+ {{ error }}
22
+ </div>
23
+ {% endif %}
13
24
  <p class="content">
14
- <div class="dropdown is-hoverable">
15
- <div class="dropdown-trigger">
16
- <button class="button is-dark" aria-haspopup="true" aria-controls="dropdown-menu">
17
- <span>Envoy Version</span>
18
- <span class="icon is-small">
19
- <i class="fas fa-angle-down" aria-hidden="true"></i>
20
- </span>
21
- </button>
22
- </div>
23
- <div class="dropdown-menu" id="dropdown-menu" role="menu">
24
- <div class="dropdown-content">
25
- <div class="dropdown-item">
26
- <p>All resources will be formatted for the selected version</p>
25
+ <div class="columns">
26
+ <div class="column is-narrow">
27
+ <div class="dropdown is-hoverable">
28
+ <div class="dropdown-trigger">
29
+ <button class="button is-primary" aria-haspopup="true" aria-controls="dropdown-menu">
30
+ <span>Template Version</span>
31
+ <span class="icon is-small">
32
+ <i class="fas fa-angle-down" aria-hidden="false"></i>
33
+ </span>
34
+ </button>
35
+ </div>
36
+ <div class="dropdown-menu" id="dropdown-menu" role="menu">
37
+ <div class="dropdown-content">
38
+ <div class="dropdown-item">
39
+ <p>All resources will be formatted for the selected version</p>
40
+ </div>
41
+ {% for v in available_versions %}
42
+ <a class="dropdown-item{% if v == version %} is-active{% endif %}"
43
+ href="#" onclick="setEnvoyVersion('{{ v }}'); return false;">
44
+ {{ v.replace('_', '') }}
45
+ </a>
46
+ {% endfor %}
47
+ </div>
27
48
  </div>
28
- {% for v in available_versions %}
29
- <a class="dropdown-item{% if v == version %} is-active{% endif %}"
30
- href="/ui/set-version?version={{ v }}">
31
- {{ v.replace('_', '') }}
32
- </a>
33
- {% endfor %}
34
49
  </div>
35
50
  </div>
36
- </div>
37
- <div class="dropdown is-hoverable">
38
- <div class="dropdown-trigger">
39
- <button class="button is-dark" aria-haspopup="true" aria-controls="dropdown-menu">
40
- <span>Service Cluster</span>
41
- <span class="icon is-small">
42
- <i class="fas fa-angle-down" aria-hidden="true"></i>
43
- </span>
44
- </button>
51
+ <!-- node expression box -->
52
+ <div class="column">
53
+ <form id="filterForm">
54
+ <input id="filterInput" class="input" type="text" placeholder="Node filter expression"/>
55
+ </form>
56
+ <p id="filterMessage" class="help"></p>
45
57
  </div>
46
- <div class="dropdown-menu" id="dropdown-menu" role="menu">
47
- <div class="dropdown-content">
48
- <div class="dropdown-item">
49
- <p>Only resources for the selected service cluster will be shown</p>
50
- </div>
51
- {% for cluster in available_service_clusters %}
52
- <a class="dropdown-item{% if cluster == service_cluster %} is-active{% endif %}"
53
- href="/ui/set-service-cluster?service_cluster={{ cluster }}">
54
- {{ cluster.replace('*', 'any') }}
55
- </a>
56
- {% endfor %}
58
+ <div class="column is-narrow">
59
+ <div class="tooltip">
60
+ <span class="button is-primary">?</span>
61
+ <div class="tooltip-text">
62
+ <p>Space-delimited Node filter.<p>
63
+ Examples:<br>
64
+ <code>id=envoy-1234</code><br>
65
+ <code>cluster=a_service_cluster</code><br>
66
+ <code>locality.zone=us-east-1</code><br>
67
+ <code>locality.sub_zone=a</code><br>
68
+ <code>metadata.field_name=abcdef</code>
69
+ </div>
57
70
  </div>
58
71
  </div>
72
+ </p>
59
73
  </div>
60
- </p>
61
74
  </div>
62
75
 
63
- <nav class="panel is-dark" id="resources">
76
+
77
+ {% set count = resources|length %}
78
+ {% if count > 0 %}
79
+ <nav class="panel is-primary" id="resources">
64
80
  <p class="panel-heading">
65
81
  {{ resource_type|capitalize }}
66
82
  </p>
@@ -71,8 +87,8 @@
71
87
  class="input"
72
88
  type="text"
73
89
  id="search_filter"
74
- onkeyup="filter_results('search_filter', 'resources')"
75
- placeholder="Filter resources by any string"
90
+ onkeyup="filter_results('search_filter')"
91
+ placeholder="Filter {{ resource_type }} by any string"
76
92
  >
77
93
  </label>
78
94
  <span class="icon is-left">
@@ -82,78 +98,163 @@
82
98
  </div>
83
99
 
84
100
  {% set res = resources|selectattr('get')|list %}
85
- {% set count = res|length %}
86
101
  {% set plural = {
87
102
  0: 'resources',
88
103
  1: 'resource'
89
104
  } %}
90
- {% for resource in res %}
91
- {% set name = resource.get('name') or resource['cluster_name'] %}
92
- <a class="panel-block has-text-weight-medium"
93
- href="/ui/resources/{{ resource_type }}/{{ name }}">
94
- <span class="panel-icon">
95
- <i class="fas fa-arrow-right" aria-hidden="true"></i>
96
- </span>
97
- {{ name }}
98
- </a>
99
- {% endfor %}
105
+ {# begin pagination #}
106
+ <div id="resource-container">
107
+ {# Will be filled in #}
108
+ </div>
109
+ {# pagination control bar #}
100
110
  <div class="panel-block">
101
- <p class="content is-small">
111
+ <p class="content is-small" id="resource-count">
102
112
  {{ count }} {{ plural.get(count, 'resources') }}
103
113
  </p>
104
114
  </div>
105
115
  </nav>
116
+ {% if count < 10 %}
117
+ {% set hidden = "display: none;" %}
118
+ {% endif %}
119
+ <div style="{{ hidden }}">
120
+ <nav class="pagination is-right is-small" role="navigation" aria-label="pagination">
121
+ <a id="prev-btn" class="pagination-previous">Previous</a>
122
+ <a id="next-btn" class="pagination-next">Next</a>
123
+ <ul id="page-numbers" class="pagination-list"></ul>
124
+ </nav>
125
+ </div>
106
126
 
107
- {% if resource_type == 'routes' %}
108
- <nav class="panel" id="virtual_hosts">
109
- <p class="panel-heading">
110
- Virtual Hosts
111
- </p>
112
- <div class="panel-block">
127
+ {% if resource_type == 'routes' %}
128
+ <nav class="panel is-primary">
129
+ <p class="panel-heading">Virtual Hosts</p>
130
+ <div class="panel-block">
113
131
  <p class="control has-icons-left">
114
- <label for="search_filter_virtual_hosts">
115
- <input
116
- class="input"
117
- type="text"
118
- id="search_filter_virtual_hosts"
119
- onkeyup="filter_results('search_filter_virtual_hosts', 'virtual_hosts')"
120
- placeholder="Filter resources by any string"
121
- >
122
- </label>
123
- <span class="icon is-left">
124
- <i class="fas fa-search" aria-hidden="true"></i>
125
- </span>
132
+ <input id="searchInput" class="input" type="text" placeholder="Filter virtual-hosts by any string" />
133
+ <span class="icon is-left">
134
+ <i class="fas fa-search" aria-hidden="true"></i>
135
+ </span>
126
136
  </p>
127
- </div>
128
-
129
- {% set vs_count = 0 %}
130
- {% for resource in res %}
131
- {% set vs_len = resource['virtual_hosts']|length %}
132
- {% set vs_count = vs_count + vs_len %}
133
- {% for virtualhost in resource['virtual_hosts'] %}
134
- {% if loop.first and loop.last %}
135
- {# A single virtualhost makes no sense to render. It will be in one of the routes above. #}
136
- {% else %}
137
- <a class="panel-block"
138
- href="/ui/resources/routes/{{ resource['name'] }}/{{ virtualhost['name'] }}">
139
- <span class="panel-icon">
140
- <i class="fas fa-arrow-right" aria-hidden="true"></i>
141
- </span>
142
- {{ virtualhost['name'] }}
143
- </a>
144
- {% endif %}
137
+ </div>
138
+ <p class="panel-tabs">
139
+ <a class="is-active" onclick="filterTabs(this, 'all')">All</a>
140
+ {% for resource in res %}
141
+ <a onclick="filterTabs(this, '{{ resource["name"] }}')">
142
+ {{ resource['name'] }}
143
+ </a>
145
144
  {% endfor %}
146
- {% endfor %}
147
- <div class="panel-block">
148
- <p class="content is-small">
149
- {{ vs_count }} {{ plural.get(vs_count, 'resources') }}
150
- </p>
151
- </div>
152
- </nav>
145
+ </p>
146
+ {% for resource in res %}
147
+ {% if resource["virtual_hosts"] %}
148
+ {% for virtualhost in resource['virtual_hosts'] %}
149
+ <a class="panel-block virtualhost"
150
+ data-category="{{ resource['name'] }}"
151
+ href="#"
152
+ onclick="loadVirtualHostInSidePanel('{{ resource['name'] }}', '{{ virtualhost['name'] }}'); return false;">
153
+ <p class="tag is-small is-primary">
154
+ {{ resource['name'] }}
155
+ </p>
156
+ <span class="panel-icon">
157
+ <i class="fas fa-arrow-right" aria-hidden="true"></i>
158
+ </span>
159
+ {{ virtualhost['name'] }}
160
+ </a>
161
+ {% endfor %}
162
+ {% else %}
163
+ <span class="panel-icon">
164
+ <i class="fas fa-arrow-right" aria-hidden="true"></i>
165
+ </span>
166
+ <div class="notification is-danger">
167
+ No resources found
168
+ <script>
169
+ setTimeout(() => location.reload(), 6000);
170
+ </script>
171
+ </div>
172
+ {% endif %}
173
+ {% endfor %}
174
+ </nav>
175
+ {% endif %}
176
+ {% else %}
177
+ <span class="panel-icon">
178
+ <i class="fas fa-arrow-right" aria-hidden="true"></i>
179
+ </span>
180
+ <div class="notification is-danger">
181
+ No resources found
182
+ <script>
183
+ setTimeout(() => location.reload(), 2000);
184
+ </script>
185
+ </div>
186
+ {% endif %}
187
+ {% if show_debuginfo %}
188
+ <pre>
189
+ {%- if discovery_request != None %}
190
+ ---
191
+ DiscoveryRequest:
192
+ Node:
193
+ Id: {{ discovery_request.node.id }}
194
+ Cluster: {{ discovery_request.node.cluster }}
195
+ Metadata: {{ discovery_request.node.metadata }}
196
+ Locality: {{ discovery_request.node.locality }}
197
+ Build Version: {{ discovery_request.node.build_version }}
198
+ Version Info: {{ discovery_request.version_info }}
199
+ Resource Names: {{ discovery_request.resources or "*" }}
200
+ # Sovereign-generated fields
201
+ Envoy Version: {{ discovery_request.envoy_version }}
202
+ Host Header: {{ discovery_request.desired_controlplane }}
203
+ {%- endif %}
204
+ {%- if discovery_response != None %}
205
+ ---
206
+ DiscoveryResponse:
207
+ Config version: {{ discovery_response.version }}
208
+ {%- endif %}
209
+ </pre>
153
210
  {% endif %}
211
+
212
+ <!-- Side Panel Backdrop -->
213
+ <div id="sidePanelBackdrop" class="side-panel-backdrop"></div>
214
+
215
+ <!-- Side Panel -->
216
+ <div id="sidePanel" class="side-panel has-background-dark has-text-white">
217
+ <div class="hero is-primary is-small">
218
+ <div class="hero-body p-4">
219
+ <div class="level">
220
+ <div class="level-left">
221
+ <h4 class="title is-4 has-text-white mb-0">
222
+ <span id="sidePanelTitle">Resource Details</span>
223
+ </h4>
224
+ </div>
225
+ <div class="level-right">
226
+ <button class="delete is-large" onclick="closeSidePanel()"></button>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ <div class="side-panel-content">
232
+ <div id="sidePanelContent">
233
+ <div class="content has-text-centered p-6">
234
+ <p>Select a resource to view its details.</p>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
154
239
  {% endblock -%}
155
240
  {% block footer %}
156
241
  <div class="content has-text-centered is-small">
157
- <p>{{ last_update }}</p>
242
+ <script src="/static/node_expression.js"></script>
243
+ <script>
244
+ document.addEventListener('DOMContentLoaded', function() {
245
+ const resourceNames = [
246
+ {% for resource in resources %}
247
+ "{{ resource.name or resource.cluster_name or 'Unknown' }}"{% if not loop.last %},{% endif %}
248
+ {% endfor %}
249
+ ];
250
+ if (typeof initializeResources === 'function') {
251
+ initializeResources(resourceNames, '{{ resource_type }}');
252
+ }
253
+ });
254
+ function setEnvoyVersion(version) {
255
+ document.cookie = `envoy_version=${version}; path=/ui/resources/; max-age=31536000`;
256
+ window.location.reload();
257
+ }
258
+ </script>
158
259
  </div>
159
260
  {% endblock %}
@@ -0,0 +1,8 @@
1
+ from typing import Any
2
+ from sovereign.dynamic_config.loaders import CustomLoader
3
+
4
+
5
+ class Multiply(CustomLoader):
6
+ def load(self, path: str) -> Any:
7
+ result = path * 2
8
+ return result
@@ -1,4 +1,3 @@
1
- from sovereign import template_context
2
1
  from sovereign.modifiers.lib import Modifier
3
2
  from sovereign.utils import eds, templates
4
3
 
@@ -8,7 +7,6 @@ class Test(Modifier):
8
7
  return True
9
8
 
10
9
  def apply(self) -> None:
11
- assert template_context
12
10
  assert eds
13
11
  assert templates
14
12
  self.instance["modifier_test_executed"] = True