From a22d0e0680286d55231220c8bcfdb1f969ea9ba2 Mon Sep 17 00:00:00 2001 From: wendrul Date: Thu, 3 Apr 2025 18:26:44 +0200 Subject: [PATCH] Ansible vault + roles --- .../parsers/windmill-parser-yaml/src/lib.rs | 22 +++++++ .../windmill-worker/src/ansible_executor.rs | 63 ++++++++++++++++--- flake.nix | 3 + 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/backend/parsers/windmill-parser-yaml/src/lib.rs b/backend/parsers/windmill-parser-yaml/src/lib.rs index c2d417eb6c..e958624f16 100644 --- a/backend/parsers/windmill-parser-yaml/src/lib.rs +++ b/backend/parsers/windmill-parser-yaml/src/lib.rs @@ -217,6 +217,8 @@ pub struct AnsibleRequirements { pub vars: Vec<(String, String)>, pub resources: Vec<(String, String)>, pub options: AnsiblePlaybookOptions, + pub vault_password_file: Option, + pub vault_id: Vec, } fn parse_inventories(inventory_yaml: &Yaml) -> anyhow::Result> { @@ -290,6 +292,8 @@ pub fn parse_ansible_reqs( vars: vec![], resources: vec![], options: opts, + vault_password_file: None, + vault_id: vec![], }; if let Yaml::Hash(doc) = &docs[0] { @@ -345,6 +349,24 @@ pub fn parse_ansible_reqs( Yaml::String(key) if key == "inventory" => { ret.inventories = parse_inventories(value)?; } + Yaml::String(key) if key == "vault_password_file" => { + let Yaml::String(filename) = value else { + return Err(anyhow!("Vault Password File expects a String containing the file name")); + }; + ret.vault_password_file = Some(filename.to_string()); + } + Yaml::String(key) if key == "vault_id" => { + let Yaml::Array(filenames) = value else { + return Err(anyhow!("Vault ID field expects an array of strings in the format: `label@filename`")); + }; + + for f in filenames { + let Yaml::String(filename) = f else { + return Err(anyhow!("The elements of the vault_id field should be strings in the format: `label@filename`")); + }; + ret.vault_id.push(filename.to_string()); + } + } Yaml::String(key) if key == "options" => { if let Yaml::Array(opts) = &value { ret.options = parse_ansible_options(opts); diff --git a/backend/windmill-worker/src/ansible_executor.rs b/backend/windmill-worker/src/ansible_executor.rs index c98837ac5d..bd69018428 100644 --- a/backend/windmill-worker/src/ansible_executor.rs +++ b/backend/windmill-worker/src/ansible_executor.rs @@ -136,15 +136,48 @@ async fn install_galaxy_collections( db, ) .await; - let mut galaxy_command = Command::new(ANSIBLE_GALAXY_PATH.as_str()); - galaxy_command + + let mut galaxy_roles_cmd = Command::new(ANSIBLE_GALAXY_PATH.as_str()); + galaxy_roles_cmd + .current_dir(job_dir) + .env_clear() + .envs(PROXY_ENVS.clone()) + .env("PATH", PATH_ENV.as_str()) + .env("TZ", TZ_ENV.as_str()) + .args(vec![ + "install", + "-r", + "requirements.yml", + "-p", + "./", + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let child = start_child_process(galaxy_roles_cmd, ANSIBLE_GALAXY_PATH.as_str()).await?; + handle_child( + job_id, + db, + mem_peak, + canceled_by, + child, + !*DISABLE_NSJAIL, + worker_name, + w_id, + "ansible galaxy install", + None, + false, + &mut Some(occupancy_metrics), + ) + .await?; + + let mut galaxy_collections_cmd = Command::new(ANSIBLE_GALAXY_PATH.as_str()); + galaxy_collections_cmd .current_dir(job_dir) .env_clear() .envs(PROXY_ENVS.clone()) .env("PATH", PATH_ENV.as_str()) .env("TZ", TZ_ENV.as_str()) - // .env("BASE_INTERNAL_URL", base_internal_url) - // .env("HOME", HOME_ENV.as_str()) .args(vec![ "collection", "install", @@ -156,7 +189,7 @@ async fn install_galaxy_collections( .stdout(Stdio::piped()) .stderr(Stdio::piped()); - let child = start_child_process(galaxy_command, ANSIBLE_GALAXY_PATH.as_str()).await?; + let child = start_child_process(galaxy_collections_cmd, ANSIBLE_GALAXY_PATH.as_str()).await?; handle_child( job_id, db, @@ -265,7 +298,7 @@ pub async fn handle_ansible_job( .unwrap_or_else(|| vec![]); let mut nsjail_extra_mounts = vec![]; - if let Some(r) = reqs { + if let Some(r) = reqs.as_ref() { nsjail_extra_mounts = create_file_resources( &job.id, &job.workspace_id, @@ -277,9 +310,9 @@ pub async fn handle_ansible_job( ) .await?; - if let Some(collections) = r.collections { + if let Some(collections) = r.collections.as_ref() { install_galaxy_collections( - collections.as_str(), + collections, job_dir, &job.id, worker_name, @@ -299,6 +332,18 @@ pub async fn handle_ansible_job( db, ) .await; + + let mut passwords_cfg = String::new(); + if let Some(vault_password_file) = reqs.as_ref().and_then(|r| r.vault_password_file.as_ref()) { + passwords_cfg.push_str(&format!("vault_password_file = {vault_password_file}\n")); + } + if let Some(vault_ids) = reqs.as_ref().map(|r| &r.vault_id) { + if !vault_ids.is_empty() { + let password_files = vault_ids.join(","); + + passwords_cfg.push_str(&format!("vault_identity_list = {password_files}\n")); + } + } let ansible_cfg_content = format!( r#" [defaults] @@ -307,8 +352,10 @@ roles_path = ./roles home={job_dir}/.ansible local_tmp={job_dir}/.ansible/tmp remote_tmp={job_dir}/.ansible/tmp +{passwords_cfg} "# ); + write_file(job_dir, "ansible.cfg", &ansible_cfg_content)?; let mut reserved_variables = diff --git a/flake.nix b/flake.nix index 506e61d68e..209a756a31 100644 --- a/flake.nix +++ b/flake.nix @@ -85,6 +85,7 @@ dotnet-sdk_9 oracle-instantclient svelte-language-server + ansible ]); packages = [ (pkgs.writeScriptBin "wm-caddy" '' @@ -145,6 +146,8 @@ DOTNET_PATH = "${pkgs.dotnet-sdk_9}/bin/dotnet"; DOTNET_ROOT = "${pkgs.dotnet-sdk_9}/share/dotnet"; ORACLE_LIB_DIR = "${pkgs.oracle-instantclient.lib}/lib"; + ANSIBLE_PLAYBOOK_PATH = "${pkgs.ansible}/bin/ansible-playbook"; + ANSIBLE_GALAXY_PATH = "${pkgs.ansible}/bin/ansible-galaxy"; }; packages.default = self.packages.${system}.windmill; packages.windmill-client = pkgs.buildNpmPackage {