Simplify ansible ssh identity interface

This commit is contained in:
wendrul
2025-04-22 17:57:49 +02:00
parent 64f73a60cd
commit 48fc0648e1
3 changed files with 114 additions and 26 deletions
+20 -19
View File
@@ -229,6 +229,7 @@ pub struct AnsibleRequirements {
pub vault_password_file: Option<String>,
pub vault_id: Vec<String>,
pub git_repos: Vec<GitRepo>,
pub git_ssh_identity_files: Vec<String>,
}
impl Default for AnsibleRequirements {
@@ -250,6 +251,7 @@ impl Default for AnsibleRequirements {
vault_password_file: None,
vault_id: vec![],
git_repos: vec![],
git_ssh_identity_files: vec![],
}
}
}
@@ -310,25 +312,7 @@ pub fn parse_ansible_reqs(
return Ok((logs, None, inner_content.to_string()));
}
let opts = AnsiblePlaybookOptions {
verbosity: None,
forks: None,
timeout: None,
flush_cache: None,
force_handlers: None,
};
let mut ret = AnsibleRequirements {
python_reqs: vec![],
collections: None,
file_resources: vec![],
inventories: vec![],
vars: vec![],
resources: vec![],
options: opts,
vault_password_file: None,
vault_id: vec![],
git_repos: vec![],
};
let mut ret = AnsibleRequirements::default();
if let Yaml::Hash(doc) = &docs[0] {
for (key, value) in doc {
@@ -420,6 +404,23 @@ pub fn parse_ansible_reqs(
);
}
}
Yaml::String(key) if key == "git_ssh_identity_files" => {
let Yaml::Array(indentity_files) = &value else {
return Err(anyhow!(
"git_ssh_identity_files expects an array of identity file names"
));
};
for r in indentity_files {
let Yaml::String(file_name) = r else {
return Err(anyhow!(
"Git ssh identity file must be a string defining a file name"
));
};
ret.git_ssh_identity_files.push(file_name.clone());
}
}
Yaml::String(key) => logs.push_str(&format!("\nUnknown field `{}`. Ignoring", key)),
_ => (),
}
@@ -5,9 +5,10 @@ use std::{collections::HashMap, os::unix::fs::PermissionsExt, path::PathBuf, pro
use std::{collections::HashMap, path::PathBuf, process::Stdio};
use anyhow::anyhow;
use futures::future::try_join_all;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use serde_json::{json, value::RawValue};
use tokio::process::Command;
use uuid::Uuid;
use windmill_common::{
@@ -54,6 +55,7 @@ async fn clone_repo(
canceled_by: &mut Option<CanceledBy>,
w_id: &str,
occupancy_metrics: &mut OccupancyMetrics,
git_ssh_cmd: &str,
) -> error::Result<String> {
let target_path = is_allowed_file_location(job_dir, &repo.target_path)?;
@@ -64,6 +66,7 @@ async fn clone_repo(
.envs(PROXY_ENVS.clone())
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("GIT_SSH_COMMAND", git_ssh_cmd)
.args(["clone", "--quiet"])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
@@ -100,6 +103,7 @@ async fn clone_repo(
.envs(PROXY_ENVS.clone())
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("GIT_SSH_COMMAND", git_ssh_cmd)
.arg("-C")
.arg(&target_path)
.args(["checkout", "--quiet", commit])
@@ -133,6 +137,7 @@ async fn clone_repo(
.envs(PROXY_ENVS.clone())
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("GIT_SSH_COMMAND", git_ssh_cmd)
.arg("-C")
.arg(&target_path)
.args(["rev-parse", "HEAD"])
@@ -188,6 +193,7 @@ async fn clone_repo_without_history(
canceled_by: &mut Option<CanceledBy>,
w_id: &str,
occupancy_metrics: &mut OccupancyMetrics,
git_ssh_cmd: &str,
) -> error::Result<()> {
let target_path = is_allowed_file_location(job_dir, &repo.target_path)?;
@@ -234,6 +240,7 @@ async fn clone_repo_without_history(
.envs(PROXY_ENVS.clone())
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("GIT_SSH_COMMAND", git_ssh_cmd)
.arg("-C")
.arg(&target_path)
.args(vec!["remote", "add", "origin", &repo.url])
@@ -265,6 +272,7 @@ async fn clone_repo_without_history(
.envs(PROXY_ENVS.clone())
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("GIT_SSH_COMMAND", git_ssh_cmd)
.arg("-C")
.arg(&target_path)
.args(vec!["fetch", "--depth=1", "--quiet", "origin", full_commit])
@@ -296,6 +304,7 @@ async fn clone_repo_without_history(
.envs(PROXY_ENVS.clone())
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("GIT_SSH_COMMAND", git_ssh_cmd)
.arg("-C")
.arg(&target_path)
.args(["checkout", "--quiet", "FETCH_HEAD"])
@@ -408,6 +417,7 @@ pub async fn install_galaxy_collections(
canceled_by: &mut Option<CanceledBy>,
conn: &Connection,
occupancy_metrics: &mut OccupancyMetrics,
git_ssh_cmd: &str,
) -> anyhow::Result<()> {
write_file(job_dir, "requirements.yml", collections_yml)?;
@@ -426,7 +436,15 @@ pub async fn install_galaxy_collections(
.envs(PROXY_ENVS.clone())
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.args(vec!["role", "install", "-r", "requirements.yml", "-p", "./roles"])
.env("GIT_SSH_COMMAND", git_ssh_cmd)
.args(vec![
"role",
"install",
"-r",
"requirements.yml",
"-p",
"./roles",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
@@ -455,6 +473,7 @@ pub async fn install_galaxy_collections(
.envs(PROXY_ENVS.clone())
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("GIT_SSH_COMMAND", git_ssh_cmd)
.args(vec![
"collection",
"install",
@@ -586,10 +605,15 @@ pub async fn get_role_locks(job_dir: &str) -> anyhow::Result<(HashMap<String, St
Ok((ret, logs))
}
pub async fn get_git_repo_full_head_commit_hash(repo: &GitRepo) -> anyhow::Result<String> {
pub async fn get_git_repo_full_head_commit_hash(
repo: &GitRepo,
git_ssh_cmd: &str,
) -> anyhow::Result<String> {
let mut git_cmd = Command::new(GIT_PATH.as_str());
git_cmd.args(["ls-remote", &repo.url, "HEAD"]);
git_cmd
.env("GIT_SSH_COMMAND", git_ssh_cmd)
.args(["ls-remote", &repo.url, "HEAD"]);
let output = git_cmd.stderr(Stdio::piped()).output().await?;
@@ -628,6 +652,7 @@ pub async fn get_git_repos_lock(
canceled_by: &mut Option<CanceledBy>,
w_id: &str,
occupancy_metrics: &mut OccupancyMetrics,
git_ssh_cmd: &str,
) -> anyhow::Result<HashMap<String, String>> {
let mut ret = HashMap::new();
@@ -645,13 +670,14 @@ pub async fn get_git_repos_lock(
canceled_by,
w_id,
occupancy_metrics,
git_ssh_cmd,
)
.await?,
);
} else {
ret.insert(
repo.url.to_string(),
get_git_repo_full_head_commit_hash(repo).await?,
get_git_repo_full_head_commit_hash(repo, git_ssh_cmd).await?,
);
}
}
@@ -688,6 +714,40 @@ remote_tmp={job_dir}/.ansible/tmp
Ok(())
}
pub async fn get_git_ssh_cmd(reqs: &AnsibleRequirements, job_dir: &str, client: &AuthedClient) -> error::Result<String> {
let ssh_id_files = try_join_all(reqs
.git_ssh_identity_files
.iter()
.enumerate()
.map(async |(i, var_path)| -> error::Result<String> {
let id_file_name = format!(".ssh_id_priv_{}", i);
let loc = is_allowed_file_location(job_dir, &id_file_name)?;
let content = client
.get_variable_value(var_path)
.await
.map_err(|e| {
error::Error::NotFound(format!("Variable {var_path} not found for git ssh id file: {e:#}"))
})?;
let file = write_file(job_dir, &id_file_name, &content)?;
#[cfg(unix)]
{
let perm = std::os::unix::fs::PermissionsExt::from_mode(0o600);
file.set_permissions(perm)?;
}
Ok(format!(
" -i '{}'",
loc.to_string_lossy().replace('\'', r"'\''")
))
})).await?;
let git_ssh_cmd = format!("ssh -o StrictHostKeyChecking=no{}", ssh_id_files.join(""));
Ok(git_ssh_cmd)
}
pub async fn handle_ansible_job(
requirements_o: Option<&String>,
job_dir: &str,
@@ -734,6 +794,11 @@ pub async fn handle_ansible_job(
)
.await?;
let git_ssh_cmd = &match &reqs {
Some(r) => get_git_ssh_cmd(r, job_dir, client).await?,
None => "ssh".to_string(),
};
let interpolated_args;
if let Some(args) = &job.args {
let mut args = args.0.clone();
@@ -818,6 +883,7 @@ pub async fn handle_ansible_job(
canceled_by,
&job.workspace_id,
occupancy_metrics,
git_ssh_cmd,
)
.await
.map_err(|e| anyhow!("Failed to clone git repo `{}`: {e}", repo.url))?;
@@ -841,6 +907,7 @@ pub async fn handle_ansible_job(
canceled_by,
&job.workspace_id,
occupancy_metrics,
git_ssh_cmd,
)
.await
.map_err(|e| anyhow!("Failed to clone git repo `{}`: {e}", repo.url))?;
@@ -866,6 +933,7 @@ pub async fn handle_ansible_job(
canceled_by,
conn,
occupancy_metrics,
git_ssh_cmd,
)
.await?;
}
@@ -1820,9 +1820,15 @@ async fn ansible_dep(
w_id: &str,
worker_dir: &str,
occupancy_metrics: &mut OccupancyMetrics,
token: &str,
base_internal_url: &str,
) -> std::result::Result<String, Error> {
use crate::ansible_executor::{
create_ansible_cfg, get_collection_locks, get_role_locks, install_galaxy_collections,
use crate::{
ansible_executor::{
create_ansible_cfg, get_collection_locks, get_git_ssh_cmd, get_role_locks,
install_galaxy_collections,
},
AuthedClient,
};
let python_lockfile = python_dep(
@@ -1843,6 +1849,15 @@ async fn ansible_dep(
let conn = &Connection::Sql(db.clone());
let authed_client = AuthedClient {
base_internal_url: base_internal_url.to_string(),
token: token.to_string(),
workspace: w_id.to_string(),
force_client: None,
};
let git_ssh_cmd = get_git_ssh_cmd(&reqs, job_dir, &authed_client).await?;
let git_repos = get_git_repos_lock(
&reqs.git_repos,
job_dir,
@@ -1853,6 +1868,7 @@ async fn ansible_dep(
canceled_by,
w_id,
occupancy_metrics,
&git_ssh_cmd,
)
.await?;
@@ -1871,6 +1887,7 @@ async fn ansible_dep(
canceled_by,
conn,
occupancy_metrics,
&git_ssh_cmd,
)
.await?;
@@ -1994,6 +2011,8 @@ async fn capture_dependency_job(
w_id,
worker_dir,
occupancy_metrics,
token,
base_internal_url,
)
.await
}