improve swarm deployments

This commit is contained in:
mbecker20
2025-12-18 01:36:31 -08:00
parent 0dd2ade574
commit bf0ab26d22
6 changed files with 118 additions and 74 deletions

View File

@@ -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<String> {
pub async fn update_swarm_deployment_cache(
deployments: Vec<Deployment>,
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::<Vec<_>>();
// 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

View File

@@ -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,
)
);

View File

@@ -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<crate::api::Args> 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(())
}

View File

@@ -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 <DeploymentTabsInner deployment={deployment} />;
};
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 <DeploymentConfig id={deployment.id} titleOther={Selector} />;
case "Tasks":
return (
<DeploymentTasksTable deployment={deployment} Selector={Selector} />
);
case "Log":
return <DeploymentLogs id={deployment.id} titleOther={Selector} />;
case "Inspect":
@@ -112,3 +129,32 @@ const DeploymentTabsInner = ({
return <ContainerTerminals target={target} titleOther={Selector} />;
}
};
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 (
<SwarmServiceTasksTable
id={deployment.info.swarm_id}
service_id={service?.ID}
titleOther={Selector}
_search={_search}
/>
);
};

View File

@@ -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<SetStateAction<string>>];
}) => {
const tasks =
useRead(
"ListSwarmTasks",
{ swarm: id },
{ enabled: !!service_id }
).data?.filter((task) => service_id && task.ServiceID === service_id) ?? [];
return (
<SwarmTasksTable
id={id}
tasks={tasks}
titleOther={titleOther}
_search={_search}
/>
);
};
export const SwarmTasksTable = ({
id,
tasks: _tasks,

View File

@@ -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 = ({
</Section>
);
};
const SwarmServiceTasksTable = ({
id,
service_id,
titleOther,
_search,
}: {
id: string;
service_id: string | undefined;
titleOther: ReactNode;
_search: [string, Dispatch<SetStateAction<string>>];
}) => {
const tasks =
useRead(
"ListSwarmTasks",
{ swarm: id },
{ enabled: !!service_id }
).data?.filter((task) => service_id && task.ServiceID === service_id) ?? [];
return (
<SwarmTasksTable
id={id}
tasks={tasks}
titleOther={titleOther}
_search={_search}
/>
);
};
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;
};