From bf0ab26d22252ffec17e920be04def5bd6d35e23 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Thu, 18 Dec 2025 01:36:31 -0800 Subject: [PATCH] improve swarm deployments --- bin/core/src/monitor/resources.rs | 30 ++--------- bin/core/src/monitor/swarm.rs | 1 - bin/periphery/src/api/swarm/service.rs | 38 ++++++++++++-- .../components/resources/deployment/tabs.tsx | 52 +++++++++++++++++-- .../src/components/resources/swarm/table.tsx | 27 ++++++++++ frontend/src/pages/swarm/service.tsx | 44 ++-------------- 6 files changed, 118 insertions(+), 74 deletions(-) diff --git a/bin/core/src/monitor/resources.rs b/bin/core/src/monitor/resources.rs index d21e113ae..db657bb8e 100644 --- a/bin/core/src/monitor/resources.rs +++ b/bin/core/src/monitor/resources.rs @@ -17,7 +17,6 @@ use komodo_client::{ image::ImageListItem, service::SwarmServiceListItem, stack::SwarmStackListItem, - task::SwarmTaskListItem, }, komodo_timestamp, stack::{Stack, StackService, StackServiceNames, StackState}, @@ -341,7 +340,6 @@ fn deployment_alert_sent_cache() -> &'static AlertCache { pub async fn update_swarm_deployment_cache( deployments: Vec, swarm_services: &[SwarmServiceListItem], - swarm_tasks: &[SwarmTaskListItem], ) { let deployment_status_cache = deployment_status_cache(); for deployment in deployments { @@ -361,30 +359,10 @@ pub async fn update_swarm_deployment_cache( .map(|s| s.curr.state); let current_state = service .as_ref() - .map(|service| { - let Some(service_id) = &service.id else { - return DeploymentState::Unknown; - }; - let tasks = swarm_tasks - .iter() - .filter(|task| { - task - .service_id - .as_ref() - .map(|id| id == service_id) - .unwrap_or_default() - }) - .collect::>(); - // If service exists but no tasks, it is unhealthy - if tasks.is_empty() { - return DeploymentState::Unhealthy; - } - for task in tasks { - if task.desired_state != task.state { - return DeploymentState::Unhealthy; - } - } - DeploymentState::Running + .map(|service| match service.state { + SwarmState::Healthy => DeploymentState::Running, + SwarmState::Unhealthy => DeploymentState::Unhealthy, + SwarmState::Unknown => DeploymentState::Unknown, }) .unwrap_or(DeploymentState::NotDeployed); deployment_status_cache diff --git a/bin/core/src/monitor/swarm.rs b/bin/core/src/monitor/swarm.rs index 32d834656..1ab20e106 100644 --- a/bin/core/src/monitor/swarm.rs +++ b/bin/core/src/monitor/swarm.rs @@ -161,7 +161,6 @@ pub async fn update_cache_for_swarm(swarm: &Swarm, force: bool) { update_swarm_deployment_cache( resources.deployments, &lists.services, - &lists.tasks, ) ); diff --git a/bin/periphery/src/api/swarm/service.rs b/bin/periphery/src/api/swarm/service.rs index 83705fd13..c03132317 100644 --- a/bin/periphery/src/api/swarm/service.rs +++ b/bin/periphery/src/api/swarm/service.rs @@ -9,7 +9,7 @@ use formatting::format_serror; use interpolate::Interpolator; use komodo_client::entities::{ deployment::{ - Deployment, DeploymentConfig, DeploymentImage, + Conversion, Deployment, DeploymentConfig, DeploymentImage, conversions_from_str, extract_registry_domain, }, docker::service::SwarmService, @@ -311,10 +311,9 @@ fn docker_service_create_command( "-p", )?; - push_conversions( + push_mounts( &mut res, &conversions_from_str(volumes).context("Invalid volumes")?, - "--mount", )?; push_environment( @@ -419,3 +418,36 @@ impl Resolve for UpdateSwarmService { Ok(log) } } + +pub fn push_mounts( + command: &mut String, + mounts: &[Conversion], +) -> anyhow::Result<()> { + for Conversion { local, container } in mounts { + let (typ, src) = if local == "tmpfs" { + ("tmpfs", None) + } else if local.starts_with('/') || local.starts_with('.') { + ("bind", Some(local)) + } else { + ("volume", Some(local)) + }; + let (dst, readonly) = + if let Some((container, mode)) = container.split_once(':') { + (container, mode == "ro") + } else { + (container.as_str(), false) + }; + write!(command, " --mount type={typ}") + .context("Failed to format mounts 'type'")?; + if let Some(src) = src { + write!(command, ",src={src}") + .context("Failed to format mounts 'src'")?; + } + write!(command, ",dst={dst}") + .context("Failed to format mounts 'dst'")?; + if readonly { + command.push_str(",ro=true"); + } + } + Ok(()) +} diff --git a/frontend/src/components/resources/deployment/tabs.tsx b/frontend/src/components/resources/deployment/tabs.tsx index 386412192..22e72c6c3 100644 --- a/frontend/src/components/resources/deployment/tabs.tsx +++ b/frontend/src/components/resources/deployment/tabs.tsx @@ -1,8 +1,8 @@ import { Types } from "komodo_client"; import { useDeployment } from "."; -import { useLocalStorage, usePermissions } from "@lib/hooks"; +import { useLocalStorage, usePermissions, useRead } from "@lib/hooks"; import { useServer } from "../server"; -import { useMemo } from "react"; +import { ReactNode, useMemo, useState } from "react"; import { MobileFriendlyTabsSelector, TabNoContent, @@ -11,6 +11,8 @@ import { DeploymentConfig } from "./config"; import { DeploymentLogs } from "./log"; import { DeploymentInspect } from "./inspect"; import { ContainerTerminals } from "@components/terminal/container"; +import { SwarmServiceTasksTable } from "../swarm/table"; +import { useWebsocketMessages } from "@lib/socket"; export const DeploymentTabs = ({ id }: { id: string }) => { const deployment = useDeployment(id); @@ -18,7 +20,7 @@ export const DeploymentTabs = ({ id }: { id: string }) => { return ; }; -type DeploymentTabsView = "Config" | "Log" | "Inspect" | "Terminals"; +type DeploymentTabsView = "Config" | "Tasks" | "Log" | "Inspect" | "Terminals"; const DeploymentTabsInner = ({ deployment, @@ -37,18 +39,24 @@ const DeploymentTabsInner = ({ useServer(deployment.info.server_id)?.info.container_terminals_disabled ?? false; const state = deployment.info.state; + const downOrUnknown = state === undefined || state === Types.DeploymentState.Unknown || + state === Types.DeploymentState.Deploying || state === Types.DeploymentState.NotDeployed; + const logsDisabled = !specificLogs || downOrUnknown; const inspectDisabled = !specificInspect || downOrUnknown; + const terminalDisabled = !specificTerminal || container_terminals_disabled || state !== Types.DeploymentState.Running; + const view = (logsDisabled && _view === "Log") || + (downOrUnknown && _view === "Tasks") || (inspectDisabled && _view === "Inspect") || (terminalDisabled && _view === "Terminals") ? "Config" @@ -59,6 +67,11 @@ const DeploymentTabsInner = ({ { value: "Config", }, + { + value: "Tasks", + disabled: downOrUnknown, + hidden: !deployment.info.swarm_id, + }, { value: "Log", disabled: logsDisabled, @@ -98,6 +111,10 @@ const DeploymentTabsInner = ({ switch (view) { case "Config": return ; + case "Tasks": + return ( + + ); case "Log": return ; case "Inspect": @@ -112,3 +129,32 @@ const DeploymentTabsInner = ({ return ; } }; + +const DeploymentTasksTable = ({ + deployment, + Selector, +}: { + deployment: Types.DeploymentListItem; + Selector: ReactNode; +}) => { + const { data, refetch } = useRead("ListSwarmServices", { + swarm: deployment.info.swarm_id, + }); + const service = data?.find((service) => service.Name === deployment.name); + useWebsocketMessages( + "deployment-swarm-tasks", + (update) => + update.operation === Types.Operation.Deploy && + update.target.id === deployment.id && + refetch() + ); + const _search = useState(""); + return ( + + ); +}; diff --git a/frontend/src/components/resources/swarm/table.tsx b/frontend/src/components/resources/swarm/table.tsx index eecb327b7..7e9a49ef3 100644 --- a/frontend/src/components/resources/swarm/table.tsx +++ b/frontend/src/components/resources/swarm/table.tsx @@ -139,6 +139,33 @@ export const SwarmServicesTable = ({ ); }; +export const SwarmServiceTasksTable = ({ + id, + service_id, + titleOther, + _search, +}: { + id: string; + service_id: string | undefined; + titleOther: ReactNode; + _search: [string, Dispatch>]; +}) => { + const tasks = + useRead( + "ListSwarmTasks", + { swarm: id }, + { enabled: !!service_id } + ).data?.filter((task) => service_id && task.ServiceID === service_id) ?? []; + return ( + + ); +}; + export const SwarmTasksTable = ({ id, tasks: _tasks, diff --git a/frontend/src/pages/swarm/service.tsx b/frontend/src/pages/swarm/service.tsx index a0202127d..8bef6c02c 100644 --- a/frontend/src/pages/swarm/service.tsx +++ b/frontend/src/pages/swarm/service.tsx @@ -21,12 +21,12 @@ import { swarm_state_intention, } from "@lib/color"; import { ResourceNotifications } from "@pages/resource-notifications"; -import { Dispatch, ReactNode, SetStateAction, useMemo, useState } from "react"; +import { ReactNode, useMemo, useState } from "react"; import { MobileFriendlyTabsSelector } from "@ui/mobile-friendly-tabs"; import { SwarmServiceLogs } from "./log"; import { Section } from "@components/layouts"; import { RemoveSwarmResource } from "./remove"; -import { SwarmTasksTable } from "@components/resources/swarm/table"; +import { SwarmServiceTasksTable } from "@components/resources/swarm/table"; export default function SwarmServicePage() { const { id, service: __service } = useParams() as { @@ -71,7 +71,7 @@ export default function SwarmServicePage() { } const Icon = SWARM_ICONS.Service; - const state = get_service_state_from_tasks(tasks); + const state = service.State; const intention = swarm_state_intention(state); const strokeColor = stroke_color_class_by_intention(intention); const service_id = service.ID; @@ -265,41 +265,3 @@ const SwarmServiceInspect = ({ ); }; - -const SwarmServiceTasksTable = ({ - id, - service_id, - titleOther, - _search, -}: { - id: string; - service_id: string | undefined; - titleOther: ReactNode; - _search: [string, Dispatch>]; -}) => { - const tasks = - useRead( - "ListSwarmTasks", - { swarm: id }, - { enabled: !!service_id } - ).data?.filter((task) => service_id && task.ServiceID === service_id) ?? []; - return ( - - ); -}; - -const get_service_state_from_tasks = ( - tasks: Types.SwarmTaskListItem[] -): Types.SwarmState => { - for (const task of tasks) { - if (task.State !== task.DesiredState) { - return Types.SwarmState.Unhealthy; - } - } - return Types.SwarmState.Healthy; -};