Add include and exclude filters
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -1126,6 +1126,8 @@ dependencies = [
|
||||
"axum",
|
||||
"log",
|
||||
"oci-client",
|
||||
"regex",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -1316,6 +1318,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
|
||||
@@ -13,3 +13,5 @@ tower-http = { version = "0.6", features = ["trace"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
tracing = "0.1"
|
||||
log = "0.4"
|
||||
semver = "1.0"
|
||||
regex = "1.1"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/bin/env python
|
||||
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import yaml
|
||||
import argparse
|
||||
from requests import Session
|
||||
|
||||
|
||||
@@ -10,9 +12,11 @@ from requests import Session
|
||||
base_url = "http://127.0.0.1:3000/api"
|
||||
|
||||
|
||||
def get_events(s, image):
|
||||
def get_events(s, image, include, exclude):
|
||||
data = {
|
||||
"image": image,
|
||||
"include": include if include is not None else [],
|
||||
"exclude": exclude if exclude is not None else [],
|
||||
}
|
||||
try:
|
||||
r = s.post(base_url + "/tags", json=data)
|
||||
@@ -30,13 +34,25 @@ def main():
|
||||
datefmt="%Y-%m-%d %H:%M:%S", # Explicit date format
|
||||
)
|
||||
|
||||
# Initialize parser
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='rdockup-client',
|
||||
description='Perform requests against rdockup REST API',
|
||||
)
|
||||
parser.add_argument('-I', '--image', required=True)
|
||||
parser.add_argument('-i', '--include', nargs='+')
|
||||
parser.add_argument('-e', '--exclude', nargs='+')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Import config from file
|
||||
with open("./config.yaml", "r", encoding="utf8") as file:
|
||||
config = yaml.safe_load(file)
|
||||
token = config["token"]
|
||||
|
||||
# Read command line argument
|
||||
image = sys.argv[1]
|
||||
image = args.image
|
||||
include = args.include
|
||||
exclude = args.exclude
|
||||
|
||||
s = Session()
|
||||
s.headers.update(
|
||||
@@ -46,7 +62,11 @@ def main():
|
||||
}
|
||||
)
|
||||
|
||||
print(get_events(s, image))
|
||||
result = get_events(s, image, include, exclude)
|
||||
try:
|
||||
print(json.dumps(json.loads(result), indent=2))
|
||||
except Exception:
|
||||
print(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
10
src/api.rs
10
src/api.rs
@@ -12,6 +12,8 @@ use crate::registry_client;
|
||||
#[derive(Deserialize)]
|
||||
struct TagRequest {
|
||||
image: String,
|
||||
include: Option<Vec<String>>,
|
||||
exclude: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn validate_token(token: &str) -> bool {
|
||||
@@ -41,7 +43,13 @@ async fn get_tags_handler(
|
||||
);
|
||||
}
|
||||
|
||||
match registry_client::get_tags(&payload.image).await {
|
||||
match registry_client::get_tags(
|
||||
&payload.image,
|
||||
payload.include.as_deref(),
|
||||
payload.exclude.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(json) => (StatusCode::OK, [("content-type", "application/json")], json),
|
||||
Err(e) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
|
||||
@@ -1,23 +1,104 @@
|
||||
use oci_client::client::ClientConfig;
|
||||
use oci_client::{Client, Reference, secrets::RegistryAuth};
|
||||
use regex::Regex;
|
||||
use semver::Version;
|
||||
use serde::Serialize;
|
||||
use serde_json;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SerializableTags<'a> {
|
||||
struct LatestTags<'a> {
|
||||
name: &'a str,
|
||||
tags: &'a [String],
|
||||
latest_tags: &'a [String],
|
||||
}
|
||||
|
||||
pub async fn get_tags(image: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let client = Client::new(ClientConfig::default());
|
||||
let img_ref: Reference = image.parse()?;
|
||||
let auth = RegistryAuth::Anonymous;
|
||||
let tags = client
|
||||
.list_tags(&img_ref, &auth, Some(100), Some("17"))
|
||||
.await?;
|
||||
let serialized_tags = SerializableTags {
|
||||
name: &tags.name,
|
||||
tags: &tags.tags,
|
||||
};
|
||||
Ok(serde_json::to_string(&serialized_tags)?)
|
||||
/// Extracts the first MAJOR.MINOR.PATCH or MAJOR.MINOR from a tag.
|
||||
/// If only MAJOR.MINOR is found, appends .0 for patch.
|
||||
fn extract_version(tag: &str) -> Option<Version> {
|
||||
// Try to find MAJOR.MINOR.PATCH
|
||||
let re_patch = Regex::new(r"\d+\.\d+\.\d+").unwrap();
|
||||
if let Some(mat) = re_patch.find(tag) {
|
||||
return Version::parse(mat.as_str()).ok();
|
||||
}
|
||||
// Try to find MAJOR.MINOR and treat as MAJOR.MINOR.0
|
||||
let re_minor = Regex::new(r"\d+\.\d+").unwrap();
|
||||
if let Some(mat) = re_minor.find(tag) {
|
||||
let version_str = format!("{}.0", mat.as_str());
|
||||
return Version::parse(&version_str).ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn tag_matches_filters(tag: &str, include: Option<&[String]>, exclude: Option<&[String]>) -> bool {
|
||||
let includes = include.map_or(true, |incs| incs.iter().all(|inc| tag.contains(inc)));
|
||||
let excludes = exclude.map_or(true, |excs| excs.iter().all(|exc| !tag.contains(exc)));
|
||||
includes && excludes
|
||||
}
|
||||
|
||||
async fn fetch_all_tags(
|
||||
client: &Client,
|
||||
image_ref: &Reference,
|
||||
auth: &RegistryAuth,
|
||||
) -> Result<Vec<String>, Box<dyn Error>> {
|
||||
let mut all_tags = Vec::new();
|
||||
let mut last_tag: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let response = client
|
||||
.list_tags(image_ref, auth, Some(100), last_tag.as_deref())
|
||||
.await?;
|
||||
|
||||
let tags = response.tags;
|
||||
if tags.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let is_last_page = tags.len() < 100;
|
||||
last_tag = tags.last().cloned();
|
||||
all_tags.extend(tags);
|
||||
|
||||
if is_last_page {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_tags)
|
||||
}
|
||||
|
||||
async fn get_sorted_tags(
|
||||
image: &str,
|
||||
include: Option<&[String]>,
|
||||
exclude: Option<&[String]>,
|
||||
) -> Result<Vec<String>, Box<dyn Error>> {
|
||||
let client = Client::new(Default::default());
|
||||
let image_ref: Reference = image.parse()?;
|
||||
let auth = RegistryAuth::Anonymous;
|
||||
|
||||
let all_tags = fetch_all_tags(&client, &image_ref, &auth).await?;
|
||||
|
||||
let mut parsed_tags: Vec<(String, Version)> = all_tags
|
||||
.into_iter()
|
||||
.filter(|tag| tag_matches_filters(tag, include, exclude))
|
||||
.filter_map(|tag| extract_version(&tag).map(|ver| (tag, ver)))
|
||||
.collect();
|
||||
|
||||
parsed_tags.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
Ok(parsed_tags
|
||||
.into_iter()
|
||||
.take(10)
|
||||
.map(|(tag, _)| tag)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_tags(
|
||||
image: &str,
|
||||
include: Option<&[String]>,
|
||||
exclude: Option<&[String]>,
|
||||
) -> Result<String, Box<dyn Error>> {
|
||||
let tags = get_sorted_tags(image, include, exclude).await?;
|
||||
let output = LatestTags {
|
||||
name: image,
|
||||
latest_tags: &tags,
|
||||
};
|
||||
Ok(serde_json::to_string_pretty(&output)?)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user