Skip to content

Commit f2e5a53

Browse files
authored
Merge branch 'main' into SEP-1664
2 parents a235da0 + 592f168 commit f2e5a53

6 files changed

Lines changed: 108 additions & 2 deletions

File tree

changelog.d/SEP-1628.config.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Snippet sync now honours the shipped extension filter. `settings.yaml` declared the filter under `SEP.SNIPPETS.FILTER_EXTENSIONS`, which is not a settings field and was silently dropped at load, so every file under `SNIPPETS_DIR` synced regardless of extension. The key is replaced with the real `SEP.SNIPPETS.SYNC_FILTER: [.sh]`, restricting sync to shell scripts. Deployments carrying `FILTER_EXTENSIONS` in their own configuration overrides should rename it to `SYNC_FILTER`. Two upgrade notes for a deployment that has been syncing unfiltered: non-approved snippet rows whose files no longer match the filter are pruned on the next sync, and an approved row whose file no longer matches is kept but stops being refreshed from disk, so it should be removed manually if unwanted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright (C) 2026 Percona LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
import { describe, expect, it } from 'vitest';
19+
20+
import viteConfig from '../vite.config';
21+
import viteQaConfig from '../vite.qa.config';
22+
23+
describe('Vite backend proxy prefixes', () => {
24+
it('proxies /static to the local dev backend', () => {
25+
const proxy = viteConfig.server?.proxy;
26+
expect(proxy).toBeDefined();
27+
expect(proxy?.['/static']).toMatchObject({
28+
target: 'http://localhost:8000',
29+
changeOrigin: true,
30+
});
31+
});
32+
33+
it('proxies /static to the QA backend alongside other entries', () => {
34+
const proxy = viteQaConfig.server?.proxy;
35+
expect(proxy).toBeDefined();
36+
expect(proxy?.['/static']).toMatchObject({
37+
target: process.env.SEP_QA_BACKEND ?? 'http://127.0.0.1:18002',
38+
changeOrigin: true,
39+
cookieDomainRewrite: 'localhost',
40+
});
41+
});
42+
});

frontend/packages/shell/vite.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ export default defineConfig({
6666
target: 'http://localhost:8000',
6767
changeOrigin: true,
6868
},
69+
// App static mounts (snippets, dipper) and anonymous legacy Jinja assets
70+
// (see app/sep/main.py). Without this, SPA fallback returns index.html.
71+
'/static': {
72+
target: 'http://localhost:8000',
73+
changeOrigin: true,
74+
},
6975
},
7076
},
7177
optimizeDeps: {
@@ -108,6 +114,7 @@ export default defineConfig({
108114
if (id.includes('/node_modules/@tanstack/react-query/')) {
109115
return 'vendor-query';
110116
}
117+
return undefined;
111118
},
112119
},
113120
},

frontend/packages/shell/vite.qa.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ export default defineConfig({
6969
'/stream-logs': proxyEntry,
7070
'/execution-events': proxyEntry,
7171
'/files': proxyEntry,
72+
// App static mounts (snippets, dipper) and anonymous legacy Jinja assets
73+
// (see app/sep/main.py). Without this, SPA fallback returns index.html.
74+
'/static': proxyEntry,
7275
},
7376
},
7477
optimizeDeps: {

settings.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ default:
4040
ANNOTATIONS_TIMEOUT: 5 # Optional: PMM annotation request timeout in seconds
4141
SEP:
4242
SNIPPETS:
43-
FILTER_EXTENSIONS:
43+
# Restrict snippet sync to shell scripts. Bare strings are parsed as
44+
# extension filters; MIME-type filters use the "mime:<value>" form.
45+
# Files that don't match are skipped, so add ".py" here to sync Python
46+
# snippets even though an interpreter for them is configured by default.
47+
SYNC_FILTER:
4448
- .sh
4549
UVICORN_HOST: "127.0.0.1"
4650
UVICORN_PORT: 8000

tests/app/sep/snippets/test_config.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,27 @@
1717

1818
import json
1919
import re
20+
from collections.abc import Iterator
2021
from typing import Any
2122

2223
import pytest
24+
import yaml
2325
from pydantic import ValidationError
2426
from sqlalchemy_celery_beat.models import Period
2527

28+
from app import BASE_DIR
2629
from app.core.celery.models import IntervalSchedule
2730
from app.core.settings_override.registry import (
2831
is_hot_reloadable,
2932
materialize_override_value,
3033
)
3134
from app.sep.apps.framework.schema import EXECUTION_HOST_LABEL
32-
from app.sep.snippets.config import SnippetsSettings, SnippetSudoOption
35+
from app.sep.snippets.config import (
36+
SnippetFilter,
37+
SnippetFilterType,
38+
SnippetsSettings,
39+
SnippetSudoOption,
40+
)
3341
from app.sep.snippets.models.snippet import BaseSnippet, SUDO_INPUT_NAME
3442

3543
EXECUTOR_HOSTS = frozenset({("host1", "host1")})
@@ -355,3 +363,44 @@ def test_invalid_override_rejected(self, bad: Any) -> None:
355363
materialize_override_value(
356364
SnippetsSettings, "SYNC_INTERVAL", field_info, bad
357365
)
366+
367+
368+
class TestShippedSyncFilterConfig:
369+
"""Guard the shipped ``settings.yaml`` snippet sync filter declaration.
370+
371+
``FILTER_EXTENSIONS`` was never a ``SnippetsSettings`` field, so the shipped
372+
key was silently dropped by ``extra="ignore"`` and snippet sync ran
373+
unfiltered. These tests pin the live ``SYNC_FILTER`` key and keep the dead
374+
key from creeping back in.
375+
"""
376+
377+
@staticmethod
378+
def _shipped_settings() -> dict[str, Any]:
379+
"""Load the repository's tracked ``settings.yaml``."""
380+
return yaml.safe_load((BASE_DIR / "settings.yaml").read_text())
381+
382+
@staticmethod
383+
def _iter_keys(node: Any) -> Iterator[str]:
384+
"""Yield every mapping key found anywhere in a parsed YAML document."""
385+
if isinstance(node, dict):
386+
for key, value in node.items():
387+
yield key
388+
yield from TestShippedSyncFilterConfig._iter_keys(value)
389+
elif isinstance(node, list):
390+
for item in node:
391+
yield from TestShippedSyncFilterConfig._iter_keys(item)
392+
393+
def test_no_dead_filter_extensions_key(self):
394+
"""Assert no section of the shipped config declares the dead key."""
395+
assert "FILTER_EXTENSIONS" not in set(self._iter_keys(self._shipped_settings()))
396+
397+
def test_sync_filter_declares_shell_scripts_only(self):
398+
"""Assert the shipped config restricts sync to ``.sh`` via the live field."""
399+
snippets = self._shipped_settings()["default"]["SEP"]["SNIPPETS"]
400+
assert snippets["SYNC_FILTER"] == [".sh"]
401+
402+
def test_shipped_sync_filter_parses_to_extension_filter(self):
403+
"""Verify the shipped value validates into an extension ``SnippetFilter``."""
404+
snippets = self._shipped_settings()["default"]["SEP"]["SNIPPETS"]
405+
parsed = SnippetsSettings(SYNC_FILTER=snippets["SYNC_FILTER"])
406+
assert {SnippetFilter(".sh", SnippetFilterType.EXTENSION)} == parsed.SYNC_FILTER

0 commit comments

Comments
 (0)