use anyhow::bail;
use colored::Colorize;
use std::collections::BTreeMap;
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::process::exit;
use crate::ARTIFACTS_DIR;
use crate::entities::ansible_opts::AnsibleOpts;
use crate::entities::custom_command::CustomCommand;
use crate::entities::driver::PipelineDriver;
use crate::entities::environment::RunEnvironment;
use crate::entities::info::ShortName;
use crate::entities::remote_host::RemoteHost;
use crate::entities::requirements::Requirement;
use crate::pipelines::DescribedPipeline;
use crate::project::DeployerProjectOptions;
use crate::rw::log;
const INVENTORY_TEMPLATE: &str = "[targets]\n{remotes}";
const PLAYBOOK_TEMPLATE_DEPL_DRIVER: &str = r#"# Generated by Deployer 2.X
- name: {pipeline_name}
hosts: {host-group}
become: {sudo}
tasks:
- name: Ensure `rsync` plugin
ansible.builtin.package:
name: rsync
state: present
- name: Create deployment directory
ansible.builtin.file:
path: '{{ ansible_env.HOME }}/.cache/deploy-cache/ansible-remote/{project_name}'
state: directory
mode: '0755'
- name: Synchronize project files
ansible.builtin.synchronize:
src: ./
dest: '{{ ansible_env.HOME }}/.cache/deploy-cache/ansible-remote/{project_name}'
archive: yes
rsync_opts:
- "--delete"{exclude_files}
delegate_to: "{{ inventory_hostname }}"
- name: Run Deployer Pipeline
ansible.builtin.shell: |
cd {{ ansible_env.HOME }}/.cache/deploy-cache/ansible-remote/{project_name}
DEPLOYER_ANSIBLE_RUN=1 {{ ansible_env.HOME }}/.cargo/bin/depl run -j -k {pipeline_name}
register: deployer_result
- name: Display Deployer output
ansible.builtin.debug:
msg: "{{ deployer_result.stdout_lines }}"{possible_artifacts}
"#;
const PLAYBOOK_TEMPLATE_SHELL_DRIVER: &str = r#"- name: {pipeline_name}
hosts: {host-group}
become: {sudo}
tasks:
- name: Ensure `rsync` plugin
ansible.builtin.package:
name: rsync
state: present
- name: Create deployment directory
ansible.builtin.file:
path: '{{ ansible_env.HOME }}/.cache/deploy-cache/ansible-remote/{project_name}'
state: directory
mode: '0755'
- name: Synchronize project files
ansible.builtin.synchronize:
src: ./
dest: '{{ ansible_env.HOME }}/.cache/deploy-cache/ansible-remote/{project_name}'
archive: yes
rsync_opts:
- "--delete"{exclude_files}
delegate_to: "{{ inventory_hostname }}"
- name: Run Deployer Pipeline
ansible.builtin.shell: |
cd {{ ansible_env.HOME }}/.cache/deploy-cache/ansible-remote/{project_name}
./{run-script}
register: deployer_result
- name: Display Deployer output
ansible.builtin.debug:
msg: "{{ deployer_result.stdout_lines }}"{possible_artifacts}
"#;
const POSSIBLE_ARTIFACTS_SYNC: &str = r#"- name: Fetch artifact
ansible.builtin.fetch:
src: '{{ ansible_env.HOME }}/.cache/deploy-cache/ansible-remote/{project_name}/artifacts/{relative-path}'
dest: './artifacts/{{ inventory_hostname }}/{relative-path}'
flat: yes
fail_on_missing: no"#;
pub fn make_inventory(
env: &RunEnvironment,
opts: &AnsibleOpts,
remotes: &BTreeMap<ShortName, RemoteHost>,
) -> anyhow::Result<()> {
if !opts.create_inventory.is_empty() && opts.use_inventory.is_some() {
bail!("Use either `create_inventory` or `use_inventory` field in Ansible options!")
}
if !opts.create_inventory.is_empty() {
let mut remote = vec![];
for short_name in opts.create_inventory.iter() {
if let Some(host) = remotes.get(short_name) {
remote.push(host.clone());
}
}
let remotes = remote
.iter()
.map(|remote| {
format!(
"{} ansible_port={} ansible_user={}",
remote.ip, remote.port, remote.username
)
})
.collect::<Vec<_>>()
.join("\n");
let inventory = INVENTORY_TEMPLATE.replace("{remotes}", &remotes);
std::fs::write(env.run_dir.join("inventory.ini"), inventory)?;
}
if let Some(inventory_path) = &opts.use_inventory {
std::fs::copy(inventory_path, env.run_dir.join("inventory.ini"))?;
}
Ok(())
}
pub async fn make_playbook(
config: &DeployerProjectOptions,
env: &RunEnvironment<'_>,
pipeline: &DescribedPipeline,
) -> anyhow::Result<String> {
let opts = pipeline.ansible_opts.as_ref().unwrap();
let playbook_name = format!(
"playbook.{}.{}.ansible.yml",
pipeline.title,
config.project_name.as_str()
);
let s = "sudo".to_string();
let needs_sudo = if pipeline
.return_all_cmds(&config.actions)
.iter()
.any(|cmd| cmd.contains(&s))
{
"true"
} else {
"false"
};
let exclude: std::collections::BTreeSet<_> = env.ignore.iter().collect();
let mut exclude = exclude
.iter()
.map(|i: &&PathBuf| {
format!(
"\n - \"--exclude={}\"",
i.to_str().expect("`exclude` file contains non-UTF8 symbols!")
)
})
.collect::<Vec<_>>();
exclude.extend_from_slice(&[
"\n - \"--exclude=artifacts\"".to_string(),
"\n - \"--exclude=inventory.ini\"".to_string(),
format!("\n - \"--exclude={}\"", playbook_name.as_str()),
]);
let exclude = exclude.join("");
let afs_sync = if !pipeline.artifacts.is_empty() {
pipeline
.artifacts
.iter()
.map(|pl| {
format!(
"\n {}",
POSSIBLE_ARTIFACTS_SYNC
.replace("{project_name}", config.project_name.as_str())
.replace("{relative-path}", pl.to.as_str())
)
})
.collect::<Vec<_>>()
.join("")
} else {
String::new()
};
let playbook_content = match env.driver {
PipelineDriver::Deployer => PLAYBOOK_TEMPLATE_DEPL_DRIVER
.replace("{pipeline_name}", &pipeline.title)
.replace("{host-group}", opts.host_group.as_deref().unwrap_or("all"))
.replace("{sudo}", needs_sudo)
.replace("{project_name}", config.project_name.as_str())
.replace("{exclude_files}", exclude.as_str())
.replace(
"{possible_artifacts}",
if !pipeline.artifacts.is_empty() {
afs_sync.as_str()
} else {
""
},
),
PipelineDriver::Shell => {
let run_env = RunEnvironment {
ansible_run: true,
daemons: Default::default(),
skipper: Default::default(),
restart_requested: env.restart_requested.clone(),
..*env
};
let run_script = pipeline.to_shell_script(config, &run_env).await?;
let run_script_filename = format!(".pipe.{}.ansible.sh", env.master_pipeline);
let run_script_filepath = env.run_dir.join(run_script_filename.as_str());
std::fs::write(&run_script_filepath, run_script)?;
std::fs::set_permissions(run_script_filepath, Permissions::from_mode(0o700))?;
PLAYBOOK_TEMPLATE_SHELL_DRIVER
.replace("{pipeline_name}", &pipeline.title)
.replace("{host-group}", opts.host_group.as_deref().unwrap_or("all"))
.replace("{sudo}", needs_sudo)
.replace("{project_name}", config.project_name.as_str())
.replace(
"{exclude_files}",
if !env.ignore.is_empty() { exclude.as_str() } else { "" },
)
.replace("{run-script}", run_script_filename.as_str())
.replace(
"{possible_artifacts}",
if !pipeline.artifacts.is_empty() {
afs_sync.as_str()
} else {
""
},
)
}
};
std::fs::write(env.run_dir.join(&playbook_name), playbook_content)?;
Ok(playbook_name)
}
pub async fn execute_pipeline_with_ansible(
config: &DeployerProjectOptions,
env: &RunEnvironment<'_>,
pipeline: &DescribedPipeline,
) -> anyhow::Result<bool> {
if pipeline.ansible_opts.is_none() {
bail!("There is no Ansible options provided!");
}
let opts = pipeline.ansible_opts.as_ref().unwrap();
let canonicalized = env.run_dir.canonicalize()?;
let canonicalized = canonicalized.to_str().expect("Can't convert `Path` to string!");
println!("Run path: {}", canonicalized);
make_inventory(env, opts, env.remotes)?;
let playbook_filename = make_playbook(config, env, pipeline).await?;
if !pipeline.artifacts.is_empty() {
let _ = std::fs::create_dir(env.run_dir.join(ARTIFACTS_DIR));
}
println!("{}", "Inventory & playbook created. Starting...".green());
if Requirement::in_path("ansible-playbook").satisfy(env).await.is_err() {
println!("{}", "Ansible is not installed!".red());
exit(1);
}
CustomCommand::run_simple_observer(env, format!("ansible-playbook -i inventory.ini {playbook_filename}")).await?;
if !pipeline.artifacts.is_empty() {
log("Trying to copy artifacts from Ansible local run dir to project dir...");
crate::rw::copy_all(
env.run_dir.join(ARTIFACTS_DIR),
env.run_dir.join(ARTIFACTS_DIR),
env.artifacts_dir,
&[] as &[&std::path::Path],
)?;
}
println!("{}", "Ansible run done.".green());
Ok(true)
}