diff --git a/app/src/Dashboard.svelte b/app/src/Dashboard.svelte index c0773f72..d1dcbcdd 100644 --- a/app/src/Dashboard.svelte +++ b/app/src/Dashboard.svelte @@ -25,20 +25,17 @@ import type { Node, Stack } from "./nodes"; import User from "carbon-icons-svelte/lib/User.svelte"; import ChangePassword from "./auth/ChangePassword.svelte"; - import { get_signedin_user_details, type Container } from "./api/swarm"; + import { + get_all_image_actual_version, + get_signedin_user_details, + type Container, + } from "./api/swarm"; import { getImageVersion } from "./helpers/swarm"; import RestartNode from "./nodes/RestartNode.svelte"; let selectedName = ""; - async function getNodeVersion(nodes: Node[]) { - //loop throug nodes - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - // if node version is latest get digest - if (node.version === "latest") { - await getImageVersion(node.name, stack, selectedNode); - } - } + async function getNodeVersion() { + await getImageVersion(stack, selectedNode); } async function pollConfig() { @@ -55,7 +52,7 @@ if (stackRemote.nodes !== $stack.nodes) { stack.set(stackRemote); // get node version - getNodeVersion(stackRemote.nodes); + getNodeVersion(); } return stackRemote.ready; } diff --git a/app/src/api/cmd.ts b/app/src/api/cmd.ts index d4adb3fc..b15ad93d 100644 --- a/app/src/api/cmd.ts +++ b/app/src/api/cmd.ts @@ -92,7 +92,9 @@ export type Cmd = | "RestartChildSwarmContainers" | "GetSignedInUserDetails" | "UpdateAwsInstanceType" - | "GetInstanceType"; + | "GetInstanceType" + | "GetAllImageActualVersion" + | "GetSwarmChildImageVersions"; interface CmdData { cmd: Cmd; diff --git a/app/src/api/swarm.ts b/app/src/api/swarm.ts index 22ccfb82..3df23d18 100644 --- a/app/src/api/swarm.ts +++ b/app/src/api/swarm.ts @@ -146,6 +146,17 @@ export async function delete_swarm(data: { host: string }) { return await swarmCmd("DeleteSwarm", { ...data }); } +export async function get_all_image_actual_version() { + return await swarmCmd("GetAllImageActualVersion"); +} +export async function get_child_swarm_image_versions({ + host, +}: { + host: string; +}) { + return await swarmCmd("GetSwarmChildImageVersions", { host }); +} + export async function login(username, password) { const r = await fetch(`${root}/login`, { method: "POST", diff --git a/app/src/helpers/swarm.ts b/app/src/helpers/swarm.ts index bce6e069..6d4131d5 100644 --- a/app/src/helpers/swarm.ts +++ b/app/src/helpers/swarm.ts @@ -1,117 +1,37 @@ import type { Writable } from "svelte/store"; -import { get_image_tags } from "../api/swarm"; +import { get_all_image_actual_version, get_image_tags } from "../api/swarm"; import type { Stack, Node } from "../nodes"; import { swarm } from "../api"; -export async function getVersionFromDigest( - digest: string, - org_image_name: string, - page: string, - page_size: string -) { - try { - const splittedDigest = digest.split("@")[1]; - const response = await get_image_tags(org_image_name, page, page_size); - - const tags = JSON.parse(response); - - for (let i = 0; i < tags.results.length; i++) { - const result = tags.results[i]; - if (result.digest === splittedDigest) { - if (result.name !== "latest") { - return result.name; - } else { - const architectureDigests = []; - for (let j = 0; j < result.images.length; j++) { - architectureDigests.push(result.images[j].digest); - } - return findArchitectureDigest(architectureDigests, tags.results); - } - } - } - - if (tags.next) { - const urlString = tags.next; - const url = new URL(urlString); - const params = new URLSearchParams(url.search); - - const page = params.get("page"); - const page_size = params.get("page_size"); - - return await getVersionFromDigest( - digest, - org_image_name, - page, - page_size - ); - } - } catch (error) { - throw error; - } -} - -function findArchitectureDigest(architectureDigests, results) { - for (let i = 0; i < results.length; i++) { - const result = results[i]; - if (result.name !== "latest") { - for (let j = 0; j < result.images.length; j++) { - const image = result.images[j]; - if (architectureDigests.includes(image.digest)) { - return result.name; - } - } - } - } -} - export async function getImageVersion( - node_name: string, stack: Writable, selectedNode: Writable ) { - let image_name = `sphinx-${node_name}`; - if (node_name === "relay") { - image_name = `sphinx-relay-swarm`; - } else if (node_name === "cln") { - image_name = `cln-sphinx`; - } else if (node_name === "navfiber") { - image_name = `sphinx-nav-fiber`; - } else if (node_name === "cache") { - image_name = ``; - } else if (node_name === "jarvis") { - image_name = `sphinx-jarvis-backend`; - } - const image_digest_response = await swarm.get_image_digest( - `sphinxlightning/${image_name}` - ); - if (image_digest_response.success) { - const version = await getVersionFromDigest( - image_digest_response.digest, - `sphinxlightning/${image_name}`, - "1", - "100" - ); + const image_versions = await get_all_image_actual_version(); + console.log(image_versions); + if (image_versions.success) { + let version_object = {}; + + for (let i = 0; i < image_versions.data.length; i++) { + const image_version = image_versions.data[i]; + version_object[image_version.name] = image_version.version; + } - if (version) { - stack.update((stack) => { - for (let i = 0; i < stack.nodes.length; i++) { - const oldNode = { ...stack.nodes[i] }; - if (oldNode.name === node_name) { - const newNode = { - ...oldNode, - version, - }; + stack.update((stack) => { + for (let i = 0; i < stack.nodes.length; i++) { + const newNode = { + ...stack.nodes[i], + version: version_object[stack.nodes[i].name], + }; - selectedNode.update((node) => - node && node.name === newNode.name ? { ...newNode } : node - ); + selectedNode.update((node) => + node && node.name === newNode.name ? { ...newNode } : node + ); - stack.nodes[i] = { ...newNode }; - break; - } - } - return stack; - }); - } + stack.nodes[i] = { ...newNode }; + } + + return stack; + }); } } diff --git a/app/src/nodes/NodeUpdate.svelte b/app/src/nodes/NodeUpdate.svelte index 566cf730..fa49c137 100644 --- a/app/src/nodes/NodeUpdate.svelte +++ b/app/src/nodes/NodeUpdate.svelte @@ -12,7 +12,7 @@ if (!name) return; updating = true; await api.swarm.update_node(name); - await getImageVersion(name, stack, selectedNode); + await getImageVersion(stack, selectedNode); updating = false; } diff --git a/app/src/nodes/RestartNode.svelte b/app/src/nodes/RestartNode.svelte index 89f96515..a353f302 100644 --- a/app/src/nodes/RestartNode.svelte +++ b/app/src/nodes/RestartNode.svelte @@ -12,7 +12,7 @@ console.log("restart!", name); restarting = true; await api.swarm.restart_node(name); - await getImageVersion(name, stack, selectedNode); + await getImageVersion(stack, selectedNode); restarting = false; } diff --git a/src/bin/super/cmd.rs b/src/bin/super/cmd.rs index a840d7d4..3fa3e7d1 100644 --- a/src/bin/super/cmd.rs +++ b/src/bin/super/cmd.rs @@ -79,6 +79,7 @@ pub enum SwarmCmd { GetAwsInstanceTypes, UpdateAwsInstanceType(UpdateInstanceDetails), GetInstanceType(GetInstanceTypeByInstanceId), + GetSwarmChildImageVersions(ChildSwarmIdentifier), } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/src/bin/super/mod.rs b/src/bin/super/mod.rs index 491a8416..642c6442 100644 --- a/src/bin/super/mod.rs +++ b/src/bin/super/mod.rs @@ -15,8 +15,8 @@ use state::RemoteStack; use state::Super; use util::{ accessing_child_container_controller, add_new_swarm_details, add_new_swarm_from_child_swarm, - get_aws_instance_types, get_child_swarm_config, get_child_swarm_containers, get_config, - get_swarm_instance_type, update_aws_instance_type, + get_aws_instance_types, get_child_swarm_config, get_child_swarm_containers, + get_child_swarm_image_versions, get_config, get_swarm_instance_type, update_aws_instance_type, }; use crate::checker::swarm_checker; @@ -373,6 +373,29 @@ pub async fn super_handle( } Some(serde_json::to_string(&res)?) } + SwarmCmd::GetSwarmChildImageVersions(info) => { + let res: SuperSwarmResponse; + match state.find_swarm_by_host(&info.host) { + Some(swarm) => match get_child_swarm_image_versions(&swarm).await { + Ok(result) => res = result, + Err(err) => { + res = SuperSwarmResponse { + success: false, + message: err.to_string(), + data: None, + } + } + }, + None => { + res = SuperSwarmResponse { + success: false, + message: "Swarm does not exist".to_string(), + data: None, + } + } + } + Some(serde_json::to_string(&res)?) + } }, }; diff --git a/src/bin/super/superapp/src/ViewNodes.svelte b/src/bin/super/superapp/src/ViewNodes.svelte index 40375979..d9594589 100644 --- a/src/bin/super/superapp/src/ViewNodes.svelte +++ b/src/bin/super/superapp/src/ViewNodes.svelte @@ -12,6 +12,7 @@ get_aws_instance_types, update_aws_instance_type, get_swarm_instance_type, + get_child_swarm_image_versions, } from "../../../../../app/src/api/swarm"; import { Button, @@ -167,6 +168,8 @@ await getAwsInstanceType(); await get_current_service_details(); + + await get_image_versions(); }); function findContainer(node_name: string) { @@ -178,6 +181,41 @@ } } + async function get_image_versions() { + try { + const response = await get_child_swarm_image_versions({ + host: $selectedNode, + }); + if (response.success === true) { + const version_object = {}; + for (let i = 0; i < response.data.data.length; i++) { + version_object[response.data.data[i].name] = + response.data.data[i].version; + } + + let tempSortedNodes = []; + + for (let i = 0; i < sortedNodes.length; i++) { + const node = sortedNodes[i]; + + tempSortedNodes.push({ + ...node, + ...(node.version === "latest" && { + version: version_object[node.name.toLowerCase()], + }), + }); + } + + sortedNodes = [...tempSortedNodes]; + } + } catch (error) { + console.log(error); + console.log( + `Error getting ${$selectedNode} image version: ${JSON.stringify}` + ); + } + } + function sortNodes() { const tempSortedNodes = []; for (let i = 0; i < nodes.length; i++) { @@ -373,7 +411,7 @@ headers={[ { key: "sn", value: "S/N" }, { key: "name", value: "Name" }, - // { key: "version", value: "Version" }, + { key: "version", value: "Version" }, { key: "update", value: "Update" }, { key: "stop", value: "Stop/Start" }, { key: "restart", value: "Restart" }, diff --git a/src/bin/super/util.rs b/src/bin/super/util.rs index fce3b2d1..3a33210d 100644 --- a/src/bin/super/util.rs +++ b/src/bin/super/util.rs @@ -199,6 +199,29 @@ pub async fn get_child_swarm_containers( }) } +pub async fn get_child_swarm_image_versions( + swarm_details: &RemoteStack, +) -> Result { + let token = login_to_child_swarm(swarm_details).await?; + let cmd = Cmd::Swarm(SwarmCmd::GetAllImageActualVersion); + let res = swarm_cmd(cmd, swarm_details.default_host.clone(), &token).await?; + + if res.status().clone() != 200 { + return Err(anyhow!(format!( + "{} status code gotten from get child swarm container", + res.status() + ))); + } + + let image_version: Value = res.json().await?; + + Ok(SuperSwarmResponse { + success: true, + message: "child swarm image versions successfully retrieved".to_string(), + data: Some(image_version), + }) +} + pub async fn access_child_swarm_containers( swarm_details: &RemoteStack, nodes: Vec, diff --git a/src/cmd.rs b/src/cmd.rs index de7e46e6..c23d499e 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -152,6 +152,7 @@ pub enum SwarmCmd { GetApiToken, SetGlobalMemLimit(u64), GetSignedInUserDetails, + GetAllImageActualVersion, } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/src/dock.rs b/src/dock.rs index 40fd9bb5..89bcc82c 100644 --- a/src/dock.rs +++ b/src/dock.rs @@ -21,14 +21,16 @@ use serde::Deserialize; use serde::Serialize; use std::default::Default; use std::error::Error; +use std::time::Duration; use tokio::io::AsyncReadExt; use crate::backup::bucket_name; use crate::builder::{find_img, make_client}; -use crate::config::{State, STATE}; +use crate::config::{Node, State, STATE}; use crate::images::{DockerConfig, DockerHubImage}; use crate::mount_backedup_volume::download_from_s3; use crate::utils::{domain, getenv, sleep_ms}; +use bollard::models::ImageInspect; use tokio::fs::File; pub fn dockr() -> Docker { @@ -538,6 +540,31 @@ pub struct GetImageDigestResponse { pub message: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct GetImageActualVersionResponse { + pub success: bool, + pub data: Option>, + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DockerHubImageResult { + pub name: String, + pub digest: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DockerHubResponse { + pub results: Vec, + pub next: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ImageVersion { + pub name: String, + pub version: String, +} + // get image digest pub async fn get_image_digest(image_name: &str) -> Result { let docker = Docker::connect_with_local_defaults()?; @@ -565,6 +592,143 @@ pub async fn get_image_digest(image_name: &str) -> Result) -> Result { + let docker = Docker::connect_with_local_defaults()?; + + let mut images_version: Vec = Vec::new(); + + for node in nodes.iter() { + let node_name = node.name(); + let host = domain(&node_name); + let image_id = get_image_id(&docker, &host).await; + if image_id.is_empty() { + images_version.push(ImageVersion { + name: node_name, + version: "unavaliable".to_string(), + }); + continue; + } + + let digest = get_image_digest_from_image_id(&docker, &image_id).await; + if digest.is_empty() { + log::error!("Error getting {} image digest", node_name); + images_version.push(ImageVersion { + name: node_name, + version: "unavaliable".to_string(), + }); + continue; + } + + let digest_parts: Vec<&str> = digest.split("@").collect(); + + let mut image_name = digest_parts[0].to_string(); + let checksome = digest_parts[1]; + + let node_image = find_img(&node_name, nodes)?; + + if node_image.repo().org == "library" { + image_name = format!("{}/{}", node_image.repo().org, image_name); + } + + let version = get_image_version_from_digest(&image_name, checksome).await; + + images_version.push(ImageVersion { + name: node_name, + version: version, + }); + } + + Ok(GetImageActualVersionResponse { + success: true, + message: "image actual versions".to_string(), + data: Some(images_version), + }) +} + +async fn get_image_id(docker: &Docker, container_name: &str) -> String { + match docker.inspect_container(container_name, None).await { + Ok(container_info) => { + if container_info.image.is_none() { + return "".to_string(); + } + return container_info.image.unwrap(); + } + Err(err) => { + log::error!("Container image is unavailable: {:?}", err); + return "".to_string(); + } + } +} + +async fn get_image_digest_from_image_id(docker: &Docker, image_id: &str) -> String { + let image_info: ImageInspect = match docker.inspect_image(image_id).await { + Ok(res) => res, + Err(err) => { + log::error!("Error getting name and digest: {:?}", err); + return "".to_string(); + } + }; + + let image_digest = image_info + .repo_digests + .as_ref() + .and_then(|digests| digests.first().cloned()); + + if image_digest.is_none() { + return "".to_string(); + } + + image_digest.unwrap() +} + +pub async fn get_image_version_from_digest(image_name: &str, digest: &str) -> String { + let docker_url = format!( + "https://hub.docker.com/v2/repositories/{}/tags?page=1&page_size=100", + image_name, + ); + let version = get_version_from_docker_hub(&docker_url, digest).await; + version +} + +pub async fn get_version_from_docker_hub(docker_url: &str, digest: &str) -> String { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(40)) + .danger_accept_invalid_certs(true) + .build() + .expect("couldnt build swarm updater reqwest client"); + + let response = match client.get(docker_url).send().await { + Ok(res) => res, + Err(err) => { + log::error!("Error making request to {}: {:?}", docker_url, err); + return "".to_string(); + } + }; + + let response_json: DockerHubResponse = match response.json().await { + Ok(parsed) => parsed, + Err(err) => { + log::error!("Error parsing response from {}: {:?}", docker_url, err); + return "".to_string(); + } + }; + + for image in response_json.results { + if let Some(image_digest) = image.digest { + if image_digest == digest && image.name != "latest" { + return image.name; + } + } + } + if let Some(next_url) = response_json.next { + if !next_url.is_empty() { + return Box::pin(get_version_from_docker_hub(&next_url, digest)).await; + } + } + + return "".to_string(); +} + pub async fn prune_images(docker: &Docker) { let mut filters = HashMap::new(); // By setting dangling to `["false"]`, it will consider both dangling and non-dangling images diff --git a/src/handler.rs b/src/handler.rs index c89a6a2e..918c8a0d 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -51,6 +51,7 @@ fn access(cmd: &Cmd, state: &State, user_id: &Option) -> bool { SwarmCmd::ListContainers => true, SwarmCmd::UpdateNode(_) => true, SwarmCmd::RestartContainer(_) => true, + SwarmCmd::GetAllImageActualVersion => true, _ => false, }, _ => false, @@ -375,6 +376,11 @@ pub async fn handle( let tags = get_image_tags(image_details).await?; return Ok(serde_json::to_string(&tags)?); } + SwarmCmd::GetAllImageActualVersion => { + log::info!("Get all Image actual version"); + let image_versions = get_image_actual_version(&state.stack.nodes).await?; + return Ok(serde_json::to_string(&image_versions)?); + } SwarmCmd::UpdateUser(body) => { log::info!("Update users details ===> {:?}", body); let boltwall = find_boltwall(&state.stack.nodes)?;