Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bin/core/src/api/read/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,14 @@ impl Resolve<ReadArgs> for GetResourceMatchingContainer {
for StackServiceNames {
service_name,
container_name,
compose_service,
..
} in stack
.info
.deployed_services
.unwrap_or(stack.info.latest_services)
{
let is_match = match compose_container_match_regex(&container_name)
let is_match = match compose_container_match_regex(&container_name, compose_service.is_some())
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Expand Down
95 changes: 70 additions & 25 deletions bin/core/src/api/read/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ use mogh_error::AddStatusCodeError as _;
use mogh_resolver::Resolve;
use periphery_client::api::{
compose::{GetComposeLog, GetComposeLogSearch},
container::InspectContainer,
container::{
GetContainerLog, GetContainerLogSearch, InspectContainer,
},
};
use reqwest::StatusCode;

Expand All @@ -31,7 +33,10 @@ use crate::{
},
permission::get_check_permissions,
resource,
stack::setup_stack_execution,
stack::{
services::{single_replica_container, to_compose_services},
setup_stack_execution,
},
state::{action_states, stack_status_cache},
};

Expand Down Expand Up @@ -312,16 +317,33 @@ impl Resolve<ReadArgs> for GetStackLog {
.await
.context("Failed to get stack service log from swarm")?
}
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(GetComposeLog {
project: stack.project_name(false),
services,
tail,
timestamps,
})
.await
.context("Failed to get stack log from periphery")?,
SwarmOrServer::Server(server) => {
let periphery = periphery_client(&server).await?;
// A single replica of a compose service with `deploy.replicas > 1`
// is not a compose service, so get its container log directly.
if let Some(name) =
single_replica_container(&stack, &services)
{
periphery
.request(GetContainerLog {
name,
tail,
timestamps,
})
.await
.context("Failed to get stack log from periphery")?
} else {
periphery
.request(GetComposeLog {
project: stack.project_name(false),
services: to_compose_services(&stack, services),
tail,
timestamps,
})
.await
.context("Failed to get stack log from periphery")?
}
}
};

Ok(log)
Expand Down Expand Up @@ -359,7 +381,11 @@ impl Resolve<ReadArgs> for SearchStackLog {
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLogSearch {
service,
// The actual service name on swarm will be stackname_servicename
service: format!(
"{}_{service}",
stack.project_name(false)
),
terms,
combinator,
invert,
Expand All @@ -372,18 +398,37 @@ impl Resolve<ReadArgs> for SearchStackLog {
.await
.context("Failed to get stack service log from swarm")?
}
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(GetComposeLogSearch {
project: stack.project_name(false),
services,
terms,
combinator,
invert,
timestamps,
})
.await
.context("Failed to search stack log from periphery")?,
SwarmOrServer::Server(server) => {
let periphery = periphery_client(&server).await?;
// A single replica of a compose service with `deploy.replicas > 1`
// is not a compose service, so search its container log directly.
if let Some(name) =
single_replica_container(&stack, &services)
{
periphery
.request(GetContainerLogSearch {
name,
terms,
combinator,
invert,
timestamps,
})
.await
.context("Failed to search stack log from periphery")?
} else {
periphery
.request(GetComposeLogSearch {
project: stack.project_name(false),
services: to_compose_services(&stack, services),
terms,
combinator,
invert,
timestamps,
})
.await
.context("Failed to search stack log from periphery")?
}
}
};

Ok(log)
Expand Down
4 changes: 2 additions & 2 deletions bin/core/src/helpers/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ pub fn get_stack_state_from_containers(
})
.collect::<Vec<_>>();
let containers = containers.iter().filter(|container| {
services.iter().any(|StackServiceNames { service_name, container_name, .. }| {
match compose_container_match_regex(container_name)
services.iter().any(|StackServiceNames { service_name, container_name, compose_service, .. }| {
match compose_container_match_regex(container_name, compose_service.is_some())
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Expand Down
4 changes: 2 additions & 2 deletions bin/core/src/monitor/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,10 @@ pub async fn update_server_stack_cache(
let stack_status_cache = stack_status_cache();
for stack in stacks {
let services = extract_services_from_stack(&stack);
let mut services_with_containers = services.iter().map(|StackServiceNames { service_name, container_name, image, .. }| {
let mut services_with_containers = services.iter().map(|StackServiceNames { service_name, container_name, image, compose_service, .. }| {
// Get the container associated with service.
let container = containers.iter().find(|container| {
match compose_container_match_regex(container_name)
match compose_container_match_regex(container_name, compose_service.is_some())
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Expand Down
32 changes: 31 additions & 1 deletion bin/core/src/stack/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,41 @@ pub async fn setup_stack_execution(
Ok((stack, swarm_or_server))
}

/// `exact` should be set for the entries Komodo lists per replica
/// of a compose service with `deploy.replicas > 1`, whose `container_name`
/// already includes the replica number. Otherwise eg `web-1` would also
/// match the containers of replicas `web-10`, `web-11`, ...
pub fn compose_container_match_regex(
container_name: &str,
exact: bool,
) -> anyhow::Result<Regex> {
let regex = format!("^{container_name}-?[0-9]*$");
let regex = if exact {
format!("^{container_name}$")
} else {
format!("^{container_name}-?[0-9]*$")
};
Regex::new(&regex).with_context(|| {
format!("failed to construct valid regex from {regex}")
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn container_match_regex() {
let regex =
compose_container_match_regex("proj-web", false).unwrap();
assert!(regex.is_match("proj-web"));
assert!(regex.is_match("proj-web-1"));
assert!(regex.is_match("proj-web-12"));

// Replica entries already include the replica number.
let regex =
compose_container_match_regex("proj-web-1", true).unwrap();
assert!(regex.is_match("proj-web-1"));
assert!(!regex.is_match("proj-web-10"));
assert!(!regex.is_match("proj-web-1-1"));
}
}
96 changes: 96 additions & 0 deletions bin/core/src/stack/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,46 @@ pub fn extract_services_from_stack(
}
}

/// If `services` is a single entry Komodo lists for one replica
/// of a compose service with `deploy.replicas > 1` (eg `web-2`),
/// returns the exact container name of that replica.
pub fn single_replica_container(
stack: &Stack,
services: &[String],
) -> Option<String> {
let [service] = services else {
return None;
};
extract_services_from_stack(stack)
.into_iter()
.find(|s| {
&s.service_name == service && s.compose_service.is_some()
})
.map(|s| s.container_name)
}

/// Maps the names Komodo lists for replicas of a compose service
/// with `deploy.replicas > 1` (eg `web-2`) back to the actual
/// compose service name (`web`), for use with `docker compose`.
pub fn to_compose_services(
stack: &Stack,
services: Vec<String>,
) -> Vec<String> {
let known = extract_services_from_stack(stack);
let mut res = Vec::with_capacity(services.len());
for service in services {
let service = known
.iter()
.find(|s| s.service_name == service)
.and_then(|s| s.compose_service.clone())
.unwrap_or(service);
if !res.contains(&service) {
res.push(service);
}
}
res
}

pub fn extract_services_into_res(
project_name: &str,
compose_contents: &str,
Expand Down Expand Up @@ -67,9 +107,65 @@ pub fn extract_services_into_res(
.cloned(),
image: image.unwrap_or_default(),
service_name,
compose_service: None,
});
}
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

fn replica_stack() -> Stack {
let mut stack = Stack::default();
let service =
|name: &str, compose_service: Option<&str>| StackServiceNames {
service_name: name.to_string(),
container_name: format!("proj-{name}"),
compose_service: compose_service.map(str::to_string),
..Default::default()
};
stack.info.deployed_services = Some(vec![
service("web-1", Some("web")),
service("web-2", Some("web")),
service("db", None),
]);
stack
}

#[test]
fn single_replica_resolves_container() {
let stack = replica_stack();
assert_eq!(
single_replica_container(&stack, &["web-2".to_string()])
.as_deref(),
Some("proj-web-2")
);
assert_eq!(
single_replica_container(&stack, &["db".to_string()]),
None
);
assert_eq!(
single_replica_container(
&stack,
&["web-1".to_string(), "web-2".to_string()]
),
None
);
}

#[test]
fn replicas_map_to_compose_service() {
let stack = replica_stack();
let services = ["web-1", "web-2", "db", "unknown"]
.map(str::to_string)
.to_vec();
assert_eq!(
to_compose_services(&stack, services),
["web", "db", "unknown"]
);
}
}
1 change: 1 addition & 0 deletions bin/periphery/src/api/swarm/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ impl Resolve<crate::api::Args> for DeploySwarmStack {
service_name,
image,
image_digest: None,
compose_service: None,
});
}
}
Expand Down
42 changes: 42 additions & 0 deletions bin/periphery/src/docker/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ pub fn parse_compose_services(
service_name: format!("{service_name}-{i}"),
image: image.clone(),
image_digest: None,
compose_service: Some(service_name.clone()),
});
}
}
Expand All @@ -111,10 +112,51 @@ pub fn parse_compose_services(
service_name,
image,
image_digest: None,
compose_service: None,
});
}
}
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn replicas_record_compose_service() {
let config = r#"
services:
web:
image: nginx
deploy:
replicas: 2
db:
image: postgres
"#;
let mut services = Vec::new();
parse_compose_services(config, "proj", &mut services).unwrap();
services.sort_by(|a, b| a.service_name.cmp(&b.service_name));

let names = services
.iter()
.map(|s| {
(
s.service_name.as_str(),
s.container_name.as_str(),
s.compose_service.as_deref(),
)
})
.collect::<Vec<_>>();
assert_eq!(
names,
[
("db", "proj-db", None),
("web-1", "proj-web-1", Some("web")),
("web-2", "proj-web-2", Some("web")),
]
);
}
}
Loading