github.com/binyushen/fabric@v2.1.1+incompatible/core/operations/system_test.go (about)

     1  /*
     2  Copyright IBM Corp All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package operations_test
     8  
     9  import (
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"io/ioutil"
    15  	"net"
    16  	"net/http"
    17  	"os"
    18  	"path/filepath"
    19  	"syscall"
    20  	"time"
    21  
    22  	"github.com/hyperledger/fabric-lib-go/healthz"
    23  	"github.com/hyperledger/fabric/common/metrics/disabled"
    24  	"github.com/hyperledger/fabric/common/metrics/prometheus"
    25  	"github.com/hyperledger/fabric/common/metrics/statsd"
    26  	"github.com/hyperledger/fabric/core/operations"
    27  	"github.com/hyperledger/fabric/core/operations/fakes"
    28  	. "github.com/onsi/ginkgo"
    29  	. "github.com/onsi/gomega"
    30  	"github.com/onsi/gomega/gbytes"
    31  	"github.com/tedsuo/ifrit"
    32  )
    33  
    34  var _ = Describe("System", func() {
    35  	var (
    36  		fakeLogger *fakes.Logger
    37  		tempDir    string
    38  
    39  		client       *http.Client
    40  		unauthClient *http.Client
    41  		options      operations.Options
    42  		system       *operations.System
    43  	)
    44  
    45  	BeforeEach(func() {
    46  		var err error
    47  		tempDir, err := ioutil.TempDir("", "opssys")
    48  		Expect(err).NotTo(HaveOccurred())
    49  
    50  		generateCertificates(tempDir)
    51  		client = newHTTPClient(tempDir, true)
    52  		unauthClient = newHTTPClient(tempDir, false)
    53  
    54  		fakeLogger = &fakes.Logger{}
    55  		options = operations.Options{
    56  			Logger:        fakeLogger,
    57  			ListenAddress: "127.0.0.1:0",
    58  			Metrics: operations.MetricsOptions{
    59  				Provider: "disabled",
    60  			},
    61  			TLS: operations.TLS{
    62  				Enabled:            true,
    63  				CertFile:           filepath.Join(tempDir, "server-cert.pem"),
    64  				KeyFile:            filepath.Join(tempDir, "server-key.pem"),
    65  				ClientCertRequired: false,
    66  				ClientCACertFiles:  []string{filepath.Join(tempDir, "client-ca.pem")},
    67  			},
    68  			Version: "test-version",
    69  		}
    70  
    71  		system = operations.NewSystem(options)
    72  	})
    73  
    74  	AfterEach(func() {
    75  		os.RemoveAll(tempDir)
    76  		if system != nil {
    77  			system.Stop()
    78  		}
    79  	})
    80  
    81  	It("hosts an unsecured endpoint for the version information", func() {
    82  		err := system.Start()
    83  		Expect(err).NotTo(HaveOccurred())
    84  
    85  		versionURL := fmt.Sprintf("https://%s/version", system.Addr())
    86  		resp, err := client.Get(versionURL)
    87  		Expect(err).NotTo(HaveOccurred())
    88  		Expect(resp.StatusCode).To(Equal(http.StatusOK))
    89  		resp.Body.Close()
    90  	})
    91  
    92  	It("hosts a secure endpoint for logging", func() {
    93  		err := system.Start()
    94  		Expect(err).NotTo(HaveOccurred())
    95  
    96  		logspecURL := fmt.Sprintf("https://%s/logspec", system.Addr())
    97  		resp, err := client.Get(logspecURL)
    98  		Expect(err).NotTo(HaveOccurred())
    99  		Expect(resp.StatusCode).To(Equal(http.StatusOK))
   100  		resp.Body.Close()
   101  
   102  		resp, err = unauthClient.Get(logspecURL)
   103  		Expect(err).NotTo(HaveOccurred())
   104  		Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized))
   105  	})
   106  
   107  	Context("when TLS is disabled", func() {
   108  		BeforeEach(func() {
   109  			options.TLS.Enabled = false
   110  			system = operations.NewSystem(options)
   111  		})
   112  
   113  		It("hosts an insecure endpoint for logging", func() {
   114  			err := system.Start()
   115  			Expect(err).NotTo(HaveOccurred())
   116  
   117  			resp, err := client.Get(fmt.Sprintf("http://%s/logspec", system.Addr()))
   118  			Expect(err).NotTo(HaveOccurred())
   119  			Expect(resp.StatusCode).To(Equal(http.StatusOK))
   120  			resp.Body.Close()
   121  		})
   122  	})
   123  
   124  	Context("when ClientCertRequired is true", func() {
   125  		BeforeEach(func() {
   126  			options.TLS.ClientCertRequired = true
   127  			system = operations.NewSystem(options)
   128  		})
   129  
   130  		It("requires a client cert to connect", func() {
   131  			err := system.Start()
   132  			Expect(err).NotTo(HaveOccurred())
   133  
   134  			_, err = unauthClient.Get(fmt.Sprintf("https://%s/healthz", system.Addr()))
   135  			Expect(err).To(MatchError(ContainSubstring("remote error: tls: bad certificate")))
   136  		})
   137  	})
   138  
   139  	Context("when listen fails", func() {
   140  		var listener net.Listener
   141  
   142  		BeforeEach(func() {
   143  			var err error
   144  			listener, err = net.Listen("tcp", "127.0.0.1:0")
   145  			Expect(err).NotTo(HaveOccurred())
   146  
   147  			options.ListenAddress = listener.Addr().String()
   148  			system = operations.NewSystem(options)
   149  		})
   150  
   151  		AfterEach(func() {
   152  			listener.Close()
   153  		})
   154  
   155  		It("returns an error", func() {
   156  			err := system.Start()
   157  			Expect(err).To(MatchError(ContainSubstring("bind: address already in use")))
   158  		})
   159  	})
   160  
   161  	Context("when a bad TLS configuration is provided", func() {
   162  		BeforeEach(func() {
   163  			options.TLS.CertFile = "cert-file-does-not-exist"
   164  			system = operations.NewSystem(options)
   165  		})
   166  
   167  		It("returns an error", func() {
   168  			err := system.Start()
   169  			Expect(err).To(MatchError("open cert-file-does-not-exist: no such file or directory"))
   170  		})
   171  	})
   172  
   173  	It("proxies Log to the provided logger", func() {
   174  		err := system.Log("key", "value")
   175  		Expect(err).NotTo(HaveOccurred())
   176  
   177  		Expect(fakeLogger.WarnCallCount()).To(Equal(1))
   178  		Expect(fakeLogger.WarnArgsForCall(0)).To(Equal([]interface{}{"key", "value"}))
   179  	})
   180  
   181  	Context("when a logger is not provided", func() {
   182  		BeforeEach(func() {
   183  			options.Logger = nil
   184  			system = operations.NewSystem(options)
   185  		})
   186  
   187  		It("does not panic when logging", func() {
   188  			Expect(func() { system.Log("key", "value") }).NotTo(Panic())
   189  		})
   190  
   191  		It("returns nil from Log", func() {
   192  			err := system.Log("key", "value")
   193  			Expect(err).NotTo(HaveOccurred())
   194  		})
   195  	})
   196  
   197  	It("hosts a health check endpoint", func() {
   198  		err := system.Start()
   199  		Expect(err).NotTo(HaveOccurred())
   200  
   201  		healthy := &fakes.HealthChecker{}
   202  		unhealthy := &fakes.HealthChecker{}
   203  		unhealthy.HealthCheckReturns(errors.New("Unfortunately, I am not feeling well."))
   204  
   205  		system.RegisterChecker("healthy", healthy)
   206  		system.RegisterChecker("unhealthy", unhealthy)
   207  
   208  		resp, err := client.Get(fmt.Sprintf("https://%s/healthz", system.Addr()))
   209  		Expect(err).NotTo(HaveOccurred())
   210  		Expect(resp.StatusCode).To(Equal(http.StatusServiceUnavailable))
   211  		body, err := ioutil.ReadAll(resp.Body)
   212  		Expect(err).NotTo(HaveOccurred())
   213  		resp.Body.Close()
   214  
   215  		var healthStatus healthz.HealthStatus
   216  		err = json.Unmarshal(body, &healthStatus)
   217  		Expect(err).NotTo(HaveOccurred())
   218  		Expect(healthStatus.Status).To(Equal(healthz.StatusUnavailable))
   219  		Expect(healthStatus.FailedChecks).To(ConsistOf(healthz.FailedCheck{
   220  			Component: "unhealthy",
   221  			Reason:    "Unfortunately, I am not feeling well.",
   222  		}))
   223  	})
   224  
   225  	Context("when the metrics provider is disabled", func() {
   226  		BeforeEach(func() {
   227  			options.Metrics = operations.MetricsOptions{
   228  				Provider: "disabled",
   229  			}
   230  			system = operations.NewSystem(options)
   231  			Expect(system).NotTo(BeNil())
   232  		})
   233  
   234  		It("sets up a disabled provider", func() {
   235  			Expect(system.Provider).To(Equal(&disabled.Provider{}))
   236  		})
   237  	})
   238  
   239  	Context("when the metrics provider is prometheus", func() {
   240  		BeforeEach(func() {
   241  			options.Metrics = operations.MetricsOptions{
   242  				Provider: "prometheus",
   243  			}
   244  			system = operations.NewSystem(options)
   245  			Expect(system).NotTo(BeNil())
   246  		})
   247  
   248  		It("sets up prometheus as a provider", func() {
   249  			Expect(system.Provider).To(Equal(&prometheus.Provider{}))
   250  		})
   251  
   252  		It("hosts a secure endpoint for metrics", func() {
   253  			err := system.Start()
   254  			Expect(err).NotTo(HaveOccurred())
   255  
   256  			metricsURL := fmt.Sprintf("https://%s/metrics", system.Addr())
   257  			resp, err := client.Get(metricsURL)
   258  			Expect(err).NotTo(HaveOccurred())
   259  			Expect(resp.StatusCode).To(Equal(http.StatusOK))
   260  			body, err := ioutil.ReadAll(resp.Body)
   261  			resp.Body.Close()
   262  			Expect(err).NotTo(HaveOccurred())
   263  			Expect(body).To(ContainSubstring("# TYPE go_gc_duration_seconds summary"))
   264  
   265  			resp, err = unauthClient.Get(metricsURL)
   266  			Expect(err).NotTo(HaveOccurred())
   267  			Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized))
   268  		})
   269  
   270  		It("records the fabric version", func() {
   271  			err := system.Start()
   272  			Expect(err).NotTo(HaveOccurred())
   273  
   274  			metricsURL := fmt.Sprintf("https://%s/metrics", system.Addr())
   275  			resp, err := client.Get(metricsURL)
   276  			Expect(err).NotTo(HaveOccurred())
   277  			Expect(resp.StatusCode).To(Equal(http.StatusOK))
   278  			body, err := ioutil.ReadAll(resp.Body)
   279  			resp.Body.Close()
   280  			Expect(err).NotTo(HaveOccurred())
   281  			Expect(string(body)).To(ContainSubstring("# TYPE fabric_version gauge"))
   282  			Expect(string(body)).To(ContainSubstring(`fabric_version{version="test-version"}`))
   283  		})
   284  	})
   285  
   286  	Context("when the metrics provider is statsd", func() {
   287  		var listener net.Listener
   288  
   289  		BeforeEach(func() {
   290  			var err error
   291  			listener, err = net.Listen("tcp", "127.0.0.1:0")
   292  			Expect(err).NotTo(HaveOccurred())
   293  
   294  			options.Metrics = operations.MetricsOptions{
   295  				Provider: "statsd",
   296  				Statsd: &operations.Statsd{
   297  					Network:       "tcp",
   298  					Address:       listener.Addr().String(),
   299  					WriteInterval: 100 * time.Millisecond,
   300  					Prefix:        "prefix",
   301  				},
   302  			}
   303  			system = operations.NewSystem(options)
   304  			Expect(system).NotTo(BeNil())
   305  		})
   306  
   307  		recordStats := func(w io.Writer) {
   308  			defer GinkgoRecover()
   309  
   310  			// handle the dial check
   311  			conn, err := listener.Accept()
   312  			if err != nil {
   313  				return
   314  			}
   315  			conn.Close()
   316  
   317  			// handle the payload
   318  			conn, err = listener.Accept()
   319  			Expect(err).NotTo(HaveOccurred())
   320  			defer conn.Close()
   321  
   322  			conn.SetReadDeadline(time.Now().Add(time.Minute))
   323  			_, err = io.Copy(w, conn)
   324  			if err != nil && err != io.EOF {
   325  				Expect(err).NotTo(HaveOccurred())
   326  			}
   327  		}
   328  
   329  		AfterEach(func() {
   330  			listener.Close()
   331  		})
   332  
   333  		It("sets up statsd as a provider", func() {
   334  			provider, ok := system.Provider.(*statsd.Provider)
   335  			Expect(ok).To(BeTrue())
   336  			Expect(provider.Statsd).NotTo(BeNil())
   337  		})
   338  
   339  		It("emits statsd metrics", func() {
   340  			statsBuffer := gbytes.NewBuffer()
   341  			go recordStats(statsBuffer)
   342  
   343  			err := system.Start()
   344  			Expect(err).NotTo(HaveOccurred())
   345  			Eventually(statsBuffer).Should(gbytes.Say(`\Qprefix.go.mem.gc_last_epoch_nanotime:\E`))
   346  		})
   347  
   348  		It("emits the fabric version statsd metric", func() {
   349  			statsBuffer := gbytes.NewBuffer()
   350  			go recordStats(statsBuffer)
   351  
   352  			err := system.Start()
   353  			Expect(err).NotTo(HaveOccurred())
   354  			Eventually(statsBuffer).Should(gbytes.Say(`\Qprefix.fabric_version.test-version:1.000000|g\E`))
   355  		})
   356  
   357  		Context("when checking the network and address fails", func() {
   358  			BeforeEach(func() {
   359  				options.Metrics.Statsd.Network = "bob-the-network"
   360  				system = operations.NewSystem(options)
   361  			})
   362  
   363  			It("returns an error", func() {
   364  				err := system.Start()
   365  				Expect(err).To(MatchError(ContainSubstring("bob-the-network")))
   366  			})
   367  		})
   368  	})
   369  
   370  	Context("when the metrics provider is unknown", func() {
   371  		BeforeEach(func() {
   372  			options.Metrics.Provider = "something-unknown"
   373  			system = operations.NewSystem(options)
   374  		})
   375  
   376  		It("sets up a disabled provider", func() {
   377  			Expect(system.Provider).To(Equal(&disabled.Provider{}))
   378  		})
   379  
   380  		It("logs the issue", func() {
   381  			Expect(fakeLogger.WarnfCallCount()).To(Equal(1))
   382  			msg, args := fakeLogger.WarnfArgsForCall(0)
   383  			Expect(msg).To(Equal("Unknown provider type: %s; metrics disabled"))
   384  			Expect(args).To(Equal([]interface{}{"something-unknown"}))
   385  		})
   386  	})
   387  
   388  	It("supports ifrit", func() {
   389  		process := ifrit.Invoke(system)
   390  		Eventually(process.Ready()).Should(BeClosed())
   391  
   392  		process.Signal(syscall.SIGTERM)
   393  		Eventually(process.Wait()).Should(Receive(BeNil()))
   394  	})
   395  
   396  	Context("when start fails and ifrit is used", func() {
   397  		BeforeEach(func() {
   398  			options.TLS.CertFile = "non-existent-file"
   399  			system = operations.NewSystem(options)
   400  		})
   401  
   402  		It("does not close the ready chan", func() {
   403  			process := ifrit.Invoke(system)
   404  			Consistently(process.Ready()).ShouldNot(BeClosed())
   405  			Eventually(process.Wait()).Should(Receive(MatchError("open non-existent-file: no such file or directory")))
   406  		})
   407  	})
   408  })