Skip to content

Commit 21afe53

Browse files
committed
added size policy
1 parent 3926815 commit 21afe53

12 files changed

+186
-20
lines changed

Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ cron = "0.12.0"
2323
dyn-clone = "1.0.16"
2424
serde_yaml = "0.9.29"
2525
notify = { version = "6.1.1", default-features = false, features = ["serde", "macos_kqueue"] }
26-
notify-debouncer-mini = { version = "0.4.1", default-features = false, features = ["serde"] }
26+
notify-debouncer-mini = { version = "0.4.1", default-features = false, features = ["serde"] }
27+
parse-size = "1.0.0"

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ to open a new issue or contribute to an open issue!
7171
- [x] Add tests to rule parsing and rule affections
7272
- [ ] Add ping to registry container in instance creation
7373
- [x] Add policy to match tags by pattern
74-
- [ ] Add policy to match tags by size
74+
- [x] Add policy to match tags by size
7575

7676
## Credits
7777

docs/policies.md

+19
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,25 @@ Any valid regex (supported by this crate) can be used.
9999
tag.pattern: .+-(beta|alpha)
100100
```
101101

102+
### Size policy
103+
> Affection type: `Target`
104+
>
105+
> Identifier: `size`
106+
>
107+
> Default: `None`
108+
109+
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.
110+
Any valid size (supported by this crate) can be used
111+
112+
>[!NOTE]
113+
> 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
114+
> instead of `1_000_000` bytes as one might expect
115+
116+
```yaml
117+
# Would match all tags whose total blob size exceed 256 MiB
118+
size: 256 MiB
119+
```
120+
102121
## Repository policies
103122

104123
Repository policies are used to determine for which images a rule should be applied

src/api/layer.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ pub struct Layer {
55
#[serde(rename = "mediaType")]
66
pub media_type: String,
77
pub digest: String,
8-
pub size: u32,
8+
pub size: u64,
99
}

src/api/repository.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,12 @@ impl Repository {
144144
for tag in raw {
145145
match self.get_manifest(&tag).await? {
146146
ManifestResponse::Manifest(manifest) => {
147-
let size: u32 = manifest.layers.iter().map(|l| l.size).sum();
147+
let size: u64 = manifest.layers.iter().map(|l| l.size).sum();
148148
let config = manifest.get_config().await?;
149149
tags.push(Tag::new(tag, manifest.digest, config.created, size));
150150
},
151151
ManifestResponse::ManifestList(list) => {
152-
let size: u32 = list.manifests.iter().map(|m| m.size).sum();
152+
let size: u64 = list.manifests.iter().map(|m| m.size).sum();
153153
let layer = list.manifests.get(0).ok_or(ApiError::EmptyManifestList)?;
154154
let manifest = list.get_manifest(layer.digest.clone()).await?;
155155
let config = manifest.get_config().await?;

src/api/tag.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ pub struct Tag {
55
pub name: String,
66
pub digest: String,
77
pub created: DateTime<Utc>,
8-
pub size: u32
8+
pub size: u64
99
}
1010

1111
impl Tag {
12-
pub fn new(name: String, digest: String, created: DateTime<Utc>, size: u32) -> Self {
12+
pub fn new(name: String, digest: String, created: DateTime<Utc>, size: u64) -> Self {
1313
Self { name, digest, created, size }
1414
}
1515
}

src/instance.rs

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::policies::age_max::{AGE_MAX_LABEL, AgeMaxPolicy};
1515
use crate::policies::age_min::{AGE_MIN_LABEL, AgeMinPolicy};
1616
use crate::policies::image_pattern::{IMAGE_PATTERN_LABEL, ImagePatternPolicy};
1717
use crate::policies::revision::{REVISION_LABEL, RevisionPolicy};
18+
use crate::policies::size::{SIZE_LABEL, SizePolicy};
1819
use crate::policies::tag_pattern::{TAG_PATTERN_LABEL, TagPatternPolicy};
1920
use crate::rule::{parse_rule, parse_schedule, Rule};
2021

@@ -161,6 +162,7 @@ impl Instance {
161162
default_rule.tag_policies.insert(AGE_MAX_LABEL, Box::<AgeMaxPolicy>::default());
162163
default_rule.tag_policies.insert(AGE_MIN_LABEL, Box::<AgeMinPolicy>::default());
163164
default_rule.tag_policies.insert(REVISION_LABEL, Box::<RevisionPolicy>::default());
165+
default_rule.tag_policies.insert(SIZE_LABEL, Box::<SizePolicy>::default());
164166

165167
// parse default rules
166168
labels.iter()

src/policies/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod age_min;
1111
pub mod image_pattern;
1212
pub mod revision;
1313
pub mod tag_pattern;
14+
pub mod size;
1415

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

@@ -55,4 +56,8 @@ pub fn parse_duration(duration_str: String) -> Option<Duration> {
5556
Ok(duration_str) => Duration::from_std(duration_str.into()).ok(),
5657
Err(_) => None
5758
}
59+
}
60+
61+
pub fn parse_size(size_str: &str) -> Option<u64> {
62+
parse_size::parse_size(size_str).ok()
5863
}

src/policies/size.rs

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use log::info;
2+
use crate::api::tag::Tag;
3+
use crate::policies::{AffectionType, parse_size, Policy};
4+
5+
pub const SIZE_LABEL: &str = "size";
6+
7+
/// Policy to match all tags which exceed a given blob size
8+
/// # Example
9+
/// ```
10+
/// let policy = SizePolicy::new(String::from("0.2 GiB"));
11+
///
12+
/// // returns all tags which are bigger than 0.2 GiB
13+
/// let affected = policy.affects(&tags);
14+
15+
#[derive(Debug, Clone, Default)]
16+
pub struct SizePolicy {
17+
size: Option<u64>
18+
}
19+
20+
impl SizePolicy {
21+
pub fn new(value: &str) -> Self {
22+
if value.is_empty() {
23+
Self { size: None }
24+
} else {
25+
let size = parse_size(value);
26+
if size.is_none() {
27+
info!("Received invalid size '{value}'")
28+
}
29+
Self { size }
30+
}
31+
}
32+
}
33+
34+
impl Policy<Tag> for SizePolicy {
35+
fn affects(&self, tags: Vec<Tag>) -> Vec<Tag> {
36+
if let Some(size) = self.size {
37+
tags.into_iter().filter(|tag| tag.size >= size).collect()
38+
} else {
39+
vec![]
40+
}
41+
}
42+
43+
fn affection_type(&self) -> AffectionType {
44+
AffectionType::Target
45+
}
46+
47+
fn id(&self) -> &'static str {
48+
SIZE_LABEL
49+
}
50+
51+
fn enabled(&self) -> bool {
52+
self.size.is_some()
53+
}
54+
}
55+
56+
#[cfg(test)]
57+
mod test {
58+
use chrono::Duration;
59+
use crate::api::tag::Tag;
60+
use crate::policies::Policy;
61+
use crate::policies::size::SizePolicy;
62+
use crate::test::get_tags;
63+
64+
fn get_current_tags() -> Vec<Tag> {
65+
get_tags(vec![
66+
("first", Duration::hours(-5), 1_200_000),
67+
("second", Duration::minutes(-5), 1_000),
68+
("third", Duration::minutes(-30), 100_000_000),
69+
("fourth", Duration::minutes(-10), 100_000),
70+
("fifth", Duration::seconds(-15), 1_300_000),
71+
("sixth", Duration::minutes(-50), 1_100_000)
72+
])
73+
}
74+
75+
#[test]
76+
pub fn test_matching() {
77+
let tags = get_current_tags();
78+
let policy = SizePolicy::new("1 MiB");
79+
assert!(policy.size.is_some());
80+
assert_eq!(policy.affects(tags.clone()), vec![tags[0].clone(), tags[2].clone(), tags[4].clone(), tags[5].clone()])
81+
}
82+
83+
#[test]
84+
pub fn test_empty() {
85+
let tags = get_current_tags();
86+
let policy = SizePolicy::new("");
87+
assert!(policy.size.is_none());
88+
assert_eq!(policy.affects(tags), vec![])
89+
}
90+
91+
#[test]
92+
pub fn test_default() {
93+
let tags = get_current_tags();
94+
let policy = SizePolicy::default();
95+
assert!(policy.size.is_none());
96+
assert_eq!(policy.affects(tags), vec![])
97+
}
98+
99+
#[test]
100+
pub fn test_invalid_size() {
101+
let tags = get_current_tags();
102+
let policy = SizePolicy::new("120 asdf");
103+
assert!(policy.size.is_none());
104+
assert_eq!(policy.affects(tags), vec![])
105+
}
106+
107+
#[test]
108+
pub fn test_negative_size() {
109+
let tags = get_current_tags();
110+
let policy = SizePolicy::new("-1 MiB");
111+
assert!(policy.size.is_none());
112+
assert_eq!(policy.affects(tags), vec![])
113+
}
114+
115+
#[test]
116+
pub fn test_without_unit() {
117+
let tags = get_current_tags();
118+
let policy = SizePolicy::new("1_048_576");
119+
assert!(policy.size.is_some());
120+
assert_eq!(policy.affects(tags.clone()), vec![tags[0].clone(), tags[2].clone(), tags[4].clone(), tags[5].clone()])
121+
}
122+
}

src/rule.rs

+20-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::policies::age_min::{AGE_MIN_LABEL, AgeMinPolicy};
99
use crate::policies::age_max::{AGE_MAX_LABEL, AgeMaxPolicy};
1010
use crate::policies::image_pattern::{IMAGE_PATTERN_LABEL, ImagePatternPolicy};
1111
use crate::policies::revision::{REVISION_LABEL, RevisionPolicy};
12+
use crate::policies::size::{SIZE_LABEL, SizePolicy};
1213
use crate::policies::tag_pattern::{TAG_PATTERN_LABEL, TagPatternPolicy};
1314

1415
#[derive(Debug)]
@@ -97,6 +98,9 @@ pub fn parse_rule(name: String, policies: Vec<(String, &str)>) -> Option<Rule> {
9798
REVISION_LABEL => {
9899
rule.tag_policies.insert(REVISION_LABEL, Box::new(RevisionPolicy::new(value.to_string())));
99100
},
101+
SIZE_LABEL => {
102+
rule.tag_policies.insert(SIZE_LABEL, Box::new(SizePolicy::new(value)));
103+
}
100104
other => {
101105
warn!("Found unknown policy '{other}' for rule '{name}'. Ignoring policy")
102106
}
@@ -134,6 +138,7 @@ mod test {
134138
use crate::policies::age_min::AGE_MIN_LABEL;
135139
use crate::policies::image_pattern::IMAGE_PATTERN_LABEL;
136140
use crate::policies::revision::REVISION_LABEL;
141+
use crate::policies::size::SIZE_LABEL;
137142
use crate::policies::tag_pattern::TAG_PATTERN_LABEL;
138143
use crate::rule::{parse_rule, parse_schedule};
139144
use crate::test::{get_repositories, get_tags, get_tags_by_name};
@@ -230,19 +235,21 @@ mod test {
230235
("image.pattern", "test-.+"),
231236
("tag.pattern", "test-.+"),
232237
("test", "10s"),
233-
("revisions", "10")
238+
("revisions", "10"),
239+
("size", "100 MiB")
234240
]);
235241
let rule = parse_rule(String::from("test-rule"), labels);
236242
assert!(rule.is_some());
237243
let parsed = rule.unwrap();
238244
assert_eq!(parsed.name, String::from("test-rule"));
239245
assert_eq!(parsed.schedule, String::from("* * * * 5 *"));
240-
assert_eq!(parsed.tag_policies.len(), 4);
246+
assert_eq!(parsed.tag_policies.len(), 5);
241247
assert_eq!(parsed.repository_policies.len(), 1);
242248
assert!(parsed.tag_policies.get(AGE_MAX_LABEL).is_some());
243249
assert!(parsed.tag_policies.get(AGE_MIN_LABEL).is_some());
244250
assert!(parsed.tag_policies.get(REVISION_LABEL).is_some());
245251
assert!(parsed.tag_policies.get(TAG_PATTERN_LABEL).is_some());
252+
assert!(parsed.tag_policies.get(SIZE_LABEL).is_some());
246253
assert!(parsed.repository_policies.get(IMAGE_PATTERN_LABEL).is_some())
247254
}
248255

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

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

333342
#[test]
@@ -401,17 +410,18 @@ mod test {
401410
("schedule", "* * * * 5 *"),
402411
("image.pattern", "test-.+"),
403412
("tag.pattern", ".*th"),
404-
("test", "10s")
413+
("test", "10s"),
414+
("size", "1 MiB")
405415
]);
406416
let rule = parse_rule(String::from("test-rule"), labels).unwrap();
407417

408418
let tags = get_tags(vec![
409-
("first", Duration::hours(-5), 1_000_000),
410-
("second", Duration::minutes(-5), 1_000_000),
411-
("third", Duration::minutes(-30), 1_000_000),
412-
("fourth", Duration::minutes(-10), 1_000_000),
419+
("first", Duration::hours(-5), 1_000),
420+
("second", Duration::minutes(-5), 1_200_000),
421+
("third", Duration::minutes(-30), 1_400_000),
422+
("fourth", Duration::minutes(-10), 100_000_000),
413423
("fifth", Duration::seconds(-15), 1_000_000),
414-
("sixth", Duration::minutes(-50), 1_000_000)
424+
("sixth", Duration::minutes(-50), 1_000)
415425
]);
416426

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

422432
let mut affected = rule.affected_tags(tags.clone());
423433
affected.sort_by(|t1, t2| t1.created.cmp(&t2.created).reverse());
424-
assert_eq!(affected, vec![tags[3].clone(), tags[2].clone(), tags[5].clone(), tags[0].clone()]);
434+
assert_eq!(affected, vec![tags[1].clone(), tags[3].clone(), tags[2].clone(), tags[5].clone(), tags[0].clone()]);
425435
}
426436
}

src/test/mod.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub fn get_repositories(names: Vec<impl Into<String>>) -> Vec<Repository> {
1818
repositories
1919
}
2020

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

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

0 commit comments

Comments
 (0)