Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1c3cd61
feat(rust): add a Url enum to support relative URLs
imobachgs Apr 28, 2025
5ba274c
feat(rust): add support for relative URLs to scripts
imobachgs Apr 28, 2025
9f119ca
feat(rust): resolve scripts URLs
imobachgs Apr 28, 2025
a86549f
feat(ruby): remove the relurl:// prefix
imobachgs Apr 29, 2025
5e17732
refactor(rust): replace our own Url with fluent_uri
imobachgs Apr 29, 2025
1223996
fix(rust): drop unneeded serde_as
imobachgs Apr 29, 2025
0c60dd4
chore(rust): imprrove ScriptSource OpenAPI description
imobachgs Apr 29, 2025
714c163
refactor(rust): unify ScriptSouce y FileSource
imobachgs Apr 29, 2025
e7b3271
refactor(rust): move logic to resolve URLs to a WithFileSource trait
imobachgs Apr 30, 2025
c5fcb9a
feat(rust): support relative URLs for files
imobachgs Apr 30, 2025
b8700ed
fix(ruby): fix conversion of file_owner
imobachgs Apr 30, 2025
b214bd9
chore(ruby): add tests for the FilesReader class
imobachgs Apr 30, 2025
720c9d8
feat(ruby): remove the relurl:// schema when reading files
imobachgs Apr 30, 2025
eeee638
fix(rust): do not export the chroot if not given
imobachgs Apr 30, 2025
f0f2b48
chore(rust): drop unneeded use
imobachgs Apr 30, 2025
60b9cee
fix(rust): fix a typo
imobachgs Apr 30, 2025
a8dc194
docs: update changes files
imobachgs Apr 30, 2025
e54ba2a
feat(rust): report the URL when curl fails
imobachgs Apr 30, 2025
0447c49
Apply suggestions from code review
imobachgs Apr 30, 2025
3bbe066
Apply suggestion from code review
imobachgs Apr 30, 2025
4640833
refactor(rust): make StoreContext source mandatory
imobachgs Apr 30, 2025
85af008
fix(rust): use 0o700 for scripts
imobachgs Apr 30, 2025
dd81167
Apply suggestions from code review
imobachgs Apr 30, 2025
153bdd2
refactor(rust): move URL solving to InstallSettings
imobachgs Apr 30, 2025
7262a2e
refactor(rust): remove duplication in scripts::model
imobachgs Apr 30, 2025
280a496
refactor(rust): remove unused code
imobachgs Apr 30, 2025
85db3a7
doc(rust): update JSON schema regarding files/scripts
imobachgs Apr 30, 2025
8728f12
fix(rust): drop an unused test
imobachgs Apr 30, 2025
77ecd10
chore(rust): add comment about Transfer::get
imobachgs May 2, 2025
f736c67
feat(rust): support relative URLs in "agama download"
imobachgs May 2, 2025
9a1deb3
Apply suggestions from code review
imobachgs May 2, 2025
0ec22e6
docs(rust): update CLI
imobachgs May 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions rust/Cargo.lock

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

1 change: 1 addition & 0 deletions rust/agama-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ chrono = "0.4.38"
regex = "1.11.1"
home = "0.5.11"
serde = { version = "1.0.219", features = ["derive"] }
fluent-uri = "0.3.2"

[[bin]]
name = "agama"
Expand Down
5 changes: 3 additions & 2 deletions rust/agama-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,14 @@ pub enum Commands {

/// Download file from given URL
///
/// The purpose of this command is to download files using AutoYaST supported schemas (e.g. device:// or relurl://).
/// The purpose of this command is to download files using AutoYaST supported schemas (e.g. device://).
/// It can be used to download additional scripts, configuration files and so on.
/// You can use it for downloading Agama autoinstallation profiles. However, unless you need additional processing,
/// the "agama profile import" is recommended.
/// If you want to convert an AutoYaST profile, use "agama profile autoyast".
Download {
/// URL pointing to file for download
/// URL reference pointing to file for download. If a relative URL is
/// provided, it will be resolved against the current working directory.
url: String,
/// File name
destination: PathBuf,
Expand Down
10 changes: 7 additions & 3 deletions rust/agama-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ use std::{

use crate::show_progress;
use agama_lib::{
base_http_client::BaseHTTPClient, install_settings::InstallSettings, Store as SettingsStore,
base_http_client::BaseHTTPClient, context::InstallationContext,
install_settings::InstallSettings, Store as SettingsStore,
};
use anyhow::anyhow;
use clap::Subcommand;
Expand Down Expand Up @@ -75,7 +76,7 @@ pub async fn run(http_client: BaseHTTPClient, subcommand: ConfigCommands) -> any
let mut stdin = io::stdin();
let mut contents = String::new();
stdin.read_to_string(&mut contents)?;
let result: InstallSettings = serde_json::from_str(&contents)?;
let result = InstallSettings::from_json(&contents, &InstallationContext::from_env()?)?;
tokio::spawn(async move {
show_progress().await.unwrap();
});
Expand Down Expand Up @@ -112,7 +113,10 @@ fn edit(model: &InstallSettings, editor: &str) -> anyhow::Result<InstallSettings
let mut command = editor_command(editor);
let status = command.arg(path.as_os_str()).status()?;
if status.success() {
return Ok(InstallSettings::from_file(path)?);
return Ok(InstallSettings::from_file(
path,
&InstallationContext::from_env()?,
)?);
}

Err(anyhow!(
Expand Down
17 changes: 14 additions & 3 deletions rust/agama-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
// find current contact information at www.suse.com.

use agama_lib::auth::AuthToken;
use agama_lib::context::InstallationContext;
use agama_lib::manager::FinishMethod;
use anyhow::Context;
use auth_tokens_file::AuthTokensFile;
use clap::{Args, Parser};
use fluent_uri::UriRef;

mod auth;
mod auth_tokens_file;
Expand Down Expand Up @@ -185,15 +187,24 @@ async fn build_manager<'a>() -> anyhow::Result<ManagerClient<'a>> {
Ok(ManagerClient::new(conn).await?)
}

pub fn download_file(url: &str, path: &PathBuf) -> Result<(), ServiceError> {
pub fn download_file(url: &str, path: &PathBuf) -> anyhow::Result<()> {
let mut file = fs::OpenOptions::new()
.create(true)
.write(true)
.mode(0o400)
.truncate(true)
.mode(0o600)
.open(path)
.context(format!("Cannot write the file '{}'", path.display()))?;

match Transfer::get(url, &mut file) {
let context = InstallationContext::from_env().unwrap();
let uri = UriRef::parse(url).context("Invalid URL")?;
let absolute_url = if uri.has_scheme() {
uri.to_string()
} else {
uri.resolve_against(&context.source)?.to_string()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation! Again we are adding a secret feature. Please locate the 2 occurrences of relurl in Agama source, both are affected by this PR, one by this commit :)

};

match Transfer::get(&absolute_url, &mut file) {
Ok(()) => println!("File saved to {}", path.display()),
Err(e) => eprintln!("Could not retrieve the file: {e}"),
}
Expand Down
34 changes: 23 additions & 11 deletions rust/agama-cli/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@

use crate::show_progress;
use agama_lib::{
base_http_client::BaseHTTPClient, install_settings::InstallSettings,
profile::ValidationOutcome, utils::FileFormat, utils::Transfer, Store as SettingsStore,
base_http_client::BaseHTTPClient,
context::InstallationContext,
install_settings::InstallSettings,
profile::ValidationOutcome,
utils::{FileFormat, Transfer},
Store as SettingsStore,
};
use anyhow::Context;
use clap::Subcommand;
use console::style;
use fluent_uri::Uri;
use std::{
io,
io::Read,
Expand Down Expand Up @@ -228,11 +233,11 @@ async fn import(client: BaseHTTPClient, url_string: String) -> anyhow::Result<()
}
});

let url = Url::parse(&url_string)?;
let path = url.path();
let url = Uri::parse(url_string.as_str())?;
let path = url.path().to_string();
let profile_json = if path.ends_with(".xml") || path.ends_with(".erb") || path.ends_with('/') {
// AutoYaST specific download and convert to JSON
let config_string = autoyast_client(&client, &url).await?;
let config_string = autoyast_client(&client, &url.to_owned()).await?;
Some(config_string)
} else {
pre_process_profile(&client, &url_string).await?
Expand All @@ -241,7 +246,10 @@ async fn import(client: BaseHTTPClient, url_string: String) -> anyhow::Result<()
// None means the profile is a script and it has been executed
if let Some(profile_json) = profile_json {
validate(&client, CliInput::Full(profile_json.clone())).await?;
store_settings(client, &profile_json).await?;
let context = InstallationContext {
source: url.to_owned(),
};
store_settings(client, &profile_json, &context).await?;
}
Ok(())
}
Expand Down Expand Up @@ -280,9 +288,13 @@ async fn pre_process_profile(
}
}

async fn store_settings(client: BaseHTTPClient, profile_json: &str) -> anyhow::Result<()> {
async fn store_settings(
client: BaseHTTPClient,
profile_json: &str,
context: &InstallationContext,
) -> anyhow::Result<()> {
let store = SettingsStore::new(client).await?;
let settings: InstallSettings = serde_json::from_str(profile_json)?;
let settings = InstallSettings::from_json(profile_json, context)?;
store.store(&settings).await?;
Ok(())
}
Expand All @@ -291,7 +303,7 @@ async fn store_settings(client: BaseHTTPClient, profile_json: &str) -> anyhow::R
/// Note that this client does not act on this *url*, it passes it as a parameter
/// to our web backend.
/// Return well-formed Agama JSON on success.
async fn autoyast_client(client: &BaseHTTPClient, url: &Url) -> anyhow::Result<String> {
async fn autoyast_client(client: &BaseHTTPClient, url: &Uri<String>) -> anyhow::Result<String> {
// FIXME: how to escape it?
let api_url = format!("/profile/autoyast?url={}", url);
let output: Box<serde_json::value::RawValue> = client.post(&api_url, &()).await?;
Expand All @@ -300,8 +312,8 @@ async fn autoyast_client(client: &BaseHTTPClient, url: &Url) -> anyhow::Result<S
}

async fn autoyast(client: BaseHTTPClient, url_string: String) -> anyhow::Result<()> {
let url = Url::parse(&url_string)?;
let output = autoyast_client(&client, &url).await?;
let url = Uri::parse(url_string.as_str())?;
let output = autoyast_client(&client, &url.to_owned()).await?;
println!("{}", output);
Ok(())
}
Expand Down
5 changes: 3 additions & 2 deletions rust/agama-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ tempfile = "3.13.0"
thiserror = "1.0.64"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
tokio-stream = "0.1.16"
url = "2.5.2"
utoipa = "5.2.0"
url = { version = "2.5.2", features = ["serde"] }
utoipa = { version = "5.2.0", features = ["url"] }
zbus = { version = "5", default-features = false, features = ["tokio"] }
# Needed to define curl error in profile errors
curl = { version = "0.4.47", features = ["protocol-ftp"] }
Expand All @@ -37,6 +37,7 @@ strum = { version = "0.27.1", features = ["derive"] }
fs_extra = "1.3.0"
serde_with = "3.12.0"
regex = "1.11.1"
fluent-uri = { version = "0.3.2", features = ["serde"] }

[dev-dependencies]
httpmock = "0.7.0"
Expand Down
20 changes: 10 additions & 10 deletions rust/agama-lib/share/profile.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -600,8 +600,8 @@
"type": "string"
},
"url": {
"title": "Script URL",
"description": "URL to fetch the script from"
"title": "Script URL reference",
"description": "Absolute or relative URL to fetch the script from"
}
},
"required": ["name"],
Expand All @@ -628,8 +628,8 @@
"type": "string"
},
"url": {
"title": "Script URL",
"description": "URL to fetch the script from"
"title": "Script URL reference",
"description": "Absolute or relative URL to fetch the script from."
}
},
"required": ["name"],
Expand All @@ -656,8 +656,8 @@
"type": "string"
},
"url": {
"title": "Script URL",
"description": "URL to fetch the script from"
"title": "Script URL reference",
"description": "Absolute or relative URL to fetch the script from."
},
"chroot": {
"title": "Whether it should run in the installed system using a chroot environment",
Expand Down Expand Up @@ -689,8 +689,8 @@
"type": "string"
},
"url": {
"title": "Script URL",
"description": "URL to fetch the script from"
"title": "Script URL reference",
"description": "Absolute or relative URL to fetch the script from."
}
},
"required": ["name"],
Expand All @@ -711,8 +711,8 @@
"type": "string"
},
"url": {
"title": "File URL",
"description": "URL to fetch the file from"
"title": "File URL reference",
"description": "Absolute or relative URL to fetch the file from."
},
"permissions": {
"title": "File permissions",
Expand Down
46 changes: 46 additions & 0 deletions rust/agama-lib/src/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) [2025] SUSE LLC
//
// All Rights Reserved.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 2 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, contact SUSE LLC.
//
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.

use fluent_uri::Uri;

#[derive(Debug, thiserror::Error)]
#[error("Could not determine the installation context")]
pub struct InstallationContextError(String);

/// It contains context information for the store.
#[derive(Debug)]
pub struct InstallationContext {
/// Where the installation settings are from.
/// Used for resolving relative URL references.
pub source: Uri<String>,
}

impl InstallationContext {
/// Sets _source_ to the current directory.
pub fn from_env() -> Result<Self, InstallationContextError> {
let current_path =
std::env::current_dir().map_err(|e| InstallationContextError(e.to_string()))?;
let url = format!("file://{}", current_path.as_path().display());
let url = Uri::parse(url.as_str()).map_err(|e| InstallationContextError(e.to_string()))?;
Ok(Self {
source: url.to_owned(),
})
}
}
2 changes: 1 addition & 1 deletion rust/agama-lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ pub enum ServiceError {
// FIXME reroute the error to a better place
#[error("Profile error: {0}")]
Profile(#[from] ProfileError),
#[error("Unsupported SSL Fingeprint algorithm '#{0}'.")]
#[error("Unsupported SSL Fingerprint algorithm '#{0}'.")]
UnsupportedSSLFingerprintAlgorithm(String),
}

Expand Down
Loading
Loading