Skip to content

Commit b261744

Browse files
committed
add env and workdir options to job submission, add command for setting current cscs system, add command to init coman config
1 parent e4169f4 commit b261744

9 files changed

Lines changed: 222 additions & 79 deletions

File tree

coman/.config/config.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,17 @@ srun --environment={{environment_file}} {{command}}
1616
edf_file_template = """
1717
image = "{{edf_image}}"
1818
mounts = ["${SCRATCH}:/scratch"]
19+
workdir = "{{container_workdir}}"
20+
21+
[env]
22+
{% for key, value in env %}
23+
{{key}} = "{{value}}"
24+
{% endfor %}
1925
"""
2026

27+
[cscs.env]
28+
29+
2130
[cscs.systems]
2231

2332
[cscs.systems.daint]

coman/src/cli.rs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::path::PathBuf;
1+
use std::{error::Error, path::PathBuf};
22

33
use clap::{Parser, Subcommand};
44

@@ -14,6 +14,11 @@ pub enum CliCommands {
1414
#[command(subcommand)]
1515
command: CscsCommands,
1616
},
17+
#[clap(about = "Create a new project configuration file")]
18+
Init {
19+
#[clap(help = "Destination folder to create config in (default = current directory)")]
20+
destination: Option<PathBuf>,
21+
},
1722
}
1823

1924
#[derive(Subcommand, Debug)]
@@ -38,20 +43,40 @@ pub enum CscsJobCommands {
3843
Get { job_id: i64 },
3944
#[clap(alias("s"))]
4045
Submit {
41-
#[clap(short, long)]
46+
#[clap(short, long, help = "the path to the srun script file to use")]
4247
script_file: Option<PathBuf>,
43-
#[clap(short, long)]
48+
#[clap(
49+
short,
50+
long,
51+
help = "the working directory path inside the container (note this is different from the working directory that the srun command is executed from)"
52+
)]
53+
workdir: Option<String>,
54+
#[clap(short='E', value_name="KEY=VALUE", value_parser=parse_key_val::<String,String>, help="Environment variables to set in the container")]
55+
env: Vec<(String, String)>,
56+
#[clap(short, long, help = "The docker image to use")]
4457
image: Option<DockerImageUrl>,
45-
#[clap(short, long, trailing_var_arg = true)]
58+
#[clap(trailing_var_arg = true, help = "The command to run in the container")]
4659
command: Option<Vec<String>>,
4760
},
4861
#[clap(alias("st"))]
4962
Stop { job_id: i64 },
5063
}
5164
#[derive(Subcommand, Debug)]
5265
pub enum CscsSystemCommands {
53-
#[clap(alias("ls"))]
66+
#[clap(alias("ls"), about = "List available compute systems")]
5467
List,
68+
#[clap(alias("s"), about = "Set system to use")]
69+
Set {
70+
#[clap(
71+
short,
72+
long,
73+
action,
74+
help = "set in global config instead of project-local one"
75+
)]
76+
global: bool,
77+
#[clap(help = "System name to use")]
78+
system_name: String,
79+
},
5580
}
5681

5782
#[derive(Parser, Debug)]
@@ -95,3 +120,16 @@ Config directory: {config_dir_path}
95120
Data directory: {data_dir_path}"
96121
)
97122
}
123+
124+
fn parse_key_val<T, U>(s: &str) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
125+
where
126+
T: std::str::FromStr,
127+
T::Err: Error + Send + Sync + 'static,
128+
U: std::str::FromStr,
129+
U::Err: Error + Send + Sync + 'static,
130+
{
131+
let pos = s
132+
.find('=')
133+
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
134+
Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
135+
}

coman/src/config.rs

Lines changed: 116 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::{collections::HashMap, env, path::PathBuf};
44

55
use color_eyre::Result;
66
use directories::ProjectDirs;
7+
use eyre::eyre;
78
use lazy_static::lazy_static;
89
use serde::{Deserialize, Serialize};
910

@@ -27,10 +28,12 @@ pub struct CscsConfig {
2728
#[serde(default)]
2829
pub current_system: String,
2930
#[serde(default)]
30-
pub name: Option<String>,
31-
#[serde(default)]
3231
pub sbatch_script_template: String,
3332
#[serde(default)]
33+
pub workdir: Option<String>,
34+
#[serde(default)]
35+
pub env: HashMap<String, String>,
36+
#[serde(default)]
3437
pub image: String,
3538
#[serde(default)]
3639
pub edf_file_template: String,
@@ -43,6 +46,8 @@ pub struct CscsConfig {
4346

4447
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
4548
pub struct Config {
49+
#[serde(default)]
50+
pub name: Option<String>,
4651
#[serde(default, flatten)]
4752
pub config: AppConfig,
4853
#[serde(default)]
@@ -59,71 +64,123 @@ lazy_static! {
5964
env::var(format!("{}_CONFIG", PROJECT_NAME.clone()))
6065
.ok()
6166
.map(PathBuf::from);
67+
pub static ref CONFIG_FILE_NAME: String = format!("{}.toml", PROJECT_NAME.clone());
68+
pub static ref CONFIG_FORMAT: config::FileFormat = config::FileFormat::Toml;
6269
}
6370

6471
impl Config {
65-
pub fn new() -> Result<Self, config::ConfigError> {
66-
let data_dir = get_data_dir();
67-
let config_dir = get_config_dir();
68-
let mut builder = config::Config::builder()
69-
.add_source(config::File::from_str(
70-
DEFAULT_CONFIG_TOML,
71-
config::FileFormat::Toml,
72-
))
73-
.set_default("data_dir", data_dir.to_str().unwrap())?
74-
.set_default("config_dir", config_dir.to_str().unwrap())?;
75-
76-
let config_files = [
77-
(
78-
format!("{}.toml", PROJECT_NAME.to_lowercase()),
79-
config::FileFormat::Toml,
80-
),
81-
(
82-
format!("{}.json5", PROJECT_NAME.to_lowercase()),
83-
config::FileFormat::Json5,
84-
),
85-
(
86-
format!("{}.json", PROJECT_NAME.to_lowercase()),
87-
config::FileFormat::Json,
88-
),
89-
(
90-
format!("{}.yaml", PROJECT_NAME.to_lowercase()),
91-
config::FileFormat::Yaml,
92-
),
93-
(
94-
format!("{}.ini", PROJECT_NAME.to_lowercase()),
95-
config::FileFormat::Ini,
96-
),
97-
];
98-
for (file, format) in &config_files {
99-
let source = config::File::from(config_dir.join(file))
100-
.format(*format)
101-
.required(false);
102-
builder = builder.add_source(source);
103-
}
72+
pub fn new() -> Result<Self> {
73+
let builder = default_config_builder()?;
74+
let builder = global_config_builder(builder)?;
75+
let builder = project_local_config_builder(builder)?;
10476

105-
// find config override in current directory
106-
let mut search_path = std::env::current_dir().expect("current directory does not exist");
107-
loop {
108-
for (file, format) in &config_files {
109-
if search_path.join(file).exists() {
110-
let source = config::File::from(search_path.join(file))
111-
.format(*format)
112-
.required(false);
113-
builder = builder.add_source(source);
114-
break;
115-
}
116-
}
117-
if let Some(p) = search_path.parent() {
118-
search_path = p.to_path_buf();
119-
} else {
120-
break;
121-
}
122-
}
77+
let cfg: Self = builder.build()?.try_deserialize()?;
78+
Ok(cfg)
79+
}
80+
pub fn new_global() -> Result<Self> {
81+
let builder = default_config_builder()?;
82+
let builder = global_config_builder(builder)?;
12383

12484
let cfg: Self = builder.build()?.try_deserialize()?;
12585
Ok(cfg)
12686
}
87+
88+
pub fn write_local(&self) -> Result<()> {
89+
match get_project_local_config_file() {
90+
Some(path) => {
91+
let content = toml::to_string_pretty(self)?;
92+
std::fs::write(path, content)?;
93+
Ok(())
94+
}
95+
None => Err(eyre!(
96+
"No config file exists in current project. Consider creating one using '{} init",
97+
PROJECT_NAME.to_lowercase().clone()
98+
)),
99+
}
100+
}
101+
102+
pub fn write_global(&self) -> Result<()> {
103+
let config_dir = get_config_dir();
104+
let path = config_dir.join(CONFIG_FILE_NAME.clone());
105+
let content = toml::to_string_pretty(self)?;
106+
std::fs::write(path, content)?;
107+
Ok(())
108+
}
109+
110+
pub fn create_config(destination: Option<PathBuf>) -> Result<()> {
111+
let mut config = Config::new()?;
112+
let project_dir = destination
113+
.unwrap_or(std::env::current_dir().expect("current directory does not exist"));
114+
if !project_dir.exists() || !project_dir.is_dir() {
115+
return Err(eyre!(
116+
"destination must exist and be a directory, got {}",
117+
project_dir.to_string_lossy()
118+
));
119+
}
120+
121+
let name = project_dir
122+
.file_name()
123+
.expect("could not get base name from destination");
124+
config.name = Some(name.to_string_lossy().to_string());
125+
126+
let config_path = project_dir.join(CONFIG_FILE_NAME.clone());
127+
std::fs::write(config_path.clone(), "")?;
128+
let content = toml::to_string_pretty(&config)?;
129+
std::fs::write(config_path, content)?;
130+
Ok(())
131+
}
132+
}
133+
134+
pub fn default_config_builder() -> Result<config::ConfigBuilder<config::builder::DefaultState>> {
135+
let data_dir = get_data_dir();
136+
let config_dir = get_config_dir();
137+
let builder = config::Config::builder()
138+
.add_source(config::File::from_str(
139+
DEFAULT_CONFIG_TOML,
140+
config::FileFormat::Toml,
141+
))
142+
.set_default("data_dir", data_dir.to_str().unwrap())?
143+
.set_default("config_dir", config_dir.to_str().unwrap())?;
144+
Ok(builder)
145+
}
146+
147+
pub fn global_config_builder(
148+
builder: config::ConfigBuilder<config::builder::DefaultState>,
149+
) -> Result<config::ConfigBuilder<config::builder::DefaultState>> {
150+
let config_dir = get_config_dir();
151+
let source = config::File::from(config_dir.join(CONFIG_FILE_NAME.clone()))
152+
.format(*CONFIG_FORMAT)
153+
.required(false);
154+
let builder = builder.add_source(source);
155+
Ok(builder)
156+
}
157+
158+
pub fn project_local_config_builder(
159+
builder: config::ConfigBuilder<config::builder::DefaultState>,
160+
) -> Result<config::ConfigBuilder<config::builder::DefaultState>> {
161+
if let Some(config_path) = get_project_local_config_file() {
162+
let source = config::File::from(config_path)
163+
.format(*CONFIG_FORMAT)
164+
.required(false);
165+
let builder = builder.add_source(source);
166+
return Ok(builder);
167+
}
168+
Ok(builder)
169+
}
170+
171+
pub fn get_project_local_config_file() -> Option<PathBuf> {
172+
let mut search_path = std::env::current_dir().expect("current directory does not exist");
173+
loop {
174+
if search_path.join(CONFIG_FILE_NAME.clone()).exists() {
175+
return Some(search_path.join(CONFIG_FILE_NAME.clone()));
176+
}
177+
if let Some(p) = search_path.parent() {
178+
search_path = p.to_path_buf();
179+
} else {
180+
break;
181+
}
182+
}
183+
None
127184
}
128185

129186
pub fn get_data_dir() -> PathBuf {

coman/src/cscs/api_client.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use firecrest_client::{
1515
JobMetadataModel, JobModelOutput, SchedulerServiceHealth, UserInfoResponse,
1616
},
1717
};
18-
use std::path::PathBuf;
18+
use std::{collections::HashMap, path::PathBuf};
1919
use strum::Display;
2020

2121
use crate::trace_dbg;
@@ -263,6 +263,7 @@ impl CscsApi {
263263
system_name: &str,
264264
name: &str,
265265
script_path: PathBuf,
266+
envvars: HashMap<String, String>,
266267
) -> Result<()> {
267268
let workingdir = script_path.clone();
268269
let workingdir = workingdir.parent();
@@ -273,6 +274,7 @@ impl CscsApi {
273274
None,
274275
Some(script_path),
275276
workingdir.map(|p| p.to_path_buf()),
277+
envvars,
276278
)
277279
.await?;
278280
let _ = trace_dbg!(result);

coman/src/cscs/cli.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use std::path::PathBuf;
55
use crate::{
66
cscs::{
77
handlers::{
8-
cscs_job_stop, cscs_job_details, cscs_job_list, cscs_start_job, cscs_system_list,
8+
cscs_job_details, cscs_job_list, cscs_job_stop, cscs_start_job, cscs_system_list,
9+
cscs_system_set,
910
},
1011
oauth2::{CLIENT_ID_SECRET_NAME, CLIENT_SECRET_SECRET_NAME, client_credentials_login},
1112
},
@@ -93,8 +94,10 @@ pub(crate) async fn cli_cscs_job_start(
9394
script_file: Option<PathBuf>,
9495
image: Option<DockerImageUrl>,
9596
command: Option<Vec<String>>,
97+
workdir: Option<String>,
98+
env: Vec<(String, String)>,
9699
) -> Result<()> {
97-
cscs_start_job(script_file, image, command).await
100+
cscs_start_job(script_file, image, command, workdir, env).await
98101
}
99102

100103
pub(crate) async fn cli_cscs_job_stop(job_id: i64) -> Result<()> {
@@ -112,3 +115,6 @@ pub(crate) async fn cli_cscs_system_list() -> Result<()> {
112115
Err(e) => Err(e),
113116
}
114117
}
118+
pub(crate) async fn cli_cscs_set_system(system_name: String, global: bool) -> Result<()> {
119+
cscs_system_set(system_name, global).await
120+
}

0 commit comments

Comments
 (0)