diff --git a/src/control-plane-services/function-autoscaler/AGENTS.md b/src/control-plane-services/function-autoscaler/AGENTS.md index 9d0ceeced..63d665b9a 100644 --- a/src/control-plane-services/function-autoscaler/AGENTS.md +++ b/src/control-plane-services/function-autoscaler/AGENTS.md @@ -46,6 +46,14 @@ cargo test -p rs-autoscaler cargo deny check advisories ``` +`crates/server` depends on `//src/libraries/rust/nvcf-info` as a Bazel target +only, so the cargo commands above do not resolve it. Build and test that crate +with Bazel from the monorepo root: + +```bash +bazel test //src/control-plane-services/function-autoscaler/crates/server:all +``` + CI subproject id: `function-autoscaler`. Native Bazel validation and release wiring live in `tools/ci/subproject-validations.yaml`, an internal GitLab CI config not present in this public snapshot. diff --git a/src/control-plane-services/function-autoscaler/crates/server/BUILD.bazel b/src/control-plane-services/function-autoscaler/crates/server/BUILD.bazel index 34d048319..9ddd7e5ea 100644 --- a/src/control-plane-services/function-autoscaler/crates/server/BUILD.bazel +++ b/src/control-plane-services/function-autoscaler/crates/server/BUILD.bazel @@ -8,19 +8,30 @@ load("@rules_rust//cargo:defs.bzl", "cargo_build_script") load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") load("//rules/oci:defs.bzl", "rust_oci_image") -# Stamp the real release version into the binary so CARGO_PKG_VERSION (used by -# `--version`, startup logs, and the OTel service.version resource attribute) -# reflects the released image tag instead of the placeholder 0.0.0-dev. The +# Stamp the real release version and commit into the binary so +# CARGO_PKG_VERSION (used by `--version`, startup logs, the OTel +# service.version resource attribute, and the GET /info handler) reflects the +# released image tag instead of the placeholder 0.0.0-dev, and NVCF_GIT_COMMIT +# (consumed by GET /info) reflects the real commit instead of "unknown". The # legacy Dockerfile did this via CICD_NEXT_VERSION -> build.rs; the Bazel build -# dropped it. tools/workspace_status.sh emits STABLE_VERSION; expand_template -# substitutes it on --stamp (release) builds and falls back to 0.0.0-dev -# otherwise. Fed to the Rust targets via rustc_env_files. +# dropped it. tools/workspace_status.sh emits STABLE_VERSION/STABLE_GIT_COMMIT_FULL; +# expand_template substitutes them on --stamp (release) builds and falls back +# to dev defaults otherwise. Fed to the Rust targets via rustc_env_files. expand_template( name = "version_env", out = "version.env", - stamp_substitutions = {"{VERSION}": "{{STABLE_VERSION}}"}, - substitutions = {"{VERSION}": "0.0.0-dev"}, - template = ["CARGO_PKG_VERSION={VERSION}"], + stamp_substitutions = { + "{VERSION}": "{{STABLE_VERSION}}", + "{GIT_COMMIT}": "{{STABLE_GIT_COMMIT_FULL}}", + }, + substitutions = { + "{VERSION}": "0.0.0-dev", + "{GIT_COMMIT}": "unknown", + }, + template = [ + "CARGO_PKG_VERSION={VERSION}", + "NVCF_GIT_COMMIT={GIT_COMMIT}", + ], ) # build.rs uses tonic-build + prost-build to compile crates/server/proto/nvcf.proto. @@ -61,7 +72,10 @@ rust_library( proc_macro_deps = all_crate_deps(proc_macro = True), rustc_env_files = [":version_env"], visibility = ["//visibility:public"], - deps = [":build_script"] + all_crate_deps(normal = True), + deps = [ + ":build_script", + "//src/libraries/rust/nvcf-info:nvcf-info-autoscaler", + ] + all_crate_deps(normal = True), ) rust_binary( diff --git a/src/control-plane-services/function-autoscaler/crates/server/src/routes/mod.rs b/src/control-plane-services/function-autoscaler/crates/server/src/routes/mod.rs index e9330c28b..aef05e7d0 100644 --- a/src/control-plane-services/function-autoscaler/crates/server/src/routes/mod.rs +++ b/src/control-plane-services/function-autoscaler/crates/server/src/routes/mod.rs @@ -53,6 +53,12 @@ impl From for ComponentHealthResponse { } } +/// Build metadata. Version and commit are stamped by the version_env template +/// in crates/server/BUILD.bazel on --stamp builds. +pub async fn get_info() -> Json { + Json(nvcf_info::info_response!("nvcf-function-autoscaler")) +} + /// Liveness: process is alive. No dependency checks. /// Used by Kubernetes liveness probe. Failure causes pod restart. /// We intentionally do not check TimeseriesDb/Cassandra here — restarting when they're @@ -124,6 +130,17 @@ mod tests { assert_eq!(result.unwrap_err(), StatusCode::SERVICE_UNAVAILABLE); } + #[tokio::test] + async fn info_reports_service_version_and_commit() { + let Json(info) = get_info().await; + assert_eq!(info.service, "nvcf-function-autoscaler"); + // Stamped only on Bazel --stamp builds; under cargo these are the + // unstamped fallbacks. Assert they are populated, not their literals, + // so the test does not break on every release bump. + assert!(!info.version.is_empty()); + assert!(!info.commit.is_empty()); + } + #[tokio::test] async fn readiness_returns_200_once_all_components_healthy() { let health = Arc::new(Health::new()); diff --git a/src/control-plane-services/function-autoscaler/crates/server/src/server.rs b/src/control-plane-services/function-autoscaler/crates/server/src/server.rs index 7c99a18f2..d8f9c9730 100644 --- a/src/control-plane-services/function-autoscaler/crates/server/src/server.rs +++ b/src/control-plane-services/function-autoscaler/crates/server/src/server.rs @@ -329,6 +329,7 @@ async fn main() -> Result<(), Box> { // Main app on 8080 let app = Router::new() + .route("/info", get(routes::get_info)) .route("/health", get(routes::get_health)) .route("/admin/health/liveness", get(routes::get_liveness)) .route("/admin/health/readiness", get(routes::get_readiness)) diff --git a/src/invocation-plane-services/http-invocation/AGENTS.md b/src/invocation-plane-services/http-invocation/AGENTS.md index cde174b8e..5ad108aa5 100644 --- a/src/invocation-plane-services/http-invocation/AGENTS.md +++ b/src/invocation-plane-services/http-invocation/AGENTS.md @@ -44,6 +44,10 @@ cargo run --package nvcf-invocation-service --bin server --release -- \ The staging-local config connects to staging NVCF. Keep secrets and bearer tokens out of committed config and examples. +`crates/server` depends on `//src/libraries/rust/nvcf-info` as a Bazel target +only, so plain cargo builds of that crate do not resolve it. Use `bazel build` +and `bazel test` for it instead. + ## Local Gotchas - `MODULE.bazel` owns the Rust toolchain, `rules_rust`, `crate_universe`, diff --git a/src/invocation-plane-services/http-invocation/crates/server/BUILD.bazel b/src/invocation-plane-services/http-invocation/crates/server/BUILD.bazel index 12e6f14ed..df71074d5 100644 --- a/src/invocation-plane-services/http-invocation/crates/server/BUILD.bazel +++ b/src/invocation-plane-services/http-invocation/crates/server/BUILD.bazel @@ -8,21 +8,33 @@ load("@rules_rust//cargo:defs.bzl", "cargo_build_script") load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") load("//rules/oci:defs.bzl", "rust_oci_image") -# Stamp the real release version into the binary. The legacy Dockerfile did -# this via the CICD_NEXT_VERSION build-arg -> build.rs -> CARGO_PKG_VERSION; the -# Bazel build dropped it, so binaries self-reported the placeholder version -# (e.g. `server --version`, startup logs, and the OTel service.version resource -# attribute all showed 0.0.0-dev regardless of the released image tag). -# tools/workspace_status.sh emits STABLE_VERSION; expand_template substitutes it -# on `--stamp` (release) builds and falls back to 0.0.0-dev otherwise. The file -# is fed to the Rust targets via rustc_env_files so env!("CARGO_PKG_VERSION") -# resolves to the release version. +# Stamp the real release version and commit into the binary. The legacy +# Dockerfile did this via the CICD_NEXT_VERSION build-arg -> build.rs -> +# CARGO_PKG_VERSION; the Bazel build dropped it, so binaries self-reported the +# placeholder version (e.g. `server --version`, startup logs, and the OTel +# service.version resource attribute all showed 0.0.0-dev regardless of the +# released image tag). tools/workspace_status.sh emits +# STABLE_VERSION/STABLE_GIT_COMMIT_FULL; expand_template substitutes them on +# `--stamp` (release) builds and falls back to dev defaults otherwise. The +# file is fed to the Rust targets via rustc_env_files so +# env!("CARGO_PKG_VERSION") resolves to the release version and the GET /info +# handler's NVCF_GIT_COMMIT (via option_env!, see the shared nvcf-info crate) +# resolves to the real commit. expand_template( name = "version_env", out = "version.env", - stamp_substitutions = {"{VERSION}": "{{STABLE_VERSION}}"}, - substitutions = {"{VERSION}": "0.0.0-dev"}, - template = ["CARGO_PKG_VERSION={VERSION}"], + stamp_substitutions = { + "{VERSION}": "{{STABLE_VERSION}}", + "{GIT_COMMIT}": "{{STABLE_GIT_COMMIT_FULL}}", + }, + substitutions = { + "{VERSION}": "0.0.0-dev", + "{GIT_COMMIT}": "unknown", + }, + template = [ + "CARGO_PKG_VERSION={VERSION}", + "NVCF_GIT_COMMIT={GIT_COMMIT}", + ], ) # build.rs uses tonic-build + prost-build to compile crates/server/proto/*.proto. @@ -77,7 +89,10 @@ rust_library( rustc_env = {"CARGO_PKG_NAME": "nvcf-invocation-service"}, rustc_env_files = [":version_env"], visibility = ["//visibility:public"], - deps = [":build_script"] + all_crate_deps(normal = True), + deps = [ + ":build_script", + "//src/libraries/rust/nvcf-info:nvcf-info-invocation", + ] + all_crate_deps(normal = True), ) rust_binary( diff --git a/src/invocation-plane-services/http-invocation/crates/server/src/app.rs b/src/invocation-plane-services/http-invocation/crates/server/src/app.rs index c7c8d356a..f421b6c82 100644 --- a/src/invocation-plane-services/http-invocation/crates/server/src/app.rs +++ b/src/invocation-plane-services/http-invocation/crates/server/src/app.rs @@ -144,6 +144,7 @@ pub async fn app( ) .layer(axum_mw::from_fn(nvcf_mw::auth::auth_middleware)) // Everything above this layer will require auth DON'T MOVE IT .route("/health", get(routes::get_health)) // Needs to be ahead of other layers to avoid auth, and after metrics layer to get recorded + .route("/info", get(routes::get_info)) // unauthenticated, same as /health above .layer(CorsLayer::very_permissive().max_age(Duration::from_secs(86400))) // only on the path based router. for full path passthrough we send the OPTIONS and all other requests all the way to the worker. .layer( PrometheusMetricLayerBuilder::new() diff --git a/src/invocation-plane-services/http-invocation/crates/server/src/routes/info.rs b/src/invocation-plane-services/http-invocation/crates/server/src/routes/info.rs new file mode 100644 index 000000000..bf63da3b7 --- /dev/null +++ b/src/invocation-plane-services/http-invocation/crates/server/src/routes/info.rs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use axum::Json; + +pub async fn get_info() -> Json { + Json(nvcf_info::info_response!("nvcf-invocation-service")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn info_reports_service_version_and_commit() { + let Json(info) = get_info().await; + assert_eq!(info.service, "nvcf-invocation-service"); + // Stamped only on Bazel --stamp builds; under cargo these are the + // unstamped fallbacks. Assert they are populated, not their literals, + // so the test does not break on every release bump. + assert!(!info.version.is_empty()); + assert!(!info.commit.is_empty()); + } +} diff --git a/src/invocation-plane-services/http-invocation/crates/server/src/routes/mod.rs b/src/invocation-plane-services/http-invocation/crates/server/src/routes/mod.rs index 27046eba1..b609fc535 100644 --- a/src/invocation-plane-services/http-invocation/crates/server/src/routes/mod.rs +++ b/src/invocation-plane-services/http-invocation/crates/server/src/routes/mod.rs @@ -20,6 +20,7 @@ mod get_exec; mod get_pexec; mod health; mod http_headers; +mod info; mod input_asset_header; mod nvcf_status_header; mod post_exec; @@ -31,6 +32,7 @@ pub use attach::post_attach::response_attach; pub use get_exec::exec_status; pub use get_pexec::pexec_status_route; pub use health::get_health; +pub use info::get_info; pub use post_exec::exec; pub use post_exec::{InvokeFunctionResponse, InvokeStatus}; pub use post_pexec::pexec; diff --git a/src/libraries/rust/nvcf-info/AGENTS.md b/src/libraries/rust/nvcf-info/AGENTS.md new file mode 100644 index 000000000..6f3b09784 --- /dev/null +++ b/src/libraries/rust/nvcf-info/AGENTS.md @@ -0,0 +1,47 @@ +# AGENTS.md - nvcf-info + +nvcf-info holds the shared `GET /info` response schema for the Rust services, +so `function-autoscaler` and `http-invocation` do not each define their own. +It is the Rust counterpart to `src/libraries/go/lib/pkg/version` and the Java +`nv-boot-starter-core` info controller. + +## Usage + +```rust +async fn get_info() -> axum::Json { + axum::Json(nvcf_info::info_response!("nvcf-my-service")) +} +``` + +Pass the deployed service name, matching how the Go services set +`version.Service` in their `BUILD.bazel` `x_defs`. Version and commit come from +the consuming service's `version_env` template in `crates/server/BUILD.bazel`, +which stamps `CARGO_PKG_VERSION` and `NVCF_GIT_COMMIT` on `--stamp` builds. + +## Adding A Consumer + +Add the service's Bazel target to `crates/server/BUILD.bazel` `deps`, and add +an entry to the list in this crate's `BUILD.bazel` if the service uses a +crate universe not already listed there. Two serde copies are two distinct +`Serialize` traits, so each universe needs its own build of `src/lib.rs`. + +This crate is a Bazel dependency only. It is not a Cargo path dependency: +`cargo-bazel splice` roots each service workspace in a temp dir, so a path +pointing outside that directory resolves above the splice root. Consuming +crates therefore build and test under Bazel rather than cargo. + +## Build And Test + +Run Bazel from the monorepo root: + +```bash +bazel test //src/libraries/rust/nvcf-info:all +``` + +Cargo works inside this crate, which has no cross-workspace path dependencies: + +```bash +cargo fmt -p nvcf-info +cargo clippy -p nvcf-info --all-targets -- -D warnings +cargo test -p nvcf-info +``` diff --git a/src/libraries/rust/nvcf-info/BUILD.bazel b/src/libraries/rust/nvcf-info/BUILD.bazel new file mode 100644 index 000000000..cf0fb42f5 --- /dev/null +++ b/src/libraries/rust/nvcf-info/BUILD.bazel @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +# One target per consuming crate universe. Each Rust service resolves serde +# from its own crate_universe repo, and two serde copies are two distinct +# Serialize traits, so a single shared target fails at the axum::Json bound in +# whichever service it was not built against. Add an entry here for any new +# consumer. See src/libraries/rust/nvcf-info/AGENTS.md. +[ + rust_library( + name = "nvcf-info-%s" % universe, + srcs = ["src/lib.rs"], + crate_name = "nvcf_info", + edition = "2021", + visibility = ["//visibility:public"], + deps = ["@%s//:serde" % repo], + ) + for universe, repo in [ + ("autoscaler", "rs_autoscaler_crates"), + ("invocation", "nvcf_invocation_crates"), + ] +] + +rust_test( + name = "nvcf_info_test", + crate = ":nvcf-info-autoscaler", + deps = [ + "@rs_autoscaler_crates//:serde", + "@rs_autoscaler_crates//:serde_json", + ], +) diff --git a/src/libraries/rust/nvcf-info/CLAUDE.md b/src/libraries/rust/nvcf-info/CLAUDE.md new file mode 100644 index 000000000..43c994c2d --- /dev/null +++ b/src/libraries/rust/nvcf-info/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/src/libraries/rust/nvcf-info/Cargo.lock b/src/libraries/rust/nvcf-info/Cargo.lock new file mode 100644 index 000000000..b9d4504fa --- /dev/null +++ b/src/libraries/rust/nvcf-info/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nvcf-info" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/src/libraries/rust/nvcf-info/Cargo.toml b/src/libraries/rust/nvcf-info/Cargo.toml new file mode 100644 index 000000000..c73ea3787 --- /dev/null +++ b/src/libraries/rust/nvcf-info/Cargo.toml @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[package] +name = "nvcf-info" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +serde = { version = "1", features = ["derive"] } + +[dev-dependencies] +serde_json = "1" diff --git a/src/libraries/rust/nvcf-info/src/lib.rs b/src/libraries/rust/nvcf-info/src/lib.rs new file mode 100644 index 000000000..294627637 --- /dev/null +++ b/src/libraries/rust/nvcf-info/src/lib.rs @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Shared GET /info response schema for the Rust services, matching what the +//! Go services serve from `src/libraries/go/lib/pkg/version` and the Java +//! services serve from `nv-boot-starter-core`. + +use serde::Serialize; + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct InfoResponse { + pub service: &'static str, + pub version: &'static str, + pub commit: &'static str, +} + +/// Builds an [`InfoResponse`] for the named service. +/// +/// A macro rather than a function so `env!` expands in the calling crate and +/// reads that crate's stamped values instead of this one's. The service name +/// is passed in because under Bazel `CARGO_PKG_NAME` resolves to the target +/// name, not the package name. +#[macro_export] +macro_rules! info_response { + ($service:expr) => { + $crate::InfoResponse { + service: $service, + version: env!("CARGO_PKG_VERSION"), + commit: option_env!("NVCF_GIT_COMMIT").unwrap_or("unknown"), + } + }; +} + +#[cfg(test)] +mod tests { + #[test] + fn info_response_uses_the_given_service_name_and_this_crate_version() { + let info = info_response!("test-service"); + assert_eq!(info.service, "test-service"); + assert_eq!(info.version, env!("CARGO_PKG_VERSION")); + assert_eq!(info.commit, "unknown"); + } + + #[test] + fn info_response_serializes_to_the_shared_json_shape() { + let info = info_response!("test-service"); + let json = serde_json::to_value(&info).expect("serializes"); + assert_eq!(json["service"], "test-service"); + assert!(json["version"].is_string()); + assert!(json["commit"].is_string()); + } +}