move code to dagger-core

This commit is contained in:
2023-02-05 22:26:58 +01:00
parent 9f0021b708
commit ec0d0b22e6
18 changed files with 132 additions and 33 deletions

View File

@@ -6,4 +6,22 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = "4.1.4"
dirs = "4.0.0"
eyre = "0.6.8"
flate2 = { version = "1.0.25", features = ["zlib"] }
genco = "0.17.3"
graphql-introspection-query = "0.2.0"
graphql_client = { version = "0.12.0", features = [
"reqwest",
"reqwest-blocking",
] }
hex = "0.4.3"
hex-literal = "0.3.4"
platform-info = "1.0.2"
reqwest = { version = "0.11.14", features = ["stream", "blocking", "deflate"] }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91"
sha2 = "0.10.6"
tar = "0.4.38"
tempfile = "3.3.0"

View File

@@ -0,0 +1,112 @@
use std::{
fs::canonicalize,
io::{BufRead, BufReader},
path::PathBuf,
process::{Child, Stdio},
sync::{mpsc::sync_channel, Arc},
};
use crate::{config::Config, connect_params::ConnectParams};
#[derive(Clone, Debug)]
pub struct CliSession {
inner: Arc<InnerCliSession>,
}
impl CliSession {
pub fn new() -> Self {
Self {
inner: Arc::new(InnerCliSession {}),
}
}
pub fn connect(
&self,
config: &Config,
cli_path: &PathBuf,
) -> eyre::Result<(ConnectParams, Child)> {
self.inner.connect(config, cli_path)
}
}
#[derive(Debug)]
struct InnerCliSession {}
impl InnerCliSession {
pub fn connect(
&self,
config: &Config,
cli_path: &PathBuf,
) -> eyre::Result<(ConnectParams, Child)> {
let proc = self.start(config, cli_path)?;
let params = self.get_conn(proc)?;
Ok(params)
}
fn start(&self, config: &Config, cli_path: &PathBuf) -> eyre::Result<std::process::Child> {
let mut args: Vec<String> = vec!["session".into()];
if let Some(workspace) = &config.workdir_path {
let abs_path = canonicalize(workspace)?;
args.extend(["--workdir".into(), abs_path.to_string_lossy().to_string()])
}
if let Some(config_path) = &config.config_path {
let abs_path = canonicalize(config_path)?;
args.extend(["--project".into(), abs_path.to_string_lossy().to_string()])
}
let proc = std::process::Command::new(
cli_path
.to_str()
.ok_or(eyre::anyhow!("could not get string from path"))?,
)
.args(args.as_slice())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
//TODO: Add retry mechanism
return Ok(proc);
}
fn get_conn(
&self,
mut proc: std::process::Child,
) -> eyre::Result<(ConnectParams, std::process::Child)> {
let stdout = proc
.stdout
.take()
.ok_or(eyre::anyhow!("could not acquire stdout from child process"))?;
let stderr = proc
.stderr
.take()
.ok_or(eyre::anyhow!("could not acquire stderr from child process"))?;
let (sender, receiver) = sync_channel(1);
std::thread::spawn(move || {
let stdout_bufr = BufReader::new(stdout);
for line in stdout_bufr.lines() {
let out = line.unwrap();
if let Ok(conn) = serde_json::from_str::<ConnectParams>(&out) {
sender.send(conn).unwrap();
}
}
});
std::thread::spawn(|| {
let stderr_bufr = BufReader::new(stderr);
for line in stderr_bufr.lines() {
let out = line.unwrap();
panic!("could not start dagger session: {}", out)
}
});
let conn = receiver.recv()?;
Ok((conn, proc))
}
}

View File

@@ -0,0 +1,30 @@
use std::path::PathBuf;
pub struct Config {
pub workdir_path: Option<PathBuf>,
pub config_path: Option<PathBuf>,
pub timeout_ms: u64,
pub execute_timeout_ms: Option<u64>,
}
impl Default for Config {
fn default() -> Self {
Self::new(None, None, None, None)
}
}
impl Config {
pub fn new(
workdir_path: Option<PathBuf>,
config_path: Option<PathBuf>,
timeout_ms: Option<u64>,
execute_timeout_ms: Option<u64>,
) -> Self {
Self {
workdir_path,
config_path,
timeout_ms: timeout_ms.unwrap_or(10 * 1000),
execute_timeout_ms,
}
}
}

View File

@@ -0,0 +1,20 @@
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct ConnectParams {
pub port: u64,
pub session_token: String,
}
impl ConnectParams {
pub fn new(port: u64, session_token: &str) -> Self {
Self {
port,
session_token: session_token.to_string(),
}
}
pub fn url(&self) -> String {
format!("http://127.0.0.1:{}/query", self.port)
}
}

View File

@@ -0,0 +1,20 @@
use std::sync::Arc;
pub fn connect() -> eyre::Result<Client> {
Client::new()
}
struct InnerClient {}
#[allow(dead_code)]
pub struct Client {
inner: Arc<InnerClient>,
}
impl Client {
pub fn new() -> eyre::Result<Self> {
Ok(Self {
inner: Arc::new(InnerClient {}),
})
}
}

View File

@@ -0,0 +1,252 @@
use std::{
fs::File,
io::{copy, Read, Write},
os::unix::prelude::PermissionsExt,
path::PathBuf,
};
use eyre::Context;
use flate2::read::GzDecoder;
use platform_info::Uname;
use sha2::Digest;
use tar::Archive;
use tempfile::tempfile;
#[allow(dead_code)]
#[derive(Clone)]
pub struct Platform {
pub os: String,
pub arch: String,
}
impl Platform {
pub fn from_system() -> eyre::Result<Self> {
let platform = platform_info::PlatformInfo::new()?;
let os_name = platform.sysname();
let arch = platform.machine().to_lowercase();
let normalize_arch = match arch.as_str() {
"x86_64" => "amd64",
"aarch" => "arm64",
arch => arch,
};
Ok(Self {
os: os_name.to_lowercase(),
arch: normalize_arch.into(),
})
}
}
#[allow(dead_code)]
pub struct TempFile {
prefix: String,
directory: PathBuf,
file: File,
}
#[allow(dead_code)]
impl TempFile {
pub fn new(prefix: &str, directory: &PathBuf) -> eyre::Result<Self> {
let prefix = prefix.to_string();
let directory = directory.clone();
let file = tempfile()?;
Ok(Self {
prefix,
directory,
file,
})
}
}
#[allow(dead_code)]
pub type CliVersion = String;
#[allow(dead_code)]
pub struct Downloader {
version: CliVersion,
platform: Platform,
}
#[allow(dead_code)]
const DEFAULT_CLI_HOST: &str = "dl.dagger.io";
#[allow(dead_code)]
const CLI_BIN_PREFIX: &str = "dagger-";
#[allow(dead_code)]
const CLI_BASE_URL: &str = "https://dl.dagger.io/dagger/releases";
#[allow(dead_code)]
impl Downloader {
pub fn new(version: CliVersion) -> eyre::Result<Self> {
Ok(Self {
version,
platform: Platform::from_system()?,
})
}
pub fn archive_url(&self) -> String {
let ext = match self.platform.os.as_str() {
"windows" => "zip",
_ => "tar.gz",
};
let version = &self.version;
let os = &self.platform.os;
let arch = &self.platform.arch;
format!("{CLI_BASE_URL}/{version}/dagger_v{version}_{os}_{arch}.{ext}")
}
pub fn checksum_url(&self) -> String {
let version = &self.version;
format!("{CLI_BASE_URL}/{version}/checksums.txt")
}
pub fn cache_dir(&self) -> eyre::Result<PathBuf> {
let env = std::env::var("XDG_CACHE_HOME").unwrap_or("".into());
let env = env.trim();
let mut path = match env {
"" => dirs::cache_dir().ok_or(eyre::anyhow!(
"could not find cache_dir, either in env or XDG_CACHE_HOME"
))?,
path => PathBuf::from(path),
};
path.push("dagger");
std::fs::create_dir_all(&path)?;
Ok(path)
}
pub fn get_cli(&self) -> eyre::Result<PathBuf> {
let version = &self.version;
let mut cli_bin_path = self.cache_dir()?;
cli_bin_path.push(format!("{CLI_BIN_PREFIX}{version}"));
if self.platform.os == "windows" {
cli_bin_path = cli_bin_path.with_extension("exe")
}
if !cli_bin_path.exists() {
cli_bin_path = self
.download(cli_bin_path)
.context("failed to download CLI from archive")?;
}
for file in self.cache_dir()?.read_dir()? {
if let Ok(entry) = file {
let path = entry.path();
if path != cli_bin_path {
std::fs::remove_file(path)?;
}
}
}
Ok(cli_bin_path)
}
fn download(&self, path: PathBuf) -> eyre::Result<PathBuf> {
let expected_checksum = self.expected_checksum()?;
let mut bytes = vec![];
let actual_hash = self.extract_cli_archive(&mut bytes)?;
if expected_checksum != actual_hash {
eyre::bail!("downloaded CLI binary checksum doesn't match checksum from checksums.txt")
}
let mut file = std::fs::File::create(&path)?;
let meta = file.metadata()?;
let mut perm = meta.permissions();
perm.set_mode(0o700);
file.set_permissions(perm)?;
file.write_all(bytes.as_slice())?;
Ok(path)
}
fn expected_checksum(&self) -> eyre::Result<String> {
let archive_url = &self.archive_url();
let archive_path = PathBuf::from(&archive_url);
let archive_name = archive_path
.file_name()
.ok_or(eyre::anyhow!("could not get file_name from archive_url"))?;
let resp = reqwest::blocking::get(self.checksum_url())?;
let resp = resp.error_for_status()?;
for line in resp.text()?.lines() {
let mut content = line.split_whitespace();
let checksum = content
.next()
.ok_or(eyre::anyhow!("could not find checksum in checksums.txt"))?;
let file_name = content
.next()
.ok_or(eyre::anyhow!("could not find file_name in checksums.txt"))?;
if file_name == archive_name {
return Ok(checksum.to_string());
}
}
eyre::bail!("could not find a matching version or binary in checksums.txt")
}
pub fn extract_cli_archive(&self, dest: &mut Vec<u8>) -> eyre::Result<String> {
let archive_url = self.archive_url();
let resp = reqwest::blocking::get(&archive_url)?;
let mut resp = resp.error_for_status()?;
let mut bytes = vec![];
let lines = resp.read_to_end(&mut bytes)?;
if lines == 0 {
eyre::bail!("nothing was downloaded")
}
let mut hasher = sha2::Sha256::new();
hasher.update(bytes.as_slice());
let res = hasher.finalize();
println!("{}", hex::encode(&res));
if archive_url.ends_with(".zip") {
// TODO: Nothing for now
todo!()
} else {
self.extract_from_tar(bytes.as_slice(), dest)?;
}
Ok(hex::encode(res))
}
fn extract_from_tar(&self, temp: &[u8], output: &mut Vec<u8>) -> eyre::Result<()> {
let decompressed_temp = GzDecoder::new(temp);
let mut archive = Archive::new(decompressed_temp);
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
println!("path: {:?}", path);
if path.ends_with("dagger") {
copy(&mut entry, output)?;
return Ok(());
}
}
eyre::bail!("could not find a matching file")
}
}
#[cfg(test)]
mod test {
use super::Downloader;
#[test]
fn download() {
let cli_path = Downloader::new("0.3.10".into()).unwrap().get_cli().unwrap();
assert_eq!(
Some("dagger-0.3.10"),
cli_path.file_name().and_then(|s| s.to_str())
)
}
}

View File

@@ -0,0 +1,48 @@
use std::process::Child;
use crate::{
cli_session::CliSession, config::Config, connect_params::ConnectParams, downloader::Downloader,
};
pub struct Engine {}
impl Engine {
pub fn new() -> Self {
Self {}
}
fn from_cli(&self, cfg: &Config) -> eyre::Result<(ConnectParams, Child)> {
let cli = Downloader::new("0.3.10".into())?.get_cli()?;
let cli_session = CliSession::new();
Ok(cli_session.connect(cfg, &cli)?)
}
pub fn start(&self, cfg: &Config) -> eyre::Result<(ConnectParams, Child)> {
// TODO: Add from existing session as well
self.from_cli(cfg)
}
}
#[cfg(test)]
mod tests {
use crate::{config::Config, connect_params::ConnectParams};
use super::Engine;
// TODO: these tests potentially have a race condition
#[test]
fn engine_can_start() {
let engine = Engine::new();
let params = engine.start(&Config::new(None, None, None, None)).unwrap();
assert_ne!(
params.0,
ConnectParams {
port: 123,
session_token: "123".into()
}
)
}
}

View File

@@ -0,0 +1,99 @@
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type {
...TypeRef
}
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,101 @@
schema {
query: Query
}
type Query {
__schema: __Schema
}
type __Schema {
types: [__Type!]!
queryType: __Type!
mutationType: __Type
subscriptionType: __Type
directives: [__Directive!]!
}
type __Type {
kind: __TypeKind!
name: String
description: String
# OBJECT and INTERFACE only
fields(includeDeprecated: Boolean = false): [__Field!]
# OBJECT only
interfaces: [__Type!]
# INTERFACE and UNION only
possibleTypes: [__Type!]
# ENUM only
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
# INPUT_OBJECT only
inputFields: [__InputValue!]
# NON_NULL and LIST only
ofType: __Type
}
type __Field {
name: String!
description: String
args: [__InputValue!]!
type: __Type!
isDeprecated: Boolean!
deprecationReason: String
}
type __InputValue {
name: String!
description: String
type: __Type!
defaultValue: String
}
type __EnumValue {
name: String!
description: String
isDeprecated: Boolean!
deprecationReason: String
}
enum __TypeKind {
SCALAR
OBJECT
INTERFACE
UNION
ENUM
INPUT_OBJECT
LIST
NON_NULL
}
type __Directive {
name: String!
description: String
locations: [__DirectiveLocation!]!
args: [__InputValue!]!
}
enum __DirectiveLocation {
QUERY
MUTATION
SUBSCRIPTION
FIELD
FRAGMENT_DEFINITION
FRAGMENT_SPREAD
INLINE_FRAGMENT
SCHEMA
SCALAR
OBJECT
FIELD_DEFINITION
ARGUMENT_DEFINITION
INTERFACE
UNION
ENUM
ENUM_VALUE
INPUT_OBJECT
INPUT_FIELD_DEFINITION
}

View File

@@ -1,4 +1,12 @@
pub mod cli_session;
pub mod config;
pub mod connect_params;
pub mod dagger;
pub mod downloader;
pub mod engine;
pub mod introspection;
pub mod schema;
pub mod session;
pub struct Scalar(String);

View File

@@ -0,0 +1,24 @@
use crate::introspection::IntrospectionResponse;
use crate::{config::Config, engine::Engine, session::Session};
pub fn get_schema() -> eyre::Result<IntrospectionResponse> {
let cfg = Config::new(None, None, None, None);
//TODO: Implement context for proc
let (conn, _proc) = Engine::new().start(&cfg)?;
let session = Session::new();
let req_builder = session.start(&cfg, &conn)?;
let schema = session.schema(req_builder)?;
Ok(schema)
}
#[cfg(test)]
mod tests {
use super::get_schema;
#[test]
fn can_get_schema() {
let _ = get_schema().unwrap();
}
}

View File

@@ -0,0 +1,78 @@
use graphql_client::GraphQLQuery;
use reqwest::{
blocking::{Client, RequestBuilder},
header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE},
};
use crate::{config::Config, connect_params::ConnectParams, introspection::IntrospectionResponse};
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "src/graphql/introspection_schema.graphql",
query_path = "src/graphql/introspection_query.graphql",
responsive_path = "Serialize",
variable_derive = "Deserialize"
)]
struct IntrospectionQuery;
pub struct Session;
impl Session {
pub fn new() -> Self {
Self {}
}
pub fn start(&self, _cfg: &Config, conn: &ConnectParams) -> eyre::Result<RequestBuilder> {
let client = Client::builder()
.user_agent("graphql-rust/0.10.0")
.connection_verbose(true)
//.danger_accept_invalid_certs(true)
.build()?;
let req_builder = client
.post(conn.url())
.headers(construct_headers())
.basic_auth::<String, String>(conn.session_token.to_string(), None);
Ok(req_builder)
}
pub fn schema(&self, req_builder: RequestBuilder) -> eyre::Result<IntrospectionResponse> {
let request_body: graphql_client::QueryBody<()> = graphql_client::QueryBody {
variables: (),
query: introspection_query::QUERY,
operation_name: introspection_query::OPERATION_NAME,
};
let res = req_builder.json(&request_body).send()?;
if res.status().is_success() {
// do nothing
} else if res.status().is_server_error() {
return Err(eyre::anyhow!("server error!"));
} else {
let status = res.status();
let error_message = match res.text() {
Ok(msg) => match serde_json::from_str::<serde_json::Value>(&msg) {
Ok(json) => {
format!("HTTP {}\n{}", status, serde_json::to_string_pretty(&json)?)
}
Err(_) => format!("HTTP {}: {}", status, msg),
},
Err(_) => format!("HTTP {}", status),
};
return Err(eyre::anyhow!(error_message));
}
let json: IntrospectionResponse = res.json()?;
Ok(json)
}
}
fn construct_headers() -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
headers
}