/* Copyright IBM Corp All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ package operations_test import ( "encoding/json" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "os" "path/filepath" "syscall" "time" "github.com/hyperledger/fabric-lib-go/healthz" "github.com/hyperledger/fabric/common/fabhttp" "github.com/hyperledger/fabric/common/metrics/disabled" "github.com/hyperledger/fabric/common/metrics/prometheus" "github.com/hyperledger/fabric/common/metrics/statsd" "github.com/hyperledger/fabric/core/operations" "github.com/hyperledger/fabric/core/operations/fakes" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" "github.com/tedsuo/ifrit" ) var _ = Describe("System", func() { const AdditionalTestApiPath = "/some-additional-test-api" var ( fakeLogger *fakes.Logger tempDir string client *http.Client unauthClient *http.Client options operations.Options system *operations.System ) BeforeEach(func() { var err error tempDir, err = ioutil.TempDir("", "opssys") Expect(err).NotTo(HaveOccurred()) generateCertificates(tempDir) client = newHTTPClient(tempDir, true) unauthClient = newHTTPClient(tempDir, false) fakeLogger = &fakes.Logger{} options = operations.Options{ Options: fabhttp.Options{ Logger: fakeLogger, ListenAddress: "127.0.0.1:0", TLS: fabhttp.TLS{ Enabled: true, CertFile: filepath.Join(tempDir, "server-cert.pem"), KeyFile: filepath.Join(tempDir, "server-key.pem"), ClientCertRequired: false, ClientCACertFiles: []string{filepath.Join(tempDir, "client-ca.pem")}, }, }, Metrics: operations.MetricsOptions{ Provider: "disabled", }, Version: "test-version", } system = operations.NewSystem(options) }) AfterEach(func() { os.RemoveAll(tempDir) if system != nil { system.Stop() } }) It("hosts an unsecured endpoint for the version information", func() { err := system.Start() Expect(err).NotTo(HaveOccurred()) versionURL := fmt.Sprintf("https://%s/version", system.Addr()) resp, err := client.Get(versionURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) resp.Body.Close() }) It("hosts a secure endpoint for logging", func() { err := system.Start() Expect(err).NotTo(HaveOccurred()) logspecURL := fmt.Sprintf("https://%s/logspec", system.Addr()) resp, err := client.Get(logspecURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) resp.Body.Close() resp, err = unauthClient.Get(logspecURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized)) }) It("does not host a secure endpoint for additional APIs by default", func() { err := system.Start() Expect(err).NotTo(HaveOccurred()) addApiURL := fmt.Sprintf("https://%s%s", system.Addr(), AdditionalTestApiPath) resp, err := client.Get(addApiURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) // service is not handled by default, i.e. in peer resp.Body.Close() resp, err = unauthClient.Get(addApiURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) }) It("hosts a secure endpoint for additional APIs when added", func() { system.RegisterHandler(AdditionalTestApiPath, &fakes.Handler{Code: http.StatusOK, Text: "secure"}, options.TLS.Enabled) err := system.Start() Expect(err).NotTo(HaveOccurred()) addApiURL := fmt.Sprintf("https://%s%s", system.Addr(), AdditionalTestApiPath) resp, err := client.Get(addApiURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) Expect(resp.Header.Get("Content-Type")).To(Equal("text/plain; charset=utf-8")) buff, err := ioutil.ReadAll(resp.Body) Expect(err).NotTo(HaveOccurred()) Expect(string(buff)).To(Equal("secure")) resp.Body.Close() resp, err = unauthClient.Get(addApiURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized)) }) Context("when TLS is disabled", func() { BeforeEach(func() { options.TLS.Enabled = false system = operations.NewSystem(options) }) It("hosts an insecure endpoint for logging", func() { err := system.Start() Expect(err).NotTo(HaveOccurred()) resp, err := client.Get(fmt.Sprintf("http://%s/logspec", system.Addr())) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) resp.Body.Close() }) It("does not host an insecure endpoint for additional APIs by default", func() { err := system.Start() Expect(err).NotTo(HaveOccurred()) addApiURL := fmt.Sprintf("http://%s%s", system.Addr(), AdditionalTestApiPath) resp, err := client.Get(addApiURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) // service is not handled by default, i.e. in peer resp.Body.Close() }) It("hosts an insecure endpoint for additional APIs when added", func() { system.RegisterHandler(AdditionalTestApiPath, &fakes.Handler{Code: http.StatusOK, Text: "insecure"}, options.TLS.Enabled) err := system.Start() Expect(err).NotTo(HaveOccurred()) addApiURL := fmt.Sprintf("http://%s%s", system.Addr(), AdditionalTestApiPath) resp, err := client.Get(addApiURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) Expect(resp.Header.Get("Content-Type")).To(Equal("text/plain; charset=utf-8")) buff, err := ioutil.ReadAll(resp.Body) Expect(err).NotTo(HaveOccurred()) Expect(string(buff)).To(Equal("insecure")) resp.Body.Close() }) }) Context("when ClientCertRequired is true", func() { BeforeEach(func() { options.TLS.ClientCertRequired = true system = operations.NewSystem(options) }) It("requires a client cert to connect", func() { err := system.Start() Expect(err).NotTo(HaveOccurred()) _, err = unauthClient.Get(fmt.Sprintf("https://%s/healthz", system.Addr())) Expect(err).To(MatchError(ContainSubstring("remote error: tls: bad certificate"))) }) }) Context("when listen fails", func() { var listener net.Listener BeforeEach(func() { var err error listener, err = net.Listen("tcp", "127.0.0.1:0") Expect(err).NotTo(HaveOccurred()) options.ListenAddress = listener.Addr().String() system = operations.NewSystem(options) }) AfterEach(func() { listener.Close() }) It("returns an error", func() { err := system.Start() Expect(err).To(MatchError(ContainSubstring("bind: address already in use"))) }) }) Context("when a bad TLS configuration is provided", func() { BeforeEach(func() { options.TLS.CertFile = "cert-file-does-not-exist" system = operations.NewSystem(options) }) It("returns an error", func() { err := system.Start() Expect(err).To(MatchError("open cert-file-does-not-exist: no such file or directory")) }) }) It("proxies Log to the provided logger", func() { err := system.Log("key", "value") Expect(err).NotTo(HaveOccurred()) Expect(fakeLogger.WarnCallCount()).To(Equal(1)) Expect(fakeLogger.WarnArgsForCall(0)).To(Equal([]interface{}{"key", "value"})) }) Context("when a logger is not provided", func() { BeforeEach(func() { options.Logger = nil system = operations.NewSystem(options) }) It("does not panic when logging", func() { Expect(func() { system.Log("key", "value") }).NotTo(Panic()) }) It("returns nil from Log", func() { err := system.Log("key", "value") Expect(err).NotTo(HaveOccurred()) }) }) It("hosts a health check endpoint", func() { err := system.Start() Expect(err).NotTo(HaveOccurred()) healthy := &fakes.HealthChecker{} unhealthy := &fakes.HealthChecker{} unhealthy.HealthCheckReturns(errors.New("Unfortunately, I am not feeling well.")) system.RegisterChecker("healthy", healthy) system.RegisterChecker("unhealthy", unhealthy) resp, err := client.Get(fmt.Sprintf("https://%s/healthz", system.Addr())) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusServiceUnavailable)) body, err := ioutil.ReadAll(resp.Body) Expect(err).NotTo(HaveOccurred()) resp.Body.Close() var healthStatus healthz.HealthStatus err = json.Unmarshal(body, &healthStatus) Expect(err).NotTo(HaveOccurred()) Expect(healthStatus.Status).To(Equal(healthz.StatusUnavailable)) Expect(healthStatus.FailedChecks).To(ConsistOf(healthz.FailedCheck{ Component: "unhealthy", Reason: "Unfortunately, I am not feeling well.", })) }) Context("when the metrics provider is disabled", func() { BeforeEach(func() { options.Metrics = operations.MetricsOptions{ Provider: "disabled", } system = operations.NewSystem(options) Expect(system).NotTo(BeNil()) }) It("sets up a disabled provider", func() { Expect(system.Provider).To(Equal(&disabled.Provider{})) }) }) Context("when the metrics provider is prometheus", func() { BeforeEach(func() { options.Metrics = operations.MetricsOptions{ Provider: "prometheus", } system = operations.NewSystem(options) Expect(system).NotTo(BeNil()) }) It("sets up prometheus as a provider", func() { Expect(system.Provider).To(Equal(&prometheus.Provider{})) }) It("hosts a secure endpoint for metrics", func() { err := system.Start() Expect(err).NotTo(HaveOccurred()) metricsURL := fmt.Sprintf("https://%s/metrics", system.Addr()) resp, err := client.Get(metricsURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() Expect(err).NotTo(HaveOccurred()) Expect(body).To(ContainSubstring("# TYPE go_gc_duration_seconds summary")) resp, err = unauthClient.Get(metricsURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized)) }) It("records the fabric version", func() { err := system.Start() Expect(err).NotTo(HaveOccurred()) metricsURL := fmt.Sprintf("https://%s/metrics", system.Addr()) resp, err := client.Get(metricsURL) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() Expect(err).NotTo(HaveOccurred()) Expect(string(body)).To(ContainSubstring("# TYPE fabric_version gauge")) Expect(string(body)).To(ContainSubstring(`fabric_version{version="test-version"}`)) }) }) Context("when the metrics provider is statsd", func() { var listener net.Listener BeforeEach(func() { var err error listener, err = net.Listen("tcp", "127.0.0.1:0") Expect(err).NotTo(HaveOccurred()) options.Metrics = operations.MetricsOptions{ Provider: "statsd", Statsd: &operations.Statsd{ Network: "tcp", Address: listener.Addr().String(), WriteInterval: 100 * time.Millisecond, Prefix: "prefix", }, } system = operations.NewSystem(options) Expect(system).NotTo(BeNil()) }) recordStats := func(w io.Writer) { defer GinkgoRecover() // handle the dial check conn, err := listener.Accept() if err != nil { return } conn.Close() // handle the payload conn, err = listener.Accept() Expect(err).NotTo(HaveOccurred()) defer conn.Close() conn.SetReadDeadline(time.Now().Add(time.Minute)) _, err = io.Copy(w, conn) if err != nil && err != io.EOF { Expect(err).NotTo(HaveOccurred()) } } AfterEach(func() { listener.Close() }) It("sets up statsd as a provider", func() { provider, ok := system.Provider.(*statsd.Provider) Expect(ok).To(BeTrue()) Expect(provider.Statsd).NotTo(BeNil()) }) It("emits statsd metrics", func() { statsBuffer := gbytes.NewBuffer() go recordStats(statsBuffer) err := system.Start() Expect(err).NotTo(HaveOccurred()) Eventually(statsBuffer).Should(gbytes.Say(`\Qprefix.go.mem.gc_last_epoch_nanotime:\E`)) }) It("emits the fabric version statsd metric", func() { statsBuffer := gbytes.NewBuffer() go recordStats(statsBuffer) err := system.Start() Expect(err).NotTo(HaveOccurred()) Eventually(statsBuffer).Should(gbytes.Say(`\Qprefix.fabric_version.test-version:1.000000|g\E`)) }) Context("when checking the network and address fails", func() { BeforeEach(func() { options.Metrics.Statsd.Network = "bob-the-network" system = operations.NewSystem(options) }) It("returns an error", func() { err := system.Start() Expect(err).To(MatchError(ContainSubstring("bob-the-network"))) }) }) }) Context("when the metrics provider is unknown", func() { BeforeEach(func() { options.Metrics.Provider = "something-unknown" system = operations.NewSystem(options) }) It("sets up a disabled provider", func() { Expect(system.Provider).To(Equal(&disabled.Provider{})) }) It("logs the issue", func() { Expect(fakeLogger.WarnfCallCount()).To(Equal(1)) msg, args := fakeLogger.WarnfArgsForCall(0) Expect(msg).To(Equal("Unknown provider type: %s; metrics disabled")) Expect(args).To(Equal([]interface{}{"something-unknown"})) }) }) It("supports ifrit", func() { process := ifrit.Invoke(system) Eventually(process.Ready()).Should(BeClosed()) process.Signal(syscall.SIGTERM) Eventually(process.Wait()).Should(Receive(BeNil())) }) Context("when start fails and ifrit is used", func() { BeforeEach(func() { options.TLS.CertFile = "non-existent-file" system = operations.NewSystem(options) }) It("does not close the ready chan", func() { process := ifrit.Invoke(system) Consistently(process.Ready()).ShouldNot(BeClosed()) Eventually(process.Wait()).Should(Receive(MatchError("open non-existent-file: no such file or directory"))) }) }) })