Skip to content

Commit 8f06129

Browse files
committed
add proxy-config, activate https/http proxy for pixi commands
1 parent e68fa93 commit 8f06129

25 files changed

+217
-1
lines changed

crates/pixi_config/src/lib.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,11 @@ pub struct Config {
646646
#[serde(skip_serializing_if = "ConcurrencyConfig::is_default")]
647647
pub concurrency: ConcurrencyConfig,
648648

649+
/// Https/Http proxy configuration for pixi
650+
#[serde(default)]
651+
#[serde(skip_serializing_if = "ProxyConfig::is_default")]
652+
pub proxy_config: ProxyConfig,
653+
649654
//////////////////////
650655
// Deprecated fields //
651656
//////////////////////
@@ -676,6 +681,7 @@ impl Default for Config {
676681
shell: ShellConfig::default(),
677682
experimental: ExperimentalConfig::default(),
678683
concurrency: ConcurrencyConfig::default(),
684+
proxy_config: ProxyConfig::default(),
679685

680686
// Deprecated fields
681687
change_ps1: None,
@@ -778,6 +784,40 @@ impl ShellConfig {
778784
}
779785
}
780786

787+
#[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq, Eq)]
788+
#[serde(rename_all = "kebab-case")]
789+
pub struct ProxyConfig {
790+
/// https proxy.
791+
#[serde(default)]
792+
#[serde(skip_serializing_if = "Option::is_none")]
793+
pub https: Option<Url>,
794+
/// http proxy.
795+
#[serde(default)]
796+
#[serde(skip_serializing_if = "Option::is_none")]
797+
pub http: Option<Url>,
798+
/// A list of no proxy pattern
799+
#[serde(default)]
800+
#[serde(skip_serializing_if = "Vec::is_empty")]
801+
pub no_proxy_domains: Vec<String>,
802+
}
803+
804+
impl ProxyConfig {
805+
pub fn is_default(&self) -> bool {
806+
self.https.is_none() && self.https.is_none() && self.no_proxy_domains.is_empty()
807+
}
808+
pub fn merge(&self, other: Self) -> Self {
809+
Self {
810+
https: other.https.as_ref().or(self.https.as_ref()).cloned(),
811+
http: other.http.as_ref().or(self.http.as_ref()).cloned(),
812+
no_proxy_domains: if other.is_default() {
813+
self.no_proxy_domains.clone()
814+
} else {
815+
other.no_proxy_domains.clone()
816+
},
817+
}
818+
}
819+
}
820+
781821
#[derive(thiserror::Error, Debug)]
782822
pub enum ConfigError {
783823
#[error("no file was found at {0}")]
@@ -1042,6 +1082,10 @@ impl Config {
10421082
"s3-options.<bucket>.region",
10431083
"s3-options.<bucket>.force-path-style",
10441084
"experimental.use-environment-activation-cache",
1085+
"proxy-config",
1086+
"proxy-config.https",
1087+
"proxy-config.http",
1088+
"proxy-config.no-proxy-domains",
10451089
]
10461090
}
10471091

@@ -1082,6 +1126,8 @@ impl Config {
10821126
// Make other take precedence over self to allow for setting the value through the CLI
10831127
concurrency: self.concurrency.merge(other.concurrency),
10841128

1129+
proxy_config: self.proxy_config.merge(other.proxy_config),
1130+
10851131
// Deprecated fields that we can ignore as we handle them inside `shell.` field
10861132
change_ps1: None,
10871133
force_activate: None,
@@ -1157,6 +1203,57 @@ impl Config {
11571203
self.concurrency.downloads
11581204
}
11591205

1206+
pub fn activate_proxy_envs(&self) {
1207+
if self.proxy_config.https.is_none() && self.proxy_config.http.is_none() {
1208+
if !self.proxy_config.no_proxy_domains.is_empty() {
1209+
tracing::info!("proxy_config.no_proxy_domains is ignored")
1210+
}
1211+
return;
1212+
}
1213+
1214+
// detect proxy env vars like curl: https://curl.se/docs/manpage.html
1215+
let env_https_proxy = ["https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"]
1216+
.iter()
1217+
.find_map(|&k| std::env::var(k).ok().filter(|v| !v.is_empty()));
1218+
let env_http_proxy = ["http_proxy", "all_proxy", "ALL_PROXY"]
1219+
.iter()
1220+
.find_map(|&k| std::env::var(k).ok().filter(|v| !v.is_empty()));
1221+
let env_no_proxy = ["no_proxy", "NO_PROXY"]
1222+
.iter()
1223+
.find_map(|&k| std::env::var(k).ok().filter(|v| !v.is_empty()));
1224+
1225+
let config_no_proxy =
1226+
Some(self.proxy_config.no_proxy_domains.iter().join(",")).filter(|v| !v.is_empty());
1227+
1228+
if env_https_proxy.is_some() || env_http_proxy.is_some() {
1229+
if env_https_proxy.as_deref() != self.proxy_config.https.as_ref().map(Url::as_str)
1230+
|| env_http_proxy.as_deref() != self.proxy_config.http.as_ref().map(Url::as_str)
1231+
|| env_no_proxy != config_no_proxy
1232+
{
1233+
tracing::info!("proxy configs are overridden by proxy environment vars.")
1234+
}
1235+
return;
1236+
}
1237+
1238+
if let Some(url) = &self.proxy_config.https {
1239+
std::env::set_var("https_proxy", url.as_str());
1240+
tracing::debug!("use https proxy: {}", url.as_str());
1241+
}
1242+
if let Some(url) = &self.proxy_config.http {
1243+
std::env::set_var("http_proxy", url.as_str());
1244+
tracing::debug!("use http proxy: {}", url.as_str());
1245+
}
1246+
1247+
if config_no_proxy != env_no_proxy {
1248+
if let Some(no_proxy_str) = &config_no_proxy {
1249+
std::env::set_var("no_proxy", no_proxy_str);
1250+
tracing::debug!("no proxy hosts: {}", no_proxy_str);
1251+
} else {
1252+
std::env::remove_var("no_proxy");
1253+
}
1254+
}
1255+
}
1256+
11601257
/// Modify this config with the given key and value
11611258
///
11621259
/// # Note
@@ -1421,6 +1518,42 @@ impl Config {
14211518
_ => return Err(err),
14221519
}
14231520
}
1521+
key if key.starts_with("proxy-config") => {
1522+
if key == "proxy-config" {
1523+
if let Some(value) = value {
1524+
self.proxy_config = serde_json::de::from_str(&value).into_diagnostic()?;
1525+
} else {
1526+
self.proxy_config = ProxyConfig::default();
1527+
}
1528+
return Ok(());
1529+
} else if !key.starts_with("proxy-config.") {
1530+
return Err(err);
1531+
}
1532+
1533+
let subkey = key.strip_prefix("proxy-config.").unwrap();
1534+
match subkey {
1535+
"https" => {
1536+
self.proxy_config.https = value
1537+
.map(|v| Url::parse(&v))
1538+
.transpose()
1539+
.into_diagnostic()?;
1540+
}
1541+
"http" => {
1542+
self.proxy_config.http = value
1543+
.map(|v| Url::parse(&v))
1544+
.transpose()
1545+
.into_diagnostic()?;
1546+
}
1547+
"no-proxy-domains" => {
1548+
self.proxy_config.no_proxy_domains = value
1549+
.map(|v| serde_json::de::from_str(&v))
1550+
.transpose()
1551+
.into_diagnostic()?
1552+
.unwrap_or_default();
1553+
}
1554+
_ => return Err(err),
1555+
}
1556+
}
14241557
_ => return Err(err),
14251558
}
14261559

@@ -1725,6 +1858,7 @@ UNUSED = "unused"
17251858
RepodataChannelConfig::default(),
17261859
)]),
17271860
},
1861+
proxy_config: ProxyConfig::default(),
17281862
// Deprecated keys
17291863
change_ps1: None,
17301864
force_activate: None,
@@ -2145,4 +2279,25 @@ UNUSED = "unused"
21452279
assert_eq!(anaconda_config.disable_zstd, Some(false));
21462280
assert_eq!(anaconda_config.disable_sharded, None);
21472281
}
2282+
2283+
#[test]
2284+
fn test_proxy_config_parse() {
2285+
let toml = r#"
2286+
[proxy-config]
2287+
https = "http://proxy-for-https"
2288+
http = "http://proxy-for-http"
2289+
no-proxy-domains = [ "a.com" ]
2290+
"#;
2291+
let (config, _) = Config::from_toml(toml, None).unwrap();
2292+
assert_eq!(
2293+
config.proxy_config.https,
2294+
Some(Url::parse("http://proxy-for-https").unwrap())
2295+
);
2296+
assert_eq!(
2297+
config.proxy_config.http,
2298+
Some(Url::parse("http://proxy-for-http").unwrap())
2299+
);
2300+
assert_eq!(config.proxy_config.no_proxy_domains.len(), 1);
2301+
assert_eq!(config.proxy_config.no_proxy_domains[0], "a.com");
2302+
}
21482303
}

crates/pixi_config/src/snapshots/pixi_config__tests__config_merge_multiple.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ Config {
133133
solves: 1,
134134
downloads: 50,
135135
},
136+
proxy_config: ProxyConfig {
137+
https: None,
138+
http: None,
139+
no_proxy_domains: [],
140+
},
136141
change_ps1: None,
137142
force_activate: None,
138143
}

src/cli/add.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
9999
.locate()?
100100
.with_cli_config(prefix_update_config.config.clone());
101101

102+
workspace.activate_proxy_envs();
103+
102104
sanity_check_project(&workspace).await?;
103105

104106
let mut workspace = workspace.modify()?;

src/cli/build.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
8989
.locate()?
9090
.with_cli_config(args.config_cli);
9191

92+
workspace.activate_proxy_envs();
93+
9294
// TODO: Implement logic to take the source code from a VCS instead of from a
9395
// local channel so that that information is also encoded in the manifest.
9496

src/cli/exec.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
5454
let config = Config::with_cli_config(&args.config);
5555
let cache_dir = pixi_config::get_cache_dir().context("failed to determine cache directory")?;
5656

57+
config.activate_proxy_envs();
58+
5759
let mut command_args = args.command.iter();
5860
let command = command_args.next().ok_or_else(|| miette::miette!(help ="i.e when specifying specs explicitly use a command at the end: `pixi exec -s python==3.12 python`", "missing required command to execute",))?;
5961
let (_, client) = build_reqwest_clients(Some(&config), None)?;

src/cli/global/expose.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ pub async fn add(args: AddArgs) -> miette::Result<()> {
7676
.await?
7777
.with_cli_config(config.clone());
7878

79+
project_original.activate_proxy_envs();
80+
7981
async fn apply_changes(
8082
args: &AddArgs,
8183
project: &mut global::Project,

src/cli/global/install.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
8686
.await?
8787
.with_cli_config(config.clone());
8888

89+
project_original.activate_proxy_envs();
90+
8991
let env_names = match &args.environment {
9092
Some(env_name) => Vec::from([env_name.clone()]),
9193
None => args

src/cli/global/remove.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
4747
miette::bail!("Environment {} doesn't exist. You can create a new environment with `pixi global install`.", env_name);
4848
}
4949

50+
project_original.activate_proxy_envs();
51+
5052
async fn apply_changes(
5153
env_name: &EnvironmentName,
5254
specs: &[MatchSpec],

src/cli/global/sync.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
1717
.await?
1818
.with_cli_config(config.clone());
1919

20+
project.activate_proxy_envs();
21+
2022
let mut has_changed = false;
2123

2224
// Prune environments that are not listed

src/cli/global/update.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
2323
.await?
2424
.with_cli_config(config.clone());
2525

26+
project_original.activate_proxy_envs();
27+
2628
async fn apply_changes(
2729
env_name: &EnvironmentName,
2830
project: &mut Project,

src/cli/install.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
4747
.locate()?
4848
.with_cli_config(args.config);
4949

50+
workspace.activate_proxy_envs();
51+
5052
// Install either:
5153
//
5254
// 1. specific environments

src/cli/lock.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
2626
.with_search_start(args.workspace_config.workspace_locator_start())
2727
.locate()?;
2828

29+
workspace.activate_proxy_envs();
30+
2931
// Save the original lockfile to compare with the new one.
3032
let original_lock_file = workspace.load_lock_file().await?;
3133
let new_lock_file = workspace

src/cli/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use clap::Parser;
22
use clap_verbosity_flag::Verbosity;
33
use indicatif::ProgressDrawTarget;
44
use miette::IntoDiagnostic;
5+
use pixi_config::Config;
56
use pixi_consts::consts;
67
use pixi_progress::global_multi_progress;
78
use pixi_utils::indicatif::IndicatifWriter;
@@ -249,7 +250,10 @@ pub async fn execute_command(command: Command) -> miette::Result<()> {
249250
Command::Clean(cmd) => clean::execute(cmd).await,
250251
Command::Run(cmd) => run::execute(cmd).await,
251252
Command::Global(cmd) => global::execute(cmd).await,
252-
Command::Auth(cmd) => rattler::cli::auth::execute(cmd).await.into_diagnostic(),
253+
Command::Auth(cmd) => {
254+
Config::load_global().activate_proxy_envs();
255+
rattler::cli::auth::execute(cmd).await.into_diagnostic()
256+
}
253257
Command::Install(cmd) => install::execute(cmd).await,
254258
Command::Shell(cmd) => shell::execute(cmd).await,
255259
Command::ShellHook(cmd) => shell_hook::execute(cmd).await,

src/cli/remove.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
108108
// TODO: update all environments touched by this feature defined.
109109
// updating prefix after removing from toml
110110
if !prefix_update_config.no_lockfile_update {
111+
workspace.activate_proxy_envs();
112+
111113
get_update_lock_file_and_prefix(
112114
&workspace.default_environment(),
113115
UpdateMode::Revalidate,

src/cli/run.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
9494
.locate()?
9595
.with_cli_config(cli_config);
9696

97+
workspace.activate_proxy_envs();
98+
9799
// Extract the passed in environment name.
98100
let environment = workspace.environment_from_name_or_env_var(args.environment.clone())?;
99101

src/cli/search.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ pub async fn execute_impl<W: Write>(
147147

148148
let config = Config::load_global();
149149

150+
project
151+
.map_or(&config, |p| p.config())
152+
.activate_proxy_envs();
153+
150154
// Fetch the all names from the repodata using gateway
151155
let gateway = config.gateway(client.clone());
152156

src/cli/self_update.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use flate2::read::GzDecoder;
44
use tar::Archive;
55

66
use miette::IntoDiagnostic;
7+
use pixi_config::Config;
78
use pixi_consts::consts;
89
use reqwest::redirect::Policy;
910
use reqwest::Client;
@@ -110,6 +111,8 @@ async fn latest_version() -> miette::Result<Version> {
110111
}
111112

112113
pub async fn execute(args: Args) -> miette::Result<()> {
114+
Config::load_global().activate_proxy_envs();
115+
113116
// Get the target version, without 'v' prefix
114117
let target_version = match &args.version {
115118
Some(version) => version,

src/cli/shell.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ pub async fn execute(args: Args) -> miette::Result<()> {
263263
.locate()?
264264
.with_cli_config(config);
265265

266+
workspace.activate_proxy_envs();
267+
266268
let environment = workspace.environment_from_name_or_env_var(args.environment)?;
267269

268270
// Make sure environment is up-to-date, default to install, users can avoid this with frozen or locked.

0 commit comments

Comments
 (0)