From bfcbf7b92db3acedb92cc32d7ce5d0e29c0c5b7d Mon Sep 17 00:00:00 2001 From: Genady Gurevich Date: Wed, 21 Jan 2026 17:16:19 +0200 Subject: [PATCH] Operations endpoints: versioninfo (issue #45) Signed-off-by: Genady Gurevich --- common/operations/system.go | 32 ++++++++++++----- node/assembler/assembler.go | 1 + node/batcher/batcher.go | 1 + node/consensus/consensus.go | 5 ++- node/router/router.go | 1 + test/faulttolerance/assembler_test.go | 51 ++++++++++++++++++++++++++ test/faulttolerance/batcher_test.go | 52 +++++++++++++++++++++++++++ test/faulttolerance/consensus_test.go | 52 +++++++++++++++++++++++++++ test/faulttolerance/router_test.go | 52 +++++++++++++++++++++++++++ testutil/network_utils.go | 43 +++++++++++++++++++++- 10 files changed, 280 insertions(+), 10 deletions(-) diff --git a/common/operations/system.go b/common/operations/system.go index cb471fde3..eee1fa87a 100644 --- a/common/operations/system.go +++ b/common/operations/system.go @@ -87,6 +87,10 @@ func HealthCheckServiceURL(system *System, logger *flogging.FabricLogger) string return operationServiceURL("healthz", system.Addr(), logger) } +func VersionInfoServiceURL(system *System, logger *flogging.FabricLogger) string { + return operationServiceURL("version", system.Addr(), logger) +} + // NewOperationsSystem creates a new operations system with the provided configuration. func NewOperationsSystem(ops Operations, metricsConfig Metrics) *System { o := Options{ @@ -122,6 +126,7 @@ func NewOperationsSystem(ops Operations, metricsConfig Metrics) *System { system.initializeMetricsProvider() system.initializeHealthCheckHandler() + system.initializeVersionInfoHandler() return system } @@ -142,14 +147,11 @@ func (s *System) RegisterChecker(component string, checker healthz.HealthChecker } func (s *System) initializeMetricsProvider() { - // case "prometheus": - // // s.Provider = provider - // s.versionGauge = versionGauge(s.Provider) - // // swagger:operation GET /metrics operations metrics - // // --- - // // responses: - // // '200': - // // description: Ok. + // swagger:operation GET /metrics operations metrics + // --- + // responses: + // '200': + // description: Ok. s.RegisterHandler("/metrics", promhttp.Handler(), s.options.TLS.Enabled) } @@ -165,3 +167,17 @@ func (s *System) initializeHealthCheckHandler() { // description: Service unavailable. s.RegisterHandler("/healthz", s.healthHandler, false) } + +func (s *System) initializeVersionInfoHandler() { + versionInfo := &VersionInfoHandler{ + CommitSHA: metadata.CommitSHA, + Version: metadata.Version, + } + // swagger:operation GET /version operations version + // --- + // summary: Returns the orderer or peer version and the commit SHA on which the release was created. + // responses: + // '200': + // description: Ok. + s.RegisterHandler("/version", versionInfo, false) +} diff --git a/node/assembler/assembler.go b/node/assembler/assembler.go index 241d3c15c..c053c5831 100644 --- a/node/assembler/assembler.go +++ b/node/assembler/assembler.go @@ -162,6 +162,7 @@ func NewDefaultAssembler( assembler.logger.Infof("Prometheus serving on URL: %s", operations.PrometheusMetricsServiceURL(assembler.opsSystem, assembler.logger)) assembler.logger.Infof("Health check serving on URL: %s", operations.HealthCheckServiceURL(assembler.opsSystem, assembler.logger)) + assembler.logger.Infof("Version info serving on URL: %s", operations.VersionInfoServiceURL(assembler.opsSystem, assembler.logger)) assembler.metrics.StartMetricsTracker() return assembler diff --git a/node/batcher/batcher.go b/node/batcher/batcher.go index 4d05931b0..cf2144e9a 100644 --- a/node/batcher/batcher.go +++ b/node/batcher/batcher.go @@ -154,6 +154,7 @@ func (b *Batcher) Run() { b.metrics.StartMetricsTracker() b.logger.Infof("Prometheus serving on URL: %s", b.MonitoringServiceAddress()) b.logger.Infof("Health check serving on URL: %s", operations.HealthCheckServiceURL(b.opsSystem, b.logger)) + b.logger.Infof("Version info serving on URL: %s", operations.VersionInfoServiceURL(b.opsSystem, b.logger)) } func (b *Batcher) GetStatus() string { diff --git a/node/consensus/consensus.go b/node/consensus/consensus.go index c0af1ec80..f269849fb 100644 --- a/node/consensus/consensus.go +++ b/node/consensus/consensus.go @@ -134,9 +134,12 @@ func (c *Consensus) Start() error { } RegisterHealthCheckers(c) + c.Metrics.StartMetricsTracker() + c.Logger.Infof("Prometheus serving on URL: %s", operations.PrometheusMetricsServiceURL(c.opsSystem, c.Logger)) c.Logger.Infof("Health check serving on URL: %s", operations.HealthCheckServiceURL(c.opsSystem, c.Logger)) - c.Metrics.StartMetricsTracker() + c.Logger.Infof("Version info serving on URL: %s", operations.VersionInfoServiceURL(c.opsSystem, c.Logger)) + bft := c.BFT c.lock.Unlock() diff --git a/node/router/router.go b/node/router/router.go index 826027d40..41fa4aecc 100644 --- a/node/router/router.go +++ b/node/router/router.go @@ -187,6 +187,7 @@ func (r *Router) initFromConfig(rconfig *nodeconfig.RouterNodeConfig, configurat r.metrics.StartMetricsTracker() r.logger.Infof("Prometheus serving on URL: %s", r.MonitoringServiceAddress()) r.logger.Infof("Health check serving on URL: %s", operations.HealthCheckServiceURL(r.opsSystem, r.logger)) + r.logger.Infof("Version info serving on URL: %s", operations.VersionInfoServiceURL(r.opsSystem, r.logger)) r.logger.Infof("Router with PartyID: %d has been initialized from config with sequence: %d", rconfig.PartyID, r.configSeq) } diff --git a/test/faulttolerance/assembler_test.go b/test/faulttolerance/assembler_test.go index 7983b2337..b24d7ec92 100644 --- a/test/faulttolerance/assembler_test.go +++ b/test/faulttolerance/assembler_test.go @@ -24,6 +24,7 @@ import ( "github.com/hyperledger/fabric-protos-go-apiv2/common" "github.com/hyperledger/fabric-x-orderer/common/tools/armageddon" "github.com/hyperledger/fabric-x-orderer/common/types" + "github.com/hyperledger/fabric-x-orderer/internal/cryptogen/metadata" "github.com/hyperledger/fabric-x-orderer/testutil" "github.com/hyperledger/fabric-x-orderer/testutil/client" "github.com/hyperledger/fabric-x-orderer/testutil/signutil" @@ -257,3 +258,53 @@ func TestStartAssemblerGetResponseFromOperationEndpoints(t *testing.T) { return testutil.GetHealthCheckStatus(t, re, url) }, 30*time.Second, 100*time.Millisecond) } + +// TestRunArmaNetworkAndGetAssemblerVersionInfo tests that the assembler's version info endpoint is up and returns the correct version information after the assembler is started. +func TestRunArmaNetworkAndGetAssemblerVersionInfo(t *testing.T) { + // 1. compile arma + armaBinaryPath, err := gexec.BuildWithEnvironment("github.com/hyperledger/fabric-x-orderer/cmd/arma", []string{"GOPRIVATE=" + os.Getenv("GOPRIVATE")}) + defer gexec.CleanupBuildArtifacts() + require.NoError(t, err) + require.NotNil(t, armaBinaryPath) + + // Number of parties in the test + numOfParties := 1 + + t.Logf("Running test with %d parties and %d shards", numOfParties, 1) + + // 2. Create a temporary directory for the test. + dir, err := os.MkdirTemp("", t.Name()) + require.NoError(t, err) + defer os.RemoveAll(dir) + + // 3. Create a config YAML file in the temporary directory. + configPath := filepath.Join(dir, "config.yaml") + netInfo := testutil.CreateNetwork(t, configPath, numOfParties, 1, "none", "none") + defer netInfo.CleanUp() + numOfArmaNodes := len(netInfo) + + // 4. Generate the config files in the temporary directory using the armageddon generate command. + armageddon.NewCLI().Run([]string{"generate", "--config", configPath, "--output", dir}) + + // 5. Run the arma nodes. + // NOTE: if one of the nodes is not started within 10 seconds, there is no point in continuing the test, so fail it + readyChan := make(chan string, numOfArmaNodes) + armaNetwork := testutil.RunArmaNodes(t, dir, armaBinaryPath, readyChan, netInfo) + defer armaNetwork.Stop() + + testutil.WaitReady(t, readyChan, numOfArmaNodes, 10) + + // 6. Query the assemblers's version info endpoint and assert the version information. + assembler := armaNetwork.GetAssembler(t, types.PartyID(1)) + url := testutil.CaptureArmaNodeVersionInfoServiceURL(t, assembler) + + re := regexp.MustCompile(`^\{\s*"CommitSHA"\s*:\s*"([^"]*)"\s*,\s*"Version"\s*:\s*"([^"]*)"\s*\}$`) + + require.Eventually(t, func() bool { + val := testutil.FetchVersionInfoValue(t, re, url) + if val == nil { + return false + } + return val.CommitSHA == metadata.CommitSHA && val.Version == metadata.Version + }, 30*time.Second, 100*time.Millisecond) +} diff --git a/test/faulttolerance/batcher_test.go b/test/faulttolerance/batcher_test.go index e56594cc4..e8701aced 100644 --- a/test/faulttolerance/batcher_test.go +++ b/test/faulttolerance/batcher_test.go @@ -11,11 +11,13 @@ import ( "math" "os" "path/filepath" + "regexp" "testing" "time" "github.com/hyperledger/fabric-x-orderer/common/tools/armageddon" "github.com/hyperledger/fabric-x-orderer/common/types" + "github.com/hyperledger/fabric-x-orderer/internal/cryptogen/metadata" test_utils "github.com/hyperledger/fabric-x-orderer/test/utils" "github.com/hyperledger/fabric-x-orderer/testutil" "github.com/hyperledger/fabric-x-orderer/testutil/client" @@ -467,3 +469,53 @@ func TestSecondaryBatcherRestartRecover(t *testing.T) { Signer: signer, }) } + +// TestRunArmaNetworkAndGetBatcherVersionInfo tests that the batcher's version info endpoint is up and returns the correct version information after the batcher is started. +func TestRunArmaNetworkAndGetBatcherVersionInfo(t *testing.T) { + // 1. compile arma + armaBinaryPath, err := gexec.BuildWithEnvironment("github.com/hyperledger/fabric-x-orderer/cmd/arma", []string{"GOPRIVATE=" + os.Getenv("GOPRIVATE")}) + defer gexec.CleanupBuildArtifacts() + require.NoError(t, err) + require.NotNil(t, armaBinaryPath) + + // Number of parties in the test + numOfParties := 1 + + t.Logf("Running test with %d parties and %d shards", numOfParties, 1) + + // 2. Create a temporary directory for the test. + dir, err := os.MkdirTemp("", t.Name()) + require.NoError(t, err) + defer os.RemoveAll(dir) + + // 3. Create a config YAML file in the temporary directory. + configPath := filepath.Join(dir, "config.yaml") + netInfo := testutil.CreateNetwork(t, configPath, numOfParties, 1, "none", "none") + defer netInfo.CleanUp() + numOfArmaNodes := len(netInfo) + + // 4. Generate the config files in the temporary directory using the armageddon generate command. + armageddon.NewCLI().Run([]string{"generate", "--config", configPath, "--output", dir}) + + // 5. Run the arma nodes. + // NOTE: if one of the nodes is not started within 10 seconds, there is no point in continuing the test, so fail it + readyChan := make(chan string, numOfArmaNodes) + armaNetwork := testutil.RunArmaNodes(t, dir, armaBinaryPath, readyChan, netInfo) + defer armaNetwork.Stop() + + testutil.WaitReady(t, readyChan, numOfArmaNodes, 10) + + // 6. Query the batcher's version info endpoint and assert the version information. + batcher := armaNetwork.GetBatcher(t, types.PartyID(1), types.ShardID(1)) + url := testutil.CaptureArmaNodeVersionInfoServiceURL(t, batcher) + + re := regexp.MustCompile(`^\{\s*"CommitSHA"\s*:\s*"([^"]*)"\s*,\s*"Version"\s*:\s*"([^"]*)"\s*\}$`) + + require.Eventually(t, func() bool { + val := testutil.FetchVersionInfoValue(t, re, url) + if val == nil { + return false + } + return val.CommitSHA == metadata.CommitSHA && val.Version == metadata.Version + }, 30*time.Second, 100*time.Millisecond) +} diff --git a/test/faulttolerance/consensus_test.go b/test/faulttolerance/consensus_test.go index fe598a1fd..76c344603 100644 --- a/test/faulttolerance/consensus_test.go +++ b/test/faulttolerance/consensus_test.go @@ -12,6 +12,7 @@ import ( "os" "path" "path/filepath" + "regexp" "strconv" "sync" "testing" @@ -19,6 +20,7 @@ import ( "github.com/hyperledger/fabric-x-orderer/common/tools/armageddon" "github.com/hyperledger/fabric-x-orderer/common/types" + "github.com/hyperledger/fabric-x-orderer/internal/cryptogen/metadata" "github.com/hyperledger/fabric-x-orderer/test/utils" "github.com/hyperledger/fabric-x-orderer/testutil" "github.com/hyperledger/fabric-x-orderer/testutil/signutil" @@ -143,3 +145,53 @@ func TestSubmitStopThenRestartConsenter(t *testing.T) { Signer: signer, }) } + +// TestRunArmaNetworkAndGetConsenterVersionInfo tests that the consenter's version info endpoint is up and returns the correct version information after the consenter is started. +func TestRunArmaNetworkAndGetConsenterVersionInfo(t *testing.T) { + // 1. compile arma + armaBinaryPath, err := gexec.BuildWithEnvironment("github.com/hyperledger/fabric-x-orderer/cmd/arma", []string{"GOPRIVATE=" + os.Getenv("GOPRIVATE")}) + defer gexec.CleanupBuildArtifacts() + require.NoError(t, err) + require.NotNil(t, armaBinaryPath) + + // Number of parties in the test + numOfParties := 1 + + t.Logf("Running test with %d parties and %d shards", numOfParties, 1) + + // 2. Create a temporary directory for the test. + dir, err := os.MkdirTemp("", t.Name()) + require.NoError(t, err) + defer os.RemoveAll(dir) + + // 3. Create a config YAML file in the temporary directory. + configPath := filepath.Join(dir, "config.yaml") + netInfo := testutil.CreateNetwork(t, configPath, numOfParties, 1, "none", "none") + defer netInfo.CleanUp() + numOfArmaNodes := len(netInfo) + + // 4. Generate the config files in the temporary directory using the armageddon generate command. + armageddon.NewCLI().Run([]string{"generate", "--config", configPath, "--output", dir}) + + // 5. Run the arma nodes. + // NOTE: if one of the nodes is not started within 10 seconds, there is no point in continuing the test, so fail it + readyChan := make(chan string, numOfArmaNodes) + armaNetwork := testutil.RunArmaNodes(t, dir, armaBinaryPath, readyChan, netInfo) + defer armaNetwork.Stop() + + testutil.WaitReady(t, readyChan, numOfArmaNodes, 10) + + // 6. Query the consenter's version info endpoint and assert the version information. + consenter := armaNetwork.GetConsenter(t, types.PartyID(1)) + url := testutil.CaptureArmaNodeVersionInfoServiceURL(t, consenter) + + re := regexp.MustCompile(`^\{\s*"CommitSHA"\s*:\s*"([^"]*)"\s*,\s*"Version"\s*:\s*"([^"]*)"\s*\}$`) + + require.Eventually(t, func() bool { + val := testutil.FetchVersionInfoValue(t, re, url) + if val == nil { + return false + } + return val.CommitSHA == metadata.CommitSHA && val.Version == metadata.Version + }, 30*time.Second, 100*time.Millisecond) +} diff --git a/test/faulttolerance/router_test.go b/test/faulttolerance/router_test.go index 8aa8a418d..771e107d5 100644 --- a/test/faulttolerance/router_test.go +++ b/test/faulttolerance/router_test.go @@ -11,11 +11,13 @@ import ( "math" "os" "path/filepath" + "regexp" "testing" "time" "github.com/hyperledger/fabric-x-orderer/common/tools/armageddon" "github.com/hyperledger/fabric-x-orderer/common/types" + "github.com/hyperledger/fabric-x-orderer/internal/cryptogen/metadata" test_utils "github.com/hyperledger/fabric-x-orderer/test/utils" "github.com/hyperledger/fabric-x-orderer/testutil" "github.com/hyperledger/fabric-x-orderer/testutil/client" @@ -281,3 +283,53 @@ func TestRouterRestartRecover(t *testing.T) { Signer: signer, }) } + +// TestRunArmaNetworkAndGetRouterVersionInfo tests that the router's version info endpoint is up and returns the correct version information after the router is started. +func TestRunArmaNetworkAndGetRouterVersionInfo(t *testing.T) { + // 1. compile arma + armaBinaryPath, err := gexec.BuildWithEnvironment("github.com/hyperledger/fabric-x-orderer/cmd/arma", []string{"GOPRIVATE=" + os.Getenv("GOPRIVATE")}) + defer gexec.CleanupBuildArtifacts() + require.NoError(t, err) + require.NotNil(t, armaBinaryPath) + + // Number of parties in the test + numOfParties := 1 + + t.Logf("Running test with %d parties and %d shards", numOfParties, 1) + + // 2. Create a temporary directory for the test. + dir, err := os.MkdirTemp("", t.Name()) + require.NoError(t, err) + defer os.RemoveAll(dir) + + // 3. Create a config YAML file in the temporary directory. + configPath := filepath.Join(dir, "config.yaml") + netInfo := testutil.CreateNetwork(t, configPath, numOfParties, 1, "none", "none") + defer netInfo.CleanUp() + numOfArmaNodes := len(netInfo) + + // 4. Generate the config files in the temporary directory using the armageddon generate command. + armageddon.NewCLI().Run([]string{"generate", "--config", configPath, "--output", dir}) + + // 5. Run the arma nodes. + // NOTE: if one of the nodes is not started within 10 seconds, there is no point in continuing the test, so fail it + readyChan := make(chan string, numOfArmaNodes) + armaNetwork := testutil.RunArmaNodes(t, dir, armaBinaryPath, readyChan, netInfo) + defer armaNetwork.Stop() + + testutil.WaitReady(t, readyChan, numOfArmaNodes, 10) + + // 6. Query the router's version info endpoint and assert the version information. + router := armaNetwork.GetRouter(t, types.PartyID(1)) + url := testutil.CaptureArmaNodeVersionInfoServiceURL(t, router) + + re := regexp.MustCompile(`^\{\s*"CommitSHA"\s*:\s*"([^"]*)"\s*,\s*"Version"\s*:\s*"([^"]*)"\s*\}$`) + + require.Eventually(t, func() bool { + val := testutil.FetchVersionInfoValue(t, re, url) + if val == nil { + return false + } + return val.CommitSHA == metadata.CommitSHA && val.Version == metadata.Version + }, 30*time.Second, 100*time.Millisecond) +} diff --git a/testutil/network_utils.go b/testutil/network_utils.go index 0ebb112e1..66731fd23 100644 --- a/testutil/network_utils.go +++ b/testutil/network_utils.go @@ -8,6 +8,7 @@ package testutil import ( "context" + "encoding/json" "io" "net" "net/http" @@ -19,6 +20,7 @@ import ( "time" "github.com/hyperledger/fabric-lib-go/healthz" + "github.com/hyperledger/fabric-x-orderer/common/operations" "github.com/stretchr/testify/require" ) @@ -78,11 +80,50 @@ func FetchPrometheusMetricValue(t *testing.T, re *regexp.Regexp, url string) int return -1 } +func FetchVersionInfoValue(t *testing.T, re *regexp.Regexp, url string) *operations.VersionInfoHandler { + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(url) + require.NoError(t, err) + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil + } + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + t.Log(string(body)) + + // Find all matches + matches := re.FindAllString(string(body), -1) + + if len(matches) > 0 { + val := &operations.VersionInfoHandler{} + err := json.Unmarshal([]byte(matches[0]), val) + require.NoError(t, err) + return val + } + + return nil +} + // CaptureArmaNodePrometheusServiceURL retrieves the Prometheus metrics endpoint URL from the given ArmaNodeInfo's session output. // It waits until the URL is found or times out, and returns the metrics endpoint as a string. func CaptureArmaNodePrometheusServiceURL(t *testing.T, armaNodeInfo *ArmaNodeInfo) string { + return captureArmaNodeServiceURL(t, armaNodeInfo, `Prometheus serving on URL:\s+(https?://[^/\s]+/metrics)`) +} + +// CaptureArmaNodeVersionInfoServiceURL retrieves the version info endpoint URL from the given ArmaNodeInfo's session output. +// It waits until the URL is found or times out, and returns the version info endpoint as a string. +func CaptureArmaNodeVersionInfoServiceURL(t *testing.T, armaNodeInfo *ArmaNodeInfo) string { + return captureArmaNodeServiceURL(t, armaNodeInfo, `Version info serving on URL:\s+(https?://[^/\s]+/version)`) +} + +func captureArmaNodeServiceURL(t *testing.T, armaNodeInfo *ArmaNodeInfo, regex string) string { var url string - re := regexp.MustCompile(`Prometheus serving on URL:\s+(https?://[^/\s]+/metrics)`) + re := regexp.MustCompile(regex) require.Eventually(t, func() bool { output := string(armaNodeInfo.RunInfo.Session.Err.Contents()) matches := re.FindStringSubmatch(output)