Skip to content

Commit

Permalink
added size policy
Browse files Browse the repository at this point in the history
  • Loading branch information
WhySoBad committed Feb 18, 2024
1 parent 3926815 commit 21afe53
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 20 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ cron = "0.12.0"
dyn-clone = "1.0.16"
serde_yaml = "0.9.29"
notify = { version = "6.1.1", default-features = false, features = ["serde", "macos_kqueue"] }
notify-debouncer-mini = { version = "0.4.1", default-features = false, features = ["serde"] }
notify-debouncer-mini = { version = "0.4.1", default-features = false, features = ["serde"] }
parse-size = "1.0.0"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ to open a new issue or contribute to an open issue!
- [x] Add tests to rule parsing and rule affections
- [ ] Add ping to registry container in instance creation
- [x] Add policy to match tags by pattern
- [ ] Add policy to match tags by size
- [x] Add policy to match tags by size

## Credits

Expand Down
19 changes: 19 additions & 0 deletions docs/policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,25 @@ Any valid regex (supported by this crate) can be used.
tag.pattern: .+-(beta|alpha)
```

### Size policy
> Affection type: `Target`
>
> Identifier: `size`
>
> Default: `None`

The size policy matches all tags which exceed the provided blob size. For size parsing the [parse-size](https://crates.io/crates/parse-size) crate is used.
Any valid size (supported by this crate) can be used

>[!NOTE]
> The library uses `MiB`, `GiB` etc. which are the binary representations instead of the usual decimal representations of the size. Therefore, `1 MiB` is `1_048_576` bytes
> instead of `1_000_000` bytes as one might expect

```yaml
# Would match all tags whose total blob size exceed 256 MiB
size: 256 MiB
```

## Repository policies

Repository policies are used to determine for which images a rule should be applied
Expand Down
2 changes: 1 addition & 1 deletion src/api/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ pub struct Layer {
#[serde(rename = "mediaType")]
pub media_type: String,
pub digest: String,
pub size: u32,
pub size: u64,
}
4 changes: 2 additions & 2 deletions src/api/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,12 @@ impl Repository {
for tag in raw {
match self.get_manifest(&tag).await? {
ManifestResponse::Manifest(manifest) => {
let size: u32 = manifest.layers.iter().map(|l| l.size).sum();
let size: u64 = manifest.layers.iter().map(|l| l.size).sum();
let config = manifest.get_config().await?;
tags.push(Tag::new(tag, manifest.digest, config.created, size));
},
ManifestResponse::ManifestList(list) => {
let size: u32 = list.manifests.iter().map(|m| m.size).sum();
let size: u64 = list.manifests.iter().map(|m| m.size).sum();
let layer = list.manifests.get(0).ok_or(ApiError::EmptyManifestList)?;
let manifest = list.get_manifest(layer.digest.clone()).await?;
let config = manifest.get_config().await?;
Expand Down
4 changes: 2 additions & 2 deletions src/api/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ pub struct Tag {
pub name: String,
pub digest: String,
pub created: DateTime<Utc>,
pub size: u32
pub size: u64
}

impl Tag {
pub fn new(name: String, digest: String, created: DateTime<Utc>, size: u32) -> Self {
pub fn new(name: String, digest: String, created: DateTime<Utc>, size: u64) -> Self {
Self { name, digest, created, size }
}
}
2 changes: 2 additions & 0 deletions src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::policies::age_max::{AGE_MAX_LABEL, AgeMaxPolicy};
use crate::policies::age_min::{AGE_MIN_LABEL, AgeMinPolicy};
use crate::policies::image_pattern::{IMAGE_PATTERN_LABEL, ImagePatternPolicy};
use crate::policies::revision::{REVISION_LABEL, RevisionPolicy};
use crate::policies::size::{SIZE_LABEL, SizePolicy};
use crate::policies::tag_pattern::{TAG_PATTERN_LABEL, TagPatternPolicy};
use crate::rule::{parse_rule, parse_schedule, Rule};

Expand Down Expand Up @@ -161,6 +162,7 @@ impl Instance {
default_rule.tag_policies.insert(AGE_MAX_LABEL, Box::<AgeMaxPolicy>::default());
default_rule.tag_policies.insert(AGE_MIN_LABEL, Box::<AgeMinPolicy>::default());
default_rule.tag_policies.insert(REVISION_LABEL, Box::<RevisionPolicy>::default());
default_rule.tag_policies.insert(SIZE_LABEL, Box::<SizePolicy>::default());

// parse default rules
labels.iter()
Expand Down
5 changes: 5 additions & 0 deletions src/policies/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod age_min;
pub mod image_pattern;
pub mod revision;
pub mod tag_pattern;
pub mod size;

pub type PolicyMap<T> = HashMap<&'static str, Box<dyn Policy<T>>>;

Expand Down Expand Up @@ -55,4 +56,8 @@ pub fn parse_duration(duration_str: String) -> Option<Duration> {
Ok(duration_str) => Duration::from_std(duration_str.into()).ok(),
Err(_) => None
}
}

pub fn parse_size(size_str: &str) -> Option<u64> {
parse_size::parse_size(size_str).ok()
}
122 changes: 122 additions & 0 deletions src/policies/size.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use log::info;
use crate::api::tag::Tag;
use crate::policies::{AffectionType, parse_size, Policy};

pub const SIZE_LABEL: &str = "size";

/// Policy to match all tags which exceed a given blob size
/// # Example
/// ```
/// let policy = SizePolicy::new(String::from("0.2 GiB"));
///
/// // returns all tags which are bigger than 0.2 GiB
/// let affected = policy.affects(&tags);
#[derive(Debug, Clone, Default)]
pub struct SizePolicy {
size: Option<u64>
}

impl SizePolicy {
pub fn new(value: &str) -> Self {
if value.is_empty() {
Self { size: None }
} else {
let size = parse_size(value);
if size.is_none() {
info!("Received invalid size '{value}'")
}
Self { size }
}
}
}

impl Policy<Tag> for SizePolicy {
fn affects(&self, tags: Vec<Tag>) -> Vec<Tag> {
if let Some(size) = self.size {
tags.into_iter().filter(|tag| tag.size >= size).collect()
} else {
vec![]
}
}

fn affection_type(&self) -> AffectionType {
AffectionType::Target
}

fn id(&self) -> &'static str {
SIZE_LABEL
}

fn enabled(&self) -> bool {
self.size.is_some()
}
}

#[cfg(test)]
mod test {
use chrono::Duration;
use crate::api::tag::Tag;
use crate::policies::Policy;
use crate::policies::size::SizePolicy;
use crate::test::get_tags;

fn get_current_tags() -> Vec<Tag> {
get_tags(vec![
("first", Duration::hours(-5), 1_200_000),
("second", Duration::minutes(-5), 1_000),
("third", Duration::minutes(-30), 100_000_000),
("fourth", Duration::minutes(-10), 100_000),
("fifth", Duration::seconds(-15), 1_300_000),
("sixth", Duration::minutes(-50), 1_100_000)
])
}

#[test]
pub fn test_matching() {
let tags = get_current_tags();
let policy = SizePolicy::new("1 MiB");
assert!(policy.size.is_some());
assert_eq!(policy.affects(tags.clone()), vec![tags[0].clone(), tags[2].clone(), tags[4].clone(), tags[5].clone()])
}

#[test]
pub fn test_empty() {
let tags = get_current_tags();
let policy = SizePolicy::new("");
assert!(policy.size.is_none());
assert_eq!(policy.affects(tags), vec![])
}

#[test]
pub fn test_default() {
let tags = get_current_tags();
let policy = SizePolicy::default();
assert!(policy.size.is_none());
assert_eq!(policy.affects(tags), vec![])
}

#[test]
pub fn test_invalid_size() {
let tags = get_current_tags();
let policy = SizePolicy::new("120 asdf");
assert!(policy.size.is_none());
assert_eq!(policy.affects(tags), vec![])
}

#[test]
pub fn test_negative_size() {
let tags = get_current_tags();
let policy = SizePolicy::new("-1 MiB");
assert!(policy.size.is_none());
assert_eq!(policy.affects(tags), vec![])
}

#[test]
pub fn test_without_unit() {
let tags = get_current_tags();
let policy = SizePolicy::new("1_048_576");
assert!(policy.size.is_some());
assert_eq!(policy.affects(tags.clone()), vec![tags[0].clone(), tags[2].clone(), tags[4].clone(), tags[5].clone()])
}
}
30 changes: 20 additions & 10 deletions src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::policies::age_min::{AGE_MIN_LABEL, AgeMinPolicy};
use crate::policies::age_max::{AGE_MAX_LABEL, AgeMaxPolicy};
use crate::policies::image_pattern::{IMAGE_PATTERN_LABEL, ImagePatternPolicy};
use crate::policies::revision::{REVISION_LABEL, RevisionPolicy};
use crate::policies::size::{SIZE_LABEL, SizePolicy};
use crate::policies::tag_pattern::{TAG_PATTERN_LABEL, TagPatternPolicy};

#[derive(Debug)]
Expand Down Expand Up @@ -97,6 +98,9 @@ pub fn parse_rule(name: String, policies: Vec<(String, &str)>) -> Option<Rule> {
REVISION_LABEL => {
rule.tag_policies.insert(REVISION_LABEL, Box::new(RevisionPolicy::new(value.to_string())));
},
SIZE_LABEL => {
rule.tag_policies.insert(SIZE_LABEL, Box::new(SizePolicy::new(value)));
}
other => {
warn!("Found unknown policy '{other}' for rule '{name}'. Ignoring policy")
}
Expand Down Expand Up @@ -134,6 +138,7 @@ mod test {
use crate::policies::age_min::AGE_MIN_LABEL;
use crate::policies::image_pattern::IMAGE_PATTERN_LABEL;
use crate::policies::revision::REVISION_LABEL;
use crate::policies::size::SIZE_LABEL;
use crate::policies::tag_pattern::TAG_PATTERN_LABEL;
use crate::rule::{parse_rule, parse_schedule};
use crate::test::{get_repositories, get_tags, get_tags_by_name};
Expand Down Expand Up @@ -230,19 +235,21 @@ mod test {
("image.pattern", "test-.+"),
("tag.pattern", "test-.+"),
("test", "10s"),
("revisions", "10")
("revisions", "10"),
("size", "100 MiB")
]);
let rule = parse_rule(String::from("test-rule"), labels);
assert!(rule.is_some());
let parsed = rule.unwrap();
assert_eq!(parsed.name, String::from("test-rule"));
assert_eq!(parsed.schedule, String::from("* * * * 5 *"));
assert_eq!(parsed.tag_policies.len(), 4);
assert_eq!(parsed.tag_policies.len(), 5);
assert_eq!(parsed.repository_policies.len(), 1);
assert!(parsed.tag_policies.get(AGE_MAX_LABEL).is_some());
assert!(parsed.tag_policies.get(AGE_MIN_LABEL).is_some());
assert!(parsed.tag_policies.get(REVISION_LABEL).is_some());
assert!(parsed.tag_policies.get(TAG_PATTERN_LABEL).is_some());
assert!(parsed.tag_policies.get(SIZE_LABEL).is_some());
assert!(parsed.repository_policies.get(IMAGE_PATTERN_LABEL).is_some())
}

Expand Down Expand Up @@ -327,7 +334,9 @@ mod test {
let parsed = rule.unwrap();

let repositories = get_repositories(vec!["test", "test-asdf", "not matching", "test-match"]);
assert_eq!(parsed.affected_repositories(repositories.clone()), vec![repositories[1].clone(), repositories[3].clone()])
let mut affected = parsed.affected_repositories(repositories.clone());
affected.sort_by(|x, y| x.name.cmp(&y.name));
assert_eq!(affected, vec![repositories[1].clone(), repositories[3].clone()])
}

#[test]
Expand Down Expand Up @@ -401,17 +410,18 @@ mod test {
("schedule", "* * * * 5 *"),
("image.pattern", "test-.+"),
("tag.pattern", ".*th"),
("test", "10s")
("test", "10s"),
("size", "1 MiB")
]);
let rule = parse_rule(String::from("test-rule"), labels).unwrap();

let tags = get_tags(vec![
("first", Duration::hours(-5), 1_000_000),
("second", Duration::minutes(-5), 1_000_000),
("third", Duration::minutes(-30), 1_000_000),
("fourth", Duration::minutes(-10), 1_000_000),
("first", Duration::hours(-5), 1_000),
("second", Duration::minutes(-5), 1_200_000),
("third", Duration::minutes(-30), 1_400_000),
("fourth", Duration::minutes(-10), 100_000_000),
("fifth", Duration::seconds(-15), 1_000_000),
("sixth", Duration::minutes(-50), 1_000_000)
("sixth", Duration::minutes(-50), 1_000)
]);

let repositories = get_repositories(vec!["test-asdf", "test-", "test-test"]);
Expand All @@ -421,6 +431,6 @@ mod test {

let mut affected = rule.affected_tags(tags.clone());
affected.sort_by(|t1, t2| t1.created.cmp(&t2.created).reverse());
assert_eq!(affected, vec![tags[3].clone(), tags[2].clone(), tags[5].clone(), tags[0].clone()]);
assert_eq!(affected, vec![tags[1].clone(), tags[3].clone(), tags[2].clone(), tags[5].clone(), tags[0].clone()]);
}
}
6 changes: 3 additions & 3 deletions src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub fn get_repositories(names: Vec<impl Into<String>>) -> Vec<Repository> {
repositories
}

pub fn get_tags(raw: Vec<(impl Into<String>, Duration, u32)>) -> Vec<Tag> {
pub fn get_tags(raw: Vec<(impl Into<String>, Duration, u64)>) -> Vec<Tag> {
let mut tags = vec![];
let now = chrono::offset::Utc::now();
for (name, offset, size) in raw {
Expand All @@ -27,6 +27,6 @@ pub fn get_tags(raw: Vec<(impl Into<String>, Duration, u32)>) -> Vec<Tag> {
tags
}

pub fn get_tags_by_name(raw: Vec<impl Into<String>>, duration: Duration, size: u32) -> Vec<Tag> {
get_tags(raw.into_iter().map(|x| (x, duration.clone(), size)).collect())
pub fn get_tags_by_name(raw: Vec<impl Into<String>>, duration: Duration, size: u64) -> Vec<Tag> {
get_tags(raw.into_iter().map(|x| (x, duration, size)).collect())
}

0 comments on commit 21afe53

Please sign in to comment.