Skip to content
This repository has been archived by the owner on May 11, 2023. It is now read-only.

Commit

Permalink
rad-ci: Initial commit
Browse files Browse the repository at this point in the history
Signed-off-by: xphoniex <[email protected]>
  • Loading branch information
xphoniex committed Feb 8, 2022
1 parent 98d698d commit 0b24d9c
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 1 deletion.
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ assets = [
["target/release/rad-checkout", "usr/bin/rad-checkout", "755"],
["target/release/rad-untrack", "usr/bin/rad-untrack", "755"],
["target/release/git-remote-rad", "usr/bin/git-remote-rad", "755"],
["target/release/rad-ci", "usr/bin/rad-ci", "755"],
["radicle-tools.1.gz", "usr/share/man/man1/radicle-tools.1.gz", "644"],
]

Expand Down Expand Up @@ -48,12 +49,14 @@ rad-untrack = { path = "./untrack" }
rad-help = { path = "./help" }
rad-ls = { path = "./ls" }
rad-rm = { path = "./rm" }
rad-ci = { path = "./ci" }
ethers = { version = "0.6.2" }
link-identities = { version = "0" }
radicle-git-helpers = { version = "0" }
url = { version = "*" }
tokio = { version = "1.10", features = ["rt"] }
futures = { version = "0.3" }
ssh2 = "0.9.3"

[workspace]
members = [
Expand All @@ -76,7 +79,8 @@ members = [
"track",
"untrack",
"proof-generator",
"authorized-keys"
"authorized-keys",
"ci"
]

[patch.crates-io.link-crypto]
Expand Down
17 changes: 17 additions & 0 deletions ci/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "rad-ci"
version = "0.1.0"
authors = ["The Radicle Team <[email protected]>"]
edition = "2018"
license = "MIT OR Apache-2.0"
description = "Install or uninstall ci on your seed node"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0"
lexopt = { version = "0.2" }
rad-common = { path = "../common" }
rad-terminal = { path = "../terminal" }
url = { version = "*" }
ssh2 = "0.9.3"
105 changes: 105 additions & 0 deletions ci/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use std::ffi::OsString;

use rad_terminal::args::{Args, Help};
use std::path::PathBuf;
use url::Host;

pub const HELP: Help = Help {
name: "ci",
description: env!("CARGO_PKG_DESCRIPTION"),
version: env!("CARGO_PKG_VERSION"),
usage: r#"
USAGE
rad ci [--install/--uninstall] [--seed <host>] [--ssh-key <path>]
OPTIONS
--install Runs docker images for concourse
--uninstall Terminates running concourse images
--seed Address of the seed node
--ssh-user SSH user on target machine. Default: root
--ssh-index (Optional) Index of SSH key in ssh-agent. Default: 0
--config (Optional) Config file to override defaults
--verbose
"#,
};

#[derive(Default, Eq, PartialEq)]
pub struct Options {
pub install: bool,
pub uninstall: bool,
pub seed: Option<Host>,
pub ssh_user: String,
pub ssh_index: usize,
pub config: Option<PathBuf>,
pub verbose: bool,
}

impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;

let mut parser = lexopt::Parser::from_args(args);
let mut install = false;
let mut uninstall = false;
let mut seed = None;
let mut ssh_user = String::from("root");
let mut ssh_index = 0;
let mut config = None;
let mut verbose = false;

while let Some(arg) = parser.next()? {
match arg {
Long("seed") => {
let value = parser.value()?;
let value = value.to_string_lossy();
let value = value.as_ref();
let addr = Host::parse(value)?;

seed = Some(addr);
}
Long("install") | Short('i') => {
install = true;
}
Long("uninstall") | Short('u') => {
uninstall = true;
}
Long("ssh-index") => {
let value = parser.value()?;

ssh_index = value.to_string_lossy().parse().unwrap();
}
Long("ssh-user") => {
let value = parser.value()?;

ssh_user = String::from(value.to_string_lossy());
}
Long("config") => {
use std::convert::TryInto;
let value = parser.value()?;
let path: PathBuf = value.try_into()?;

config = Some(path);
}
Long("verbose") | Short('v') => {
verbose = true;
}
_ => {
return Err(anyhow::anyhow!(arg.unexpected()));
}
}
}

Ok((
Options {
seed,
install,
uninstall,
ssh_user,
ssh_index,
config,
verbose,
},
vec![],
))
}
}
1 change: 1 addition & 0 deletions help/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ rad-checkout = { path = "../checkout" }
rad-push = { path = "../push" }
rad-sync = { path = "../sync" }
rad-ens = { path = "../ens" }
rad-ci = { path = "../ci" }
1 change: 1 addition & 0 deletions help/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const COMMANDS: &[Help] = &[
rad_untrack::HELP,
rad_sync::HELP,
rad_ens::HELP,
rad_ci::HELP,
crate::HELP,
];

Expand Down
132 changes: 132 additions & 0 deletions src/bin/rad-ci.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use rad_ci::{Options, HELP};
use rad_terminal::components as term;

use ssh2::Session;
use std::net::TcpStream;

use anyhow::Context;

fn main() {
rad_terminal::args::run_command::<Options, _>(HELP, "CI", run);
}

fn run(options: Options) -> anyhow::Result<()> {
let seed = options.seed.context("Seed is invalid")?;
// SSH into server
let tcp = TcpStream::connect(format!("{}:22", &seed))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp);
sess.handshake()?;

// Authenticate
let spinner = term::spinner(&format!("SSH into remote {}", &seed));
if options.ssh_index == 0 {
sess.userauth_agent(&options.ssh_user)?;
} else {
let mut agent = sess.agent()?;
agent.connect()?;
agent.list_identities()?;
let identities = agent.identities()?;
let identity = identities.get(options.ssh_index).context(format!(
"Couldn't retrieve ssh-agent key #{}",
options.ssh_index
))?;
agent.userauth(&options.ssh_user, identity)?;
}
if sess.authenticated() {
spinner.finish();
} else {
spinner.failed();
}

let verbosity = options.verbose;
let execute_fn = |cmd, spinner| execute_cmd_with_spinner(&sess, cmd, spinner, verbosity);

// Check requirements
term::blank();
term::info!("Checking requirements:");
term::blank();

if ["wget", "docker", "docker-compose"]
.iter()
.map(|req| {
let spinner = term::spinner(&format!(
"Checking if {} exists",
term::format::highlight(req)
));
let (_, status) =
execute_cmd_with_spinner(&sess, format!("which {}", req), spinner, verbosity);
status
})
.sum::<i32>()
> 0
{
term::info!("Requirements are not installed.");
std::process::exit(1);
}
term::blank();

// Apply changes
if options.install {
// Copy the docker-compose.yml over
let spinner = term::spinner("Downloading docker-compose.yml for Concourse CI");
execute_fn("wget -nc -O /etc/concourse-docker-compose.yml https://concourse-ci.org/docker-compose.yml", spinner);

// Run `docker-compose up -d`
let spinner = term::spinner("Installing Concourse CI");
let (_, status) = execute_fn(
"docker-compose -f /etc/concourse-docker-compose.yml up -d",
spinner,
);
if status == 0 {
term::success!("Concourse CI installed!");
}
} else if options.uninstall {
// Run `docker-compose down`
let spinner = term::spinner("Uninstalling Concourse CI");
let (_, status) = execute_fn(
"docker-compose -f /etc/concourse-docker-compose.yml down",
spinner,
);
if status == 0 {
term::success!("Concourse CI uninstalled!");
}

// Delete docker-compose.yml file
let mut channel = sess.channel_session().unwrap();
execute_cmd(&mut channel, "rm /etc/concourse-docker-compose.yml");
}

Ok(())
}

fn execute_cmd<S: AsRef<str>>(channel: &mut ssh2::Channel, cmd: S) -> (String, i32) {
let cmd = cmd.as_ref();
use std::io::Read;
let mut res = String::new();
channel.exec(cmd).unwrap();
channel.read_to_string(&mut res).unwrap();
channel.stderr().read_to_string(&mut res).unwrap();
channel.wait_close().unwrap();
(res, channel.exit_status().unwrap())
}

fn execute_cmd_with_spinner<S: AsRef<str>>(
sess: &ssh2::Session,
cmd: S,
spinner: term::Spinner,
verbose: bool,
) -> (String, i32) {
let cmd = cmd.as_ref();
let mut channel = sess.channel_session().unwrap();
let (res, status) = execute_cmd(&mut channel, cmd);
if status == 0 {
spinner.finish();
} else {
spinner.failed();
}
if verbose {
term::blob(&res);
}
(res, status)
}

0 comments on commit 0b24d9c

Please sign in to comment.