feat: add CLI config options and refactor steps

This commit is contained in:
rzmk 2025-10-14 04:23:02 -04:00
parent 3727d60786
commit 25bb877fb6
2 changed files with 180 additions and 109 deletions

View file

@ -4,7 +4,11 @@ mod styles;
use crate::{ use crate::{
questions::{question_ckan_version, question_ssh, question_sysadmin}, questions::{question_ckan_version, question_ssh, question_sysadmin},
styles::{highlighted_text, important_text, step_text, success_text}, steps::{
step_install_ahoy, step_install_and_run_ckan_compose, step_install_curl,
step_install_docker, step_install_openssh, step_package_updates,
},
styles::{important_text, step_text, success_text},
}; };
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
@ -15,15 +19,28 @@ use std::{path::PathBuf, str::FromStr};
use xshell::cmd; use xshell::cmd;
use xshell_venv::{Shell, VirtualEnv}; use xshell_venv::{Shell, VirtualEnv};
/// ckan-devstaller CLI /// CLI to help install a CKAN instance for development within minutes. Learn more at: https://ckan-devstaller.dathere.com
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
struct Args { struct Args {
/// Skip interactive steps and install CKAN with default features /// Skip interactive steps and install CKAN with datHere's default config
#[arg(short, long)] #[arg(short, long)]
default: bool, default: bool,
/// Preset configuration.
#[arg(short, long)]
preset: Option<String>,
#[arg(short, long)]
/// CKAN version to install defined by semantic versioning from official releases from https://github.com/ckan/ckan, or a custom git repository.
ckan_version: Option<String>,
/// List of CKAN extensions to install, separated by either commas or spaces.
#[arg(short, long)]
extensions: Option<Vec<String>>,
/// List of custom features, separated by either commas or spaces.
#[arg(short, long)]
features: Option<Vec<String>>,
} }
#[derive(Clone)]
struct Sysadmin { struct Sysadmin {
username: String, username: String,
password: String, password: String,
@ -45,37 +62,52 @@ fn main() -> Result<()> {
.homepage("https://dathere.com") .homepage("https://dathere.com")
.support("- Create a support ticket at https://support.dathere.com or report an issue at https://github.com/dathere/ckan-devstaller")); .support("- Create a support ticket at https://support.dathere.com or report an issue at https://github.com/dathere/ckan-devstaller"));
let args = Args::parse();
// Set up default config // Set up default config
let args = Args::parse();
let sh = Shell::new()?; let sh = Shell::new()?;
let username = cmd!(sh, "whoami").read()?; let username = cmd!(sh, "whoami").read()?;
let default_sysadmin = Sysadmin {
username: username.clone(),
password: "password".to_string(),
email: format!("{username}@localhost"),
};
let config = Config {
ssh: args.features.is_some_and(|features| features.contains(&"enable-ssh".to_string())),
ckan_version: if args.ckan_version.is_some() { args.ckan_version.unwrap() } else { "2.11.3".to_string() },
sysadmin: default_sysadmin.clone(),
extension_datastore: args.extensions.clone().is_some_and(|extensions| extensions.contains(&"DataStore".to_string())),
extension_ckanext_scheming: args.extensions.clone().is_some_and(|extensions| extensions.contains(&"ckanext-scheming".to_string())),
extension_datapusher_plus: args.extensions.is_some_and(|extensions| extensions.contains(&"DataPusher+".to_string())),
druf_mode: false,
};
steps::step_intro(); steps::step_intro();
let default_config_text = r#" let mut default_config_text = String::from("The current configuration for ckan-devstaller does the following:");
The default configuration for ckan-devstaller does the following: if config.ssh {
- Install openssh-server to enable SSH access default_config_text.push_str("\n- Install openssh-server to enable SSH access");
- Install ckan-compose (https://github.com/tino097/ckan-compose) which sets up the CKAN backend (PostgreSQL, SOLR, Redis) }
- Install CKAN v2.11.3 default_config_text.push_str("\n- Install ckan-compose (https://github.com/tino097/ckan-compose) which sets up the CKAN backend (PostgreSQL, SOLR, Redis)");
- Install the DataStore extension default_config_text.push_str(format!("\n- Install CKAN v{}", config.ckan_version).as_str());
- Install the ckanext-scheming extension if config.extension_datastore {
- Install the DataPusher+ extension default_config_text.push_str("\n- Install the DataStore extension");
- Disable DRUF mode for DataPusher+ }
"#; if config.extension_ckanext_scheming {
default_config_text.push_str("\n- Install the ckanext-scheming extension");
}
if config.extension_datapusher_plus {
default_config_text.push_str("\n- Install the DataPusher+ extension");
}
default_config_text.push_str("\n- Disable DRUF mode for DataPusher+");
println!("{default_config_text}"); println!("{default_config_text}");
let answer_customize = if args.default { let answer_customize = if args.default {
false false
} else { } else {
Confirm::new( Confirm::new(
"Would you like to customize any of these features for your CKAN installation?", "Would you like to customize the configuration for your CKAN installation?",
) )
.prompt()? .prompt()?
}; };
let default_sysadmin = Sysadmin {
username: username.clone(),
password: "password".to_string(),
email: format!("{username}@localhost"),
};
let config = if answer_customize { let config = if answer_customize {
let answer_ssh = question_ssh()?; let answer_ssh = question_ssh()?;
let answer_ckan_version = question_ckan_version()?; let answer_ckan_version = question_ckan_version()?;
@ -107,15 +139,7 @@ fn main() -> Result<()> {
druf_mode: answer_druf_mode, druf_mode: answer_druf_mode,
} }
} else { } else {
Config { config
ssh: true,
ckan_version: "2.11.3".to_string(),
sysadmin: default_sysadmin,
extension_datastore: true,
extension_ckanext_scheming: true,
extension_datapusher_plus: true,
druf_mode: false,
}
}; };
let begin_installation = if args.default { let begin_installation = if args.default {
@ -126,90 +150,22 @@ fn main() -> Result<()> {
if begin_installation { if begin_installation {
println!("{}", important_text("Starting installation...")); println!("{}", important_text("Starting installation..."));
println!( // Run sudo apt update and sudo apt upgrade
"\n{} Running {} and {}...", step_package_updates("1.".to_string(), &sh)?;
step_text("1."),
highlighted_text("sudo apt update -y"),
highlighted_text("sudo apt upgrade -y")
);
println!(
"{}",
important_text("You may need to provide your sudo password.")
);
cmd!(sh, "sudo apt update -y").run()?;
// Ignoring xrdp error with .ignore_status() for now
cmd!(sh, "sudo apt upgrade -y").ignore_status().run()?;
println!(
"{}",
success_text("✅ 1. Successfully ran update and upgrade commands.")
);
println!( // Install curl
"\n{} Installing {}...", step_install_curl("2.".to_string(), &sh)?;
step_text("2."), // If user wants SSH capability, install openssh-server
highlighted_text("curl")
);
cmd!(sh, "sudo apt install curl -y").run()?;
println!("{}", success_text("✅ 2.1. Successfully installed curl."));
if config.ssh { if config.ssh {
println!("\n{} Installing openssh-server...", step_text("2.")); step_install_openssh("2.".to_string(), &sh)?;
cmd!(sh, "sudo apt install openssh-server -y").run()?;
}
println!(
"{}",
success_text("✅ 2.2. Successfully installed openssh-server.")
);
let dpkg_l_output = cmd!(sh, "dpkg -l").read()?;
let has_docker = cmd!(sh, "grep docker")
.stdin(dpkg_l_output.clone())
.ignore_status()
.output()?
.status
.success();
if !has_docker {
println!("{} Installing Docker...", step_text("3."),);
cmd!(
sh,
"curl -fsSL https://get.docker.com -o /home/{username}/get-docker.sh"
)
.run()?;
cmd!(sh, "sudo sh /home/{username}/get-docker.sh").run()?;
println!("{}", success_text("✅ 3. Successfully installed Docker."));
} }
let has_docker_compose = cmd!(sh, "grep docker-compose") // Install docker CLI if user does not have it installed
.stdin(dpkg_l_output) step_install_docker("3.".to_string(), &sh, username.clone())?;
.ignore_status()
.output()?
.status
.success();
if !has_docker_compose {
cmd!(sh, "sudo apt install docker-compose -y").run()?;
}
println!("\n{} Installing Ahoy...", step_text("4."),); step_install_ahoy("4.".to_string(), &sh, username.clone())?;
sh.change_dir(format!("/home/{username}"));
cmd!(sh, "sudo curl -LO https://github.com/ahoy-cli/ahoy/releases/download/v2.5.0/ahoy-bin-linux-amd64").run()?;
cmd!(sh, "mv ./ahoy-bin-linux-amd64 ./ahoy").run()?;
cmd!(sh, "sudo chmod +x ./ahoy").run()?;
println!("{}", success_text("✅ 4. Successfully installed Ahoy."));
println!( step_install_and_run_ckan_compose("5.".to_string(), &sh, username.clone())?;
"\n{} Downloading, installing, and starting ckan-compose...",
step_text("5."),
);
if !std::fs::exists(format!("/home/{username}/ckan-compose"))? {
cmd!(sh, "git clone https://github.com/tino097/ckan-compose.git").run()?;
}
sh.change_dir(format!("/home/{username}/ckan-compose"));
cmd!(sh, "git switch ckan-devstaller").run()?;
let env_data = "PROJECT_NAME=ckan-devstaller-project
DATASTORE_READONLY_PASSWORD=pass
POSTGRES_PASSWORD=pass";
std::fs::write(format!("/home/{username}/ckan-compose/.env"), env_data)?;
cmd!(sh, "sudo ../ahoy up").run()?;
println!("{}", success_text("✅ 5. Successfully ran ckan-compose."));
println!( println!(
"\n{} Installing CKAN {}...", "\n{} Installing CKAN {}...",

View file

@ -1,4 +1,6 @@
use crate::styles::{highlighted_text, important_text}; use crate::styles::{highlighted_text, important_text, step_text, success_text};
use anyhow::Result;
use xshell::{Shell, cmd};
pub fn step_intro() { pub fn step_intro() {
println!("Welcome to the ckan-devstaller!"); println!("Welcome to the ckan-devstaller!");
@ -17,3 +19,116 @@ pub fn step_intro() {
) )
); );
} }
pub fn step_package_updates(step_prefix: String, sh: &Shell) -> Result<()> {
println!(
"\n{} Running {} and {}...",
step_text(step_prefix.as_str()),
highlighted_text("sudo apt update -y"),
highlighted_text("sudo apt upgrade -y")
);
println!(
"{}",
important_text("You may need to provide your sudo password.")
);
cmd!(sh, "sudo apt update -y").run()?;
// Ignoring xrdp error with .ignore_status() for now
cmd!(sh, "sudo apt upgrade -y").ignore_status().run()?;
println!(
"{}",
success_text(
format!("{step_prefix} Successfully ran update and upgrade commands.").as_str()
)
);
Ok(())
}
pub fn step_install_curl(step_prefix: String, sh: &Shell) -> Result<()> {
println!(
"\n{} Installing {}...",
step_text("2."),
highlighted_text("curl")
);
cmd!(sh, "sudo apt install curl -y").run()?;
println!(
"{}",
success_text(format!("{step_prefix} Successfully installed curl.").as_str())
);
Ok(())
}
pub fn step_install_openssh(step_prefix: String, sh: &Shell) -> Result<()> {
println!(
"\n{} Installing openssh-server...",
step_text(step_prefix.as_str())
);
cmd!(sh, "sudo apt install openssh-server -y").run()?;
println!(
"{}",
success_text(format!("{step_prefix} Successfully installed openssh-server.").as_str())
);
Ok(())
}
pub fn step_install_docker(step_prefix: String, sh: &Shell, username: String) -> Result<()> {
let dpkg_l_output = cmd!(sh, "dpkg -l").read()?;
let has_docker = cmd!(sh, "grep docker")
.stdin(dpkg_l_output.clone())
.ignore_status()
.output()?
.status
.success();
if !has_docker {
println!("{} Installing Docker...", step_text(step_prefix.as_str()),);
cmd!(
sh,
"curl -fsSL https://get.docker.com -o /home/{username}/get-docker.sh"
)
.run()?;
cmd!(sh, "sudo sh /home/{username}/get-docker.sh").run()?;
println!(
"{}",
success_text(format!("{step_prefix} Successfully installed Docker.").as_str())
);
}
Ok(())
}
pub fn step_install_ahoy(step_prefix: String, sh: &Shell, username: String) -> Result<()> {
println!("\n{} Installing Ahoy...", step_text(step_prefix.as_str()),);
sh.change_dir(format!("/home/{username}"));
cmd!(sh, "sudo curl -LO https://github.com/ahoy-cli/ahoy/releases/download/v2.5.0/ahoy-bin-linux-amd64").run()?;
cmd!(sh, "mv ./ahoy-bin-linux-amd64 ./ahoy").run()?;
cmd!(sh, "sudo chmod +x ./ahoy").run()?;
println!(
"{}",
success_text(format!("{step_prefix} Successfully installed Ahoy.").as_str())
);
Ok(())
}
pub fn step_install_and_run_ckan_compose(
step_prefix: String,
sh: &Shell,
username: String,
) -> Result<()> {
println!(
"\n{} Downloading, installing, and starting ckan-compose...",
step_text(step_prefix.as_str()),
);
if !std::fs::exists(format!("/home/{username}/ckan-compose"))? {
cmd!(sh, "git clone https://github.com/tino097/ckan-compose.git").run()?;
}
sh.change_dir(format!("/home/{username}/ckan-compose"));
cmd!(sh, "git switch ckan-devstaller").run()?;
let env_data = "PROJECT_NAME=ckan-devstaller-project
DATASTORE_READONLY_PASSWORD=pass
POSTGRES_PASSWORD=pass";
std::fs::write(format!("/home/{username}/ckan-compose/.env"), env_data)?;
cmd!(sh, "sudo ../ahoy up").run()?;
println!(
"{}",
success_text(format!("{step_prefix} Successfully ran ckan-compose.").as_str())
);
Ok(())
}