diff --git a/Cargo.lock b/Cargo.lock index e75d30481f1cc..dc61fafcb5b8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13520,7 +13520,7 @@ dependencies = [ [[package]] name = "sui-graphql-rpc" -version = "2024.7.0" +version = "1.33.0" dependencies = [ "anyhow", "async-graphql", diff --git a/crates/sui-graphql-e2e-tests/tests/call/simple.exp b/crates/sui-graphql-e2e-tests/tests/call/simple.exp index c9fc964fa9eb3..f3fd3364ecf81 100644 --- a/crates/sui-graphql-e2e-tests/tests/call/simple.exp +++ b/crates/sui-graphql-e2e-tests/tests/call/simple.exp @@ -1,4 +1,4 @@ -processed 26 tasks +processed 25 tasks init: validator_0: object(0,0) @@ -114,11 +114,11 @@ task 15, lines 63-68: Headers: { "content-type": "application/json", "content-length": "157", - "x-sui-rpc-version": "2024.7.0-testing-no-sha", + "x-sui-rpc-version": "42.43.44-testing-no-sha", "vary": "origin, access-control-request-method, access-control-request-headers", "access-control-allow-origin": "*", } -Service version: 2024.7.0-testing-no-sha +Service version: 42.43.44-testing-no-sha Response: { "data": { "checkpoint": { @@ -314,15 +314,3 @@ task 24, line 181: created: object(24,0) mutated: object(0,1) gas summary: computation_cost: 235000, storage_cost: 2302800, storage_rebate: 978120, non_refundable_storage_fee: 9880 - -task 25, lines 183-188: -//# run-graphql -Response: { - "data": { - "serviceConfig": { - "availableVersions": [ - "2024.7" - ] - } - } -} diff --git a/crates/sui-graphql-e2e-tests/tests/call/simple.move b/crates/sui-graphql-e2e-tests/tests/call/simple.move index 6f95ee2072da3..4b68daee1d41b 100644 --- a/crates/sui-graphql-e2e-tests/tests/call/simple.move +++ b/crates/sui-graphql-e2e-tests/tests/call/simple.move @@ -179,10 +179,3 @@ module Test::M1 { //# run Test::M1::create --args 0 @A --gas-price 1000 //# run Test::M1::create --args 0 @A --gas-price 235 - -//# run-graphql -{ - serviceConfig { - availableVersions - } -} diff --git a/crates/sui-graphql-rpc/Cargo.toml b/crates/sui-graphql-rpc/Cargo.toml index bbc3993caa2e4..43873a3adde83 100644 --- a/crates/sui-graphql-rpc/Cargo.toml +++ b/crates/sui-graphql-rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sui-graphql-rpc" -version = "2024.7.0" +version.workspace = true authors = ["Mysten Labs "] license = "Apache-2.0" publish = false diff --git a/crates/sui-graphql-rpc/schema.graphql b/crates/sui-graphql-rpc/schema.graphql index a04042877ddfd..b848cd8529973 100644 --- a/crates/sui-graphql-rpc/schema.graphql +++ b/crates/sui-graphql-rpc/schema.graphql @@ -3470,10 +3470,6 @@ type ServiceConfig { """ isEnabled(feature: Feature!): Boolean! """ - List the available versions for this GraphQL service. - """ - availableVersions: [String!]! - """ List of all features that are enabled on this GraphQL service. """ enabledFeatures: [Feature!]! diff --git a/crates/sui-graphql-rpc/src/config.rs b/crates/sui-graphql-rpc/src/config.rs index c61e87fec45db..b4e1f87436720 100644 --- a/crates/sui-graphql-rpc/src/config.rs +++ b/crates/sui-graphql-rpc/src/config.rs @@ -60,7 +60,6 @@ pub struct ConnectionConfig { #[GraphQLConfig] #[derive(Default)] pub struct ServiceConfig { - pub versions: Versions, pub limits: Limits, pub disabled_features: BTreeSet, pub experiments: Experiments, @@ -70,11 +69,6 @@ pub struct ServiceConfig { pub move_registry: MoveRegistryConfig, } -#[GraphQLConfig] -pub struct Versions { - versions: Vec, -} - #[GraphQLConfig] pub struct Limits { /// Maximum depth of nodes in the requests. @@ -148,17 +142,18 @@ pub(crate) enum ResolutionType { /// The `full` version is `year.month.patch-sha`. #[derive(Copy, Clone, Debug)] pub struct Version { - /// The year of this release. - pub year: &'static str, - /// The month of this release. - pub month: &'static str, - /// The patch is a positive number incremented for every compatible release on top of the major.month release. + /// The major version for the release + pub major: &'static str, + /// The minor version of the release + pub minor: &'static str, + /// The patch version of the release pub patch: &'static str, - /// The commit sha for this release. + /// The full commit SHA that the release was built from pub sha: &'static str, - /// The full version string. - /// Note that this extra field is used only for the uptime_metric function which requries a - /// &'static str. + /// The full version string: {MAJOR}.{MINOR}.{PATCH}-{SHA} + /// + /// The full version is pre-computed as a &'static str because that is what is required for + /// `uptime_metric`. pub full: &'static str, } @@ -166,19 +161,12 @@ impl Version { /// Use for testing when you need the Version obj and a year.month &str pub fn for_testing() -> Self { Self { - year: env!("CARGO_PKG_VERSION_MAJOR"), - month: env!("CARGO_PKG_VERSION_MINOR"), - patch: env!("CARGO_PKG_VERSION_PATCH"), + major: "42", + minor: "43", + patch: "44", sha: "testing-no-sha", // note that this full field is needed for metrics but not for testing - full: const_str::concat!( - env!("CARGO_PKG_VERSION_MAJOR"), - ".", - env!("CARGO_PKG_VERSION_MINOR"), - ".", - env!("CARGO_PKG_VERSION_PATCH"), - "-testing-no-sha" - ), + full: "42.43.44-testing-no-sha", } } } @@ -230,11 +218,6 @@ impl ServiceConfig { !self.disabled_features.contains(&feature) } - /// List the available versions for this GraphQL service. - async fn available_versions(&self) -> Vec { - self.versions.versions.clone() - } - /// List of all features that are enabled on this GraphQL service. async fn enabled_features(&self) -> Vec { FunctionalGroup::all() @@ -504,18 +487,6 @@ impl MoveRegistryConfig { } } -impl Default for Versions { - fn default() -> Self { - Self { - versions: vec![format!( - "{}.{}", - env!("CARGO_PKG_VERSION_MAJOR"), - env!("CARGO_PKG_VERSION_MINOR") - )], - } - } -} - impl Default for Ide { fn default() -> Self { Self { diff --git a/crates/sui-graphql-rpc/src/error.rs b/crates/sui-graphql-rpc/src/error.rs index eefe9b696b0a1..8ef133f94dba8 100644 --- a/crates/sui-graphql-rpc/src/error.rs +++ b/crates/sui-graphql-rpc/src/error.rs @@ -1,8 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use async_graphql::{ErrorExtensionValues, ErrorExtensions, Pos, Response, ServerError}; -use async_graphql_axum::GraphQLResponse; +use async_graphql::{ErrorExtensionValues, ErrorExtensions, Pos, ServerError}; use sui_indexer::errors::IndexerError; use sui_json_rpc::name_service::NameServiceError; @@ -12,24 +11,12 @@ use crate::types::dot_move::error::MoveRegistryError; /// GraphQL. /// `` pub(crate) mod code { - pub const BAD_REQUEST: &str = "BAD_REQUEST"; pub const BAD_USER_INPUT: &str = "BAD_USER_INPUT"; pub const INTERNAL_SERVER_ERROR: &str = "INTERNAL_SERVER_ERROR"; pub const REQUEST_TIMEOUT: &str = "REQUEST_TIMEOUT"; pub const UNKNOWN: &str = "UNKNOWN"; } -/// Create a GraphQL Response containing an Error. -/// -/// Most errors produced by the service will automatically be wrapped in a `GraphQLResponse`, -/// because they will originate from within the GraphQL implementation. This function is intended -/// for errors that originated from outside of GraphQL (such as in middleware), but that need to be -/// ingested by GraphQL clients. -pub(crate) fn graphql_error_response(code: &str, message: impl Into) -> GraphQLResponse { - let error = graphql_error(code, message); - Response::from_errors(error.into()).into() -} - /// Create a generic GraphQL Server Error. /// /// This error has no path, source, or locations, just a message and an error code. diff --git a/crates/sui-graphql-rpc/src/main.rs b/crates/sui-graphql-rpc/src/main.rs index cedc55b39e72a..7dde900b1d86c 100644 --- a/crates/sui-graphql-rpc/src/main.rs +++ b/crates/sui-graphql-rpc/src/main.rs @@ -18,8 +18,8 @@ bin_version::git_revision!(); // VERSION mimics what other sui binaries use for the same const static VERSION: Version = Version { - year: env!("CARGO_PKG_VERSION_MAJOR"), - month: env!("CARGO_PKG_VERSION_MINOR"), + major: env!("CARGO_PKG_VERSION_MAJOR"), + minor: env!("CARGO_PKG_VERSION_MINOR"), patch: env!("CARGO_PKG_VERSION_PATCH"), sha: GIT_REVISION, full: const_str::concat!( diff --git a/crates/sui-graphql-rpc/src/server/builder.rs b/crates/sui-graphql-rpc/src/server/builder.rs index cdcd59f208259..870682f3efefd 100644 --- a/crates/sui-graphql-rpc/src/server/builder.rs +++ b/crates/sui-graphql-rpc/src/server/builder.rs @@ -30,7 +30,7 @@ use crate::{ query_limits_checker::{PayloadSize, QueryLimitsChecker, ShowUsage}, timeout::Timeout, }, - server::version::{check_version_middleware, set_version_middleware}, + server::version::set_version_middleware, types::query::{Query, SuiGraphQLSchema}, }; use async_graphql::extensions::ApolloTracing; @@ -351,10 +351,6 @@ impl ServerBuilder { state.version, set_version_middleware, )) - .route_layer(middleware::from_fn_with_state( - state.version, - check_version_middleware, - )) .layer(axum::extract::Extension(schema)) .layer(axum::extract::Extension(watermark_task.lock())) .layer(axum::extract::Extension(watermark_task.chain_id_lock())) diff --git a/crates/sui-graphql-rpc/src/server/version.rs b/crates/sui-graphql-rpc/src/server/version.rs index b73bc9b9d215d..6c467df671bc7 100644 --- a/crates/sui-graphql-rpc/src/server/version.rs +++ b/crates/sui-graphql-rpc/src/server/version.rs @@ -3,94 +3,16 @@ use axum::{ body::Body, - extract::{Path, State}, - http::{HeaderName, HeaderValue, Request, StatusCode}, + extract::State, + http::{HeaderName, HeaderValue, Request}, middleware::Next, - response::{IntoResponse, Response}, + response::Response, }; -use axum_extra::headers; -use crate::{ - config::Version, - error::{code, graphql_error_response}, -}; +use crate::config::Version; pub(crate) static VERSION_HEADER: HeaderName = HeaderName::from_static("x-sui-rpc-version"); -#[allow(unused)] -pub(crate) struct SuiRpcVersion(Vec, Vec>); -const NAMED_VERSIONS: [&str; 3] = ["beta", "legacy", "stable"]; - -impl headers::Header for SuiRpcVersion { - fn name() -> &'static HeaderName { - &VERSION_HEADER - } - - fn decode<'i, I>(values: &mut I) -> Result - where - I: Iterator, - { - let mut values = values.map(|v| v.as_bytes().to_owned()); - let Some(value) = values.next() else { - // No values for this header -- it doesn't exist. - return Err(headers::Error::invalid()); - }; - - // Extract the header values as bytes. Distinguish the first value as we expect there to be - // just one under normal operation. Do not attempt to parse the value, as a header parsing - // failure produces a generic error. - Ok(SuiRpcVersion(value, values.collect())) - } - - fn encode>(&self, _values: &mut E) { - unimplemented!() - } -} - -/// Middleware to check for the existence of a version constraint in the request header, and confirm -/// that this instance of the RPC matches that version constraint. Each RPC instance only supports -/// one version of the RPC software, and it is the responsibility of the load balancer to make sure -/// version constraints are met. -pub(crate) async fn check_version_middleware( - version: Option>, - State(service_version): State, - request: Request, - next: Next, -) -> Response { - let Some(Path(version)) = version else { - return next.run(request).await; - }; - - if NAMED_VERSIONS.contains(&version.as_str()) || version.is_empty() { - return next.run(request).await; - } - let Some((year, month)) = parse_version(&version) else { - return ( - StatusCode::BAD_REQUEST, - graphql_error_response( - code::BAD_REQUEST, - format!( - "Failed to parse version path: {version}. Expected either a `beta | legacy | stable` \ - version or . version.", - ), - ), - ) - .into_response(); - }; - - if year != service_version.year || month != service_version.month { - return ( - StatusCode::MISDIRECTED_REQUEST, - graphql_error_response( - code::INTERNAL_SERVER_ERROR, - format!("Version '{version}' not supported."), - ), - ) - .into_response(); - } - next.run(request).await -} - /// Mark every outgoing response with a header indicating the precise version of the RPC that was /// used (including the patch version and sha). pub(crate) async fn set_version_middleware( @@ -107,25 +29,6 @@ pub(crate) async fn set_version_middleware( response } -/// Split a `version` string into two parts (year and month) separated by a ".". -/// -/// Confirms that the version specifier contains exactly two components, and that both -/// components are entirely comprised of digits. -fn parse_version(version: &str) -> Option<(&str, &str)> { - let mut parts = version.split('.'); - let year = parts.next()?; - let month = parts.next()?; - - if year.is_empty() || month.is_empty() { - return None; - } - - (parts.next().is_none() - && year.chars().all(|c| c.is_ascii_digit()) - && month.chars().all(|c| c.is_ascii_digit())) - .then_some((year, month)) -} - #[cfg(test)] mod tests { use std::net::SocketAddr; @@ -137,7 +40,7 @@ mod tests { server::builder::AppState, }; use axum::{body::Body, middleware, routing::get, Router}; - use expect_test::expect; + use http::StatusCode; use mysten_metrics; use tokio_util::sync::CancellationToken; use tower::ServiceExt; @@ -147,6 +50,7 @@ mod tests { let registry = mysten_metrics::start_prometheus_server(binding_address).default_registry(); Metrics::new(®istry) } + fn service() -> Router { let version = Version::for_testing(); let metrics = metrics(); @@ -163,13 +67,7 @@ mod tests { Router::new() .route("/", get(|| async { "Hello, Versioning!" })) - .route("/:version", get(|| async { "Hello, Versioning!" })) .route("/graphql", get(|| async { "Hello, Versioning!" })) - .route("/graphql/:version", get(|| async { "Hello, Versioning!" })) - .layer(middleware::from_fn_with_state( - state.version, - check_version_middleware, - )) .layer(middleware::from_fn_with_state( state.version, set_version_middleware, @@ -187,40 +85,6 @@ mod tests { Request::builder().uri("/").body(Body::empty()).unwrap() } - fn version_request(version: &str) -> Request { - if version.is_empty() { - return plain_request(); - } - Request::builder() - .uri(format!("/graphql/{}", version)) - .body(Body::empty()) - .unwrap() - } - - async fn response_body(response: Response) -> String { - let bytes = axum::body::to_bytes(response.into_body(), usize::MAX) - .await - .unwrap(); - let value: serde_json::Value = serde_json::from_slice(bytes.as_ref()).unwrap(); - serde_json::to_string_pretty(&value).unwrap() - } - - #[tokio::test] - async fn successful() { - let version = Version::for_testing(); - let major_version = format!("{}.{}", version.year, version.month); - let service = service(); - let response = service - .oneshot(version_request(&major_version)) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(&VERSION_HEADER), - Some(&HeaderValue::from_static(version.full)) - ); - } - #[tokio::test] async fn default_graphql_route() { let version = Version::for_testing(); @@ -234,25 +98,7 @@ mod tests { } #[tokio::test] - async fn named_version() { - let version = Version::for_testing(); - let service = service(); - for named_version in NAMED_VERSIONS { - let response = service - .clone() - .oneshot(version_request(named_version)) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(&VERSION_HEADER), - Some(&HeaderValue::from_static(version.full)) - ); - } - } - - #[tokio::test] - async fn default_version() { + async fn default_plain_route() { let version = Version::for_testing(); let service = service(); let response = service.oneshot(plain_request()).await.unwrap(); @@ -262,72 +108,4 @@ mod tests { Some(&HeaderValue::from_static(version.full)) ); } - - #[tokio::test] - async fn wrong_path() { - let version = Version::for_testing(); - let service = service(); - let response = service.oneshot(version_request("")).await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(&VERSION_HEADER), - Some(&HeaderValue::from_static(version.full)) - ); - } - - #[tokio::test] - async fn incompatible_version() { - let version = Version::for_testing(); - let service = service(); - let response = service.oneshot(version_request("0.0")).await.unwrap(); - - assert_eq!(response.status(), StatusCode::MISDIRECTED_REQUEST); - assert_eq!( - response.headers().get(&VERSION_HEADER), - Some(&HeaderValue::from_static(version.full)) - ); - - let expect = expect![[r#" - { - "data": null, - "errors": [ - { - "message": "Version '0.0' not supported.", - "extensions": { - "code": "INTERNAL_SERVER_ERROR" - } - } - ] - }"#]]; - expect.assert_eq(&response_body(response).await); - } - - #[tokio::test] - async fn not_a_version() { - let version = Version::for_testing(); - let service = service(); - let response = service - .oneshot(version_request("not-a-version")) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.headers().get(&VERSION_HEADER), - Some(&HeaderValue::from_static(version.full)) - ); - - let expect = expect![[r#" - { - "data": null, - "errors": [ - { - "message": "Failed to parse version path: not-a-version. Expected either a `beta | legacy | stable` version or . version.", - "extensions": { - "code": "BAD_REQUEST" - } - } - ] - }"#]]; - expect.assert_eq(&response_body(response).await); - } } diff --git a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap index fbb4d549cb1b6..7b781d6e1de02 100644 --- a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap +++ b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap @@ -3474,10 +3474,6 @@ type ServiceConfig { """ isEnabled(feature: Feature!): Boolean! """ - List the available versions for this GraphQL service. - """ - availableVersions: [String!]! - """ List of all features that are enabled on this GraphQL service. """ enabledFeatures: [Feature!]! @@ -4734,3 +4730,4 @@ schema { query: Query mutation: Mutation } +