use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
pub struct InstructionPos {
pub instruction: Instruction,
pub line_number: u32,
pub source_text: String,
}
impl InstructionPos {
pub fn new(instruction: Instruction, line_number: u32, source_text: String) -> Self {
Self {
instruction,
line_number,
source_text,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Instruction {
From(BaseImage),
Run(RunArgs),
Copy(CopyArgs, CopyFlags),
Add(AddArgs, AddFlags),
Env(Vec<(String, String)>),
Label(Vec<(String, String)>),
Expose(Vec<Port>),
Arg(String, Option<String>),
Entrypoint(Arguments),
Cmd(Arguments),
Shell(Arguments),
User(String),
Workdir(String),
Volume(String),
Maintainer(String),
Healthcheck(HealthCheck),
OnBuild(Box<Instruction>),
Stopsignal(String),
Comment(String),
}
impl Instruction {
pub fn is_from(&self) -> bool {
matches!(self, Self::From(_))
}
pub fn is_run(&self) -> bool {
matches!(self, Self::Run(_))
}
pub fn is_copy(&self) -> bool {
matches!(self, Self::Copy(_, _))
}
pub fn is_onbuild(&self) -> bool {
matches!(self, Self::OnBuild(_))
}
pub fn unwrap_onbuild(&self) -> Option<&Instruction> {
match self {
Self::OnBuild(inner) => Some(inner.as_ref()),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BaseImage {
pub image: Image,
pub tag: Option<String>,
pub digest: Option<String>,
pub alias: Option<ImageAlias>,
pub platform: Option<String>,
}
impl BaseImage {
pub fn new(name: impl Into<String>) -> Self {
Self {
image: Image::new(name),
tag: None,
digest: None,
alias: None,
platform: None,
}
}
pub fn is_variable(&self) -> bool {
self.image.name.starts_with('$')
}
pub fn is_scratch(&self) -> bool {
self.image.name.eq_ignore_ascii_case("scratch")
}
pub fn has_version(&self) -> bool {
self.tag.is_some() || self.digest.is_some()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Image {
pub registry: Option<String>,
pub name: String,
}
impl Image {
pub fn new(name: impl Into<String>) -> Self {
Self {
registry: None,
name: name.into(),
}
}
pub fn with_registry(registry: impl Into<String>, name: impl Into<String>) -> Self {
Self {
registry: Some(registry.into()),
name: name.into(),
}
}
pub fn full_name(&self) -> String {
match &self.registry {
Some(reg) => format!("{}/{}", reg, self.name),
None => self.name.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ImageAlias(pub String);
impl ImageAlias {
pub fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RunArgs {
pub arguments: Arguments,
pub flags: RunFlags,
}
impl RunArgs {
pub fn shell(cmd: impl Into<String>) -> Self {
Self {
arguments: Arguments::Text(cmd.into()),
flags: RunFlags::default(),
}
}
pub fn exec(args: Vec<String>) -> Self {
Self {
arguments: Arguments::List(args),
flags: RunFlags::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct RunFlags {
pub mount: HashSet<RunMount>,
pub network: Option<String>,
pub security: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RunMount {
Bind(BindOpts),
Cache(CacheOpts),
Tmpfs(TmpOpts),
Secret(SecretOpts),
Ssh(SshOpts),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct BindOpts {
pub target: Option<String>,
pub source: Option<String>,
pub from: Option<String>,
pub read_only: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct CacheOpts {
pub target: Option<String>,
pub id: Option<String>,
pub sharing: Option<String>,
pub from: Option<String>,
pub source: Option<String>,
pub mode: Option<String>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub read_only: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct TmpOpts {
pub target: Option<String>,
pub size: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct SecretOpts {
pub id: Option<String>,
pub target: Option<String>,
pub required: bool,
pub mode: Option<String>,
pub uid: Option<u32>,
pub gid: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct SshOpts {
pub id: Option<String>,
pub target: Option<String>,
pub required: bool,
pub mode: Option<String>,
pub uid: Option<u32>,
pub gid: Option<u32>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CopyArgs {
pub sources: Vec<String>,
pub dest: String,
}
impl CopyArgs {
pub fn new(sources: Vec<String>, dest: impl Into<String>) -> Self {
Self {
sources,
dest: dest.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct CopyFlags {
pub from: Option<String>,
pub chown: Option<String>,
pub chmod: Option<String>,
pub link: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AddArgs {
pub sources: Vec<String>,
pub dest: String,
}
impl AddArgs {
pub fn new(sources: Vec<String>, dest: impl Into<String>) -> Self {
Self {
sources,
dest: dest.into(),
}
}
pub fn has_url(&self) -> bool {
self.sources
.iter()
.any(|s| s.starts_with("http://") || s.starts_with("https://"))
}
pub fn has_archive(&self) -> bool {
const ARCHIVE_EXTENSIONS: &[&str] = &[
".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".tar.xz", ".txz", ".zip", ".gz",
".bz2", ".xz", ".Z", ".lz", ".lzma",
];
self.sources
.iter()
.any(|s| ARCHIVE_EXTENSIONS.iter().any(|ext| s.ends_with(ext)))
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct AddFlags {
pub chown: Option<String>,
pub chmod: Option<String>,
pub link: bool,
pub checksum: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Port {
pub number: u16,
pub protocol: PortProtocol,
}
impl Port {
pub fn tcp(number: u16) -> Self {
Self {
number,
protocol: PortProtocol::Tcp,
}
}
pub fn udp(number: u16) -> Self {
Self {
number,
protocol: PortProtocol::Udp,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum PortProtocol {
#[default]
Tcp,
Udp,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Arguments {
Text(String),
List(Vec<String>),
}
impl Arguments {
pub fn is_shell_form(&self) -> bool {
matches!(self, Self::Text(_))
}
pub fn is_exec_form(&self) -> bool {
matches!(self, Self::List(_))
}
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(s) => Some(s),
_ => None,
}
}
pub fn as_list(&self) -> Option<&[String]> {
match self {
Self::List(v) => Some(v),
_ => None,
}
}
pub fn to_string_lossy(&self) -> String {
match self {
Self::Text(s) => s.clone(),
Self::List(v) => v.join(" "),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum HealthCheck {
None,
Cmd {
cmd: Arguments,
interval: Option<String>,
timeout: Option<String>,
start_period: Option<String>,
retries: Option<u32>,
},
}
impl HealthCheck {
pub fn cmd(cmd: Arguments) -> Self {
Self::Cmd {
cmd,
interval: None,
timeout: None,
start_period: None,
retries: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base_image() {
let img = BaseImage::new("ubuntu");
assert!(!img.is_scratch());
assert!(!img.is_variable());
assert!(!img.has_version());
let scratch = BaseImage::new("scratch");
assert!(scratch.is_scratch());
let var = BaseImage::new("${BASE_IMAGE}");
assert!(var.is_variable());
let tagged = BaseImage {
tag: Some("20.04".to_string()),
..BaseImage::new("ubuntu")
};
assert!(tagged.has_version());
}
#[test]
fn test_image() {
let img = Image::new("ubuntu");
assert_eq!(img.full_name(), "ubuntu");
let img_with_reg = Image::with_registry("gcr.io", "my-project/my-image");
assert_eq!(img_with_reg.full_name(), "gcr.io/my-project/my-image");
}
#[test]
fn test_arguments() {
let shell = Arguments::Text("apt-get update".to_string());
assert!(shell.is_shell_form());
assert_eq!(shell.as_text(), Some("apt-get update"));
let exec = Arguments::List(vec!["apt-get".to_string(), "update".to_string()]);
assert!(exec.is_exec_form());
assert_eq!(
exec.as_list(),
Some(&["apt-get".to_string(), "update".to_string()][..])
);
}
#[test]
fn test_add_args() {
let add = AddArgs::new(vec!["app.tar.gz".to_string()], "/app");
assert!(add.has_archive());
assert!(!add.has_url());
let add_url = AddArgs::new(vec!["https://example.com/file.txt".to_string()], "/app");
assert!(add_url.has_url());
assert!(!add_url.has_archive());
}
}