Skip to content

Commit

Permalink
feat(notification): implement notification system with toast and inva…
Browse files Browse the repository at this point in the history
…lidation support
  • Loading branch information
elikoga committed Feb 3, 2025
1 parent 2bda989 commit 058cdb8
Show file tree
Hide file tree
Showing 15 changed files with 291 additions and 69 deletions.
22 changes: 22 additions & 0 deletions .github/npm_test_integration.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
OUR_NIX=$(readlink -f $(which nix))
OUR_NIX_PARENT=$(dirname $OUR_NIX)
TEE=$(which tee)
OLD_PATH=$PATH
export PATH=$OUR_NIX_PARENT
$OUR_NIX develop .#forNpmTesting --command npm run test:integration "$@" 2>&1 | $TEE output.log
export PATH=$OLD_PATH
PLAYWRIGHT_EXIT_CODE=$?
if grep -q -e "Error: A snapshot doesn't exist at" -e "Screenshot comparison failed" output.log; then
echo "Playwright tests failed due to a snapshot issue"
echo "SNAPSHOT_DIFFERENCES=true" >> $GITHUB_ENV
fi
if grep -q -E -e "npx playwright install" -e "error: attribute '\"[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+\"' missing" output.log; then
echo "Playwright tests failed due to missing browsers"
echo "MISSING_BROWSERS=true" >> $GITHUB_ENV
fi
if grep -q -E -e "[[:digit:]]+ failed" -e "was not able to start" output.log; then
echo "Playwright tests failed"
exit 1
fi
exit $PLAYWRIGHT_EXIT_CODE
2 changes: 1 addition & 1 deletion .github/workflows/approve-playwright-snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ jobs:
with:
message: |
### 🎉 Successfully updated and committed Playwright snapshots! 🎉
comment-tag: playwright-snapshots-update-success
comment-tag: playwright-snapshots-update-success
mode: recreate
github-token: ${{ steps.generate-token.outputs.token }}
152 changes: 108 additions & 44 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,28 +90,7 @@ jobs:
script: |
set +e # revert the default `set -e`
export THYMIS_FLAKE_ROOT='..'
cd frontend
OUR_NIX=$(readlink -f $(which nix))
OUR_NIX_PARENT=$(dirname $OUR_NIX)
TEE=$(which tee)
OLD_PATH=$PATH
export PATH=$OUR_NIX_PARENT
$OUR_NIX develop .#forNpmTesting --command npm run test 2>&1 | $TEE output.log
export PATH=$OLD_PATH
PLAYWRIGHT_EXIT_CODE=$?
if grep -q -e "Error: A snapshot doesn't exist at" -e "Screenshot comparison failed" output.log; then
echo "Playwright tests failed due to a snapshot issue"
echo "SNAPSHOT_DIFFERENCES=true" >> $GITHUB_ENV
fi
if grep -q -E -e "npx playwright install" -e "error: attribute '\"[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+\"' missing" output.log; then
echo "Playwright tests failed due to missing browsers"
echo "MISSING_BROWSERS=true" >> $GITHUB_ENV
fi
if grep -q -E -e "[[:digit:]]+ failed" -e "was not able to start" output.log; then
echo "Playwright tests failed"
exit 1
fi
exit $PLAYWRIGHT_EXIT_CODE
../.github/npm_test_integration.sh
working-directory: frontend
- uses: actions/upload-artifact@v4
id: artifact-upload
Expand Down Expand Up @@ -209,28 +188,113 @@ jobs:
script: |
set +e # revert the default `set -e`
# export THYMIS_FLAKE_ROOT='..' # stable input needs THYMIS_FLAKE_ROOT to be unset
cd frontend
OUR_NIX=$(readlink -f $(which nix))
OUR_NIX_PARENT=$(dirname $OUR_NIX)
TEE=$(which tee)
OLD_PATH=$PATH
export PATH=$OUR_NIX_PARENT
$OUR_NIX develop .#forNpmTesting --command npm run test 2>&1 | $TEE output.log
export PATH=$OLD_PATH
PLAYWRIGHT_EXIT_CODE=$?
if grep -q -e "Error: A snapshot doesn't exist at" -e "Screenshot comparison failed" output.log; then
echo "Playwright tests failed due to a snapshot issue"
echo "SNAPSHOT_DIFFERENCES=true" >> $GITHUB_ENV
fi
if grep -q -E -e "npx playwright install" -e "error: attribute '\"[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+\"' missing" output.log; then
echo "Playwright tests failed due to missing browsers"
echo "MISSING_BROWSERS=true" >> $GITHUB_ENV
fi
if grep -q -E -e "[[:digit:]]+ failed" -e "was not able to start" output.log; then
echo "Playwright tests failed"
exit 1
fi
exit $PLAYWRIGHT_EXIT_CODE
../.github/npm_test_integration.sh
working-directory: frontend
- uses: actions/upload-artifact@v4
id: artifact-upload
if: always()
with:
name: playwright-report-stable-input
path: frontend/playwright-report/
retention-days: 30
- name: Comment on PR with report link
uses: thollander/actions-comment-pull-request@v3
if: ${{ always() && github.event_name == 'pull_request' && env.SNAPSHOT_DIFFERENCES == 'true' }}
with:
message: |
### Playwright visual snapshot differences were detected.
View the [Playwright report](${{ steps.artifact-upload.outputs.artifact-url }}) to review the visual differences.
**To approve the snapshot changes and update the snapshots, please comment:** /approve-snapshots
comment-tag: playwright-snapshots
mode: recreate

test-frontend-integration-x64:
runs-on: ubuntu-latest
needs: changes
if: ${{ ! (needs.changes.outputs.package-lock-json == 'true' && github.actor == 'renovate[bot]' && github.event_name == 'pull_request') }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-nix
with:
attic_token: ${{ secrets.ATTIC_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: |
npm ci
working-directory: frontend
- name: Build application
uses: ./.github/actions/run-command-with-nix-cache-upload
with:
attic_token: ${{ secrets.ATTIC_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
script: |
nix build .#thymis-controller --print-build-logs
- name: Run tests
uses: ./.github/actions/run-command-with-nix-cache-upload
with:
attic_token: ${{ secrets.ATTIC_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
script: |
set +e # revert the default `set -e`
export THYMIS_FLAKE_ROOT='..'
../.github/npm_test_integration.sh -- tests/x86_vm.spec.ts
working-directory: frontend
- uses: actions/upload-artifact@v4
id: artifact-upload
if: always()
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 30
- name: Comment on PR with report link
uses: thollander/actions-comment-pull-request@v3
if: ${{ always() && github.event_name == 'pull_request' && env.SNAPSHOT_DIFFERENCES == 'true' }}
with:
message: |
### Playwright visual snapshot differences were detected.
View the [Playwright report](${{ steps.artifact-upload.outputs.artifact-url }}) to review the visual differences.
**To approve the snapshot changes and update the snapshots, please comment:** /approve-snapshots
comment-tag: playwright-snapshots
mode: recreate

test-frontend-integration-stable-input-x64:
runs-on: ubuntu-latest
needs: changes
if: ${{ ! (needs.changes.outputs.package-lock-json == 'true' && github.actor == 'renovate[bot]' && github.event_name == 'pull_request') }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-nix
with:
attic_token: ${{ secrets.ATTIC_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: |
npm ci
working-directory: frontend
- name: Build application
uses: ./.github/actions/run-command-with-nix-cache-upload
with:
attic_token: ${{ secrets.ATTIC_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
script: |
nix build .#thymis-controller --print-build-logs
- name: Run tests
uses: ./.github/actions/run-command-with-nix-cache-upload
with:
attic_token: ${{ secrets.ATTIC_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
script: |
set +e # revert the default `set -e`
# export THYMIS_FLAKE_ROOT='..' # stable input needs THYMIS_FLAKE_ROOT to be unset
../.github/npm_test_integration.sh -- tests/x86_vm.spec.ts
working-directory: frontend
- uses: actions/upload-artifact@v4
id: artifact-upload
Expand Down
1 change: 1 addition & 0 deletions controller/thymis_controller/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class GlobalSettings(BaseSettings):
ALEMBIC_INI_PATH: str = f"{pathlib.Path(__file__).parent.parent}/alembic.ini"

BASE_URL: str = "http://localhost:8000"
AGENT_ACCESS_URL: str | None = None

FRONTEND_BINARY_PATH: str | None = None
AUTH_BASIC: bool = True
Expand Down
2 changes: 1 addition & 1 deletion controller/thymis_controller/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ async def lifespan(app: FastAPI):
notification_manager.start()
host, port = detect_host_port()
db_engine = create_sqlalchemy_engine()
network_relay = NetworkRelay(db_engine)
network_relay = NetworkRelay(db_engine, notification_manager)
task_controller = TaskController(
f"ws://{host}:{port}/agent/relay_for_clients", network_relay
)
Expand Down
19 changes: 15 additions & 4 deletions controller/thymis_controller/modules/thymis.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,9 @@ class ThymisDevice(modules.Module):
en="Thymis Controller URL",
de="Thymis Controller-URL",
),
nix_attr_name="thymis.config.agent.controller-url",
# nix_attr_name="thymis.config.agent.controller-url",
type="string",
default="",
default=global_settings.AGENT_ACCESS_URL or global_settings.BASE_URL or "",
description=modules.LocalizedString(
en="URL of this Thymis Controller instance",
de="URL dieser Thymis Controller-Instanz",
Expand Down Expand Up @@ -625,6 +625,12 @@ def write_nix_settings(
else self.timezone.default
)

agent_controller_url = (
module_settings.settings["agent_controller_url"]
if "agent_controller_url" in module_settings.settings
else self.agent_controller_url.default
)

f.write(" imports = [\n")

if write_target_type == "hosts":
Expand All @@ -638,15 +644,20 @@ def write_nix_settings(

f.write(" ];\n")

if agent_controller_url:
f.write(
f" thymis.config.agent.controller-url = lib.mkOverride {priority} {convert_python_value_to_nix(agent_controller_url)};\n"
)

if authorized_keys:
keys = list(
map(lambda x: x["key"] if "key" in x else None, authorized_keys)
)
else:
keys = []

if project.public_key:
keys.append(project.public_key)
# if project.public_key:
# keys.append(project.public_key)

if len(keys) > 0:
key_list_nix = convert_python_value_to_nix(keys, ident=1)
Expand Down
16 changes: 11 additions & 5 deletions controller/thymis_controller/network_relay.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from thymis_controller.config import global_settings

if TYPE_CHECKING:
from thymis_controller.notifications import NotificationManager
from thymis_controller.task.controller import TaskController

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -75,7 +76,7 @@ class NetworkRelay(nr.NetworkRelay):
CustomAgentToRelayMessage = agent.AgentToRelayMessage
CustomAgentToRelayStartMessage = agent.EdgeAgentToRelayStartMessage

def __init__(self, db_engine):
def __init__(self, db_engine, notification_manager):
super().__init__()
self.db_engine = db_engine
self.public_key_to_connection_id = {}
Expand All @@ -84,6 +85,7 @@ def __init__(self, db_engine):
str, agent.EdgeAgentToRelayStartMessage
] = {}
self.task_controller: Optional["TaskController"] = None
self.notification_manager: "NotificationManager" = notification_manager

async def handle_custom_agent_message(self, message: agent.AgentToRelayMessage):
match message.inner:
Expand Down Expand Up @@ -173,12 +175,12 @@ async def accept_ws_and_start_msg_loop_for_edge_agents(
self,
edge_agent_connection: WebSocket,
):
(
msg_loop,
connection_id,
) = await super().accept_ws_and_start_msg_loop_for_edge_agents(
res = await super().accept_ws_and_start_msg_loop_for_edge_agents(
edge_agent_connection
)
if res is None:
return
msg_loop, connection_id = res
# we can establish ssh connections here
# check that the public key is valid

Expand Down Expand Up @@ -281,6 +283,10 @@ async def msg_loop_but_close_connection_at_end():
del self.connection_id_to_public_key[connection_id]
del self.connection_id_to_start_message[connection_id]

self.notification_manager.broadcast_invalidate_notification(
["/api/connected_deployment_infos_by_config_id"]
)

return msg_loop_but_close_connection_at_end(), connection_id

async def get_access_client_permission(
Expand Down
33 changes: 27 additions & 6 deletions controller/thymis_controller/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@
import threading
import traceback
from queue import Queue
from typing import Literal, Union

from fastapi import WebSocket, WebSocketDisconnect
from fastapi.websockets import WebSocketState
from pydantic import BaseModel, Field

NotificationDataInner = Union["ShouldInvalidate", "FrontendToast"]


class Notification:
message: str
data: "NotificationData"
creation_time: datetime.datetime
last_try: datetime.datetime
send_to: list[WebSocket]

def __init__(self, message: str):
self.message = message
def __init__(self, message: NotificationDataInner):
self.data = NotificationData(inner=message)
self.creation_time = datetime.datetime.now()
self.last_try = datetime.datetime.max
self.send_to = []
Expand All @@ -29,6 +33,20 @@ def can_retry(self):
return now - self.creation_time < datetime.timedelta(seconds=5)


class NotificationData(BaseModel):
inner: NotificationDataInner = Field(discriminator="kind")


class ShouldInvalidate(BaseModel):
kind: Literal["should_invalidate"] = "should_invalidate"
should_invalidate_paths: list[str]


class FrontendToast(BaseModel):
kind: Literal["frontend_toast"] = "frontend_toast"
message: str


class NotificationManager:
queue: Queue[Notification] = Queue()
retry_queue: Queue[Notification] = Queue()
Expand Down Expand Up @@ -95,8 +113,8 @@ def is_connection_healthy(self, websocket: WebSocket):
websocket.client_state,
)

def broadcast(self, message: str):
self.queue.put(Notification(message))
def broadcast_toast_notification(self, message: str):
self.queue.put(Notification(FrontendToast(message=message)))

async def _broadcast(self, message: Notification):
for connection in self.active_connections:
Expand All @@ -105,8 +123,11 @@ async def _broadcast(self, message: Notification):
if not self.is_connection_healthy(connection):
continue
try:
await connection.send_json({"message": message.message})
await connection.send_text(message.data.model_dump_json())
message.send_to.append(connection)
except Exception:
self.disconnect(connection)
traceback.print_exc()

def broadcast_invalidate_notification(self, paths: list[str]):
self.queue.put(Notification(ShouldInvalidate(should_invalidate_paths=paths)))
3 changes: 0 additions & 3 deletions controller/thymis_controller/task/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,6 @@ def listen_child_messages(self, conn: Connection, task_id: uuid.UUID):
new_task = self.controller.submit(
new_task_submission, db_session
)
if task.children is None:
task.children = []
task.children.append(new_task.id)
db_session.commit()
case (
models_task.AgentShouldSwitchToNewConfigurationUpdate()
Expand Down
Loading

0 comments on commit 058cdb8

Please sign in to comment.