diff --git a/src/control-plane-services/event-ledger/cmd/api/BUILD.bazel b/src/control-plane-services/event-ledger/cmd/api/BUILD.bazel index 0596cc1f9..52f557f89 100644 --- a/src/control-plane-services/event-ledger/cmd/api/BUILD.bazel +++ b/src/control-plane-services/event-ledger/cmd/api/BUILD.bazel @@ -16,6 +16,11 @@ go_library( go_binary( name = "api", embed = [":api_lib"], + x_defs = { + "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/version.Service": "nvcf-event-ledger", + "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/version.Version": "{STABLE_VERSION}", + "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/version.GitHash": "{STABLE_GIT_COMMIT_FULL}", + }, visibility = ["//visibility:public"], ) diff --git a/src/control-plane-services/event-ledger/cmd/api/startup/BUILD.bazel b/src/control-plane-services/event-ledger/cmd/api/startup/BUILD.bazel index 7580e3561..52ae2bf0a 100644 --- a/src/control-plane-services/event-ledger/cmd/api/startup/BUILD.bazel +++ b/src/control-plane-services/event-ledger/cmd/api/startup/BUILD.bazel @@ -31,6 +31,7 @@ go_library( "@com_github_lestrrat_go_jwx_v2//jwk", "@com_github_nvidia_nvcf_src_libraries_go_lib//pkg/nvkit/auth", "@com_github_nvidia_nvcf_src_libraries_go_lib//pkg/nvkit/clients", + "@com_github_nvidia_nvcf_src_libraries_go_lib//pkg/version", "@com_github_spf13_cobra//:cobra", "@com_github_spf13_viper//:viper", "@com_github_uptrace_opentelemetry_go_extra_otelzap//:otelzap", @@ -47,10 +48,16 @@ alias( go_test( name = "startup_test", - srcs = ["root_cmd_test.go"], + srcs = [ + "info_test.go", + "root_cmd_test.go", + ], embed = [":startup"], deps = [ + "//src/control-plane-services/event-ledger/cmd/api/service", "//src/control-plane-services/event-ledger/internal/config", + "@com_github_gorilla_mux//:mux", + "@com_github_nvidia_nvcf_src_libraries_go_lib//pkg/version", "@com_github_spf13_cobra//:cobra", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", diff --git a/src/control-plane-services/event-ledger/cmd/api/startup/info_test.go b/src/control-plane-services/event-ledger/cmd/api/startup/info_test.go new file mode 100644 index 000000000..c26a64d95 --- /dev/null +++ b/src/control-plane-services/event-ledger/cmd/api/startup/info_test.go @@ -0,0 +1,101 @@ +/* +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. +*/ + +package startup + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + golibversion "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/version" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/NVIDIA/nvcf/src/control-plane-services/event-ledger/cmd/api/service" +) + +// TestRegisterUnauthenticatedRoutes_Info verifies GET /info returns the stamped +// service/version/commit as JSON. It wires /info onto the router before auth +// middleware, so an empty *service.Server is safe as long as /health is not +// exercised. +func TestRegisterUnauthenticatedRoutes_Info(t *testing.T) { + prevService, prevVersion, prevHash := golibversion.Service, golibversion.Version, golibversion.GitHash + golibversion.Service = "nvcf-event-ledger" + golibversion.Version = "test-1.0.0" + golibversion.GitHash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + t.Cleanup(func() { + golibversion.Service = prevService + golibversion.Version = prevVersion + golibversion.GitHash = prevHash + }) + + // A recording middleware confirms registerUnauthenticatedRoutes actually + // wraps /info with the injected middleware (tracing + logging in production). + mwApplied := false + infoMiddleware := func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mwApplied = true + w.Header().Set("X-Info-Middleware", "applied") + h.ServeHTTP(w, r) + }) + } + + router := mux.NewRouter() + registerUnauthenticatedRoutes(router, &service.Server{}, infoMiddleware) + + w := httptest.NewRecorder() + r := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/info", nil) + router.ServeHTTP(w, r) + + assert.True(t, mwApplied, "/info should be wrapped with the injected middleware") + assert.Equal(t, "applied", w.Header().Get("X-Info-Middleware")) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var info map[string]string + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &info)) + assert.Equal(t, "nvcf-event-ledger", info["service"]) + assert.Equal(t, "test-1.0.0", info["version"]) + assert.Equal(t, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", info["commit"]) +} + +// TestRegisterUnauthenticatedRoutes_Info_RejectsNonGET verifies non-GET methods on +// /info return 405 with an Allow: GET header, as enforced by the go-lib handler. +func TestRegisterUnauthenticatedRoutes_Info_RejectsNonGET(t *testing.T) { + router := mux.NewRouter() + registerUnauthenticatedRoutes(router, &service.Server{}, func(h http.Handler) http.Handler { return h }) + + for _, method := range []string{ + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + } { + t.Run(method, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequestWithContext(t.Context(), method, "/info", nil) + router.ServeHTTP(w, r) + + assert.Equal(t, http.StatusMethodNotAllowed, w.Code) + assert.Equal(t, http.MethodGet, w.Header().Get("Allow")) + assert.Empty(t, w.Body.String()) + }) + } +} diff --git a/src/control-plane-services/event-ledger/cmd/api/startup/run_service.go b/src/control-plane-services/event-ledger/cmd/api/startup/run_service.go index badf43ab9..b06b78702 100644 --- a/src/control-plane-services/event-ledger/cmd/api/startup/run_service.go +++ b/src/control-plane-services/event-ledger/cmd/api/startup/run_service.go @@ -30,6 +30,7 @@ import ( "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/auth" "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/nvkit/clients" + golibversion "github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/version" "github.com/golang-jwt/jwt/v5" "github.com/gorilla/handlers" "github.com/gorilla/mux" @@ -53,6 +54,16 @@ import ( "github.com/NVIDIA/nvcf/src/control-plane-services/event-ledger/internal/registrations" ) +// registerUnauthenticatedRoutes registers routes that must be reachable before +// (and regardless of) auth middleware: /health for liveness/readiness probes, +// and /info for build-version discovery. /info is wrapped with tracing and +// request logging via infoMiddleware; /health probes are left uninstrumented to +// avoid span and log spam. +func registerUnauthenticatedRoutes(router *mux.Router, server *service.Server, infoMiddleware func(http.Handler) http.Handler) { + router.HandleFunc("/health", server.Health) + router.Handle("/info", infoMiddleware(golibversion.Handler())) +} + func runService(cfg config.Config) error { ctx := context.Background() @@ -149,16 +160,21 @@ func runService(cfg config.Config) error { router.Use(middleware.BodyLimitMiddleware(10 * 1024 * 1024)) // 10MB limit router.Use(metricsMiddleware) - router.HandleFunc("/health", server.Health) - spanNameFormatter := func(operation string, r *http.Request) string { return r.Method + " " + operation // e.g., "GET /api/resource" } - authRouter := router.PathPrefix("").Subrouter() - authRouter.Use(otelmux.Middleware("deployment-stages", otelmux.WithSpanNameFormatter(spanNameFormatter))) - - // Initialize request logger mw + tracingMW := otelmux.Middleware("deployment-stages", otelmux.WithSpanNameFormatter(spanNameFormatter)) loggerMW := logging.LoggerMiddleware(logger) + + // /info runs through tracing and request logging (RED metrics already apply + // on the base router). /health is left uninstrumented to avoid probe span and + // log spam. + registerUnauthenticatedRoutes(router, server, func(h http.Handler) http.Handler { + return tracingMW(loggerMW(h)) + }) + + authRouter := router.PathPrefix("").Subrouter() + authRouter.Use(tracingMW) authRouter.Use(loggerMW) // If we're not using Policy, we need to handle scope checks locally in our middleware diff --git a/src/control-plane-services/event-ledger/go.mod b/src/control-plane-services/event-ledger/go.mod index 1a9de39bd..4c441b732 100644 --- a/src/control-plane-services/event-ledger/go.mod +++ b/src/control-plane-services/event-ledger/go.mod @@ -54,7 +54,7 @@ require ( ) require ( - github.com/NVIDIA/nvcf/src/libraries/go/lib v0.0.0-20260710034659-973443ac16c3 + github.com/NVIDIA/nvcf/src/libraries/go/lib v0.0.0-20260728185909-afca4ec2fb26 github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/src/control-plane-services/event-ledger/go.sum b/src/control-plane-services/event-ledger/go.sum index 5c07c9bb0..821a73626 100644 --- a/src/control-plane-services/event-ledger/go.sum +++ b/src/control-plane-services/event-ledger/go.sum @@ -2,8 +2,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/NVIDIA/nvcf/src/libraries/go/lib v0.0.0-20260710034659-973443ac16c3 h1:pBymv2IQia2bswak0wHracVj2kdDXB5xcCgm3BVxfmg= -github.com/NVIDIA/nvcf/src/libraries/go/lib v0.0.0-20260710034659-973443ac16c3/go.mod h1:nj3yBW2weO0qzi1ML45tBqviLa2Y/KG9qCalxQuJVB8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -369,3 +367,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +github.com/NVIDIA/nvcf/src/libraries/go/lib v0.0.0-20260728185909-afca4ec2fb26 h1:P9OSmvy6MQPqfzRgaLl0XpvK5UYHAFtNnQQb+yk+6b0= +github.com/NVIDIA/nvcf/src/libraries/go/lib v0.0.0-20260728185909-afca4ec2fb26/go.mod h1:nj3yBW2weO0qzi1ML45tBqviLa2Y/KG9qCalxQuJVB8=