mirror of
https://github.com/moghtech/komodo.git
synced 2026-04-27 19:33:08 -05:00
improve swarm deployments
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user