Clean up
This commit is contained in:
parent
3f60b72c3b
commit
b414c05400
18 changed files with 905 additions and 985 deletions
|
|
@ -2,8 +2,13 @@ use crate::registry::ImageRef;
|
|||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
use std::fs;
|
||||
use std::sync::LazyLock;
|
||||
use tracing::{info, warn};
|
||||
|
||||
static IMAGE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r#"^\s*image:\s*(?:["']([^"']+)["']|([^\s#]+))\s*(#.*)?$"#).unwrap()
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceImage {
|
||||
pub service_name: String,
|
||||
|
|
@ -27,7 +32,6 @@ impl ComposeParser {
|
|||
pub fn parse_file(&self, file_path: &str) -> Result<ComposeFile> {
|
||||
let content = fs::read_to_string(file_path)?;
|
||||
let services = self.extract_services(&content)?;
|
||||
|
||||
Ok(ComposeFile { content, services })
|
||||
}
|
||||
|
||||
|
|
@ -36,41 +40,77 @@ impl ComposeParser {
|
|||
let lines: Vec<&str> = content.lines().collect();
|
||||
let mut in_services = false;
|
||||
let mut current_service: Option<String> = None;
|
||||
let image_regex = Regex::new(r#"^\s*image:\s*(?:["']([^"']+)["']|([^\s#]+))\s*(#.*)?$"#)?;
|
||||
let mut service_indent: Option<usize> = None;
|
||||
let mut service_child_indent: Option<usize> = None;
|
||||
|
||||
for (line_number, line) in lines.iter().enumerate() {
|
||||
if line.trim_start().starts_with("services:") {
|
||||
in_services = true;
|
||||
let trimmed = line.trim_start();
|
||||
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_services
|
||||
&& line.chars().next().is_some_and(|c| c.is_alphabetic())
|
||||
&& !line.starts_with(" ")
|
||||
{
|
||||
let indent = line.len() - trimmed.len();
|
||||
|
||||
if indent == 0 && trimmed.starts_with("services:") {
|
||||
in_services = true;
|
||||
current_service = None;
|
||||
service_indent = None;
|
||||
service_child_indent = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_services && indent == 0 {
|
||||
in_services = false;
|
||||
current_service = None;
|
||||
service_indent = None;
|
||||
service_child_indent = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_services {
|
||||
if let Some(service_name) = self.extract_service_name(line) {
|
||||
current_service = Some(service_name);
|
||||
continue;
|
||||
}
|
||||
if !in_services {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(ref service_name) = current_service {
|
||||
if let Some(image_ref) = self.extract_image_from_line(line, &image_regex)? {
|
||||
info!(
|
||||
"Found service '{}' with image '{}'",
|
||||
service_name, image_ref
|
||||
);
|
||||
services.push(ServiceImage {
|
||||
service_name: service_name.clone(),
|
||||
image_ref,
|
||||
original_line: line.to_string(),
|
||||
line_number,
|
||||
});
|
||||
if (service_indent.is_none() || indent == service_indent.unwrap())
|
||||
&& trimmed.ends_with(':')
|
||||
&& !trimmed.contains(' ')
|
||||
&& trimmed
|
||||
.trim_end_matches(':')
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
|
||||
{
|
||||
let name = trimmed.trim_end_matches(':');
|
||||
if !name.is_empty() {
|
||||
service_indent = Some(indent);
|
||||
service_child_indent = None;
|
||||
current_service = Some(name.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(ref service_name) = current_service {
|
||||
if let Some(si) = service_indent {
|
||||
if indent > si {
|
||||
// The first key under a service fixes the direct-child
|
||||
// indent; only keys at that exact level are the service's
|
||||
// own properties. This avoids picking up an `image:` nested
|
||||
// under `build:` or other deeper mappings.
|
||||
let child_indent = *service_child_indent.get_or_insert(indent);
|
||||
if indent == child_indent {
|
||||
if let Some(image_ref) = self.extract_image_from_line(line)? {
|
||||
info!(
|
||||
"Found service '{}' with image '{}'",
|
||||
service_name, image_ref
|
||||
);
|
||||
services.push(ServiceImage {
|
||||
service_name: service_name.clone(),
|
||||
image_ref,
|
||||
original_line: line.to_string(),
|
||||
line_number,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,62 +119,37 @@ impl ComposeParser {
|
|||
Ok(services)
|
||||
}
|
||||
|
||||
fn extract_service_name(&self, line: &str) -> Option<String> {
|
||||
fn extract_image_from_line(&self, line: &str) -> Result<Option<ImageRef>> {
|
||||
let trimmed = line.trim_start();
|
||||
let indent_level = line.len() - trimmed.len();
|
||||
|
||||
if indent_level > 0 && indent_level <= 8 && trimmed.ends_with(':') && !trimmed.contains(' ')
|
||||
{
|
||||
let potential_service = trimmed.trim_end_matches(':');
|
||||
if !potential_service.is_empty()
|
||||
&& potential_service
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
|
||||
{
|
||||
return Some(potential_service.to_string());
|
||||
}
|
||||
if !trimmed.starts_with("image:") {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
let Some(captures) = IMAGE_REGEX.captures(line) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
fn extract_image_from_line(&self, line: &str, image_regex: &Regex) -> Result<Option<ImageRef>> {
|
||||
let trimmed = line.trim_start();
|
||||
let indent_level = line.len() - trimmed.len();
|
||||
let image_str = captures
|
||||
.get(1)
|
||||
.or_else(|| captures.get(2))
|
||||
.unwrap()
|
||||
.as_str();
|
||||
|
||||
if indent_level > 2 && trimmed.starts_with("image:") {
|
||||
if let Some(captures) = image_regex.captures(line) {
|
||||
let image_str = captures
|
||||
.get(1)
|
||||
.or_else(|| captures.get(2))
|
||||
.unwrap()
|
||||
.as_str();
|
||||
if image_str.contains('@') {
|
||||
info!("Skipping digest-pinned image: {}", image_str);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if image_str.find('@').is_some() {
|
||||
info!("Found digest in image string, skipping: {}", image_str);
|
||||
return Ok(None);
|
||||
}
|
||||
if image_str.trim().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if image_str.trim().is_empty()
|
||||
|| image_str.trim() == "\"\""
|
||||
|| image_str.trim() == "''"
|
||||
{
|
||||
info!("Empty image string found in line: {}, skipping", line);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match ImageRef::parse(image_str) {
|
||||
Ok(image_ref) => Ok(Some(image_ref)),
|
||||
Err(e) => {
|
||||
warn!("Failed to parse image '{}': {}", image_str, e);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match ImageRef::parse(image_str) {
|
||||
Ok(image_ref) => Ok(Some(image_ref)),
|
||||
Err(e) => {
|
||||
warn!("Failed to parse image '{}': {}", image_str, e);
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -160,12 +175,12 @@ services:
|
|||
image: nginx:1.21.0 # Web server
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
|
||||
db:
|
||||
image: postgres:13.7
|
||||
environment:
|
||||
POSTGRES_DB: myapp
|
||||
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine
|
||||
"#;
|
||||
|
|
@ -183,4 +198,52 @@ services:
|
|||
assert_eq!(result.services[0].image_ref.name, "nginx");
|
||||
assert_eq!(result.services[0].image_ref.tag, "1.21.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_names_with_dots() {
|
||||
let compose_content = r#"
|
||||
services:
|
||||
my.service:
|
||||
image: nginx:1.21.0
|
||||
another-service:
|
||||
image: redis:7.0
|
||||
"#;
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
temp_file.write_all(compose_content.as_bytes()).unwrap();
|
||||
|
||||
let parser = ComposeParser::new();
|
||||
let result = parser
|
||||
.parse_file(temp_file.path().to_str().unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.services.len(), 2);
|
||||
assert_eq!(result.services[0].service_name, "my.service");
|
||||
assert_eq!(result.services[1].service_name, "another-service");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignores_image_nested_under_build() {
|
||||
let compose_content = r#"
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
image: should-not-match:9.9.9
|
||||
image: nginx:1.21.0
|
||||
"#;
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
temp_file.write_all(compose_content.as_bytes()).unwrap();
|
||||
|
||||
let parser = ComposeParser::new();
|
||||
let result = parser
|
||||
.parse_file(temp_file.path().to_str().unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.services.len(), 1);
|
||||
assert_eq!(result.services[0].service_name, "web");
|
||||
assert_eq!(result.services[0].image_ref.name, "nginx");
|
||||
assert_eq!(result.services[0].image_ref.tag, "1.21.0");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue