Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ exclude = ["tests/e2e-ui"]
resolver = "2"

[workspace.package]
version = "0.1.0"
version = "1.0.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/patrickleet/distributed"

[workspace.dependencies.distributed_macros]
version = "0.1.0"
version = "1.0.0"
path = "distributed_macros"

[package]
Expand Down
1 change: 1 addition & 0 deletions distributed_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ name = "distributed"
path = "src/main.rs"

[dependencies]
distributed = { path = "..", default-features = false }
async-graphql-parser = "7"
async-graphql-value = "7"
base64 = "0.22.1"
Expand Down
81 changes: 79 additions & 2 deletions distributed_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ use crate::{
MetricsTarget, PostCreateAction, ServiceScaffoldSpec, ServiceTransport, StoreTarget,
};

const DISTRIBUTED_MANIFEST_SCHEMA_VERSION: u64 = 1;
const DISTRIBUTED_CLIENT_MANIFEST_VERSION: u64 = 2;
const DISTRIBUTED_MANIFEST_SCHEMA_VERSION: u64 =
distributed::APPLICATION_MANIFEST_SCHEMA_VERSION as u64;
const DISTRIBUTED_CLIENT_MANIFEST_VERSION: u64 =
distributed::graphql::DISTRIBUTED_CLIENT_MANIFEST_VERSION as u64;

/// Top-level standalone CLI arguments for the `distributed` binary.
#[derive(Args, Debug)]
Expand All @@ -58,6 +60,8 @@ pub enum DistributedCommands {
Schema(SchemaArgs),
/// Extract the embedded Distributed agent skills into a project
Skills(SkillsArgs),
/// Validate and render a resolved deployment from one manifest + plan
Deployment(DeploymentArgs),
}

/// Library adapter for embedding service-related commands under another CLI.
Expand Down Expand Up @@ -133,6 +137,40 @@ pub enum ContractsOutput {
Json,
}

#[derive(Args, Debug)]
pub struct DeploymentArgs {
#[command(subcommand)]
pub command: DeploymentCommands,
}

#[derive(Subcommand, Debug)]
pub enum DeploymentCommands {
/// Validate that one manifest + plan resolve
Validate(DeploymentValidateArgs),
/// Render kubernetes, knative, or hops-xr from the same pair
Render(DeploymentRenderArgs),
}

#[derive(Args, Debug)]
pub struct DeploymentValidateArgs {
#[arg(long)]
pub manifest: PathBuf,
#[arg(long)]
pub plan: PathBuf,
}

#[derive(Args, Debug)]
pub struct DeploymentRenderArgs {
#[arg(long)]
pub manifest: PathBuf,
#[arg(long)]
pub plan: PathBuf,
#[arg(long)]
pub target: String,
#[arg(long, default_value = "dist")]
pub out: PathBuf,
}

#[derive(Args, Debug)]
pub struct SkillsArgs {
#[command(subcommand)]
Expand Down Expand Up @@ -516,6 +554,44 @@ impl From<GitopsPromote> for GitopsPromoteTarget {
}
}

fn run_deployment(args: &DeploymentArgs) -> Result<(), Box<dyn Error>> {
match &args.command {
DeploymentCommands::Validate(validate) => {
let resolved = load_resolved(&validate.manifest, &validate.plan)?;
println!(
"deployment validate: ok application={} plan={} digest={}",
resolved.application, resolved.plan, resolved.inventory_digest
);
Ok(())
}
DeploymentCommands::Render(render) => {
let resolved = load_resolved(&render.manifest, &render.plan)?;
let target = distributed::RenderTarget::parse(&render.target)?;
let files = distributed::render_resolved(&resolved, target)?;
fs::create_dir_all(&render.out)?;
for file in files {
let path = render.out.join(&file.path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&path, file.contents.as_bytes())?;
println!("{}", path.display());
}
Ok(())
}
}
}

fn load_resolved(
manifest_path: &Path,
plan_path: &Path,
) -> Result<distributed::ResolvedDeployment, Box<dyn Error>> {
let manifest: distributed::ApplicationManifest =
serde_json::from_str(&fs::read_to_string(manifest_path)?)?;
let plan: distributed::DeploymentPlan = serde_json::from_str(&fs::read_to_string(plan_path)?)?;
Ok(distributed::resolve_deployment(&manifest, &plan)?)
}

/// Dispatch the standalone `distributed` binary command tree.
pub fn run_distributed(args: &DistributedArgs) -> Result<(), Box<dyn Error>> {
match &args.command {
Expand All @@ -529,6 +605,7 @@ pub fn run_distributed(args: &DistributedArgs) -> Result<(), Box<dyn Error>> {
SkillsCommands::Init(init) => run_skills_init(init),
SkillsCommands::List => run_skills_list(),
},
DistributedCommands::Deployment(deployment) => run_deployment(deployment),
}
}

Expand Down
21 changes: 17 additions & 4 deletions distributed_cli/src/client_compiler/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ pub(crate) use projectors::*;
pub(crate) use roots::*;
pub(crate) use util::{validate_hash, validate_nonempty};

const MANIFEST_VERSION: u64 = 2;
const PROTOCOL_VERSION: u64 = 1;
const PROTOCOL_FINGERPRINT: &str =
"sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782";
const MANIFEST_VERSION: u64 =
distributed::graphql::DISTRIBUTED_CLIENT_MANIFEST_VERSION as u64;
const PROTOCOL_VERSION: u64 =
distributed::graphql::DISTRIBUTED_CLIENT_PROTOCOL_VERSION as u64;

pub(crate) fn protocol_fingerprint() -> Result<String, ClientCompileError> {
distributed::graphql::protocol_fingerprint().map_err(|error| {
ClientCompileError::manifest(
"client.manifest.protocol_fingerprint",
error.to_string(),
)
})
}

pub(crate) fn expected_manifest_version() -> u64 {
MANIFEST_VERSION
}

pub(crate) const CLIENT_PROJECTION_PROGRAM_VERSION: u32 = 2;
pub(crate) const CLIENT_PROJECTION_BINDING_VERSION: u32 = 1;
Expand Down
5 changes: 3 additions & 2 deletions distributed_cli/src/client_compiler/manifest/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ impl ClientManifest {
validate_hash(&wire.schema_fingerprint, "manifest.schema_fingerprint")?;
validate_hash(&wire.protocol_fingerprint, "manifest.protocol_fingerprint")?;
validate_execution_limits(&wire.execution)?;
if wire.protocol_fingerprint != PROTOCOL_FINGERPRINT {
let expected_protocol = protocol_fingerprint()?;
if wire.protocol_fingerprint != expected_protocol {
return Err(ClientCompileError::manifest(
"client.manifest.protocol_fingerprint",
format!(
"client compiler protocol contract is `{PROTOCOL_FINGERPRINT}`, received `{}`; regenerate the manifest and use a matching distributed version",
"client compiler protocol contract is `{expected_protocol}`, received `{}`; regenerate the manifest and use a matching distributed version",
wire.protocol_fingerprint
),
));
Expand Down
8 changes: 8 additions & 0 deletions distributed_cli/src/client_compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ use serde_json::Value as JsonValue;

use graphql::{compile_document, CompiledOperation};
use manifest::ClientManifest;

pub(crate) fn expected_protocol_fingerprint() -> Result<String, ClientCompileError> {
manifest::protocol_fingerprint()
}

pub(crate) fn expected_manifest_version() -> u64 {
manifest::expected_manifest_version()
}
use render::render_project;

/// Complete input to one deterministic client compilation.
Expand Down
12 changes: 12 additions & 0 deletions distributed_cli/src/contracts/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1616,3 +1616,15 @@ fn unavailable_merge_base_is_reported_not_verified() {
assert!(result.human().contains("history evidence unavailable"));
assert!(result.human().contains("merge_base_available=false"));
}

#[test]
fn cli_protocol_identity_is_the_library_function() {
let library = distributed::graphql::protocol_fingerprint().expect("library fingerprint");
let compiler = crate::client_compiler::expected_protocol_fingerprint()
.expect("compiler uses the same library function");
assert_eq!(library, compiler);
assert_eq!(
u64::from(distributed::graphql::DISTRIBUTED_CLIENT_MANIFEST_VERSION),
crate::client_compiler::expected_manifest_version()
);
}
3 changes: 2 additions & 1 deletion distributed_cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ mod skills;
pub use atlas::{render_atlas_schema, AtlasDatabaseUrl, AtlasSchemaSpec};
pub use cli::{
run, run_distributed, AgentHarness, Bus, ClientArgs, ClientManifestArgs, ContractsAcceptArgs,
ContractsArgs, ContractsCheckArgs, ContractsCommands, ContractsOutput, DescribeArgs,
ContractsArgs, ContractsCheckArgs, ContractsCommands, ContractsOutput, DeploymentArgs,
DeploymentCommands, DeploymentRenderArgs, DeploymentValidateArgs, DescribeArgs,
DistributedArgs, DistributedCommands, Framework, GitopsPromote, ManifestFormat, Metrics,
ScaffoldArgs, SchemaArgs, SchemaDialect, SchemaFormat, ServiceArgs, ServiceCommands,
SkillsArgs, SkillsCommands, SkillsInitArgs, Store, Transport,
Expand Down
129 changes: 129 additions & 0 deletions distributed_cli/tests/cli_deployment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! Drive the shipped `distributed` binary for deployment validate/render.

use distributed::application::{
compile_deployment_plan, Application, CommandDefinition, CommandSpec, CommandTypeSpec,
ModelFieldSpec, ModelSpec, Module, ProcessIntent, ProcessPreset, ProjectionSpec,
};
use distributed::graphql::CommandConsistency;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

fn sample_pair() -> (distributed::ApplicationManifest, distributed::DeploymentPlan) {
let model = ModelSpec::try_new(
"TodoView",
"todos",
[ModelFieldSpec {
name: "todo_id".into(),
scalar: "String".into(),
nullable: false,
}],
["todo_id"],
)
.unwrap();
let command = CommandSpec::try_new(
"todo.create",
"todo_create",
CommandTypeSpec {
name: "In".into(),
fields: vec![],
},
CommandTypeSpec {
name: "Out".into(),
fields: vec![],
},
CommandConsistency::Eventual,
)
.unwrap();
let module = Module::new("todo")
.command_definitions([CommandDefinition::contract(command)])
.models([model])
.projections([
ProjectionSpec::try_new("project_todos", ["todo.created"], ["TodoView"]).unwrap(),
])
.build()
.unwrap();
let manifest = Application::new("todo-app")
.module(module)
.build()
.unwrap()
.manifest()
.clone();
let plan = compile_deployment_plan(
"split",
&manifest,
[
ProcessIntent::with_preset("writer", &manifest, ProcessPreset::Writer).unwrap(),
ProcessIntent::with_preset("projector", &manifest, ProcessPreset::Projector).unwrap(),
],
)
.unwrap();
(manifest, plan)
}

fn write_pair(dir: &Path) -> (PathBuf, PathBuf) {
let (manifest, plan) = sample_pair();
let manifest_path = dir.join("manifest.json");
let plan_path = dir.join("plan.json");
fs::write(&manifest_path, serde_json::to_vec_pretty(&manifest).unwrap()).unwrap();
fs::write(&plan_path, serde_json::to_vec_pretty(&plan).unwrap()).unwrap();
(manifest_path, plan_path)
}

fn distributed() -> Command {
Command::new(env!("CARGO_BIN_EXE_distributed"))
}

#[test]
fn deployment_validate_and_render_use_one_pair() {
let root = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli-deployment");
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
let (manifest, plan) = write_pair(&root);

let validate = distributed()
.args([
"deployment",
"validate",
"--manifest",
manifest.to_str().unwrap(),
"--plan",
plan.to_str().unwrap(),
])
.output()
.expect("distributed binary");
assert!(
validate.status.success(),
"{}",
String::from_utf8_lossy(&validate.stderr)
);

for target in ["kubernetes", "knative", "hops-xr"] {
let out = root.join(target);
let render = distributed()
.args([
"deployment",
"render",
"--manifest",
manifest.to_str().unwrap(),
"--plan",
plan.to_str().unwrap(),
"--target",
target,
"--out",
out.to_str().unwrap(),
])
.output()
.expect("distributed binary");
assert!(
render.status.success(),
"{target}: {}",
String::from_utf8_lossy(&render.stderr)
);
assert!(
out.join("deploy").exists(),
"{target} wrote {}",
String::from_utf8_lossy(&render.stdout)
);
}
}
8 changes: 8 additions & 0 deletions src/application/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ mod module;
mod mount;
mod plan;
mod registration;
mod render;
mod resolve;
mod runtime_host;
mod topology;

Expand Down Expand Up @@ -48,6 +50,12 @@ pub use plan::{
DEPLOYMENT_PLAN_SCHEMA_VERSION, MAX_DEPLOYMENT_PLAN_BYTES,
};
pub use registration::{Application, ApplicationBuilder, ContractCompiler};
pub use render::{
normalize_resolved, render_resolved, NormalizedInventory, RenderTarget, RenderedFile,
};
pub use resolve::{
resolve_deployment, ResolvedDeployment, ResolvedProcess, RESOLVED_DEPLOYMENT_SCHEMA_VERSION,
};
pub use runtime_host::{bind_single_process, CapabilityProviders, RuntimeHost};
pub use topology::TopologyIntent;

Expand Down
Loading
Loading