Unverified Commit 7ef3323e authored by Vasili Novikov's avatar Vasili Novikov
Browse files

Add support for running kubernetes containers

parent 022eadf8
Showing with 149 additions and 47 deletions
+149 -47
......@@ -29,7 +29,7 @@ rusqlite = { version = "0.24.2", features = ["sqlcipher"] }
serde = { version = "1.0.123", features = ["derive"] }
serde_json = "1.0.64"
sha2 = "0.9.3"
structopt = { version = "0.3.21", features = ["color"] }
structopt = { version = "0.3.21", features = ["color", "suggestions"] }
tokio = { version = "1.2.0", features = ["full"] }
warp = { version = "0.3.0", default-features = false, features = ["tls"] }
zeroize = "1.2.0"
......
......@@ -26,15 +26,31 @@ pub struct CliOptions {
/// Pod does not store any data on owners in any external databases.
///
/// A magic value of "ANY" will allow any owner to connect to the Pod.
#[structopt(short = "o", long, required = true, env = "POD_OWNER_HASHES")]
#[structopt(
short = "o",
long,
name = "OWNERS",
required = true,
env = "POD_OWNER_HASHES"
)]
pub owners: String,
/// If specified, all Plugin containers will be started using kubernetes (`kubectl`).
/// Otherwise and by default, docker containers are used.
#[structopt(long, env = "POD_USE_KUBERNETES")]
pub use_kubernetes: bool,
/// Set the callback address for plugins launched from within Pod.
/// This should be the Pod-s address as seen by external plugins.
/// It defaults to "pod_pod_1:3030" if Pod is inside docker,
/// or "localhost:3030" on Linux,
/// or "host.docker.internal:3030" on other operating systems.
#[structopt(short = "s", long, name = "ADDRESS", env = "PLUGINS_CALLBACK_ADDRESS")]
#[structopt(
short = "s",
long,
name = "ADDRESS",
env = "POD_PLUGINS_CALLBACK_ADDRESS"
)]
pub plugins_callback_address: Option<String>,
/// Docker network to use when running plugins, e.g. `docker run --network=XXX ...`
......@@ -45,8 +61,8 @@ pub struct CliOptions {
/// (this is covered in docker-compose.yml by default).
#[structopt(
long,
name = "SERVICES_DOCKER_NETWORK",
env = "SERVICES_DOCKER_NETWORK"
name = "PLUGINS_DOCKER_NETWORK",
env = "POD_PLUGINS_DOCKER_NETWORK"
)]
pub plugins_docker_network: Option<String>,
......@@ -76,14 +92,14 @@ pub struct CliOptions {
pub non_tls: bool,
/// Unsafe version of --non-tls that runs on a public network, e.g. "http://0.0.0.0".
/// This option will force Pod to not use https when starting the server,
/// instead run on http on the provided network interface.
/// This option will force Pod to not use https,
/// and instead run http on the provided network interface.
/// WARNING: This is heavily discouraged as an intermediary
/// (even your router on a local network)
/// could spoof the traffic sent to the server and do a MiTM attack.
/// Please consider running Pod on a non-public network (--non-tls),
/// or use Pod with https encryption.
#[structopt(long, name = "NETWORK_INTERFACE", env = "INSECURE_NON_TLS")]
#[structopt(long, name = "NETWORK_INTERFACE", env = "POD_INSECURE_NON_TLS")]
pub insecure_non_tls: Option<IpAddr>,
/// Add `Access-Control-Allow-Origin: *` header to all HTTP responses,
......
......@@ -9,19 +9,11 @@ use rusqlite::Transaction;
use std::process::Command;
use warp::http::status::StatusCode;
/// Run a plugin container in docker.
/// Run a plugin, making sure that the correct ENV variables and settings are passed
/// to the containerization / deployment processes.
///
/// Example command that this function could run:
/// docker run \
/// --network=host \
/// --env=POD_FULL_ADDRESS="http://localhost:3030" \
/// --env=POD_TARGET_ITEM="{...json...}" \
/// --env=POD_OWNER="...64-hex-chars..." \
/// --env=POD_AUTH_JSON="{...json...}" \
/// --rm \
/// --name="$container-$trigger_item_id" \
/// -- \
/// "$container"
/// Internally passes to docker / kubernetes / scripts
/// depending on how Pod is configured by the user.
#[allow(clippy::too_many_arguments)]
pub fn run_plugin_container(
tx: &Transaction,
......@@ -46,27 +38,112 @@ pub fn run_plugin_container(
),
})?;
let item = serde_json::to_string(&item)?;
let mut args: Vec<String> = Vec::with_capacity(12);
args.push("run".to_string());
for arg in docker_arguments(cli_options) {
args.push(arg);
}
args.push(format!("--env=POD_TARGET_ITEM={}", item));
args.push(format!("--env=POD_OWNER={}", pod_owner));
let auth = database_key.create_plugin_auth()?;
let auth = serde_json::to_string(&auth)?;
args.push(format!("--env=POD_AUTH_JSON={}", auth));
args.push("--rm".to_string());
if cli_options.use_kubernetes {
run_kubernetes_container(
&container,
&item,
pod_owner,
&auth,
triggered_by_item_id,
cli_options,
)
} else {
run_docker_container(
&container,
&item,
pod_owner,
&auth,
triggered_by_item_id,
cli_options,
)
}
}
/// Example:
/// docker run \
/// --network=host \
/// --env=POD_FULL_ADDRESS="http://localhost:3030" \
/// --env=POD_TARGET_ITEM="{...json...}" \
/// --env=POD_OWNER="...64-hex-chars..." \
/// --env=POD_AUTH_JSON="{...json...}" \
/// --name="$container-$trigger_item_id" \
/// --rm \
/// -- \
/// "$container"
fn run_docker_container(
container: &str,
target_item: &str,
pod_owner: &str,
pod_auth: &str,
triggered_by_item_id: &str,
cli_options: &CliOptions,
) -> Result<()> {
let docker_network = match &cli_options.plugins_docker_network {
Some(net) => net.to_string(),
None => "host".to_string(),
};
let mut args: Vec<String> = Vec::with_capacity(10);
args.push("run".to_string());
args.push(format!("--network={}", docker_network));
args.push(format!(
"--env=POD_FULL_ADDRESS={}",
callback_address(cli_options)
));
args.push(format!("--env=POD_TARGET_ITEM={}", target_item));
args.push(format!("--env=POD_OWNER={}", pod_owner));
args.push(format!("--env=POD_AUTH_JSON={}", pod_auth));
args.push(format!("--name={}-{}", &container, triggered_by_item_id));
args.push("--rm".to_string());
args.push("--".to_string());
args.push(container);
log::info!("Starting plugin docker command {:?}", args);
let command = Command::new("docker").args(&args).spawn();
args.push(container.to_string());
run_any_command("docker", &args, triggered_by_item_id)
}
/// Example:
/// kubectl run plugin
/// --image="$container" \
/// --env=POD_FULL_ADDRESS="http://localhost:3030" \
/// --env=POD_TARGET_ITEM="{...json...}" \
/// --env=POD_OWNER="...64-hex-chars..." \
/// --env=POD_AUTH_JSON="{...json...}" \
fn run_kubernetes_container(
container: &str,
target_item: &str,
pod_owner: &str,
pod_auth: &str,
triggered_by_item_id: &str,
cli_options: &CliOptions,
) -> Result<()> {
let mut args: Vec<String> = Vec::with_capacity(7);
args.push("run".to_string());
args.push("plugin".to_string());
args.push(format!("--image={}", container));
args.push(format!(
"--env=POD_FULL_ADDRESS={}",
callback_address(cli_options)
));
args.push(format!("--env=POD_TARGET_ITEM={}", target_item));
args.push(format!("--env=POD_OWNER={}", pod_owner));
args.push(format!("--env=POD_AUTH_JSON={}", pod_auth));
run_any_command("kubectl", &args, triggered_by_item_id)
}
fn run_any_command(cmd: &str, args: &[String], container_id: &str) -> Result<()> {
let debug_print = args
.iter()
.map(|p| escape_bash_arg(p))
.collect::<Vec<_>>()
.join(" ");
log::info!("Starting command {} {}", cmd, debug_print);
let command = Command::new(cmd).args(args).spawn();
match command {
Ok(_child) => {
log::debug!(
"Successfully started Plugin container for {}",
triggered_by_item_id
"Successfully started {} process for Plugin container item {}",
cmd,
container_id
);
Ok(())
}
......@@ -74,20 +151,14 @@ pub fn run_plugin_container(
code: StatusCode::INTERNAL_SERVER_ERROR,
msg: format!(
"Failed to run plugin container triggered by item.rowid{}, {}",
triggered_by_item_id, err
container_id, err
),
}),
}
}
fn docker_arguments(cli_options: &CliOptions) -> Vec<String> {
fn callback_address(cli_options: &CliOptions) -> String {
let is_https = cli_options.insecure_non_tls.is_none() && !cli_options.non_tls;
let schema = if is_https { "https" } else { "http" };
let port: u16 = cli_options.port;
let network: &str = match &cli_options.plugins_docker_network {
Some(net) => net,
None => "host",
};
let callback = match &cli_options.plugins_callback_address {
Some(addr) => addr.to_string(),
None => {
......@@ -99,11 +170,26 @@ fn docker_arguments(cli_options: &CliOptions) -> Vec<String> {
} else {
"host.docker.internal"
};
format!("{}:{}", pod_domain, port)
format!("{}:{}", pod_domain, cli_options.port)
}
};
vec![
format!("--network={}", network),
format!("--env=POD_FULL_ADDRESS={}://{}", schema, callback),
]
let schema = if is_https { "https" } else { "http" };
format!("{}://{}", schema, callback)
}
/// Debug-print a bash argument.
/// Never use this for running real code, but for debugging that's good enough.
///
/// From bash manual:
/// > Enclosing characters in single quotes preserves the literal value of each character
/// > within the quotes. A single quote may not occur between single quotes,
/// > even when preceded by a backslash.
pub fn escape_bash_arg(str: &str) -> String {
let ok = str.chars().all(|c| c.is_ascii_alphanumeric() || "_-=%".contains(c));
if ok {
str.to_string()
} else {
let quoted = str.replace("'", "'\\''"); // end quoting, append the literal, start quoting
return format!("'{}'", quoted);
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment