From b50bbd20f24bb875fd6bee264f4533f90e469aad Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Fri, 30 Aug 2024 16:27:24 -0700 Subject: [PATCH] very messy WIP shows moderately useful graph --- Cargo.lock | 1 + api-manifest.toml | 118 +++++ dev-tools/xtask/Cargo.toml | 1 + dev-tools/xtask/src/ls_clients.rs | 780 ++++++++++++++++++++++-------- 4 files changed, 702 insertions(+), 198 deletions(-) create mode 100644 api-manifest.toml diff --git a/Cargo.lock b/Cargo.lock index b62594f6f0..a5e67adad2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12399,6 +12399,7 @@ dependencies = [ "fs-err", "macaddr", "omicron-zone-package", + "petgraph", "serde", "swrite", "tabled", diff --git a/api-manifest.toml b/api-manifest.toml new file mode 100644 index 0000000000..1230933086 --- /dev/null +++ b/api-manifest.toml @@ -0,0 +1,118 @@ +# Describes all the Dropshot/OpenAPI/Progenitor APIs in the system +# XXX-dap there's similar metadata in the openapi manager +# XXX-dap can server_component, group be determined from package metadata? + +extra_repos = [ "crucible", "maghemite", "propolis", "dendrite" ] + +[[apis]] +client_package_name = "bootstrap-agent-client" +label = "Bootstrap Agent" +server_package_name = "bootstrap-agent-api" +server_component = "omicron-sled-agent" +group = "Host OS" + +[[apis]] +client_package_name = "cockroach-admin-client" +label = "CockroachDB Cluster Admin" +server_package_name = "cockroach-admin-api" +server_component = "omicron-cockroach-admin" + +[[apis]] +client_package_name = "crucible-agent-client" +label = "Crucible Agent" +server_package_name = "crucible-agent" +server_component = "crucible-agent" + +[[apis]] +client_package_name = "crucible-pantry-client" +label = "Crucible Pantry" +server_package_name = "crucible-pantry" +server_component = "crucible-pantry" + +[[apis]] +client_package_name = "ddm-admin-client" +label = "Maghemite DDM Admin" +server_package_name = "ddmd" +server_component = "ddmd" +# XXX-dap confirm +group = "Host OS" + +[[apis]] +client_package_name = "dns-service-client" +label = "DNS Server" +server_package_name = "dns-server-api" +server_component = "dns-server" + +[[apis]] +client_package_name = "dpd-client" +label = "Dendrite DPD" +server_package_name = "dpd" +server_component = "dpd" +# XXX-dap confirm +group = "Host OS" + +[[apis]] +client_package_name = "gateway-client" +label = "Management Gateway Service" +server_package_name = "gateway-api" +server_component = "omicron-gateway" +# XXX-dap confirm +group = "Host OS" + +[[apis]] +client_package_name = "installinator-client" +label = "Wicketd Installinator" +server_package_name = "installinator-api" +server_component = "wicketd" +# XXX-dap confirm +group = "Host OS" + +[[apis]] +client_package_name = "mg-admin-client" +label = "Maghemite MG Admin" +server_package_name = "mgd" +server_component = "mgd" +# XXX-dap confirm +group = "Host OS" + +[[apis]] +client_package_name = "nexus-client" +label = "Nexus Internal API" +server_package_name = "nexus-internal-api" +server_component = "omicron-nexus" + +[[apis]] +client_package_name = "oxide-client" +label = "External API" +server_package_name = "nexus-external-api" +server_component = "omicron-nexus" + +[[apis]] +client_package_name = "oximeter-client" +label = "Oximeter" +server_package_name = "oximeter-api" +server_component = "oximeter-collector" + +[[apis]] +client_package_name = "propolis-client" +label = "Propolis" +server_package_name = "propolis-server" +server_component = "propolis-server" +# XXX-dap confirm +group = "Host OS" + +[[apis]] +client_package_name = "sled-agent-client" +label = "Sled Agent" +server_package_name = "sled-agent-api" +server_component = "omicron-sled-agent" +# XXX-dap confirm +group = "Host OS" + +[[apis]] +client_package_name = "wicketd-client" +label = "Wicketd" +server_package_name = "wicketd-api" +server_component = "wicketd" +# XXX-dap confirm +group = "Host OS" diff --git a/dev-tools/xtask/Cargo.toml b/dev-tools/xtask/Cargo.toml index ee82a35df7..7924132e40 100644 --- a/dev-tools/xtask/Cargo.toml +++ b/dev-tools/xtask/Cargo.toml @@ -31,6 +31,7 @@ clap.workspace = true fs-err.workspace = true macaddr.workspace = true omicron-zone-package.workspace = true +petgraph.workspace = true serde.workspace = true swrite.workspace = true tabled.workspace = true diff --git a/dev-tools/xtask/src/ls_clients.rs b/dev-tools/xtask/src/ls_clients.rs index f93d1b3047..dc13e752f1 100644 --- a/dev-tools/xtask/src/ls_clients.rs +++ b/dev-tools/xtask/src/ls_clients.rs @@ -11,82 +11,154 @@ use cargo_metadata::DependencyKind; use cargo_metadata::Metadata; use cargo_metadata::Package; use clap::Args; +use petgraph::dot::Dot; +use serde::Deserialize; +use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::fmt::Display; use url::Url; #[derive(Args)] pub struct LsClientsArgs { + #[arg(long)] + api_manifest: Option, + #[arg(long)] package_manifest: Option, + #[arg(long)] + extra_repos: Option, + #[arg(long)] adoc: bool, } pub fn run_cmd(args: LsClientsArgs) -> Result<()> { - // Load information about the Cargo workspace. - let workspace = crate::load_workspace()?; + let manifest_dir = Utf8PathBuf::from( + std::env::var("CARGO_MANIFEST_DIR") + .context("looking up CARGO_MANIFEST_DIR in environment")?, + ); - // Find all packages with a direct non-dev, non-build dependency on - // "progenitor". These generally ought to be suffixed with "-client". - let progenitor_clients = direct_dependents(&workspace, "progenitor")? + // First, read the metadata we have about APIs. + let api_manifest_path = args.api_manifest.clone().unwrap_or_else(|| { + manifest_dir.join("..").join("..").join("api-manifest.toml") + }); + let api_manifest: AllApiMetadata = toml::from_str( + &std::fs::read_to_string(&api_manifest_path).with_context(|| { + format!("read API manifest {:?}", &api_manifest_path) + })?, + ) + .with_context(|| format!("parse API manifest {:?}", &api_manifest_path))?; + + let apis_by_client_package: BTreeMap<_, _> = api_manifest + .apis .into_iter() - .filter_map(|mypkg| { - if mypkg.name.ends_with("-client") { - Some(ClientPackage::new(&workspace, mypkg)) - } else { - eprintln!("ignoring apparent non-client: {}", mypkg.name); - None + .map(|api| (api.client_package_name.clone(), api)) + .collect(); + + // Now, for each repo, load its metadata. Omicron is a little special since + // we're already in that repo. + let mut metadata_by_repo = BTreeMap::new(); + metadata_by_repo.insert( + String::from("omicron"), + Workspace::load("omicron", None) + .context("loading Omicron repo metadata")?, + ); + let extra_repos = args.extra_repos.clone().unwrap_or_else(|| { + manifest_dir.join("..").join("..").join("extra_repos") + }); + for repo in &api_manifest.extra_repos { + metadata_by_repo.insert( + repo.clone(), + Workspace::load(repo, Some(&extra_repos)) + .with_context(|| format!("load metadata for repo {}", repo))?, + ); + } + + let mut metadata_used = BTreeSet::new(); + for (_, workspace) in &metadata_by_repo { + println!("WORKSPACE: {}", workspace.name); + for (_, c) in &workspace.progenitor_clients { + let metadata = apis_by_client_package.get(&c.me.name); + if metadata.is_none() { + eprintln!( + "missing metadata for client package: {:#}", + c.me.name + ); } - }) - .collect::>>()?; - // Parse the package manifest. - let pkg_file = args - .package_manifest - .as_ref() - .map(|c| Ok::<_, anyhow::Error>(c.clone())) - .unwrap_or_else(|| { - Ok(Utf8PathBuf::from( - std::env::var("CARGO_MANIFEST_DIR") - .context("looking up CARGO_MANIFEST_DIR in environment")?, - ) - .join("..") - .join("..") - .join("package-manifest.toml")) - })?; - let pkgs = parse_packages(&pkg_file)?; - pkgs.dump(); + metadata_used.insert(c.me.name.clone()); + print_package(c, &args); + } + + println!(""); + } - for c in &progenitor_clients { - print_package(c, &args); + let clients_in_metadata = + apis_by_client_package.keys().cloned().collect::>(); + let metadata_extra = + clients_in_metadata.difference(&metadata_used).collect::>(); + for extra in metadata_extra { + eprintln!("unused metadata: {}", extra); } + // Parse the package manifest. + // let pkg_file = args + // .package_manifest + // .as_ref() + // .map(|c| Ok::<_, anyhow::Error>(c.clone())) + // .unwrap_or_else(|| { + // Ok(Utf8PathBuf::from( + // std::env::var("CARGO_MANIFEST_DIR") + // .context("looking up CARGO_MANIFEST_DIR in environment")?, + // ) + // .join("..") + // .join("..") + // .join("package-manifest.toml")) + // })?; + // let pkgs = parse_packages(&pkg_file)?; + // pkgs.dump(); + + // for c in &progenitor_clients { + // let metadata = apis_by_client_package.remove(c.me.name); + // if metadata.is_none() { + // eprintln!("missing metadata for client package: {:#}", c.me.name); + // } + // // print_package(c, &args); + // } + + // for (client_package, _) in apis_by_client_package { + // eprintln!("metadata matches no known package: {}", client_package); + // } + + let graph = make_graph( + &apis_by_client_package, + metadata_by_repo.values().collect(), + )?; + println!("{}", graph); + Ok(()) } -struct ClientPackage<'a> { - me: MyPackage<'a>, - rdeps: Vec>, +struct ClientPackage { + me: MyPackage, + rdeps: Vec, } -impl<'a> ClientPackage<'a> { - fn new( - workspace: &'a Metadata, - me: MyPackage<'a>, - ) -> Result> { +impl ClientPackage { + fn new(workspace: &Metadata, me: MyPackage) -> Result { let rdeps = direct_dependents(workspace, &me.name)?; Ok(ClientPackage { me, rdeps }) } } -struct MyPackage<'a> { - name: &'a str, - location: MyPackageLocation<'a>, +struct MyPackage { + name: String, + location: MyPackageLocation, } -impl<'a> MyPackage<'a> { - fn new(workspace: &'a Metadata, pkg: &'a Package) -> Result> { +impl MyPackage { + fn new(workspace: &Metadata, pkg: &Package) -> Result { // Figure out where this thing is. It's generally one of two places: // (1) In a remote repository. In that case, it will have a "source" // property that's the URL to a package. @@ -155,26 +227,29 @@ impl<'a> MyPackage<'a> { "unexpected manifest path for local package: {:?}", manifest_path ); - let path = relative_path.parent().ok_or_else(|| { - anyhow!( - "unexpected manifest path for local package: {:?}", - manifest_path - ) - })?; + let path = relative_path + .parent() + .ok_or_else(|| { + anyhow!( + "unexpected manifest path for local package: {:?}", + manifest_path + ) + })? + .to_owned(); MyPackageLocation::Omicron { path } }; - Ok(MyPackage { name: &pkg.name, location }) + Ok(MyPackage { name: pkg.name.clone(), location }) } } -enum MyPackageLocation<'a> { - Omicron { path: &'a Utf8Path }, +enum MyPackageLocation { + Omicron { path: Utf8PathBuf }, RemoteRepo { oxide_github_repo: String, path: Utf8PathBuf }, } -impl<'a> Display for MyPackageLocation<'a> { +impl Display for MyPackageLocation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MyPackageLocation::Omicron { path } => { @@ -211,10 +286,10 @@ fn source_repo_name(raw: &str) -> Result { Ok(path_segments[1].to_string()) } -fn direct_dependents<'a, 'b>( - workspace: &'a Metadata, - pkg_name: &'b str, -) -> Result>> { +fn direct_dependents( + workspace: &Metadata, + pkg_name: &str, +) -> Result> { workspace .packages .iter() @@ -236,169 +311,478 @@ fn direct_dependents<'a, 'b>( .collect() } -fn parse_packages(pkg_file: &Utf8Path) -> Result { - let contents = std::fs::read_to_string(pkg_file) - .with_context(|| format!("read package file {:?}", pkg_file))?; - let raw_packages = - toml::from_str::(&contents) - .with_context(|| format!("parse package file {:?}", pkg_file))?; - Ok(OmicronPackageConfig::from(raw_packages)) +// fn parse_packages(pkg_file: &Utf8Path) -> Result { +// let contents = std::fs::read_to_string(pkg_file) +// .with_context(|| format!("read package file {:?}", pkg_file))?; +// let raw_packages = +// toml::from_str::(&contents) +// .with_context(|| format!("parse package file {:?}", pkg_file))?; +// Ok(OmicronPackageConfig::from(raw_packages)) +// } +// +// struct OmicronPackageConfig { +// deployable_zones: Vec, +// dont_care: Vec<(OmicronPackage, &'static str)>, +// dont_know: Vec, +// } +// +// struct OmicronPackage { +// name: String, +// package: omicron_zone_package::package::Package, +// } +// +// impl From<(String, omicron_zone_package::package::Package)> for OmicronPackage { +// fn from( +// (pkgname, package): (String, omicron_zone_package::package::Package), +// ) -> Self { +// OmicronPackage { name: pkgname, package } +// } +// } +// +// impl From for OmicronPackageConfig { +// fn from(raw: omicron_zone_package::config::Config) -> Self { +// let mut deployable_zones = Vec::new(); +// let mut dont_care = Vec::new(); +// let mut dont_know = Vec::new(); +// for (pkgname, package) in raw.packages { +// let ompkg = OmicronPackage::from((pkgname, package)); +// +// match &ompkg.package.output { +// omicron_zone_package::package::PackageOutput::Zone { +// intermediate_only: true, +// } => { +// dont_care.push((ompkg, "marked intermediate-only")); +// } +// omicron_zone_package::package::PackageOutput::Zone { +// intermediate_only: false, +// } => { +// deployable_zones.push(ompkg); +// } +// omicron_zone_package::package::PackageOutput::Tarball => { +// dont_know.push(ompkg); +// } +// } +// } +// +// OmicronPackageConfig { deployable_zones, dont_care, dont_know } +// } +// } +// +// impl OmicronPackageConfig { +// pub fn dump(&self) { +// println!("deployable zones"); +// for ompkg in &self.deployable_zones { +// println!(" {}", ompkg.name); +// } +// println!(""); +// +// println!("stuff I think we can ignore"); +// for (ompkg, reason) in &self.dont_care { +// println!(" {}: {}", ompkg.name, reason); +// } +// println!(""); +// +// println!("stuff I'm not sure about yet"); +// for ompkg in &self.dont_know { +// println!(" {}", ompkg.name); +// } +// println!(""); +// } +// // pub fn dump(&self) { +// // for (pkgname, package) in &self.raw.packages { +// // print!("found Omicron package {:?}: ", pkgname); +// // match &package.source { +// // omicron_zone_package::package::PackageSource::Local { +// // blobs, +// // buildomat_blobs, +// // rust, +// // paths, +// // } => { +// // if rust.is_some() { +// // println!("rust package"); +// // } else { +// // println!(""); +// // } +// // +// // if let Some(blobs) = blobs { +// // println!(" blobs: ({})", blobs.len()); +// // for b in blobs { +// // println!(" {}", b); +// // } +// // } +// // +// // if let Some(buildomat_blobs) = blobs { +// // println!( +// // " buildomat blobs: ({})", +// // buildomat_blobs.len() +// // ); +// // for b in buildomat_blobs { +// // println!(" {}", b); +// // } +// // } +// // +// // if !paths.is_empty() { +// // println!(" plus some mapped paths: {}", paths.len()); +// // } +// // } +// // omicron_zone_package::package::PackageSource::Prebuilt { +// // repo, +// // commit, +// // sha256, +// // } => { +// // println!("prebuilt from repo: {repo}"); +// // } +// // omicron_zone_package::package::PackageSource::Composite { +// // packages, +// // } => { +// // println!( +// // "composite of: {}", +// // packages +// // .iter() +// // .map(|p| format!("{:?}", p)) +// // .collect::>() +// // .join(", ") +// // ); +// // } +// // omicron_zone_package::package::PackageSource::Manual => { +// // println!("ERROR: unsupported manual package"); +// // } +// // } +// // } +// // } +// } +// +fn print_package(p: &ClientPackage, args: &LsClientsArgs) { + if !args.adoc { + println!(" package: {} from {}", p.me.name, p.me.location); + for d in &p.rdeps { + println!(" consumer: {} from {}", d.name, d.location); + } + } else { + println!("|?"); + println!("|?"); + println!("|{}", p.me.location); + print!( + "|{}", + p.rdeps + .iter() + .map(|d| d.location.to_string()) + .collect::>() + .join(",\n ") + ); + println!("\n"); + } } -struct OmicronPackageConfig { - deployable_zones: Vec, - dont_care: Vec<(OmicronPackage, &'static str)>, - dont_know: Vec, +#[derive(Deserialize)] +struct AllApiMetadata { + extra_repos: Vec, + apis: Vec, } -struct OmicronPackage { - name: String, - package: omicron_zone_package::package::Package, +#[derive(Deserialize)] +struct ApiMetadata { + /// primary key for APIs is the client package name + client_package_name: String, + /// human-readable label for the API + label: String, + /// package name that the corresponding API lives in + // XXX-dap unused right now + server_package_name: String, + /// package name that the corresponding server lives in + server_component: String, + /// name of the unit of deployment + group: Option, } -impl From<(String, omicron_zone_package::package::Package)> for OmicronPackage { - fn from( - (pkgname, package): (String, omicron_zone_package::package::Package), - ) -> Self { - OmicronPackage { name: pkgname, package } +impl ApiMetadata { + fn group(&self) -> &str { + self.group.as_ref().unwrap_or_else(|| &self.server_component) } } -impl From for OmicronPackageConfig { - fn from(raw: omicron_zone_package::config::Config) -> Self { - let mut deployable_zones = Vec::new(); - let mut dont_care = Vec::new(); - let mut dont_know = Vec::new(); - for (pkgname, package) in raw.packages { - let ompkg = OmicronPackage::from((pkgname, package)); - - match &ompkg.package.output { - omicron_zone_package::package::PackageOutput::Zone { - intermediate_only: true, - } => { - dont_care.push((ompkg, "marked intermediate-only")); - } - omicron_zone_package::package::PackageOutput::Zone { - intermediate_only: false, - } => { - deployable_zones.push(ompkg); - } - omicron_zone_package::package::PackageOutput::Tarball => { - dont_know.push(ompkg); - } - } - } +struct Workspace { + name: String, + metadata: cargo_metadata::Metadata, + all_packages: BTreeMap, // XXX-dap memory usage + progenitor_clients: BTreeMap, +} + +impl Workspace { + pub fn load(name: &str, extra_repos: Option<&Utf8Path>) -> Result { + eprintln!("loading metadata for workspace {name}"); - OmicronPackageConfig { - deployable_zones, - dont_care, - dont_know, + let mut cmd = cargo_metadata::MetadataCommand::new(); + if let Some(extra_repos) = extra_repos { + cmd.manifest_path(extra_repos.join(name).join("Cargo.toml")); } + let metadata = cmd.exec().context("loading metadata")?; + + // Find all packages with a direct non-dev, non-build dependency on + // "progenitor". These generally ought to be suffixed with "-client". + let progenitor_clients = direct_dependents(&metadata, "progenitor")? + .into_iter() + .filter_map(|mypkg| { + if mypkg.name.ends_with("-client") { + Some(ClientPackage::new(&metadata, mypkg)) + } else { + eprintln!("ignoring apparent non-client: {}", mypkg.name); + None + } + }) + .collect::>>()? + .into_iter() + .map(|cpkg| (cpkg.me.name.to_owned(), cpkg)) + .collect(); + + let all_packages = metadata + .packages + .iter() + .map(|p| (p.name.clone(), p.clone())) + .collect(); + + Ok(Workspace { + name: name.to_owned(), + metadata, + all_packages, + progenitor_clients, + }) + } + + pub fn find_package(&self, pkgname: &str) -> Option<&Package> { + self.all_packages.get(pkgname) + } + + pub fn name(&self) -> &str { + &self.name } } -impl OmicronPackageConfig { - pub fn dump(&self) { - println!("deployable zones"); - for ompkg in &self.deployable_zones { - println!(" {}", ompkg.name); +// XXX-dap Okay, so now I have all the information I think I could want in +// memory. Which is: I have the metadata, plus the set of all packages +// referenced in all relevant workspaces. Now what do I do with it? +// +// In the end, my goal is to: +// +// - map "server" packages to top-level deployable components +// I can do this manually or I can presumably do it with the package manifest. +// - make a DAG +// - nodes: deployable components (could start with server packages) +// - edges: component depends on a client of another API +// +// To do this, I need to do ONE of the following: +// +// - for each server package, walk its dependencies recursively and note each of +// known client packages that it uses +// - for each client package, walk its reverse dependencies recursively and note +// each of the known server packages +// +// but this is tricky because: +// +// - client packages could have reverse dependencies in any repo, and I will +// likely find a lot of dups +// - server packages have dependencies in other repos, too +// - If we start from the server, the entire chain should be represented in +// the server's workspace's metadata +// - by contrast, if we start from the client, it may have reverse-deps in +// other workspaces and we have to try all of them +fn make_graph( + apis_by_client_package: &BTreeMap, + workspaces: Vec<&Workspace>, +) -> Result { + let mut graph = petgraph::graph::Graph::new(); + + // Some server components have more than one API in them. Deduplicate the + // server component names. We'll iterate these to compute relationships. + let server_component_groups: BTreeMap<_, _> = apis_by_client_package + .values() + .map(|api| (&api.server_component, api.group())) + .collect(); + + // Identify the distinct units of deployment. Create a graph node for each + // one. + let deployment_unit_names: BTreeSet<_> = + apis_by_client_package.values().map(|api| api.group()).collect(); + let nodes: BTreeMap<_, _> = deployment_unit_names + .into_iter() + .map(|name| (name, graph.add_node(name))) + .collect(); + + for (server_pkgname, group) in &server_component_groups { + // Figure out which workspace has this package. + // XXX-dap make some data structures maybe? + let found_in_workspaces: Vec<_> = workspaces + .iter() + .filter_map(|w| w.find_package(&server_pkgname).map(|p| (w, p))) + .collect(); + if found_in_workspaces.is_empty() { + eprintln!( + "error: server package {:?} was not found in any workspace", + server_pkgname + ); + // XXX-dap exit non-zero + continue; } - println!(""); - println!("stuff I think we can ignore"); - for (ompkg, reason) in &self.dont_care { - println!(" {}: {}", ompkg.name, reason); + if found_in_workspaces.len() > 1 { + eprintln!( + "error: server package {:?} was found in more than one \ + workspace: {}", + server_pkgname, + found_in_workspaces + .into_iter() + .map(|(w, _)| w.name()) + .collect::>() + .join(", ") + ); + // XXX-dap exit non-zero + continue; } - println!(""); - println!("stuff I'm not sure about yet"); - for ompkg in &self.dont_know { - println!(" {}", ompkg.name); + let (found_workspace, found_server_package) = found_in_workspaces[0]; + eprintln!( + "server package {} found in repo {}", + server_pkgname, + found_workspace.name(), + ); + + // Walk the server package's dependencies recursively, keeping track of + // known clients used. + let mut clients_used = BTreeSet::new(); + walk_required_deps_recursively( + &found_workspace, + &found_server_package, + &mut |parent: &Package, p: &Package| { + // TODO + // omicron_common depends on mg-admin-client solely to impl + // some `From` conversions. That makes it look like just about + // everything depends on mg-admin-client, which isn't true. We + // should consider reversing this, since most clients put those + // conversions into the client rather than omicron_common. But + // for now, let's just ignore this particular dependency. + if p.name == "mg-admin-client" + && parent.name == "omicron-common" + { + return; + } + + // TODO internal-dns depends on dns-service-client to use its + // types. They're only used when *configuring* DNS, which is + // only done in a couple of components. But many components use + // internal-dns to *read* DNS. So like above, this makes it + // look like everything uses the DNS server API, but that's not + // true. We should consider splitting this crate in two. But + // for now, just ignore the specific dependency from + // internal-dns to dns-service-client. If a consumer actually + // calls the DNS server, it will have a separate dependency. + if p.name == "dns-service-client" + && (parent.name == "internal-dns") + { + return; + } + + // TODO nexus-types depends on dns-service-client and + // gateway-client for defining some types, but again, this + // doesn't mean that somebody using nexus-types is actually + // calling out to these services. If they were, they'd need to + // have some other dependency on them. + if parent.name == "nexus-types" + && (p.name == "dns-service-client" + || p.name == "gateway-client") + { + return; + } + + // TODO + // This one's debatable. Everything with an Oximeter producer + // talks to Nexus. But let's ignore those for now. + // Maybe we could improve this by creating a separate API inside + // Nexus for this? + if parent.name == "oximeter-producer" + && p.name == "nexus-client" + { + eprintln!( + "warning: ignoring legit dependency from \ + oximeter-producer -> nexus_client" + ); + return; + } + + if apis_by_client_package.contains_key(&p.name) { + clients_used.insert(p.name.clone()); + } + }, + ) + .with_context(|| { + format!( + "iterating dependencies of workspace {:?} package {:?}", + found_workspace.name(), + server_pkgname + ) + })?; + + // unwrap(): we created a node for every group above. + let my_node = nodes.get(group).unwrap(); + + println!("server package {}:", server_pkgname); + for c in &clients_used { + println!(" uses client {}", c); + + // unwrap(): The values in "clients_used" were populated above after + // checking whether they were in `apis_by_client_package` already. + let other_api = apis_by_client_package.get(c).unwrap(); + // unwrap(): We created nodes for all of the groups up front. + let other_node = nodes.get(other_api.group()).unwrap(); + + graph.add_edge( + *my_node, + *other_node, + &other_api.client_package_name, + ); } println!(""); } - // pub fn dump(&self) { - // for (pkgname, package) in &self.raw.packages { - // print!("found Omicron package {:?}: ", pkgname); - // match &package.source { - // omicron_zone_package::package::PackageSource::Local { - // blobs, - // buildomat_blobs, - // rust, - // paths, - // } => { - // if rust.is_some() { - // println!("rust package"); - // } else { - // println!(""); - // } - // - // if let Some(blobs) = blobs { - // println!(" blobs: ({})", blobs.len()); - // for b in blobs { - // println!(" {}", b); - // } - // } - // - // if let Some(buildomat_blobs) = blobs { - // println!( - // " buildomat blobs: ({})", - // buildomat_blobs.len() - // ); - // for b in buildomat_blobs { - // println!(" {}", b); - // } - // } - // - // if !paths.is_empty() { - // println!(" plus some mapped paths: {}", paths.len()); - // } - // } - // omicron_zone_package::package::PackageSource::Prebuilt { - // repo, - // commit, - // sha256, - // } => { - // println!("prebuilt from repo: {repo}"); - // } - // omicron_zone_package::package::PackageSource::Composite { - // packages, - // } => { - // println!( - // "composite of: {}", - // packages - // .iter() - // .map(|p| format!("{:?}", p)) - // .collect::>() - // .join(", ") - // ); - // } - // omicron_zone_package::package::PackageSource::Manual => { - // println!("ERROR: unsupported manual package"); - // } - // } - // } - // } + + Ok(Dot::new(&graph).to_string()) } -fn print_package(p: &ClientPackage<'_>, args: &LsClientsArgs) { - if !args.adoc { - println!("package: {} from {}", p.me.name, p.me.location); - for d in &p.rdeps { - println!(" consumer: {} from {}", d.name, d.location); +fn walk_required_deps_recursively( + workspace: &Workspace, + root: &Package, + func: &mut dyn FnMut(&Package, &Package), +) -> Result<()> { + let mut remaining = vec![root]; + let mut seen: BTreeSet = BTreeSet::new(); + + while let Some(next) = remaining.pop() { + for d in &next.dependencies { + if seen.contains(&d.name) { + continue; + } + + seen.insert(d.name.clone()); + + if d.optional { + continue; + } + + if !matches!(d.kind, DependencyKind::Normal | DependencyKind::Build) + { + continue; + } + + let pkg = workspace.find_package(&d.name).ok_or_else(|| { + anyhow!( + "package {:?} has dependency {:?} not in workspace \ + metadata", + next.name, + d.name + ) + })?; + func(next, pkg); + remaining.push(pkg); } - } else { - println!("|?"); - println!("|?"); - println!("|{}", p.me.location); - print!( - "|{}", - p.rdeps - .iter() - .map(|d| d.location.to_string()) - .collect::>() - .join(",\n ") - ); - println!("\n"); } + + Ok(()) }