starlane 0.1.0

Starlane - a Resource Mesh
#[macro_use]
extern crate lazy_static;

use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;

use clap::{App, Arg, ArgMatches, SubCommand};
use tokio::runtime::Runtime;
use tracing::dispatcher::set_global_default;
use tracing_subscriber::FmtSubscriber;

use starlane_core::error::Error;
use starlane_core::resource::{
    ResourceAddress,
};
use starlane_core::resource::selector::MultiResourceSelector;
use starlane_core::starlane::{
    ConstellationCreate, StarlaneCommand, StarlaneMachine, StarlaneMachineRunner,
};
use starlane_core::starlane::api::StarlaneApi;
use starlane_core::template::{ConstellationData, ConstellationLayout, ConstellationTemplate};
use starlane_core::util;
use starlane_core::util::shutdown;

use starlane_resources::{ResourceCreate, KeyCreationSrc, AddressCreationSrc, ResourceArchetype, AssignResourceStateSrc, ResourceCreateStrategy, ResourceSelector, ResourcePath, ResourcePathAndKind, ResourceKind, FileKind };
use starlane_resources::data::{DataSet, BinSrc, Meta};
use starlane_resources::property::{ResourcePropertyValueSelector, ResourceValueSelector};
use starlane_core::watch::{WatchResourceSelector, Property};
use starlane_resources::message::MessageFrom;

mod cli;
mod resource;

fn main() -> Result<(), Error> {
    let subscriber = FmtSubscriber::default();
    set_global_default(subscriber.into()).expect("setting global default tracer failed");

    ctrlc::set_handler(move || {
        std::process::exit(1);
    })
    .expect("expected to be able to set ctrl-c handler");

    let mut clap_app = App::new("Starlane")
        .version("0.1.0")
        .author("Scott Williams <scott@mightydevco.com>")
        .about("A Resource Mesh").subcommands(vec![SubCommand::with_name("serve").usage("serve a starlane machine instance").arg(Arg::with_name("with-external").long("with-external").takes_value(false).required(false)).display_order(0),
                                                            SubCommand::with_name("config").subcommands(vec![SubCommand::with_name("set-host").usage("set the host that the starlane CLI connects to").arg(Arg::with_name("hostname").required(true).help("the hostname of the starlane instance you wish to connect to")).display_order(0),
                                                                                                                            SubCommand::with_name("get-host").usage("get the host that the starlane CLI connects to")]).usage("read or manipulate the cli config").display_order(1).display_order(1),
                                                            SubCommand::with_name("publish").usage("publish an artifact bundle").args(vec![Arg::with_name("dir").required(true).help("the source directory for this bundle"),Arg::with_name("address").required(true).help("the publish address of this bundle i.e. 'space:sub_space:bundle:1.0.0'")].as_slice()),
                                                            SubCommand::with_name("cp").usage("copy a file").args(vec![Arg::with_name("src").takes_value(true).index(1).required(true).help("the source file [local file or starlane resource address]"),Arg::with_name("dst").takes_value(true).index(2).required(true).help("the  destination [local file or starlane resource address]")].as_slice()),
                                                            SubCommand::with_name("create").usage("create a resource").setting(clap::AppSettings::TrailingVarArg).args(vec![Arg::with_name("address").required(true).help("address of your new resource"),Arg::with_name("create-args").multiple(true).required(false)].as_slice()),
                                                            SubCommand::with_name("ls").usage("list resources").args(vec![Arg::with_name("address").required(true).help("the resource address to list"),Arg::with_name("child-pattern").required(false).help("a pattern describing the children to be listed .i.e '<File>' for returning resource type File")].as_slice()),
                                                            SubCommand::with_name("get").usage("get resources property value").args(vec![Arg::with_name("address").required(true).help("the resource property value")].as_slice()),
                                                            SubCommand::with_name("watch").usage("watch resources property value for changes").args(vec![Arg::with_name("address").required(true).help("the resource property value to watch")].as_slice())
    ]);

    let matches = clap_app.clone().get_matches();

    if let Option::Some(serve) = matches.subcommand_matches("serve") {
        let rt = Runtime::new().unwrap();
        rt.block_on(async move {
            let starlane = StarlaneMachine::new("server".to_string()).unwrap();
            let layout = match serve.is_present("with-external") {
                false => ConstellationLayout::standalone().unwrap(),
                true => ConstellationLayout::standalone_with_external().unwrap(),
            };

            starlane
                .create_constellation("standalone", layout)
                .await
                .unwrap();
            starlane.listen().await.expect("expected listen to work");
            starlane.join().await;
        });
    } else if let Option::Some(matches) = matches.subcommand_matches("config") {
        if let Option::Some(_) = matches.subcommand_matches("get-host") {
            let config = crate::cli::CLI_CONFIG.lock()?;
            println!("{}", config.hostname);
        } else if let Option::Some(args) = matches.subcommand_matches("set-host") {
            let mut config = crate::cli::CLI_CONFIG.lock()?;
            config.hostname = args
                .value_of("hostname")
                .ok_or("expected hostname")?
                .to_string();
            config.save()?;
        } else {
            clap_app.print_long_help().unwrap_or_default();
        }
    } else if let Option::Some(args) = matches.subcommand_matches("publish") {
        let rt = Runtime::new().unwrap();
        rt.block_on(async {
            publish(args.clone()).await.unwrap();
        });
        shutdown();
    } else if let Option::Some(args) = matches.subcommand_matches("cp") {
        let rt = Runtime::new().unwrap();
        rt.block_on(async {
            cp(args.clone()).await.unwrap();
        });
        shutdown();
    }
    else if let Option::Some(args) = matches.subcommand_matches("create") {
        let rt = Runtime::new().unwrap();
        rt.block_on(async {
            create(args.clone()).await.unwrap();
        });
        shutdown();
    } else if let Option::Some(args) = matches.subcommand_matches("ls") {
        let rt = Runtime::new().unwrap();
        rt.block_on(async {
            list(args.clone()).await.unwrap();
        });
        shutdown();
    } else if let Option::Some(args) = matches.subcommand_matches("get") {
        let rt = Runtime::new().unwrap();
        rt.block_on(async {
            get(args.clone()).await.unwrap();
        });
        shutdown();
    } else if let Option::Some(args) = matches.subcommand_matches("watch") {
        let rt = Runtime::new().unwrap();
        rt.block_on(async {
            watch(args.clone()).await.unwrap();
        });
        shutdown();
    } else {
        clap_app.print_long_help().unwrap_or_default();
    }

    Ok(())
}

async fn publish(args: ArgMatches<'_>) -> Result<(), Error> {
    let bundle = ResourcePath::from_str(args.value_of("address").ok_or("expected address")?)?;

    let input = Path::new(args.value_of("dir").ok_or("expected directory")?);

    let mut zipfile = if input.is_dir() {
        let zipfile = tempfile::NamedTempFile::new()?;
        util::zip(
            args.value_of("dir")
                .expect("expected directory")
                .to_string()
                .as_str(),
            &zipfile.reopen()?,
            zip::CompressionMethod::Deflated,
        )?;
        zipfile.reopen()?
    } else {
        File::open(input)?
    };

    let mut data = Vec::with_capacity(zipfile.metadata()?.len() as _);
    zipfile.read_to_end(&mut data).unwrap();
    let data = Arc::new(data);

    let starlane_api = starlane_api().await?;
    starlane_api.create_artifact_bundle(&bundle, data).await?;

    Ok(())
}

async fn cp(args: ArgMatches<'_>) -> Result<(), Error> {

    let starlane_api = starlane_api().await?;

    let src = args.value_of("src").ok_or("expected src")?;
    let dst = args.value_of("dst").ok_or( "expected dst")?;

    if dst.contains(":") {
        let dst = ResourcePath::from_str(dst)?;
        let src = Path::new(src );
        // copying from src to dst
        let mut src = File::open(src )?;
        let mut content= Vec::with_capacity(src.metadata()?.len() as _);
        src.read_to_end(&mut content ).unwrap();
        let content = Arc::new(content);
        let content = BinSrc::Memory(content);
        let mut state = DataSet::new();
        state.insert("content".to_string(), content );

        let meta = Meta::new();
        let meta = BinSrc::Memory(Arc::new(meta.bin()?));
        state.insert("meta".to_string(), meta );

        let create = ResourceCreate {
            parent: dst
                .parent()
                .ok_or("must have an address with a parent")?
                .into(),
            key: KeyCreationSrc::None,
            address: AddressCreationSrc::Exact(dst),
            archetype: ResourceArchetype {
                kind: ResourceKind::File(FileKind::File),
                specific: None,
                config: None,
            },
            state_src: AssignResourceStateSrc::Direct(state),
            registry_info: Option::None,
            owner: Option::None,
            strategy: ResourceCreateStrategy::CreateOrUpdate,
            from: MessageFrom::Inject
        };

        starlane_api.create_resource(create).await?;
        println!("CP DONE.");

        starlane_api.shutdown();

    } else  if src.contains(":") {
      let src = ResourcePath::from_str(src)?;
      let content = starlane_api.get_resource_state(src.into()).await?.remove("content").expect("expected 'content' state aspect");
      let filename = dst.clone();
      let dst = Path::new(dst );
      let mut dst = File::create(dst).expect(format!("could not open file for writing: {}", filename ).as_str() );
      match content {
          BinSrc::Memory(bin) => {
              dst.write_all(bin.as_slice() ).expect(format!("could not write to file: {}", filename ).as_str() )
          }
      }
    } else {
        unimplemented!("copy from starlane to local not yet supported")
    }

    Ok(())
}

async fn list(args: ArgMatches<'_>) -> Result<(), Error> {
    let address = ResourcePath::from_str(
        args.value_of("address")
            .ok_or("expected resource address")?,
    )?;
    let starlane_api = starlane_api().await?;

    let selector = if args.value_of("child-pattern").is_some() {
        let selector = MultiResourceSelector::from_str(args.value_of("child-pattern").unwrap())?;
        selector.into()
    } else {
        ResourceSelector::new()
    };

    let resources = starlane_api.select(&address.into(), selector).await?;

    println!();
    for resource in resources {
        println!("{}", resource.stub.address.to_string());
    }
    println!();

    starlane_api.shutdown();

    Ok(())
}

async fn create(args: ArgMatches<'_>) -> Result<(), Error> {
println!("CREATE...");
    let address = ResourcePathAndKind::from_str(
        args.value_of("address")
            .ok_or("expected resource address")?,
    )?;
    let kind = address.kind.clone();
    let address: ResourcePath = address.into();

    let create_args = match args.values_of("create-args") {
        None => "".to_string(),
        Some(args) => {
            let create_args: Vec<&str> = args.collect();
            let create_args: Vec<String> = create_args.iter().map(|s| (*s).to_string()).collect();
            create_args.join(" ")
        }
    };

println!("creating starlane api...");
    let starlane_api = starlane_api().await?;
println!("starlane api created.");

    let create = ResourceCreate {
        parent: address
            .parent()
            .ok_or("must have an address with a parent")?
            .into(),
        key: KeyCreationSrc::None,
        address: AddressCreationSrc::Exact(address),
        archetype: ResourceArchetype {
            kind: kind,
            specific: None,
            config: None,
        },
        state_src: AssignResourceStateSrc::CreateArgs(create_args),
        registry_info: Option::None,
        owner: Option::None,
        strategy: ResourceCreateStrategy::Create,
        from: MessageFrom::Inject
    };

println!("SENDING CREATE REQUEST...");
    starlane_api.create_resource(create).await?;
println!("CREATE DONE.");

    starlane_api.shutdown();

    Ok(())
}

async fn get(args: ArgMatches<'_>) -> Result<(), Error> {
    let address = ResourceValueSelector::from_str(
        args.value_of("address")
            .ok_or("expected resource property value address")?,
    )?;
    let starlane_api = starlane_api().await?;

    let values = starlane_api.select_values(address.resource, address.property).await?;

    println!();
    for (k,v) in values.values {
        println!("{}",v.to_string());
    }
    println!();

    starlane_api.shutdown();

    Ok(())
}

async fn watch(args: ArgMatches<'_>) -> Result<(), Error> {
    let address = ResourcePath::from_str(
        args.value_of("address")
            .ok_or("expected resource property value address")?,
    )?;
    let starlane_api = starlane_api().await?;

    let selector = WatchResourceSelector::new( address.into(), Property::State );

    let mut listener = starlane_api.watch(selector).await?;

    while let Option::Some(notification)  = listener.rx.recv().await {
        for change in notification.changes {
            println!("received notification: {}", change.to_string() );
        }
    }

    starlane_api.shutdown();

    Ok(())
}


pub async fn starlane_api() -> Result<StarlaneApi, Error> {
    let starlane = StarlaneMachine::new("client".to_string()).unwrap();
    let mut layout = ConstellationLayout::client("host".to_string())?;
    let host = {
        let config = crate::cli::CLI_CONFIG.lock()?;
        config.hostname.clone()
    };
    layout.set_machine_host_address("host".to_string(), host);
    starlane.create_constellation("client", layout).await?;
    Ok(starlane.get_starlane_api().await?)
}