summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/dockerfile.rs263
-rw-r--r--src/main.rs78
2 files changed, 326 insertions, 15 deletions
diff --git a/src/dockerfile.rs b/src/dockerfile.rs
new file mode 100644
index 0000000..0449805
--- /dev/null
+++ b/src/dockerfile.rs
@@ -0,0 +1,263 @@
+use std::ops::Range;
+
+#[derive(Debug, PartialEq)]
+pub struct DockerfileFile {
+ pub image_refs: Vec<Range<usize>>,
+}
+
+impl DockerfileFile {
+ pub fn parse(content: &str) -> Self {
+ let bytes = content.as_bytes();
+ let mut image_refs = Vec::new();
+ let mut pos = 0;
+
+ while pos < bytes.len() {
+ pos = parse_line(bytes, pos, &mut image_refs);
+ }
+
+ return DockerfileFile { image_refs };
+ }
+}
+
+fn parse_line(bytes: &[u8], pos: usize, image_refs: &mut Vec<Range<usize>>) -> usize {
+ let pos = skip_whitespace(bytes, pos);
+
+ if let Some(end) = parse_comment(bytes, pos) {
+ return end;
+ }
+
+ if let Some(end) = parse_from_instruction(bytes, pos, image_refs) {
+ return end;
+ }
+
+ return skip_to_next_line(bytes, pos);
+}
+
+fn skip_whitespace(bytes: &[u8], mut pos: usize) -> usize {
+ while pos < bytes.len() && is_whitespace(bytes[pos]) {
+ pos += 1;
+ }
+ return pos;
+}
+
+fn skip_whitespace_and_continuations(bytes: &[u8], mut pos: usize) -> usize {
+ loop {
+ while pos < bytes.len() && is_whitespace(bytes[pos]) {
+ pos += 1;
+ }
+
+ if pos < bytes.len() && bytes[pos] == b'\\' {
+ let next = pos + 1;
+ if next < bytes.len() && bytes[next] == b'\n' {
+ pos = next + 1;
+ continue;
+ }
+ if next < bytes.len() && bytes[next] == b'\r' {
+ pos = next + 1;
+ if pos < bytes.len() && bytes[pos] == b'\n' {
+ pos += 1;
+ }
+ continue;
+ }
+ }
+
+ break;
+ }
+ return pos;
+}
+
+fn is_whitespace(b: u8) -> bool {
+ return b == b' ' || b == b'\t';
+}
+
+fn parse_comment(bytes: &[u8], pos: usize) -> Option<usize> {
+ if pos < bytes.len() && bytes[pos] == b'#' {
+ return Some(skip_to_next_line(bytes, pos));
+ } else {
+ return None;
+ }
+}
+
+fn skip_to_next_line(bytes: &[u8], mut pos: usize) -> usize {
+ while pos < bytes.len() {
+ if bytes[pos] == b'\n' {
+ return pos + 1;
+ }
+ if bytes[pos] == b'\r' {
+ pos += 1;
+ if pos < bytes.len() && bytes[pos] == b'\n' {
+ return pos + 1;
+ }
+ return pos;
+ }
+ pos += 1;
+ }
+ return pos;
+}
+
+fn parse_keyword_ci(bytes: &[u8], pos: usize, keyword: &[u8]) -> Option<usize> {
+ if pos + keyword.len() > bytes.len() {
+ return None;
+ }
+
+ for (i, &k) in keyword.iter().enumerate() {
+ if !bytes[pos + i].eq_ignore_ascii_case(&k) {
+ return None;
+ }
+ }
+
+ return Some(pos + keyword.len());
+}
+
+fn parse_from_instruction(
+ bytes: &[u8],
+ pos: usize,
+ image_refs: &mut Vec<Range<usize>>,
+) -> Option<usize> {
+ let pos = parse_keyword_ci(bytes, pos, b"FROM")?;
+
+ // Must have at least one whitespace (or continuation) after FROM
+ if pos >= bytes.len() || (!is_whitespace(bytes[pos]) && bytes[pos] != b'\\') {
+ return None;
+ }
+ let pos = skip_whitespace_and_continuations(bytes, pos);
+
+ let pos = skip_flags(bytes, pos);
+
+ let (image_start, image_end) = parse_image_ref(bytes, pos)?;
+
+ if image_start < image_end {
+ image_refs.push(image_start..image_end);
+ }
+
+ return Some(skip_to_next_line(bytes, pos));
+}
+
+fn skip_flags(bytes: &[u8], mut pos: usize) -> usize {
+ while pos + 1 < bytes.len() && bytes[pos] == b'-' && bytes[pos + 1] == b'-' {
+ pos += 2;
+ while pos < bytes.len()
+ && !is_whitespace(bytes[pos])
+ && bytes[pos] != b'\n'
+ && bytes[pos] != b'\\'
+ {
+ pos += 1;
+ }
+ pos = skip_whitespace_and_continuations(bytes, pos);
+ }
+ return pos;
+}
+
+fn parse_image_ref(bytes: &[u8], pos: usize) -> Option<(usize, usize)> {
+ let start = pos;
+ let mut end = pos;
+
+ while end < bytes.len()
+ && bytes[end] != b'\n'
+ && bytes[end] != b'\r'
+ && !is_whitespace(bytes[end])
+ {
+ end += 1;
+ }
+
+ if end > start {
+ return Some((start, end));
+ } else {
+ return None;
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn simple_from() {
+ let result = DockerfileFile::parse("FROM nginx:1.21");
+ assert_eq!(result.image_refs, vec![5..15]);
+ }
+
+ #[test]
+ fn from_with_registry() {
+ let result = DockerfileFile::parse("FROM reg.io/lib/nginx:1.21");
+ assert_eq!(result.image_refs, vec![5..26]);
+ }
+
+ #[test]
+ fn from_with_digest() {
+ let result = DockerfileFile::parse("FROM nginx@sha256:abc123");
+ assert_eq!(result.image_refs, vec![5..24]);
+ }
+
+ #[test]
+ fn from_without_tag() {
+ let result = DockerfileFile::parse("FROM nginx");
+ assert_eq!(result.image_refs, vec![5..10]);
+ }
+
+ #[test]
+ fn ends_in_from() {
+ let result = DockerfileFile::parse("FRO");
+ assert!(result.image_refs.is_empty());
+ }
+
+ #[test]
+ fn from_with_as_alias() {
+ let result = DockerfileFile::parse("FROM nginx:1.21 AS build");
+ assert_eq!(result.image_refs, vec![5..15]);
+ }
+
+ #[test]
+ fn from_with_flag() {
+ let result = DockerfileFile::parse("FROM --platform=linux/amd64 nginx:1.21");
+ assert_eq!(result.image_refs, vec![28..38]);
+ }
+
+ #[test]
+ fn multiple_from_statements() {
+ let result = DockerfileFile::parse("FROM nginx:1.21\nRUN echo\nFROM nginx:1.21");
+ assert_eq!(result.image_refs, vec![5..15, 30..40]);
+ }
+
+ #[test]
+ fn skips_comments() {
+ let result = DockerfileFile::parse("# comment\nFROM nginx:1.21");
+ assert_eq!(result.image_refs, vec![15..25]);
+ }
+
+ #[test]
+ fn case_insensitive() {
+ let result = DockerfileFile::parse("from nginx:1.21");
+ assert_eq!(result.image_refs, vec![5..15]);
+ }
+
+ #[test]
+ fn leading_whitespace() {
+ let result = DockerfileFile::parse(" FROM nginx:1.21");
+ assert_eq!(result.image_refs, vec![7..17]);
+ }
+
+ #[test]
+ fn line_continuation() {
+ let result = DockerfileFile::parse("FROM \\\n nginx:1.21");
+ assert_eq!(result.image_refs, vec![9..19]);
+ }
+
+ #[test]
+ fn crlf_continuation() {
+ let result = DockerfileFile::parse("FROM \\\r\n nginx:1.21");
+ assert_eq!(result.image_refs, vec![10..20]);
+ }
+
+ #[test]
+ fn empty_file() {
+ let result = DockerfileFile::parse("");
+ assert!(result.image_refs.is_empty());
+ }
+
+ #[test]
+ fn fromage_is_not_from() {
+ let result = DockerfileFile::parse("FROMAGE nginx:1.21");
+ assert!(result.image_refs.is_empty());
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index afb6e77..a4a731a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,11 +2,13 @@ mod parser;
mod version;
mod docker;
mod manifest;
+mod dockerfile;
use crate::parser::*;
use crate::version::{VersionPattern, CompareOutcome};
use crate::docker::DockerRef;
use crate::manifest::ManifestFile;
+use crate::dockerfile::DockerfileFile;
use base64::prelude::*;
use rand::distr::{Alphanumeric, SampleString};
@@ -148,7 +150,13 @@ impl AuthState{
}
}
-fn perform_registry_request(registry: &str, url: &str, accept: &'static str, auth: &mut Auth) -> Result<ureq::http::Response<ureq::Body>, String> {
+#[derive(Debug)]
+enum RegistryError {
+ NotFound,
+ Other(String),
+}
+
+fn perform_registry_request(registry: &str, url: &str, accept: &'static str, auth: &mut Auth) -> Result<ureq::http::Response<ureq::Body>, RegistryError> {
let mut state = auth.states.get_mut(registry);
let url = format!("https://{}{}", registry, &url);
@@ -169,23 +177,29 @@ fn perform_registry_request(registry: &str, url: &str, accept: &'static str, aut
if response.status() == 401 {
if !authentication_retry {
if let Some(ref mut state) = state {
- state.authenticate(&response)?;
+ state.authenticate(&response).map_err(RegistryError::Other)?;
authentication_retry = true;
continue;
} else {
- return Err(format!("Server {} returned 401 but we have no credentials", registry));
+ return Err(RegistryError::Other(format!("Server {} returned 401 but we have no credentials", registry)));
}
} else {
- return Err(format!("Authentication failed"));
+ return Err(RegistryError::Other(format!("Authentication failed")));
}
}
if response.status() == 429 {
- panic!();
+ println!("Too many requests");
+ std::thread::sleep(std::time::Duration::from_secs(8));
+ continue;
+ }
+
+ if response.status() == 404 {
+ return Err(RegistryError::NotFound);
}
if response.status() != 200 {
- return Err(format!("Unexpected status code: {}", response.status()))
+ return Err(RegistryError::Other(format!("Unexpected status code: {}", response.status())));
}
return Ok(response);
@@ -225,7 +239,7 @@ fn update_images(db: &Connection, file: &str, auth: &mut Auth, img: DockerRef, e
", (registry, &image, time)).unwrap();
time
},
- Err(_x) => panic!("Database Failure"),
+ Err(_x) => return Err("Database Failure".to_string()),
};
@@ -236,7 +250,14 @@ fn update_images(db: &Connection, file: &str, auth: &mut Auth, img: DockerRef, e
let mut url = format!("/v2/{}/tags/list", &image);
loop {
- let mut response = perform_registry_request(registry, &url, "application/vnd.oci.image.index.v1+json", auth).unwrap();
+ let mut response = match perform_registry_request(registry, &url, "application/vnd.oci.image.index.v1+json", auth) {
+ Ok(r) => r,
+ Err(RegistryError::NotFound) => {
+ println!("Warning: image not found, skipping: {}", image);
+ return Ok(());
+ },
+ Err(RegistryError::Other(msg)) => return Err(msg),
+ };
let content_type = response.headers()["Content-Type"].to_str().unwrap();
if !content_type.starts_with("application/json") {
@@ -286,7 +307,14 @@ fn update_images(db: &Connection, file: &str, auth: &mut Auth, img: DockerRef, e
if let Some(ref digest) = img.digest {
// Find the digest for the selected tag
let url = format!("/v2/{}/manifests/{}", &file[img.image.clone()], tag.as_deref().unwrap_or("latest"));
- let response = perform_registry_request(registry, &url, "application/vnd.oci.image.manifest.v1+json,application/vnd.oci.image.index.v1+json", auth).unwrap();
+ let response = match perform_registry_request(registry, &url, "application/vnd.oci.image.manifest.v1+json,application/vnd.oci.image.index.v1+json", auth) {
+ Ok(r) => r,
+ Err(RegistryError::NotFound) => {
+ println!("Warning: image not found, skipping: {}", image);
+ return Ok(());
+ },
+ Err(RegistryError::Other(msg)) => return Err(msg),
+ };
let digest_string = response.headers()["docker-content-digest"].to_str().unwrap().to_string();
@@ -373,6 +401,24 @@ impl std::io::Write for Output {
}
}
+fn is_dockerfile(path: &std::path::Path) -> bool {
+ if let Some(name) = path.file_name() {
+ if let Some(name) = name.to_str() {
+ return name == "Dockerfile" || name == "dockerfile";
+ }
+ }
+ return false
+}
+
+fn is_yaml(path: &std::path::Path) -> bool {
+ if let Some(ext) = path.extension() {
+ if let Some(ext) = ext.to_str() {
+ return ext == "yaml";
+ }
+ }
+ return false
+}
+
fn run_tool(db: &Connection, auth: &mut Auth, repos: &Vec<Repository>, mut infile_paths: Vec<std::path::PathBuf>, overwrite: bool) {
for repo in repos {
if repo.dest.exists() {
@@ -462,10 +508,8 @@ fn run_tool(db: &Connection, auth: &mut Auth, repos: &Vec<Repository>, mut infil
continue;
}
- if let Some(ext) = path.extension() {
- if ext == "yaml" {
- inpaths.push(path);
- }
+ if is_yaml(&path) || is_dockerfile(&path) {
+ inpaths.push(path);
}
}
}
@@ -479,12 +523,16 @@ fn run_tool(db: &Connection, auth: &mut Auth, repos: &Vec<Repository>, mut infil
let mut file_content = String::new();
file.read_to_string(&mut file_content).unwrap();
- let images = ManifestFile::parse(&file_content);
+ let images: Vec<std::ops::Range<usize>> = if is_dockerfile(&infile_path) {
+ DockerfileFile::parse(&file_content).image_refs
+ } else {
+ ManifestFile::parse(&file_content).image_tags
+ };
let mut failed = None;
let mut edits = vec![];
- for ref image in images.image_tags {
+ for ref image in images {
println!("Checking image {}", &file_content[image.clone()]);
let image_ref = DockerRef::parse(&file_content, image);
if let Err(msg) = update_images(db, &file_content, auth, image_ref, &mut edits) {