diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index f14a098f5a..69cd9828b0 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -28,6 +28,9 @@ jobs: sh -c "$(curl --location https://raw.githubusercontent.com/F1bonacc1/process-compose/main/scripts/get-pc.sh)" -- -d -b ~/bin + - name: Install grpcurl + run: ./deployments/scripts/install-grpcurl + - name: Run the smoke test suite run: | export PATH="$HOME/bin:$PATH" diff --git a/Cargo.lock b/Cargo.lock index e16c7cd9f8..5b7d93ffe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4526,6 +4526,7 @@ version = "0.76.0-alpha.1" dependencies = [ "anyhow", "ark-ff", + "assert_cmd", "async-stream", "async-trait", "axum", @@ -4581,7 +4582,9 @@ dependencies = [ "penumbra-transaction", "pin-project", "pin-project-lite", + "predicates 2.1.5", "prost", + "prost-reflect", "prost-types", "rand 0.8.5", "rand_chacha 0.3.1", @@ -6369,6 +6372,17 @@ dependencies = [ "syn 2.0.51", ] +[[package]] +name = "prost-reflect" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5eec97d5d34bdd17ad2db2219aabf46b054c6c41bd5529767c9ce55be5898f" +dependencies = [ + "once_cell", + "prost", + "prost-types", +] + [[package]] name = "prost-types" version = "0.12.3" diff --git a/crates/bin/pd/Cargo.toml b/crates/bin/pd/Cargo.toml index e3a6e1c080..6c8e12451f 100644 --- a/crates/bin/pd/Cargo.toml +++ b/crates/bin/pd/Cargo.toml @@ -128,3 +128,6 @@ penumbra-proof-params = { workspace = true, features = [ "bundled-proving-keys", "download-proving-keys", ], default-features = true } +assert_cmd = { workspace = true } +predicates = "2.1" +prost-reflect = "0.13.1" diff --git a/crates/bin/pd/tests/network_integration.rs b/crates/bin/pd/tests/network_integration.rs index 17585ee39d..5449ce25a0 100644 --- a/crates/bin/pd/tests/network_integration.rs +++ b/crates/bin/pd/tests/network_integration.rs @@ -4,7 +4,12 @@ //! headers in all contexts. Does NOT evaluate application logic; see the //! integration tests for pcli/pclientd for that. +use assert_cmd::Command; use http::StatusCode; +use penumbra_proto::FILE_DESCRIPTOR_SET; +use predicates::prelude::*; +use prost_reflect::{DescriptorPool, ServiceDescriptor}; +use url::Url; #[ignore] #[tokio::test] @@ -39,3 +44,72 @@ async fn check_minifront_http_ok() -> anyhow::Result<()> { assert_eq!(r.status(), StatusCode::OK); Ok(()) } + +#[ignore] +#[tokio::test] +/// Validate that gRPC server reflection is enabled and working, by calling out +/// to `grpcurl` and verifying that it can view methods. See GH4392 for context. +async fn check_grpc_server_reflection() -> anyhow::Result<()> { + let pd_url: Url = std::env::var("PENUMBRA_NODE_PD_URL") + .unwrap_or("http://localhost:8080".to_string()) + .parse() + .unwrap(); + let pd_hostname = format!("{}:{}", pd_url.host().unwrap(), pd_url.port().unwrap()); + let mut args = Vec::::new(); + if pd_url.scheme() == "http" { + args.push("-plaintext".to_owned()); + } + args.push(pd_hostname); + // grpcurl takes `list` as a command, to inspect the server reflection API. + args.push("list".to_owned()); + + // Permit override of the fullpath to the `grpcurl` binary, in case we want + // to test multiple versions in CI. + let grpcurl_path = std::env::var("GRPCURL_PATH").unwrap_or("grpcurl".to_string()); + let std_cmd = std::process::Command::new(grpcurl_path); + let mut cmd = Command::from_std(std_cmd); + cmd.args(args); + + // Here we hardcode a few specific checks, to verify they're present. + // This ensures reflection is ostensibly working, and doesn't assume + // that the FILE_DESCRIPTOR tonic-build logic is wired up. + let methods = vec![ + "penumbra.core.app.v1.QueryService", + // "grpc.reflection.v1alpha.ServerReflection", + "grpc.reflection.v1.ServerReflection", + "ibc.core.channel.v1.Query", + ]; + for m in methods { + cmd.assert().stdout(predicate::str::contains(m)); + } + + // Here we look up the gRPC services exported from the proto crate, + // as FILE_DESCRIPTOR_SET. All of these methods should be visible + // to the `grpcurl` list command, if reflection is working. + let grpc_service_names = get_all_grpc_services()?; + // Sanity-check that we actually got results. + assert!(grpc_service_names.len() > 5); + for m in grpc_service_names { + cmd.assert().stdout(predicate::str::contains(m)); + } + Ok(()) +} + +/// Returns a Vec where each String is a fully qualified gRPC query service name, +/// such as: +/// +/// - penumbra.core.component.community_pool.v1.QueryService +/// - penumbra.view.v1.ViewService +/// - penumbra.core.component.dex.v1.SimulationService +/// +/// The gRPC service names are read from the [penumbra_proto] crate's [FILE_DESCRIPTOR_SET], +/// which is exported at build time. +fn get_all_grpc_services() -> anyhow::Result> { + // Intentionally verbose to be explicit. + let services: Vec = DescriptorPool::decode(FILE_DESCRIPTOR_SET)? + .services() + .into_iter() + .collect(); + let service_names: Vec = services.iter().map(|x| x.full_name().to_owned()).collect(); + Ok(service_names) +} diff --git a/deployments/scripts/install-grpcurl b/deployments/scripts/install-grpcurl new file mode 100755 index 0000000000..e2ff288552 --- /dev/null +++ b/deployments/scripts/install-grpcurl @@ -0,0 +1,34 @@ +#!/bin/bash +# Utility script to download a specific version of grpcurl for use +# in testing Penumbra, specifically in validating the gRPC reflection +# APIs via integration tests. +# Designed to be used in CI contexts, to bootstrap a testing setup quickly. +set -euo pipefail + + +# Sane defaults +GRPCURL_VERSION="${GRPCURL_VERSION:-1.9.1}" + +# Download and extract +grpcurl_download_url="https://github.com/fullstorydev/grpcurl/releases/download/v${GRPCURL_VERSION}/grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz" +grpcurl_temp_dir="$(mktemp -d)" +pushd "$grpcurl_temp_dir" > /dev/null +curl -sSfL -O "$grpcurl_download_url" +tar -xzf "grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz" grpcurl +trap 'rm -r "$grpcurl_temp_dir"' EXIT + +# Try to write to system-wide location. +if [[ -w /usr/local/bin/ ]] ; then + mv -v grpcurl /usr/local/bin/ +else + grpcurl_install_dir="${HOME:?}/bin" + >&2 echo "WARNING: /usr/local/bin/ not writable, installing grpcurl to $grpcurl_install_dir" + mkdir -p "$grpcurl_install_dir" + mv -v grpcurl "${grpcurl_install_dir}/" + export PATH="$PATH:$grpcurl_install_dir" +fi + +# Sanity checks +echo "Checking that grpcurl is installed:" +which grpcurl +grpcurl --version diff --git a/deployments/scripts/smoke-test.sh b/deployments/scripts/smoke-test.sh index 728332e91b..8fc78dbe61 100755 --- a/deployments/scripts/smoke-test.sh +++ b/deployments/scripts/smoke-test.sh @@ -23,6 +23,12 @@ if ! hash process-compose > /dev/null 2>&1 ; then exit 1 fi +if ! hash grpcurl > /dev/null 2>&1 ; then + >&2 echo "ERROR: grpcurl not found in PATH" + >&2 echo "Install it via https://github.com/fullstorydev/grpcurl/" + exit 1 +fi + # Check for interactive terminal session, enable TUI if yes. if [[ -t 1 ]] ; then use_tui="true"