Skip to content

Commit bb0924f

Browse files
authored
feat: add support for tags in manifest structure and related functions (#1032)
1 parent 4b4c170 commit bb0924f

File tree

9 files changed

+111
-15
lines changed

9 files changed

+111
-15
lines changed

core/src/ten_manager/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/ten_manager/src/manifest_lock/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ impl<'a> From<&'a ManifestLockItem> for PkgInfo {
365365
type_and_name: type_and_name.clone(),
366366
version: locked_item.version.clone(),
367367
dependencies: dependencies_option.clone(),
368+
tags: None,
368369
supports: locked_item.supports.clone(),
369370
api: None,
370371
package: None,

core/src/ten_manager/src/registry/found_result.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ pub struct PkgRegistryInfo {
2929
#[serde(skip_serializing_if = "Option::is_none")]
3030
#[serde(rename = "contentFormat")]
3131
pub content_format: Option<String>,
32+
33+
#[serde(skip_serializing_if = "Option::is_none")]
34+
pub tags: Option<Vec<String>>,
3235
}
3336

3437
mod dependencies_conversion {
@@ -79,6 +82,7 @@ impl From<&PkgInfo> for PkgRegistryInfo {
7982
hash: pkg_info.hash.clone(),
8083
download_url: String::new(),
8184
content_format: None,
85+
tags: pkg_info.manifest.tags.clone(),
8286
}
8387
}
8488
}
@@ -97,6 +101,7 @@ impl From<&PkgRegistryInfo> for PkgInfo {
97101
.clone(),
98102
version: pkg_registry_info.basic_info.version.clone(),
99103
dependencies: Some(pkg_registry_info.dependencies.clone()),
104+
tags: pkg_registry_info.tags.clone(),
100105
supports: Some(pkg_registry_info.basic_info.supports.clone()),
101106
api: None,
102107
package: None,

core/src/ten_manager/src/registry/remote.rs

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ use crate::http::create_http_client_with_proxies;
3232
use crate::output::TmanOutput;
3333
use crate::{config::TmanConfig, registry::found_result::PkgRegistryInfo};
3434

35+
/// Checks if a package requires admin token authorization based on its tags.
36+
///
37+
/// Returns true if the package has any tag starting with "ten:".
38+
fn requires_admin_token_based_on_tags(pkg_info: &PkgInfo) -> bool {
39+
pkg_info
40+
.manifest
41+
.tags
42+
.as_ref()
43+
.map(|tags| tags.iter().any(|tag| tag.starts_with("ten:")))
44+
.unwrap_or(false)
45+
}
46+
3547
async fn retry_async<'a, F, T>(
3648
tman_config: Arc<tokio::sync::RwLock<TmanConfig>>,
3749
max_retries: u32,
@@ -123,6 +135,7 @@ async fn get_package_upload_info(
123135
hash: pkg_info.hash.clone(),
124136
download_url: String::new(),
125137
content_format: Some("gzip".to_string()),
138+
tags: pkg_info.manifest.tags.clone(),
126139
});
127140

128141
if is_verbose(tman_config.clone()).await {
@@ -134,23 +147,49 @@ async fn get_package_upload_info(
134147

135148
let mut headers = HeaderMap::new();
136149

137-
if let Some(user_token) = &tman_config.read().await.user_token {
138-
let basic_token = format!("Basic {}", user_token);
139-
headers.insert(
140-
AUTHORIZATION,
141-
basic_token.parse().map_err(|e| {
142-
out.error_line(&format!(
143-
"Failed to parse authorization token: {}",
150+
// Check if the package requires admin token based on its tags.
151+
let requires_admin = requires_admin_token_based_on_tags(&pkg_info);
152+
153+
if requires_admin {
154+
// If a tag starts with "ten:", we must use admin_token.
155+
if let Some(admin_token) = &tman_config.read().await.admin_token {
156+
let basic_token = format!("Basic {}", admin_token);
157+
headers.insert(
158+
AUTHORIZATION,
159+
basic_token.parse().map_err(|e| {
160+
out.error_line(&format!(
161+
"Failed to parse authorization token: {}",
162+
e
163+
));
144164
e
145-
));
146-
e
147-
})?,
148-
);
165+
})?,
166+
);
167+
} else {
168+
if is_verbose(tman_config.clone()).await {
169+
out.normal_line("Admin token is required for packages with 'ten:' tags but is missing");
170+
}
171+
return Err(anyhow!("Admin token is required for packages with 'ten:' tags but is missing"));
172+
}
149173
} else {
150-
if is_verbose(tman_config.clone()).await {
151-
out.normal_line("Authorization token is missing");
174+
// Use user_token as before for regular packages.
175+
if let Some(user_token) = &tman_config.read().await.user_token {
176+
let basic_token = format!("Basic {}", user_token);
177+
headers.insert(
178+
AUTHORIZATION,
179+
basic_token.parse().map_err(|e| {
180+
out.error_line(&format!(
181+
"Failed to parse authorization token: {}",
182+
e
183+
));
184+
e
185+
})?,
186+
);
187+
} else {
188+
if is_verbose(tman_config.clone()).await {
189+
out.normal_line("Authorization token is missing");
190+
}
191+
return Err(anyhow!("Authorization token is missing"));
152192
}
153-
return Err(anyhow!("Authorization token is missing"));
154193
}
155194

156195
let response = client

core/src/ten_rust/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/ten_rust/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ futures = "0.3.31"
3131
json5 = { version = "0.4" }
3232
jsonschema = { version = "0.28", default-features = false }
3333
libc = { version = "0.2" }
34+
once_cell = "1.19.0"
3435
prometheus = "0.13.4"
3536
regex = { version = "1.11" }
3637
semver = { version = "1.0", features = ["serde"] }

core/src/ten_rust/src/json_schema/data/manifest.schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,13 @@
426426
"version": {
427427
"$ref": "#/$defs/version"
428428
},
429+
"tags": {
430+
"type": "array",
431+
"items": {
432+
"type": "string",
433+
"pattern": "^(ten:)?[A-Za-z_][A-Za-z0-9_]*$"
434+
}
435+
},
429436
"dependencies": {
430437
"$ref": "#/$defs/dependencies"
431438
},

core/src/ten_rust/src/pkg_info/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ pub const ADDON_LOADER_DIR: &str = "addon_loader";
1717
pub const SYSTEM_DIR: &str = "system";
1818

1919
pub const TEN_STR_PREDEFINED_GRAPHS: &str = "predefined_graphs";
20+
pub const TEN_STR_TAGS: &str = "tags";

core/src/ten_rust/src/pkg_info/manifest/mod.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use std::collections::HashMap;
1313
use std::{fmt, fs, path::Path, str::FromStr};
1414

1515
use anyhow::{anyhow, Context, Result};
16+
use once_cell::sync::Lazy;
17+
use regex::Regex;
1618
use semver::Version;
1719
use serde::{Deserialize, Serialize};
1820
use serde_json::{Map, Value};
@@ -26,6 +28,7 @@ use dependency::ManifestDependency;
2628
use publish::PackageConfig;
2729
use support::ManifestSupport;
2830

31+
use super::constants::TEN_STR_TAGS;
2932
use super::pkg_type_and_name::PkgTypeAndName;
3033

3134
// Define a structure that mirrors the structure of the JSON file.
@@ -34,6 +37,7 @@ pub struct Manifest {
3437
pub type_and_name: PkgTypeAndName,
3538
pub version: Version,
3639
pub dependencies: Option<Vec<ManifestDependency>>,
40+
pub tags: Option<Vec<String>>,
3741

3842
// Note: For future extensions, use the 'features' field to describe the
3943
// functionality of each package.
@@ -62,13 +66,15 @@ impl<'de> Deserialize<'de> for Manifest {
6266
{
6367
let all_fields = Map::deserialize(deserializer)?;
6468

65-
// Now extract the fields from all_fields
69+
// Now extract the fields from all_fields.
6670
let type_and_name = extract_type_and_name(&all_fields)
6771
.map_err(serde::de::Error::custom)?;
6872
let version =
6973
extract_version(&all_fields).map_err(serde::de::Error::custom)?;
7074
let dependencies = extract_dependencies(&all_fields)
7175
.map_err(serde::de::Error::custom)?;
76+
let tags =
77+
extract_tags(&all_fields).map_err(serde::de::Error::custom)?;
7278
let supports =
7379
extract_supports(&all_fields).map_err(serde::de::Error::custom)?;
7480
let api = extract_api(&all_fields).map_err(serde::de::Error::custom)?;
@@ -81,6 +87,7 @@ impl<'de> Deserialize<'de> for Manifest {
8187
type_and_name,
8288
version,
8389
dependencies,
90+
tags,
8491
supports,
8592
api,
8693
package,
@@ -106,6 +113,7 @@ impl Default for Manifest {
106113
},
107114
version: Version::new(0, 0, 0),
108115
dependencies: None,
116+
tags: None,
109117
supports: None,
110118
api: None,
111119
package: None,
@@ -131,6 +139,7 @@ impl FromStr for Manifest {
131139
let type_and_name = extract_type_and_name(&all_fields)?;
132140
let version = extract_version(&all_fields)?;
133141
let dependencies = extract_dependencies(&all_fields)?;
142+
let tags = extract_tags(&all_fields)?;
134143
let supports = extract_supports(&all_fields)?;
135144
let api = extract_api(&all_fields)?;
136145
let package = extract_package(&all_fields)?;
@@ -141,6 +150,7 @@ impl FromStr for Manifest {
141150
type_and_name,
142151
version,
143152
dependencies,
153+
tags,
144154
supports,
145155
api,
146156
package,
@@ -248,6 +258,36 @@ fn extract_scripts(
248258
}
249259
}
250260

261+
fn extract_tags(map: &Map<String, Value>) -> Result<Option<Vec<String>>> {
262+
// Lazy static initialization of regex that validates the tag format.
263+
static TAG_REGEX: Lazy<Regex> =
264+
Lazy::new(|| Regex::new(r"^(ten:)?[A-Za-z_][A-Za-z0-9_]*$").unwrap());
265+
266+
if let Some(Value::Array(tags)) = map.get(TEN_STR_TAGS) {
267+
let mut result = Vec::new();
268+
for tag in tags {
269+
if let Value::String(tag_str) = tag {
270+
// Validate tag string format.
271+
if !TAG_REGEX.is_match(tag_str) {
272+
return Err(anyhow!(
273+
"Invalid tag format: '{}'. Tags must contain only alphanumeric characters \
274+
and underscores, must not start with a digit, and can only have 'ten:' as prefix",
275+
tag_str
276+
));
277+
}
278+
result.push(tag_str.clone());
279+
} else {
280+
return Err(anyhow!("Tag value must be a string"));
281+
}
282+
}
283+
Ok(Some(result))
284+
} else if map.contains_key(TEN_STR_TAGS) {
285+
Err(anyhow!("'tags' field is not an array"))
286+
} else {
287+
Ok(None)
288+
}
289+
}
290+
251291
impl fmt::Display for Manifest {
252292
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
253293
match serde_json::to_string_pretty(&self.all_fields) {

0 commit comments

Comments
 (0)